// ApiErrorPanel.tsx — , 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 // `` 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 (
{context ? `${context}: ` : ''} {heading}
{problem.detail ?

{problem.detail}

: null}
); }