Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
79 lines
3.3 KiB
TypeScript
79 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>
|
|
);
|
|
}
|