hyperhive/frontend/packages/shared/src/api-error-panel/ApiErrorPanel.tsx
iris ca84079a21 frontend: add WarnBanner Preact component, compose ApiErrorPanel from it
mara, on review: "i want each component to import its own css file
itself[;] if you need a bunch of extra css externally, its not a proper
component ... you may need to migrate other components that you would
want to use first".

WarnBanner is the Preact-component successor to the shadow-DOM
<hive-warn> custom element (same three-tier info/warning/error visual
language, colours copied faithfully from hive-warn.css). ApiErrorPanel
now composes it instead of owning a copy of the border/colour/pulse
rules itself — its own CSS is back down to just the layout that's
actually specific to it (heading, copy button, detail text).

Re-verified with a fresh mock-server screenshot: same rendered output as
before, now via composition instead of a duplicated banner shape.
2026-08-17 23:11:11 +02:00

75 lines
3.3 KiB
TypeScript

// ApiErrorPanel.tsx — <ApiErrorPanel>, the shared "show the operator why
// an API call failed" component (mara, on the issue that asked for this:
// "the error display component is for showing ProblemDetails in a nicer
// way with copy button [...] wherever we want to show an error, this
// component should be used"). Renders a `ProblemDetails` (./api-error.ts)
// — heading + the full `detail` text, unclamped (it can be a raw NATS/
// JetStream error, and on the incident that prompted this component
// *that string was the entire diagnosis* — truncating it defeats the
// point) — plus a copy button for pasting straight into a bug report.
//
// Composed from `WarnBanner` (../warn-banner/WarnBanner.js) rather than
// owning the border/colour/pulse styling itself — mara, on review: "i
// want each component to import its own css file itself[;] if you need
// a bunch of extra css externally, its not a proper component". This
// file's own `.css` only lays out what's specific to it (heading, copy
// button, detail text); the banner "shape" belongs to `WarnBanner` and
// stays there so any future error/warning surface reuses it too, rather
// than every caller growing its own copy of the same rules.
//
// Same shared-component shape as `JobqRollup`/`JobqGraph`:
// `render(h(ApiErrorPanel, { problem }), container)` from vanilla JS, or
// `<ApiErrorPanel problem={...} />` from swarm-ui's JSX.
import { useState } from 'preact/hooks';
import { WarnBanner } from '../warn-banner/WarnBanner.js';
import type { ProblemDetails } from '../api-error.js';
import './api-error-panel.css';
export interface ApiErrorPanelProps {
problem: ProblemDetails;
// Optional short prefix naming what failed ("failed to load the hive
// roster") — the panel is otherwise just the server's own words, which
// don't always say what the caller was trying to do.
context?: string;
}
function formatForCopy(p: ProblemDetails): string {
const lines: string[] = [];
if (p.status !== undefined) lines.push(`status: ${p.status}`);
if (p.title) lines.push(`title: ${p.title}`);
if (p.type) lines.push(`type: ${p.type}`);
if (p.detail) lines.push(`detail: ${p.detail}`);
return lines.length ? lines.join('\n') : 'request failed';
}
export function ApiErrorPanel({ problem, context }: ApiErrorPanelProps) {
const [copied, setCopied] = useState(false);
const heading = problem.title || (problem.status !== undefined ? `http ${problem.status}` : 'request failed');
async function copy() {
try {
await navigator.clipboard.writeText(formatForCopy(problem));
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// clipboard permission denied / unavailable (e.g. non-secure
// context) — the text is still fully visible to select by hand,
// so this is a silent no-op rather than a second error to show.
}
}
return (
<WarnBanner level="error" class="api-error-panel">
<div class="api-error-heading">
<span class="api-error-title">
{context ? `${context}: ` : ''}
{heading}
</span>
<button type="button" class="api-error-copy" onClick={copy}>
{copied ? 'copied' : 'copy'}
</button>
</div>
{problem.detail ? <p class="api-error-detail">{problem.detail}</p> : null}
</WarnBanner>
);
}