2026-04-15 15:28:42 +02:00
# Cameleer UI Interaction Patterns Audit
Add UX polish design spec with comprehensive audit findings
Playwright-driven audit of the live UI (build 69dcce2, 60+ screenshots)
covering all pages, CRUD lifecycles, design consistency, and interaction
patterns. Spec defines 8 batches of work: critical bugs, layout
consistency, interaction consistency, contrast/readability, data
formatting, chart fixes, admin polish, and nice-to-have items.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:00:50 +02:00
Audit date: 2026-04-09
Scope: All `.tsx` files under `ui/src/pages/` and `ui/src/components/`
---
## 1. Delete / Destructive Operations
### 1.1 Delete User
- **File**: `ui/src/pages/Admin/UsersTab.tsx` (lines 155-172, 358-365, 580-587)
- **Button location**: Detail pane header, top-right, inline with avatar and name
- **Button**: `<Button size="sm" variant="danger">Delete</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete user "${name}"? This cannot be undone.`
- Confirm text: user's `displayName`
- Has `loading` prop bound to mutation
- **Self-delete guard**: Button is `disabled={isSelf}` (cannot delete yourself)
- **Toast on success**: `variant: 'warning'` , title: "User deleted"
- **Toast on error**: `variant: 'error'` , `duration: 86_400_000`
### 1.2 Remove User From Group (via User detail)
- **File**: `ui/src/pages/Admin/UsersTab.tsx` (lines 588-613)
- **Button location**: Tag `onRemove` handler on group tags in detail pane
- **Confirmation**: `AlertDialog` (simple confirm, no type-to-confirm)
- Title: "Remove group membership"
- Description: "Removing this group may also revoke inherited roles. Continue?"
- Confirm label: "Remove"
- Variant: `warning`
- **Toast on success**: `variant: 'success'` , title: "Group removed"
### 1.3 Remove Role From User (via User detail)
- **File**: `ui/src/pages/Admin/UsersTab.tsx` (lines 504-528)
- **Button location**: Tag `onRemove` handler on role tags in detail pane
- **Confirmation**: NONE -- immediate mutation on tag remove click
- **Toast on success**: `variant: 'success'` , title: "Role removed"
**INCONSISTENCY**: Removing a group shows an AlertDialog confirmation but removing a role does not, even though both can have cascading effects.
### 1.4 Delete Group
- **File**: `ui/src/pages/Admin/GroupsTab.tsx` (lines 140-155, 340-347, 434-441)
- **Button location**: Detail pane header, top-right
- **Button**: `<Button size="sm" variant="danger">Delete</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete group "${name}"? This cannot be undone.`
- Confirm text: group's `name`
- Has `loading` prop
- **Built-in guard**: Button is `disabled={isBuiltinAdmins}`
- **Toast on success**: `variant: 'warning'` , title: "Group deleted"
### 1.5 Remove Role From Group
- **File**: `ui/src/pages/Admin/GroupsTab.tsx` (lines 404-427, 442-455)
- **Button location**: Tag `onRemove` handler on role tags in group detail
- **Confirmation**: `AlertDialog` shown ONLY when the group has members (conditional)
- Title: "Remove role from group"
- Description: `Removing this role will affect ${members.length} member(s) who inherit it. Continue?`
- Confirm label: "Remove"
- Variant: `warning`
- **If group has no members**: Immediate mutation, no confirmation
- **Toast on success**: `variant: 'success'` , title: "Role removed"
### 1.6 Remove Member From Group
- **File**: `ui/src/pages/Admin/GroupsTab.tsx` (lines 366-372)
- **Button location**: Tag `onRemove` handler on member tags in group detail
- **Confirmation**: NONE -- immediate mutation on tag remove click
- **Toast on success**: `variant: 'success'` , title: "Member removed"
### 1.7 Delete Role
- **File**: `ui/src/pages/Admin/RolesTab.tsx` (lines 93-110, 261-265, 223-231)
- **Button location**: Detail pane header, top-right
- **Button**: `<Button size="sm" variant="danger">Delete</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete role "${name}"? This cannot be undone.`
- Confirm text: role's `name`
- Has `loading` prop
- **System role guard**: Button hidden for system roles (`!role.system` )
- **Toast on success**: `variant: 'warning'` , title: "Role deleted"
### 1.8 Delete Environment
- **File**: `ui/src/pages/Admin/EnvironmentsPage.tsx` (lines 101-112, 245-252, 319-327)
- **Button location**: Detail pane header, top-right
- **Button**: `<Button size="sm" variant="danger">Delete</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete environment "${displayName}"? All apps and deployments in this environment will be removed. This cannot be undone.`
- Confirm text: environment's `slug` (NOT the display name)
- Has `loading` prop
- **Default guard**: Button is `disabled={isDefault}` (cannot delete default environment)
- **Toast on success**: `variant: 'warning'` , title: "Environment deleted"
**NOTE**: The confirm text requires the slug but the message shows the display name. This is intentional (slug is the unique identifier) but differs from Users/Groups/Roles which use the display name.
### 1.9 Delete OIDC Configuration
- **File**: `ui/src/pages/Admin/OidcConfigPage.tsx` (lines 113-124, 253-264)
- **Button location**: Bottom of page in a "Danger Zone" section
- **Button**: `<Button size="sm" variant="danger">Delete OIDC Configuration</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete OIDC configuration? All users signed in via OIDC will lose access.`
- Confirm text: `"delete oidc"` (static string)
- NO `loading` prop
- **Toast on success**: `variant: 'warning'` , title: "Configuration deleted"
**INCONSISTENCY**: No `loading` prop on this ConfirmDialog, unlike all other delete confirmations.
### 1.10 Delete App
- **File**: `ui/src/pages/AppsTab/AppsTab.tsx` (lines 533-539, 565, 589-596)
- **Button location**: App detail header, top-right, in `detailActions` div alongside "Upload JAR"
- **Button**: `<Button size="sm" variant="danger">Delete App</Button>`
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Message: `Delete app "${displayName}"? All versions and deployments will be removed. This cannot be undone.`
- Confirm text: app's `slug`
- Has `loading` prop
- **Toast on success**: `variant: 'warning'` , title: "App deleted"
- **Post-delete**: Navigates to `/apps`
### 1.11 Stop Deployment
- **File**: `ui/src/pages/AppsTab/AppsTab.tsx` (lines 526-531, 672)
- **Button location**: Inline in deployments table, right-aligned actions column
- **Button**: `<Button size="sm" variant="danger">Stop</Button>`
- **Confirmation**: NONE -- immediate mutation on click
- **Toast on success**: `variant: 'warning'` , title: "Deployment stopped"
**INCONSISTENCY**: Stopping a deployment is a destructive operation that affects live services but has NO confirmation dialog. Route stop/suspend in RouteControlBar uses a ConfirmDialog, but deployment stop does not.
### 1.12 Stop/Suspend Route
- **File**: `ui/src/pages/Exchanges/RouteControlBar.tsx` (lines 43-154)
- **Button location**: Route control bar (segmented button group)
- **Button**: Custom segmented `<button>` elements (not design system Button)
- **Confirmation**: `ConfirmDialog` (type-to-confirm) -- only for `stop` and `suspend` actions
- Title: `"Stop route?"` or `"Suspend route?"`
- Message: `This will ${action} route "${routeId}" on ${application}. This affects all live agents.`
- Confirm text: the action name (e.g., `"stop"` or `"suspend"` )
- Confirm label: `"Stop Route"` or `"Suspend Route"`
- Variant: `danger` for stop, `warning` for suspend
- Has `loading` prop
- **Start and Resume**: No confirmation (immediate action)
- **Toast patterns match others**
### 1.13 Delete Tap (Route Detail page)
- **File**: `ui/src/pages/Routes/RouteDetail.tsx` (lines 991-1001)
- **Button location**: Inline delete icon button in taps table row
- **Confirmation**: `ConfirmDialog` (type-to-confirm)
- Title: "Delete Tap"
- Message: `This will remove the tap "${attributeName}" from the configuration.`
- Confirm text: tap's `attributeName`
- Confirm label: "Delete"
- Variant: `danger`
- **No `loading` prop on this dialog**
**INCONSISTENCY**: No `loading` prop, unlike entity delete confirmations.
### 1.14 Delete Tap (TapConfigModal)
- **File**: `ui/src/components/TapConfigModal.tsx` (lines 117-122, 249-253)
- **Button location**: Inside the modal footer, left-aligned (only shown when editing)
- **Button**: `<Button variant="danger">Delete</Button>`
- **Confirmation**: NONE -- immediate call to `onDelete` then `onClose`
- **Toast**: Handled by parent component (ExchangesPage)
**INCONSISTENCY**: Deleting a tap from the TapConfigModal has no confirmation, but deleting from the RouteDetail table shows a ConfirmDialog.
### 1.15 Kill Database Query
- **File**: `ui/src/pages/Admin/DatabaseAdminPage.tsx` (line 30)
- **Button location**: Inline in active queries table
- **Button**: `<Button variant="danger" size="sm">Kill</Button>`
- **Confirmation**: NONE -- immediate mutation
- **Toast**: None visible
**INCONSISTENCY**: Killing a database query is a destructive action with no confirmation and no toast feedback.
---
## 2. Button Placement & Order
### 2.1 Create Forms (Users, Groups, Roles, Environments)
All four entity create forms use an identical pattern:
| Page | File | Line | Left Button | Right Button |
|------|------|------|-------------|--------------|
| Users | `UsersTab.tsx` | 254-274 | Cancel (ghost) | Create (primary) |
| Groups | `GroupsTab.tsx` | 251-268 | Cancel (ghost) | Create (primary) |
| Roles | `RolesTab.tsx` | 142-159 | Cancel (ghost) | Create (primary) |
| Environments | `EnvironmentsPage.tsx` | 181-194 | Cancel (ghost) | Create (primary) |
- **Position**: Bottom of inline create form in the list pane
- **Container class**: `styles.createFormActions`
- **Order**: Cancel (left) | Create (right) -- **CONSISTENT **
- **Variants**: Cancel = `ghost` , Create = `primary` -- **CONSISTENT **
- **Size**: Both `sm` -- **CONSISTENT **
### 2.2 App Creation Page
- **File**: `ui/src/pages/AppsTab/AppsTab.tsx` (lines 282-287)
- **Position**: Top of page in `detailActions` header area
- **Order**: Cancel (ghost, left) | Create & Deploy / Create (primary, right)
- **Size**: Both `sm`
- **CONSISTENT** with the pattern (Cancel left, Submit right)
### 2.3 OIDC Config Page (Toolbar)
- **File**: `ui/src/pages/Admin/OidcConfigPage.tsx` (lines 130-137)
- **Position**: Top toolbar
- **Order**: Test Connection (secondary, left) | Save (primary, right)
- **No Cancel button** -- form is always editable
**NOTE**: This is the only admin page without a Cancel button or Edit mode toggle.
### 2.4 App Detail Header
- **File**: `ui/src/pages/AppsTab/AppsTab.tsx` (lines 560-566)
- **Position**: Top-right header area in `detailActions`
- **Order**: Upload JAR (primary) | Delete App (danger)
**NOTE**: The primary action (Upload) is on the LEFT and the destructive action (Delete) is on the RIGHT.
### 2.5 App Config Detail Page (AppConfigDetailPage)
- **File**: `ui/src/pages/Admin/AppConfigDetailPage.tsx` (lines 308-319)
- **Position**: Top toolbar
- **Read mode**: Back (ghost) ... Edit (secondary)
- **Edit mode**: Back (ghost) ... Save (default/no variant specified!) | Cancel (secondary)
- **Order when editing**: Save (left) | Cancel (right)
**INCONSISTENCY #1 **: Save button has NO `variant` prop set -- it renders as default, not `primary` . Every other Save button uses `variant="primary"` .
**INCONSISTENCY #2 **: Button order is REVERSED from every other form. Here it is Save (left) | Cancel (right). Everywhere else it is Cancel (left) | Save (right).
### 2.6 App Config Sub-Tab (AppsTab ConfigSubTab)
- **File**: `ui/src/pages/AppsTab/AppsTab.tsx` (lines 922-936)
- **Position**: Top banner bar (editBanner)
- **Read mode**: Banner text + Edit (secondary)
- **Edit mode**: Banner text + Cancel (ghost) | Save Configuration (primary)
- **Order when editing**: Cancel (left) | Save (right) -- **CONSISTENT **
### 2.7 Environment Default Resources / JAR Retention Sections
- **File**: `ui/src/pages/Admin/EnvironmentsPage.tsx` (lines 437-446, 505-514)
- **Position**: Bottom of section, right-aligned (`justifyContent: 'flex-end'` )
- **Read mode**: Edit Defaults / Edit Policy (secondary)
- **Edit mode**: Cancel (ghost) | Save (primary) -- **CONSISTENT **
- **Size**: Both `sm`
### 2.8 User Password Reset
- **File**: `ui/src/pages/Admin/UsersTab.tsx` (lines 407-431)
- **Position**: Inline in Security section
- **Order**: Cancel (ghost) | Set (primary)
- **CONSISTENT** pattern (Cancel left, Submit right)
### 2.9 Tap Modal (TapConfigModal)
- **File**: `ui/src/components/TapConfigModal.tsx` (lines 249-257)
- **Position**: Modal footer
- **Order (edit mode)**: Delete (danger, left, in `footerLeft` ) | Cancel (secondary) | Save (primary)
- **Order (create mode)**: Cancel (secondary) | Save (primary)
- **No `size` prop specified** -- renders at default size
**NOTE**: Uses `variant="secondary"` for Cancel, not `variant="ghost"` like create forms.
### 2.10 Tap Modal (RouteDetail inline version)
- **File**: `ui/src/pages/Routes/RouteDetail.tsx` (lines 984-986)
- **Position**: Modal footer (`tapModalFooter` )
- **Order**: Cancel (secondary) | Save (primary)
- **No `size` prop specified**
- **CONSISTENT** with TapConfigModal
### 2.11 About Me Dialog
- **File**: `ui/src/components/AboutMeDialog.tsx` (lines 14, 72)
- **Uses `Modal` with built-in close button** (no explicit action buttons)
- **Close via**: Modal `onClose` handler (X button and backdrop click)
### 2.12 Login Page
- **File**: `ui/src/auth/LoginPage.tsx` (lines 176-184)
- **Single button**: Sign in (primary, full width, submit type)
- **Optional SSO button above**: Sign in with SSO (secondary)
### Summary of Button Order Patterns
| Location | Cancel Side | Submit Side | Consistent? |
|----------|------------|-------------|-------------|
| User create form | Left (ghost) | Right (primary) | YES |
| Group create form | Left (ghost) | Right (primary) | YES |
| Role create form | Left (ghost) | Right (primary) | YES |
| Env create form | Left (ghost) | Right (primary) | YES |
| App create page | Left (ghost) | Right (primary) | YES |
| Env Default Resources edit | Left (ghost) | Right (primary) | YES |
| Env JAR Retention edit | Left (ghost) | Right (primary) | YES |
| AppsTab config sub-tab edit | Left (ghost) | Right (primary) | YES |
| User password reset | Left (ghost) | Right (primary) | YES |
| TapConfigModal | Left (secondary) | Right (primary) | Variant mismatch |
| RouteDetail tap modal | Left (secondary) | Right (primary) | Variant mismatch |
| **AppConfigDetailPage ** | **Left (NO variant) ** | **Right (secondary) ** | **REVERSED ** |
---
## 3. Edit / Save Patterns
### 3.1 Users (UsersTab)
- **Edit mode**: No explicit toggle. Display name uses `InlineEdit` (click-to-edit). Everything else is managed via tag add/remove.
- **No Save/Cancel for the detail view** -- all changes are immediate mutations.
- **Unsaved changes indicator**: N/A (no batched editing)
- **On success**: Toast with `variant: 'success'`
- **On error**: Toast with `variant: 'error'` , `duration: 86_400_000` (effectively permanent)
### 3.2 Groups (GroupsTab)
- **Edit mode**: Name uses `InlineEdit` . All other changes (members, roles) are immediate mutations.
- **Pattern**: Same as Users -- no batched edit mode.
### 3.3 Roles (RolesTab)
- **Edit mode**: Read-only detail panel. No editing of role fields.
- **Only action**: Delete
### 3.4 Environments (EnvironmentsPage)
- **Edit mode (name)**: `InlineEdit`
- **Edit mode (production/enabled toggles)**: Immediate mutations per toggle change
- **Edit mode (Default Resources)**: Explicit Edit toggle (`setEditing(true)` )
- Cancel/Save buttons appear at bottom-right
- Resets form on cancel
- No unsaved changes indicator
- On success: Toast `variant: 'success'`
- **Edit mode (JAR Retention)**: Same pattern as Default Resources
- **On environment switch**: Both sub-sections auto-reset to read mode
### 3.5 OIDC Config (OidcConfigPage)
- **Edit mode**: ALWAYS editable (no toggle)
- **Save button**: Always visible in top toolbar
- **No Cancel button** -- cannot discard changes
- **No unsaved changes indicator**
- **On success**: Toast `variant: 'success'`
- **On error**: Toast `variant: 'error'` + inline `<Alert variant="error">` both shown
**INCONSISTENCY**: Only page that is always editable with no way to discard changes. Also the only page that shows BOTH a toast AND an inline alert on error.
### 3.6 App Config Detail (AppConfigDetailPage)
- **Edit mode**: Explicit toggle via `Edit` button (Pencil icon) in toolbar
- **Toolbar in edit mode**: Save (unstyled!) | Cancel (secondary)
- **Save button text**: Shows "Saving..." while pending
- **No unsaved changes indicator**
- **On success**: Toast `variant: 'success'` , exits edit mode
- **On error**: Toast `variant: 'error'` , stays in edit mode
### 3.7 App Config Sub-Tab (AppsTab ConfigSubTab)
- **Edit mode**: Explicit toggle via banner + Edit button
- **Banner in read mode**: "Configuration is read-only. Enter edit mode to make changes."
- **Banner in edit mode**: "Editing configuration. Changes are not saved until you click Save." (styled differently with `editBannerActive` )
- **This IS an unsaved changes indicator** (the banner text changes)
- **Cancel/Save in edit banner**: Cancel (ghost) | Save Configuration (primary)
- **On success**: Toast `variant: 'success'` , exits edit mode, shows redeploy notice
- **On error**: Toast `variant: 'error'` , stays in edit mode
### 3.8 App Create Page
- **Edit mode**: N/A (always a creation form)
- **Multi-step indicator**: Shows step text like "Creating app...", "Uploading JAR..." during submission
- **On success**: Toast `variant: 'success'` , navigates to app detail page
- **On error**: Toast `variant: 'error'` with step context
### 3.9 Tap Editing (TapConfigModal + RouteDetail inline)
- **Edit mode**: Modal opens for edit or create
- **Save/Cancel**: In modal footer
- **On success**: Modal closes, parent handles toast
- **On error**: Parent handles toast
### Summary of Edit Patterns
| Page | Explicit Edit Toggle? | Unsaved Changes Indicator? | Consistent? |
|------|----------------------|---------------------------|-------------|
| Users | No (inline edits) | N/A | N/A |
| Groups | No (inline edits) | N/A | N/A |
| Roles | No (read-only) | N/A | N/A |
| Environments - name | No (InlineEdit) | N/A | OK |
| Environments - resources | YES | No | Missing |
| Environments - JAR retention | YES | No | Missing |
| OIDC Config | No (always editable) | No | Deviation |
| AppConfigDetailPage | YES | No | Missing |
| AppsTab ConfigSubTab | YES (banner) | YES (banner text) | Best pattern |
**INCONSISTENCY**: The AppsTab ConfigSubTab is the only one with a proper unsaved-changes indicator. AppConfigDetailPage (which edits the same data for a different entry point) has no such indicator.
---
## 4. Toast / Notification Patterns
### 4.1 Toast Provider
- **File**: `ui/src/components/LayoutShell.tsx` (line 783)
- **Provider**: `<ToastProvider>` from `@cameleer/design-system` wraps the entire app layout
- **Hook**: `useToast()` returns `{ toast }` function
### 4.2 Toast Call Signature
All toast calls use the same shape:
```typescript
toast({
title: string,
description?: string,
variant: 'success' | 'error' | 'warning',
duration?: number
})
```
### 4.3 Toast Variants Used
| Variant | Used For | Duration |
|---------|----------|----------|
| `success` | Successful operations | Default (auto-dismiss) |
| `error` | Failed operations | `86_400_000` (24 hours = effectively permanent) |
| `warning` | Destructive successes (delete, stop) AND partial failures | Mixed (see below) |
### 4.4 Duration Patterns
- **Success toasts**: No explicit duration (uses design system default) -- **CONSISTENT **
- **Error toasts**: Always `duration: 86_400_000` -- **CONSISTENT ** (49 occurrences across 10 files)
- **Warning toasts for deletion success** (user/group/role/env/OIDC/app deleted): No explicit duration (auto-dismiss) -- **CONSISTENT **
- **Warning toasts for partial push failures**: `duration: 86_400_000` -- **CONSISTENT **
### 4.5 Naming Conventions for Toast Titles
**Success pattern**: Action-noun format
- "User created", "Group created", "Role created", "Environment created"
- "Display name updated", "Password updated", "Group renamed"
- "Config saved", "Configuration saved", "Tap configuration saved"
**Error pattern**: "Failed to [action]" format
- "Failed to create user", "Failed to delete group", "Failed to update password"
- "Save failed", "Upload failed", "Deploy failed" (shorter form)
**INCONSISTENCY**: Error messages mix two patterns:
1. "Failed to [verb] [noun]" (e.g., "Failed to create user") -- used in RBAC pages
2. "[Noun] failed" (e.g., "Save failed", "Upload failed") -- used in AppsTab, AppConfigDetailPage
### 4.6 Warning Variant for Deletions
Successful deletions use `variant: 'warning'` consistently:
- "User deleted" (UsersTab:162)
- "Group deleted" (GroupsTab:147)
- "Role deleted" (RolesTab:100)
- "Environment deleted" (EnvironmentsPage:105)
- "Configuration deleted" (OidcConfigPage:119)
- "App deleted" (AppsTab:536)
- "Deployment stopped" (AppsTab:529)
**CONSISTENT** -- all destructive-but-successful operations use warning.
---
## 5. Loading / Empty States
### 5.1 Full-Page Loading States
| Page | Component | Size | Wrapper |
|------|-----------|------|---------|
| UsersTab | `<Spinner size="md" />` | md | Bare return |
| GroupsTab | `<Spinner size="md" />` | md | Bare return |
| RolesTab | `<Spinner size="md" />` | md | Bare return |
| EnvironmentsPage | `<Spinner size="md" />` | md | Bare return |
| AppListView | `<Spinner size="md" />` | md | Bare return |
| AppDetailView | `<Spinner size="md" />` | md | Bare return |
| AgentInstance | `<Spinner size="lg" />` | **lg ** | Bare return |
| AppConfigDetailPage | `<Spinner size="lg" />` | **lg ** | Wrapped in `div.loading` |
| DashboardPage | `<PageLoader />` | lg | Centered container |
| RuntimePage | `<PageLoader />` | lg | Centered container |
| OidcConfigPage | `return null` | N/A | Returns nothing |
**INCONSISTENCY #1 **: Most admin pages use `<Spinner size="md" />` as a bare return. AgentInstance and AppConfigDetailPage use `size="lg"` . DashboardPage and RuntimePage use the `<PageLoader />` component which wraps `<Spinner size="lg" />` in a centered container.
**INCONSISTENCY #2 **: OidcConfigPage returns `null` while loading (shows a blank page), unlike every other page.
**INCONSISTENCY #3 **: SplitPane detail loading (GroupsTab line 317, RolesTab line 212) uses `<Spinner size="md" />` -- consistent within that context.
### 5.2 Section Loading States
- **RouteDetail charts**: `<Spinner size="sm" />` inline in chart containers (lines 713, 804)
- **AboutMeDialog**: `<Spinner size="md" />` in a `div.loading` wrapper
### 5.3 Empty States
| Context | Pattern | Component Used |
|---------|---------|----------------|
| SplitPane list (no search match) | `emptyMessage="No X match your search"` | EntityList built-in |
| SplitPane detail (nothing selected) | `emptyMessage="Select a X to view details"` | SplitPane built-in |
| Deployments table (none) | `<p className={styles.emptyNote}>No deployments yet.</p>` | Plain `<p>` |
| Versions list (none) | `<p className={styles.emptyNote}>No versions uploaded yet.</p>` | Plain `<p>` |
| Env vars (none, not editing) | `<p className={styles.emptyNote}>No environment variables configured.</p>` | Plain `<p>` |
| Traces/Taps (none) | `<p className={styles.emptyNote}>No processor traces or taps configured.</p>` | Plain `<p>` |
| Route recording (none) | `<p className={styles.emptyNote}>No routes found for this application.</p>` | Plain `<p>` |
| AgentInstance metrics | `<EmptyState title="No data" description="No X available" />` | EmptyState (DS component) |
| Log/Event panels | `<div className={logStyles.logEmpty}>No events...</div>` | Styled `<div>` |
| OIDC default roles | `<span className={styles.noRoles}>No default roles configured</span>` | `<span>` |
| Group members (none) | `<span className={styles.inheritedNote}>(no members)</span>` | `<span>` |
| AppConfigDetailPage (not found) | `<div>No configuration found for "{appId}".</div>` | Plain `<div>` |
| RouteDetail error patterns | `<div className={styles.emptyText}>No error patterns found...</div>` | Styled `<div>` |
| RouteDetail taps (none) | `<div className={styles.emptyState}>No taps configured...</div>` | Styled `<div>` |
**INCONSISTENCY**: Empty states use at least 5 different approaches:
1. Design system `EmptyState` component (only in AgentInstance)
2. `<p className={styles.emptyNote}>` (AppsTab)
3. `<span className={styles.inheritedNote}>` with parenthetical format "(none)" (RBAC pages)
4. `<div className={styles.emptyText}>` (RouteDetail)
5. Unstyled inline text (AppConfigDetailPage)
The design system provides an `EmptyState` component but it is only used in one place (AgentInstance).
---
## 6. Inconsistency Summary
### HIGH Priority (User-facing confusion)
1. **AppConfigDetailPage button order is reversed ** (Save|Cancel instead of Cancel|Save) and Save button has no `variant="primary"` . File: `ui/src/pages/Admin/AppConfigDetailPage.tsx` , lines 311-315.
2. **Deployment Stop has no confirmation dialog ** . Stopping a running deployment immediately executes with no confirmation, while stopping/suspending a route shows a ConfirmDialog. File: `ui/src/pages/AppsTab/AppsTab.tsx` , line 672.
3. **Tap deletion is inconsistent ** . Deleting from TapConfigModal: no confirmation. Deleting from RouteDetail table: ConfirmDialog. File: `ui/src/components/TapConfigModal.tsx` line 117 vs `ui/src/pages/Routes/RouteDetail.tsx` line 992.
4. **Kill Query has no confirmation and no feedback ** . File: `ui/src/pages/Admin/DatabaseAdminPage.tsx` , line 30.
### MEDIUM Priority (Pattern deviations)
5. **Cancel button variant inconsistency ** . Create forms use `variant="ghost"` for Cancel. Modal dialogs (TapConfigModal, RouteDetail tap modal) use `variant="secondary"` . File: `ui/src/components/TapConfigModal.tsx` line 255, vs `ui/src/pages/Admin/UsersTab.tsx` line 258.
6. **Removing a role from a user has no confirmation ** but removing a group from a user shows an AlertDialog. Both can cascade. File: `ui/src/pages/Admin/UsersTab.tsx` , lines 504-528 vs 588-613.
7. **OIDC Config is always editable with no Cancel/discard ** . Every other editable form either has inline-edit (immediate save) or explicit edit mode with Cancel. File: `ui/src/pages/Admin/OidcConfigPage.tsx` .
8. **OIDC Config delete ConfirmDialog missing `loading` prop ** . All other delete ConfirmDialogs pass `loading={mutation.isPending}` . File: `ui/src/pages/Admin/OidcConfigPage.tsx` , line 258.
9. **Loading state size inconsistency ** . Most pages use `Spinner size="md"` , some use `size="lg"` , some use `PageLoader` , and OidcConfigPage returns `null` . No single standard.
10. **Error toast title format inconsistency ** . RBAC pages use "Failed to [verb] [noun]" while AppsTab/AppConfigDetailPage use "[Noun] failed". Should pick one.
### LOW Priority (Minor deviations)
11. **Empty state presentation varies widely ** . Five different approaches used. Should standardize on the design system `EmptyState` component or at least a consistent CSS class.
12. **ConfirmDialog confirmText varies between display name and slug ** . Users/Groups/Roles use display name; Environments and Apps use slug. This is arguably intentional (slug is the technical identifier) but may confuse users.
13. **OIDC Config shows both toast and inline Alert on error ** . No other page shows both simultaneously. File: `ui/src/pages/Admin/OidcConfigPage.tsx` , line 92 (toast) + line 139 (inline Alert).
14. **AppConfigDetailPage Save button text changes to "Saving..." ** using string interpolation, while every other page uses the `loading` prop on Button (which shows a spinner). File: `ui/src/pages/Admin/AppConfigDetailPage.tsx` , line 313.
15. **Unsaved changes indicator ** only present on AppsTab ConfigSubTab (banner text). AppConfigDetailPage, Environment resource sections, and JAR retention section have no indicator even though they use explicit edit mode.
---
## 7. ConfirmDialog Usage Matrix
| Object | File | Line | confirmText Source | Has `loading` ? | Has `variant` ? | Has `confirmLabel` ? |
|--------|------|------|-------------------|----------------|----------------|---------------------|
| User | UsersTab.tsx | 580 | displayName | YES | No (default) | No (default) |
| Group | GroupsTab.tsx | 434 | name | YES | No (default) | No (default) |
| Role | RolesTab.tsx | 223 | name | YES | No (default) | No (default) |
| Environment | EnvironmentsPage.tsx | 319 | slug | YES | No (default) | No (default) |
| OIDC Config | OidcConfigPage.tsx | 258 | "delete oidc" | **NO ** | No (default) | No (default) |
| App | AppsTab.tsx | 589 | slug | YES | No (default) | No (default) |
| Tap (RouteDetail) | RouteDetail.tsx | 992 | attributeName | **NO ** | `danger` | `"Delete"` |
| Route Stop | RouteControlBar.tsx | 139 | action name | YES | `danger` /`warning` | `"Stop Route"` / `"Suspend Route"` |
**NOTE**: RouteControlBar and RouteDetail set explicit `variant` and `confirmLabel` on ConfirmDialog while all RBAC/admin pages use defaults. This creates visual differences in the confirmation dialogs.
---
## 8. AlertDialog Usage Matrix
| Context | File | Line | Title | Confirm Label | Variant |
|---------|------|------|-------|---------------|---------|
| Remove group from user | UsersTab.tsx | 588 | "Remove group membership" | "Remove" | `warning` |
| Remove role from group | GroupsTab.tsx | 442 | "Remove role from group" | "Remove" | `warning` |
AlertDialog is used consistently where present (both use `warning` variant and "Remove" label).
---
## 9. Files Examined
All `.tsx` files under `ui/src/pages/` and `ui/src/components/` :
- `ui/src/pages/Admin/UsersTab.tsx`
- `ui/src/pages/Admin/GroupsTab.tsx`
- `ui/src/pages/Admin/RolesTab.tsx`
- `ui/src/pages/Admin/EnvironmentsPage.tsx`
- `ui/src/pages/Admin/OidcConfigPage.tsx`
- `ui/src/pages/Admin/AppConfigDetailPage.tsx`
- `ui/src/pages/Admin/DatabaseAdminPage.tsx`
- `ui/src/pages/Admin/ClickHouseAdminPage.tsx`
- `ui/src/pages/Admin/AuditLogPage.tsx`
- `ui/src/pages/AppsTab/AppsTab.tsx`
- `ui/src/pages/Routes/RouteDetail.tsx`
- `ui/src/pages/Exchanges/ExchangesPage.tsx`
- `ui/src/pages/Exchanges/RouteControlBar.tsx`
- `ui/src/pages/AgentHealth/AgentHealth.tsx`
- `ui/src/pages/AgentInstance/AgentInstance.tsx`
- `ui/src/pages/DashboardTab/DashboardPage.tsx`
- `ui/src/pages/RuntimeTab/RuntimePage.tsx`
- `ui/src/components/TapConfigModal.tsx`
- `ui/src/components/AboutMeDialog.tsx`
- `ui/src/components/PageLoader.tsx`
- `ui/src/components/LayoutShell.tsx`
- `ui/src/auth/LoginPage.tsx`