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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 18:39:22 +02:00
parent 3d910af491
commit fb53dc6dfc
7 changed files with 95 additions and 31 deletions

View File

@@ -309,10 +309,8 @@ export default function AppConfigDetailPage() {
<Button variant="ghost" size="sm" onClick={() => navigate('/admin/appconfig')}><ArrowLeft size={14} /> Back</Button>
{editing ? (
<div className={styles.toolbarActions}>
<Button onClick={handleSave} disabled={updateConfig.isPending}>
{updateConfig.isPending ? 'Saving\u2026' : 'Save'}
</Button>
<Button variant="secondary" size="sm" onClick={cancelEditing}>Cancel</Button>
<Button variant="ghost" size="sm" onClick={cancelEditing}>Cancel</Button>
<Button variant="primary" size="sm" onClick={handleSave} loading={updateConfig.isPending}>Save</Button>
</div>
) : (
<Button variant="secondary" size="sm" onClick={startEditing}><Pencil size={14} /> Edit</Button>

View File

@@ -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<number | null>(null);
const poolPct = pool ? (pool.activeConnections / pool.maximumPoolSize) * 100 : 0;
@@ -28,7 +31,7 @@ export default function DatabaseAdminPage() {
{ key: 'query', header: 'Query', render: (v) => <span className={styles.querySnippet}>{String(v).slice(0, 80)}</span> },
{
key: 'pid', header: '', width: '80px',
render: (v) => <Button variant="danger" size="sm" onClick={() => killQuery.mutate(v as number)}>Kill</Button>,
render: (v) => <Button variant="danger" size="sm" onClick={() => setKillTarget(v as number)}>Kill</Button>,
},
];
@@ -68,6 +71,29 @@ export default function DatabaseAdminPage() {
</div>
<DataTable columns={queryColumns} data={(queries || []).map((q: any) => ({ ...q, id: String(q.pid) }))} />
</div>
<AlertDialog
open={killTarget !== null}
onClose={() => 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"
/>
</div>
);
}

View File

@@ -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}
/>
</section>
</div>

View File

@@ -49,6 +49,7 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig
const [creating, setCreating] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<UserDetail | null>(null);
const [removeGroupTarget, setRemoveGroupTarget] = useState<string | null>(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"
/>
<AlertDialog
open={removeRoleTarget !== null}
onClose={() => 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"
/>
</>
);
}