From 74bfabf61894c018efc57a3704ea958c280367de Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:37:16 +0200 Subject: [PATCH] fix(ui): use describeApiError across remaining error-surface sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/src/pages/Admin/ClaimMappingRulesModal.tsx | 7 ++++--- ui/src/pages/Admin/OidcConfigPage.tsx | 13 +++++++------ ui/src/pages/Admin/OutboundConnectionsPage.tsx | 2 +- ui/src/pages/AgentHealth/AgentHealth.tsx | 3 ++- ui/src/pages/Alerts/InboxPage.tsx | 2 +- ui/src/pages/Alerts/RulesListPage.tsx | 2 +- ui/src/pages/Alerts/SilencesPage.tsx | 2 +- ui/src/pages/Exchanges/RouteControlBar.tsx | 5 +++-- 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx index dde57ec8..37dea3a7 100644 --- a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx +++ b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx @@ -10,6 +10,7 @@ import { } 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 { describeApiError } from '../../api/errors'; import styles from './ClaimMappingRulesModal.module.css'; const MATCH_OPTIONS = [ @@ -231,8 +232,8 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) { toast({ title: 'Rules saved', variant: 'success' }); handleClose(); - } catch (e: any) { - toast({ title: 'Failed to save rules', description: e.message, variant: 'error', duration: 86_400_000 }); + } catch (e) { + toast({ title: 'Failed to save rules', description: describeApiError(e), variant: 'error', duration: 86_400_000 }); } finally { setApplying(false); } @@ -256,7 +257,7 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) { })); testRules.mutate({ rules, claims }, { onSuccess: (result) => setTestResult(result), - onError: (e) => setTestError(e.message), + onError: (e) => setTestError(describeApiError(e)), }); } diff --git a/ui/src/pages/Admin/OidcConfigPage.tsx b/ui/src/pages/Admin/OidcConfigPage.tsx index 72116eff..69cd971a 100644 --- a/ui/src/pages/Admin/OidcConfigPage.tsx +++ b/ui/src/pages/Admin/OidcConfigPage.tsx @@ -8,6 +8,7 @@ import { PageLoader } from '../../components/PageLoader'; import { adminFetch } from '../../api/queries/admin/admin-api'; import ClaimMappingRulesModal from './ClaimMappingRulesModal'; import { useClaimMappingRules } from '../../api/queries/admin/claim-mappings'; +import { describeApiError } from '../../api/errors'; import styles from './OidcConfigPage.module.css'; import sectionStyles from '../../styles/section-card.module.css'; @@ -114,8 +115,8 @@ export default function OidcConfigPage() { setFormDraft(null); setEditing(false); toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' }); - } catch (e: any) { - toast({ title: 'Failed to save OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); + } catch (e) { + toast({ title: 'Failed to save OIDC configuration', description: describeApiError(e), variant: 'error', duration: 86_400_000 }); } finally { setSaving(false); } @@ -127,8 +128,8 @@ export default function OidcConfigPage() { try { const result = await adminFetch<{ status: string; authorizationEndpoint?: string }>('/oidc/test', { method: 'POST' }); toast({ title: 'Connection test', description: `OIDC provider responded: ${result.status}`, variant: 'success' }); - } catch (e: any) { - toast({ title: 'Connection test failed', description: e.message, variant: 'error', duration: 86_400_000 }); + } catch (e) { + toast({ title: 'Connection test failed', description: describeApiError(e), variant: 'error', duration: 86_400_000 }); } finally { setTesting(false); } @@ -142,8 +143,8 @@ export default function OidcConfigPage() { setFormDraft(null); setEditing(false); toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' }); - } catch (e: any) { - toast({ title: 'Failed to delete OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); + } catch (e) { + toast({ title: 'Failed to delete OIDC configuration', description: describeApiError(e), variant: 'error', duration: 86_400_000 }); } } diff --git a/ui/src/pages/Admin/OutboundConnectionsPage.tsx b/ui/src/pages/Admin/OutboundConnectionsPage.tsx index de4c3429..1d34729f 100644 --- a/ui/src/pages/Admin/OutboundConnectionsPage.tsx +++ b/ui/src/pages/Admin/OutboundConnectionsPage.tsx @@ -16,7 +16,7 @@ export default function OutboundConnectionsPage() { const { toast } = useToast(); if (isLoading) return ; - if (error) return
Failed to load outbound connections: {String(error)}
; + if (error) return
Failed to load outbound connections: {describeApiError(error)}
; const rows = data ?? []; diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index dc2813ec..eb8c3946 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -23,6 +23,7 @@ import type { ConfigUpdateResponse } from '../../api/queries/commands'; import type { AgentInstance } from '../../api/types'; import { timeAgo } from '../../utils/format-utils'; import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils'; +import { describeApiError } from '../../api/errors'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -506,7 +507,7 @@ export default function AgentHealth() { navigate('/runtime'); }, 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 }); }, }); }} diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 3c158532..e80eb7e1 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -344,7 +344,7 @@ export default function InboxPage() { // ── render ───────────────────────────────────────────────────────────────── if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; + if (error) return
Failed to load alerts: {describeApiError(error)}
; const selectedIds = Array.from(selected); diff --git a/ui/src/pages/Alerts/RulesListPage.tsx b/ui/src/pages/Alerts/RulesListPage.tsx index 7bfe127a..149ad096 100644 --- a/ui/src/pages/Alerts/RulesListPage.tsx +++ b/ui/src/pages/Alerts/RulesListPage.tsx @@ -32,7 +32,7 @@ export default function RulesListPage() { const [pendingDelete, setPendingDelete] = useState(null); if (isLoading) return ; - if (error) return
Failed to load rules: {String(error)}
; + if (error) return
Failed to load rules: {describeApiError(error)}
; const rows = rules ?? []; const otherEnvs = (envs ?? []).filter((e) => e.slug !== env); diff --git a/ui/src/pages/Alerts/SilencesPage.tsx b/ui/src/pages/Alerts/SilencesPage.tsx index 75a3819a..dd7572fc 100644 --- a/ui/src/pages/Alerts/SilencesPage.tsx +++ b/ui/src/pages/Alerts/SilencesPage.tsx @@ -37,7 +37,7 @@ export default function SilencesPage() { }, [searchParams]); if (isLoading) return ; - if (error) return
Failed to load silences: {String(error)}
; + if (error) return
Failed to load silences: {describeApiError(error)}
; const rows = data ?? []; diff --git a/ui/src/pages/Exchanges/RouteControlBar.tsx b/ui/src/pages/Exchanges/RouteControlBar.tsx index 094f2c14..6c1524dd 100644 --- a/ui/src/pages/Exchanges/RouteControlBar.tsx +++ b/ui/src/pages/Exchanges/RouteControlBar.tsx @@ -4,6 +4,7 @@ import { useToast, ConfirmDialog } from '@cameleer/design-system'; import { useSendRouteCommand, useReplayExchange } from '../../api/queries/commands'; import type { CommandGroupResponse } from '../../api/queries/commands'; import { useEnvironmentStore } from '../../api/environment-store'; +import { describeApiError } from '../../api/errors'; import styles from './RouteControlBar.module.css'; interface RouteControlBarProps { @@ -68,7 +69,7 @@ export function RouteControlBar({ application, routeId, routeState, hasRouteCont setSendingAction(null); }, 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); }, }, @@ -92,7 +93,7 @@ export function RouteControlBar({ application, routeId, routeState, hasRouteCont setSendingAction(null); }, 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); }, },