From fb53dc6dfc14d0c609245235440f65a12ab2a43a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:39:22 +0200 Subject: [PATCH] fix: standardize button order, add confirmation dialogs for destructive actions - Fix Cancel|Save order and add primary/loading props (AppConfigDetailPage) - Add AlertDialog before stopping deployments (AppsTab) - Add ConfirmDialog before deleting taps (TapConfigModal) - Add AlertDialog before killing queries with toast feedback (DatabaseAdminPage) - Add AlertDialog before removing roles from users (UsersTab) - Standardize Cancel button to variant="ghost" (TapConfigModal, RouteDetail) - Add loading prop to ConfirmDialogs (OidcConfigPage, RouteDetail) Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/TapConfigModal.tsx | 19 +++++++-- ui/src/pages/Admin/AppConfigDetailPage.tsx | 6 +-- ui/src/pages/Admin/DatabaseAdminPage.tsx | 30 +++++++++++++- ui/src/pages/Admin/OidcConfigPage.tsx | 1 + ui/src/pages/Admin/UsersTab.tsx | 46 +++++++++++++--------- ui/src/pages/AppsTab/AppsTab.tsx | 21 +++++++++- ui/src/pages/Routes/RouteDetail.tsx | 3 +- 7 files changed, 95 insertions(+), 31 deletions(-) diff --git a/ui/src/components/TapConfigModal.tsx b/ui/src/components/TapConfigModal.tsx index 5a2c500e..9aae620f 100644 --- a/ui/src/components/TapConfigModal.tsx +++ b/ui/src/components/TapConfigModal.tsx @@ -1,6 +1,6 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import { - Modal, FormField, Input, Select, Textarea, Toggle, Button, Collapsible, + Modal, FormField, Input, Select, Textarea, Toggle, Button, Collapsible, ConfirmDialog, } from '@cameleer/design-system'; import type { TapDefinition, ApplicationConfig } from '../api/queries/commands'; import { useTestExpression } from '../api/queries/commands'; @@ -65,6 +65,7 @@ export function TapConfigModal({ const [attrType, setAttrType] = useState('BUSINESS_OBJECT'); const [enabled, setEnabled] = useState(true); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [testTab, setTestTab] = useState('custom'); const [testPayload, setTestPayload] = useState(''); const [testResult, setTestResult] = useState<{ result?: string; error?: string } | null>(null); @@ -118,6 +119,7 @@ export function TapConfigModal({ if (tap && onDelete) { onDelete(tap); onClose(); + setShowDeleteConfirm(false); } } @@ -249,13 +251,24 @@ export function TapConfigModal({
{isEdit && onDelete && (
- +
)} - +
+ + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + title="Delete Tap" + message={`Delete tap "${name}"? This will remove the tap from the configuration.`} + confirmText={name} + confirmLabel="Delete" + variant="danger" + /> ); } diff --git a/ui/src/pages/Admin/AppConfigDetailPage.tsx b/ui/src/pages/Admin/AppConfigDetailPage.tsx index 9841b166..0c4d69af 100644 --- a/ui/src/pages/Admin/AppConfigDetailPage.tsx +++ b/ui/src/pages/Admin/AppConfigDetailPage.tsx @@ -309,10 +309,8 @@ export default function AppConfigDetailPage() { {editing ? (
- - + +
) : ( diff --git a/ui/src/pages/Admin/DatabaseAdminPage.tsx b/ui/src/pages/Admin/DatabaseAdminPage.tsx index 2448b9fc..cd437195 100644 --- a/ui/src/pages/Admin/DatabaseAdminPage.tsx +++ b/ui/src/pages/Admin/DatabaseAdminPage.tsx @@ -1,16 +1,19 @@ -import { StatCard, Card, DataTable, Badge, Button, ProgressBar, Spinner } from '@cameleer/design-system'; +import { useState } from 'react'; +import { StatCard, Card, DataTable, Badge, Button, ProgressBar, Spinner, AlertDialog, useToast } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useDatabaseStatus, useConnectionPool, useDatabaseTables, useActiveQueries, useKillQuery } from '../../api/queries/admin/database'; import styles from './DatabaseAdminPage.module.css'; import tableStyles from '../../styles/table-section.module.css'; export default function DatabaseAdminPage() { + const { toast } = useToast(); const { data: status, isError: statusError } = useDatabaseStatus(); const unreachable = statusError || (status && !status.connected); const { data: pool } = useConnectionPool(); const { data: tables } = useDatabaseTables(); const { data: queries } = useActiveQueries(); const killQuery = useKillQuery(); + const [killTarget, setKillTarget] = useState(null); const poolPct = pool ? (pool.activeConnections / pool.maximumPoolSize) * 100 : 0; @@ -28,7 +31,7 @@ export default function DatabaseAdminPage() { { key: 'query', header: 'Query', render: (v) => {String(v).slice(0, 80)} }, { key: 'pid', header: '', width: '80px', - render: (v) => , + render: (v) => , }, ]; @@ -68,6 +71,29 @@ export default function DatabaseAdminPage() { ({ ...q, id: String(q.pid) }))} /> + + setKillTarget(null)} + onConfirm={() => { + if (killTarget !== null) { + killQuery.mutate(killTarget, { + onSuccess: () => { + toast({ title: 'Query killed', description: `PID ${killTarget}`, variant: 'success' }); + setKillTarget(null); + }, + onError: () => { + toast({ title: 'Failed to kill query', variant: 'error', duration: 86_400_000 }); + setKillTarget(null); + }, + }); + } + }} + title="Kill query?" + description={`Terminate the query running on PID ${killTarget}? This will cancel the operation.`} + confirmLabel="Kill" + variant="warning" + /> ); } diff --git a/ui/src/pages/Admin/OidcConfigPage.tsx b/ui/src/pages/Admin/OidcConfigPage.tsx index e8eb357c..cdfad6d1 100644 --- a/ui/src/pages/Admin/OidcConfigPage.tsx +++ b/ui/src/pages/Admin/OidcConfigPage.tsx @@ -261,6 +261,7 @@ export default function OidcConfigPage() { onConfirm={handleDelete} message="Delete OIDC configuration? All users signed in via OIDC will lose access." confirmText="delete oidc" + loading={saving} /> diff --git a/ui/src/pages/Admin/UsersTab.tsx b/ui/src/pages/Admin/UsersTab.tsx index 50b66890..5d4e5f2c 100644 --- a/ui/src/pages/Admin/UsersTab.tsx +++ b/ui/src/pages/Admin/UsersTab.tsx @@ -49,6 +49,7 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig const [creating, setCreating] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [removeGroupTarget, setRemoveGroupTarget] = useState(null); + const [removeRoleTarget, setRemoveRoleTarget] = useState<{ id: string; name: string } | null>(null); // Auto-select highlighted item from cmd-k navigation useEffect(() => { @@ -515,25 +516,7 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig key={r.id} label={r.name} color="warning" - onRemove={() => { - removeRole.mutate( - { userId: selected.userId, roleId: r.id }, - { - onSuccess: () => - toast({ - title: 'Role removed', - description: r.name, - variant: 'success', - }), - onError: () => - toast({ - title: 'Failed to remove role', - variant: 'error', - duration: 86_400_000, - }), - }, - ); - }} + onRemove={() => setRemoveRoleTarget(r)} /> ))} {inheritedRoles.map((r) => ( @@ -621,6 +604,31 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig confirmLabel="Remove" variant="warning" /> + setRemoveRoleTarget(null)} + onConfirm={() => { + if (removeRoleTarget && selected) { + removeRole.mutate( + { userId: selected.userId, roleId: removeRoleTarget.id }, + { + onSuccess: () => { + toast({ title: 'Role removed', description: removeRoleTarget.name, variant: 'success' }); + setRemoveRoleTarget(null); + }, + onError: () => { + toast({ title: 'Failed to remove role', variant: 'error', duration: 86_400_000 }); + setRemoveRoleTarget(null); + }, + }, + ); + } + }} + title="Remove role" + description={`Remove the "${removeRoleTarget?.name}" role from this user?`} + confirmLabel="Remove" + variant="warning" + /> ); } diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index 9140af35..e03df4be 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router'; import { + AlertDialog, Badge, Button, ConfirmDialog, @@ -506,6 +507,7 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s const fileInputRef = useRef(null); const [subTab, setSubTab] = useState<'overview' | 'config'>('config'); const [deleteConfirm, setDeleteConfirm] = useState(false); + const [stopTarget, setStopTarget] = useState<{ id: string; name: string } | null>(null); const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); const sortedVersions = useMemo(() => [...versions].sort((a, b) => b.version - a.version), [versions]); @@ -531,11 +533,17 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s } catch { toast({ title: 'Deploy failed', variant: 'error', duration: 86_400_000 }); } } - async function handleStop(deploymentId: string) { + function handleStop(deploymentId: string) { + setStopTarget({ id: deploymentId, name: app?.displayName ?? appSlug }); + } + + async function confirmStop() { + if (!stopTarget) return; try { - await stopDeployment.mutateAsync({ appId: appSlug, deploymentId }); + await stopDeployment.mutateAsync({ appId: appSlug, deploymentId: stopTarget.id }); toast({ title: 'Deployment stopped', variant: 'warning' }); } catch { toast({ title: 'Stop failed', variant: 'error', duration: 86_400_000 }); } + setStopTarget(null); } async function handleDelete() { @@ -602,6 +610,15 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s confirmText={app.slug} loading={deleteApp.isPending} /> + setStopTarget(null)} + onConfirm={confirmStop} + title="Stop deployment?" + description={`Stop deployment for "${stopTarget?.name}"? This will take the service offline.`} + confirmLabel="Stop" + variant="warning" + /> ); } diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx index 042adb04..1ae332c6 100644 --- a/ui/src/pages/Routes/RouteDetail.tsx +++ b/ui/src/pages/Routes/RouteDetail.tsx @@ -982,7 +982,7 @@ export default function RouteDetail() {
- +
@@ -998,6 +998,7 @@ export default function RouteDetail() { confirmText={deletingTap?.attributeName ?? ''} confirmLabel="Delete" variant="danger" + loading={updateConfig.isPending} /> );