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
|
|
@ -23,6 +23,7 @@ import { $, esc, fmtAgeSecs, renderServerWarnings } from './common.js';
|
|||
import { el } from '@hive/shared/dom.js';
|
||||
import '@hive/shared/hive-tab-strip.js';
|
||||
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
|
||||
import { readApiError, problemMessage } from '@hive/shared/api-error.js';
|
||||
|
||||
let agents = [];
|
||||
// agent name → container running (bool), from /api/state. Cross-referenced by
|
||||
|
|
@ -65,28 +66,13 @@ function renderAgentPicker() {
|
|||
for (const a of agents) sel.append(el('option', { value: a }, a));
|
||||
}
|
||||
|
||||
// ─── shape-agnostic error-body parsing (shared by both tabs' submit
|
||||
// handlers) ───────────────────────────────────────────────────────────────
|
||||
// The BE error-body shape is in transition: today hive-c0re's
|
||||
// error_response sends a bare plain-text body; the RFC 9457 rework moves it
|
||||
// to application/problem+json ({ type, title, detail, … }). Read shape-
|
||||
// agnostically: pull the body once as text, and if it parses as JSON
|
||||
// surface `detail` (problem+json) → `error`/`title` fallback, else use the
|
||||
// raw text. A bare HTTP code is the last resort.
|
||||
async function readErrorBody(resp) {
|
||||
try {
|
||||
const raw = (await resp.text()).trim();
|
||||
if (raw && (raw[0] === '{' || raw[0] === '[')) {
|
||||
try {
|
||||
const body = JSON.parse(raw);
|
||||
return body.detail || body.error || body.title || raw;
|
||||
} catch { /* not JSON after all — keep the raw text */ }
|
||||
}
|
||||
return raw;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Shape-agnostic error-body parsing (shared by both tabs' submit handlers)
|
||||
// lives in `@hive/shared/api-error.js` now — `readApiError` +
|
||||
// `problemMessage` (this page only needs the one-line message, not the
|
||||
// full `ApiErrorPanel`; its result lines are single-line `aria-live`
|
||||
// regions, not a swap-in-a-panel context). Was a local function here
|
||||
// originally; promoted so swarm-ui shares the same
|
||||
// shape-agnostic reader instead of each side maintaining its own copy.
|
||||
|
||||
// ─── MATRIX tab ────────────────────────────────────────────────────────────
|
||||
// Live status dot — the daemon heartbeats every ~30s (advances as_of_unix),
|
||||
|
|
@ -253,7 +239,7 @@ async function submitLogin(e) {
|
|||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = await readErrorBody(resp);
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('login failed (HTTP ' + resp.status + ')'));
|
||||
clearSecrets(formEl);
|
||||
|
|
@ -337,7 +323,7 @@ async function submitGithub(e) {
|
|||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = await readErrorBody(resp);
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('store failed (HTTP ' + resp.status + ')'));
|
||||
clearSecrets(formEl);
|
||||
|
|
@ -419,7 +405,7 @@ async function onForgeRemoveClick(agent, forge, btn) {
|
|||
loadForgeAccounts(agent);
|
||||
return;
|
||||
}
|
||||
const msg = await readErrorBody(resp);
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast('✗ ' + (msg || ('remove failed (HTTP ' + resp.status + ')')), { type: 'error' });
|
||||
|
|
@ -474,7 +460,7 @@ async function submitForgeAccount(e) {
|
|||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = await readErrorBody(resp);
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('store failed (HTTP ' + resp.status + ')'));
|
||||
clearSecrets(formEl);
|
||||
|
|
|
|||
Loading…
Reference in a new issue