fix(ui): use describeApiError across remaining error-surface sites
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m3s
CI / docker (push) Successful in 1m15s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Failing after 29s

Extends the previous describeApiError rollout to the rest of the UI.
Two symptom classes covered:

 - Bare e.message / err.message in toast descriptions would render
   "undefined" on Spring error bodies (plain objects without a proper
   Error prototype). Affected: OidcConfigPage (save/test/delete),
   ClaimMappingRulesModal (save + test), AgentHealth (dismiss),
   RouteControlBar (route action + replay).

 - Inline {String(error)} on load-failure banners would render
   "[object Object]". Affected: InboxPage, RulesListPage, SilencesPage,
   OutboundConnectionsPage.

Not touched: auth-store, AppsTab, UsersTab — they already guard with
`e instanceof Error` and fall back to a static string; replacing the
fallback with describeApiError would be a behavioral change best
evaluated separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 20:37:16 +02:00
parent b7d201d743
commit 74bfabf618
8 changed files with 20 additions and 16 deletions

View File

@@ -10,6 +10,7 @@ import {
} from '../../api/queries/admin/claim-mappings'; } from '../../api/queries/admin/claim-mappings';
import type { ClaimMappingRule, TestResponse } from '../../api/queries/admin/claim-mappings'; import type { ClaimMappingRule, TestResponse } from '../../api/queries/admin/claim-mappings';
import { useRoles, useGroups } from '../../api/queries/admin/rbac'; import { useRoles, useGroups } from '../../api/queries/admin/rbac';
import { describeApiError } from '../../api/errors';
import styles from './ClaimMappingRulesModal.module.css'; import styles from './ClaimMappingRulesModal.module.css';
const MATCH_OPTIONS = [ const MATCH_OPTIONS = [
@@ -231,8 +232,8 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) {
toast({ title: 'Rules saved', variant: 'success' }); toast({ title: 'Rules saved', variant: 'success' });
handleClose(); handleClose();
} catch (e: any) { } catch (e) {
toast({ title: 'Failed to save rules', description: e.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Failed to save rules', description: describeApiError(e), variant: 'error', duration: 86_400_000 });
} finally { } finally {
setApplying(false); setApplying(false);
} }
@@ -256,7 +257,7 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) {
})); }));
testRules.mutate({ rules, claims }, { testRules.mutate({ rules, claims }, {
onSuccess: (result) => setTestResult(result), onSuccess: (result) => setTestResult(result),
onError: (e) => setTestError(e.message), onError: (e) => setTestError(describeApiError(e)),
}); });
} }

View File

@@ -8,6 +8,7 @@ import { PageLoader } from '../../components/PageLoader';
import { adminFetch } from '../../api/queries/admin/admin-api'; import { adminFetch } from '../../api/queries/admin/admin-api';
import ClaimMappingRulesModal from './ClaimMappingRulesModal'; import ClaimMappingRulesModal from './ClaimMappingRulesModal';
import { useClaimMappingRules } from '../../api/queries/admin/claim-mappings'; import { useClaimMappingRules } from '../../api/queries/admin/claim-mappings';
import { describeApiError } from '../../api/errors';
import styles from './OidcConfigPage.module.css'; import styles from './OidcConfigPage.module.css';
import sectionStyles from '../../styles/section-card.module.css'; import sectionStyles from '../../styles/section-card.module.css';
@@ -114,8 +115,8 @@ export default function OidcConfigPage() {
setFormDraft(null); setFormDraft(null);
setEditing(false); setEditing(false);
toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' }); toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' });
} catch (e: any) { } catch (e) {
toast({ title: 'Failed to save OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Failed to save OIDC configuration', description: describeApiError(e), variant: 'error', duration: 86_400_000 });
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -127,8 +128,8 @@ export default function OidcConfigPage() {
try { try {
const result = await adminFetch<{ status: string; authorizationEndpoint?: string }>('/oidc/test', { method: 'POST' }); const result = await adminFetch<{ status: string; authorizationEndpoint?: string }>('/oidc/test', { method: 'POST' });
toast({ title: 'Connection test', description: `OIDC provider responded: ${result.status}`, variant: 'success' }); toast({ title: 'Connection test', description: `OIDC provider responded: ${result.status}`, variant: 'success' });
} catch (e: any) { } catch (e) {
toast({ title: 'Connection test failed', description: e.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Connection test failed', description: describeApiError(e), variant: 'error', duration: 86_400_000 });
} finally { } finally {
setTesting(false); setTesting(false);
} }
@@ -142,8 +143,8 @@ export default function OidcConfigPage() {
setFormDraft(null); setFormDraft(null);
setEditing(false); setEditing(false);
toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' }); toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' });
} catch (e: any) { } catch (e) {
toast({ title: 'Failed to delete OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Failed to delete OIDC configuration', description: describeApiError(e), variant: 'error', duration: 86_400_000 });
} }
} }

View File

@@ -16,7 +16,7 @@ export default function OutboundConnectionsPage() {
const { toast } = useToast(); const { toast } = useToast();
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
if (error) return <div>Failed to load outbound connections: {String(error)}</div>; if (error) return <div>Failed to load outbound connections: {describeApiError(error)}</div>;
const rows = data ?? []; const rows = data ?? [];

View File

@@ -23,6 +23,7 @@ import type { ConfigUpdateResponse } from '../../api/queries/commands';
import type { AgentInstance } from '../../api/types'; import type { AgentInstance } from '../../api/types';
import { timeAgo } from '../../utils/format-utils'; import { timeAgo } from '../../utils/format-utils';
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils'; import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
import { describeApiError } from '../../api/errors';
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -506,7 +507,7 @@ export default function AgentHealth() {
navigate('/runtime'); navigate('/runtime');
}, },
onError: (err) => { onError: (err) => {
toast({ title: 'Dismiss failed', description: err.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Dismiss failed', description: describeApiError(err), variant: 'error', duration: 86_400_000 });
}, },
}); });
}} }}

View File

@@ -344,7 +344,7 @@ export default function InboxPage() {
// ── render ───────────────────────────────────────────────────────────────── // ── render ─────────────────────────────────────────────────────────────────
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
if (error) return <div className={css.page}>Failed to load alerts: {String(error)}</div>; if (error) return <div className={css.page}>Failed to load alerts: {describeApiError(error)}</div>;
const selectedIds = Array.from(selected); const selectedIds = Array.from(selected);

View File

@@ -32,7 +32,7 @@ export default function RulesListPage() {
const [pendingDelete, setPendingDelete] = useState<AlertRuleResponse | null>(null); const [pendingDelete, setPendingDelete] = useState<AlertRuleResponse | null>(null);
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
if (error) return <div className={css.page}>Failed to load rules: {String(error)}</div>; if (error) return <div className={css.page}>Failed to load rules: {describeApiError(error)}</div>;
const rows = rules ?? []; const rows = rules ?? [];
const otherEnvs = (envs ?? []).filter((e) => e.slug !== env); const otherEnvs = (envs ?? []).filter((e) => e.slug !== env);

View File

@@ -37,7 +37,7 @@ export default function SilencesPage() {
}, [searchParams]); }, [searchParams]);
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
if (error) return <div className={css.page}>Failed to load silences: {String(error)}</div>; if (error) return <div className={css.page}>Failed to load silences: {describeApiError(error)}</div>;
const rows = data ?? []; const rows = data ?? [];

View File

@@ -4,6 +4,7 @@ import { useToast, ConfirmDialog } from '@cameleer/design-system';
import { useSendRouteCommand, useReplayExchange } from '../../api/queries/commands'; import { useSendRouteCommand, useReplayExchange } from '../../api/queries/commands';
import type { CommandGroupResponse } from '../../api/queries/commands'; import type { CommandGroupResponse } from '../../api/queries/commands';
import { useEnvironmentStore } from '../../api/environment-store'; import { useEnvironmentStore } from '../../api/environment-store';
import { describeApiError } from '../../api/errors';
import styles from './RouteControlBar.module.css'; import styles from './RouteControlBar.module.css';
interface RouteControlBarProps { interface RouteControlBarProps {
@@ -68,7 +69,7 @@ export function RouteControlBar({ application, routeId, routeState, hasRouteCont
setSendingAction(null); setSendingAction(null);
}, },
onError: (err) => { onError: (err) => {
toast({ title: `Route ${action} failed`, description: err.message, variant: 'error', duration: 86_400_000 }); toast({ title: `Route ${action} failed`, description: describeApiError(err), variant: 'error', duration: 86_400_000 });
setSendingAction(null); setSendingAction(null);
}, },
}, },
@@ -92,7 +93,7 @@ export function RouteControlBar({ application, routeId, routeState, hasRouteCont
setSendingAction(null); setSendingAction(null);
}, },
onError: (err) => { onError: (err) => {
toast({ title: 'Replay failed', description: err.message, variant: 'error', duration: 86_400_000 }); toast({ title: 'Replay failed', description: describeApiError(err), variant: 'error', duration: 86_400_000 });
setSendingAction(null); setSendingAction(null);
}, },
}, },