frontend: shared ApiErrorPanel component, promote readApiError from credentials.js
Closes #3410. `ApiErrorPanel` renders a ProblemDetails (RFC 9457) error nicely, with a copy button so the full text can be pasted straight into a bug report. No truncation of `detail` — on the #3363 incident that motivated this issue, that string was the entire diagnosis. `readApiError`/`problemMessage`/`ProblemDetails` are promoted out of credentials.js's original `readErrorBody` into `@hive/shared/api-error.js` (comment rewritten: the RFC 9457 rework has already landed everywhere except swarm-controller's status route, #3412 in flight, so the raw-text fallback is a compat shim for that one gap, not a general transition). credentials.js's 4 call sites switch to the shared reader (kept as one-line messages there, its result slots are single-line aria-live regions, not a panel context). Wired ApiErrorPanel into OverviewPage.tsx (the issue's own worked example) and CreateAgentPage.tsx (second real call site). Not built on <hive-warn> despite matching its visual language — that custom element's CSS-as-text import only works under a build with loader: 'text' for .css (dashboard's), and silently renders unstyled under swarm-ui's default css loader (filed separately as #3415). ApiErrorPanel is a self-contained light-DOM component instead, per mara's own suggestion to keep it independent of the old UI's shapes.
This commit is contained in:
parent
bfe921c254
commit
f60aab4717
8 changed files with 230 additions and 45 deletions
61
frontend/packages/shared/src/api-error.ts
Normal file
61
frontend/packages/shared/src/api-error.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// api-error.ts — reads a failed fetch `Response` into a `ProblemDetails`
|
||||
// (RFC 9457, `application/problem+json`) object, shape-agnostically.
|
||||
//
|
||||
// Promoted from `dashboard/src/credentials.js`'s original `readErrorBody`,
|
||||
// which returned a flat message string. This returns the structured object
|
||||
// instead so a caller — chiefly `ApiErrorPanel` (./api-error-panel/) — can
|
||||
// render title/status/detail separately and build a useful copy-button
|
||||
// payload, rather than re-parsing a pre-squashed string.
|
||||
//
|
||||
// RFC 9457 is this hive's committed error-body contract for first-party
|
||||
// APIs (mara: "any api of our own responding with error that is not rfc
|
||||
// shaped is a bug to be filed against backend") — so the non-JSON
|
||||
// fallback below is a *compatibility* path, not a standing requirement.
|
||||
// As of this writing it exists for exactly one known gap: swarm-
|
||||
// controller's status route, being converted in a separate in-flight fix.
|
||||
// Once every first-party route emits problem+json, the raw-text path only
|
||||
// ever fires against a genuine bug (worth filing, not silently absorbing).
|
||||
|
||||
export interface ProblemDetails {
|
||||
type?: string;
|
||||
title?: string;
|
||||
status?: number;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export async function readApiError(resp: Response): Promise<ProblemDetails> {
|
||||
const status = resp.status;
|
||||
let raw: string;
|
||||
try {
|
||||
raw = (await resp.text()).trim();
|
||||
} catch {
|
||||
return { status };
|
||||
}
|
||||
if (raw && (raw[0] === '{' || raw[0] === '[')) {
|
||||
try {
|
||||
const body = JSON.parse(raw) as Record<string, unknown>;
|
||||
return {
|
||||
type: typeof body.type === 'string' ? body.type : undefined,
|
||||
title: typeof body.title === 'string' ? body.title : undefined,
|
||||
status: typeof body.status === 'number' ? body.status : status,
|
||||
detail:
|
||||
typeof body.detail === 'string'
|
||||
? body.detail
|
||||
: typeof body.error === 'string'
|
||||
? body.error
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
// not JSON after all — fall through to the raw-text path below
|
||||
}
|
||||
}
|
||||
return { status, detail: raw || undefined };
|
||||
}
|
||||
|
||||
// One-line message for a context that only has room for a single string
|
||||
// (a form's inline result line, a toast) rather than the full panel —
|
||||
// same fallback order `readErrorBody` used: detail (or its `error` alias,
|
||||
// already folded in above) → title → a bare status line.
|
||||
export function problemMessage(p: ProblemDetails): string {
|
||||
return p.detail || p.title || (p.status !== undefined ? `http ${p.status}` : 'request failed');
|
||||
}
|
||||
Loading…
Reference in a new issue