Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
245 lines
6.9 KiB
TypeScript
245 lines
6.9 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
|
import * as stylex from "@stylexjs/stylex";
|
|
import InlineError from "@/lib/InlineError";
|
|
import { formatDuration, formatTime } from "@/lib/format";
|
|
import { diagnosticPurgeMutation, diagnosticQuery } from "@/lib/queries";
|
|
import ConfirmDialog from "@/ui/ConfirmDialog";
|
|
import { styles as shared } from "@/ui/styles";
|
|
import { colors } from "@/ui/tokens.stylex";
|
|
import SeverityBadge from "./SeverityBadge";
|
|
import { componentLabel, copyFor } from "./eventCopy";
|
|
|
|
const styles = stylex.create({
|
|
back: {
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
color: colors.primaryOnSurface,
|
|
textDecorationLine: "none",
|
|
},
|
|
headingRow: {
|
|
marginTop: "0.5rem",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
flexWrap: "wrap",
|
|
gap: "0.5rem",
|
|
},
|
|
heading: {
|
|
fontSize: "1.5rem",
|
|
lineHeight: "2rem",
|
|
fontWeight: 600,
|
|
},
|
|
purgeAction: {
|
|
marginInlineStart: "auto",
|
|
},
|
|
subject: {
|
|
marginTop: "0.25rem",
|
|
color: colors.textSecondary,
|
|
wordBreak: "break-all",
|
|
},
|
|
panel: {
|
|
marginTop: "1rem",
|
|
maxWidth: "48rem",
|
|
borderRadius: "0.25rem",
|
|
borderWidth: 1,
|
|
borderStyle: "solid",
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.surfaceRaised,
|
|
padding: "1rem",
|
|
},
|
|
facts: {
|
|
display: "grid",
|
|
gap: "0.5rem 1rem",
|
|
gridTemplateColumns: {
|
|
default: "auto",
|
|
"@media (min-width: 640px)": "max-content 1fr",
|
|
},
|
|
margin: 0,
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
term: {
|
|
color: colors.textMuted,
|
|
},
|
|
value: {
|
|
margin: 0,
|
|
},
|
|
sectionHeading: {
|
|
marginTop: "1.5rem",
|
|
fontSize: "1.125rem",
|
|
lineHeight: "1.75rem",
|
|
fontWeight: 600,
|
|
},
|
|
prose: {
|
|
marginTop: "0.5rem",
|
|
maxWidth: "48rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.5rem",
|
|
},
|
|
detail: {
|
|
marginTop: "0.5rem",
|
|
maxWidth: "48rem",
|
|
overflowX: "auto",
|
|
borderRadius: "0.25rem",
|
|
borderWidth: 1,
|
|
borderStyle: "solid",
|
|
borderColor: colors.border,
|
|
padding: "0.75rem",
|
|
fontSize: "0.8125rem",
|
|
lineHeight: "1.25rem",
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-all",
|
|
},
|
|
links: {
|
|
marginTop: "1rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
link: {
|
|
color: colors.primaryOnSurface,
|
|
},
|
|
loading: {
|
|
marginTop: "1rem",
|
|
color: colors.textMuted,
|
|
},
|
|
});
|
|
|
|
export default function DiagnosticDetailPage() {
|
|
const { id } = useParams({ from: "/shell/diagnostics/$id" });
|
|
const eventId = Number(id);
|
|
const { data, error, isPending, refetch } = useQuery(diagnosticQuery(eventId));
|
|
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const purge = useMutation(diagnosticPurgeMutation(queryClient));
|
|
const [confirming, setConfirming] = useState(false);
|
|
|
|
function confirmPurge() {
|
|
setConfirming(false);
|
|
// The row this page is about is gone, so staying here would show the
|
|
// 404 the purge itself caused.
|
|
purge.mutate(eventId, { onSuccess: () => void navigate({ to: "/diagnostics" }) });
|
|
}
|
|
|
|
if (isPending) {
|
|
return (
|
|
<p {...stylex.props(styles.loading, shared.pulse)} role="status">
|
|
Loading event…
|
|
</p>
|
|
);
|
|
}
|
|
if (data === undefined) {
|
|
return (
|
|
<section>
|
|
<Link to="/diagnostics" {...stylex.props(styles.back, shared.focusRing)}>
|
|
← All diagnostics
|
|
</Link>
|
|
<InlineError error={error} onRetry={() => void refetch()} />
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const copy = copyFor(data.code);
|
|
const resolvedAt = data.resolved_at;
|
|
const span = (resolvedAt ?? Math.floor(Date.now() / 1000)) - data.first_seen;
|
|
|
|
return (
|
|
<section>
|
|
<Link to="/diagnostics" {...stylex.props(styles.back, shared.focusRing)}>
|
|
← All diagnostics
|
|
</Link>
|
|
<div {...stylex.props(styles.headingRow)}>
|
|
<h1 {...stylex.props(styles.heading)}>{copy.title}</h1>
|
|
<SeverityBadge severity={data.severity} />
|
|
{/* Only history can be purged: an open episode is the current state of the box. */}
|
|
{resolvedAt !== null && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setConfirming(true)}
|
|
disabled={purge.isPending}
|
|
{...stylex.props(styles.purgeAction, shared.dangerLinkButton, shared.focusRing)}
|
|
>
|
|
Purge
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p {...stylex.props(styles.subject)}>{data.subject}</p>
|
|
<InlineError error={purge.error} />
|
|
|
|
<div {...stylex.props(styles.panel)}>
|
|
<dl {...stylex.props(styles.facts)}>
|
|
<dt {...stylex.props(styles.term)}>State</dt>
|
|
<dd {...stylex.props(styles.value)}>
|
|
{resolvedAt === null
|
|
? `Active for ${formatDuration(span)}`
|
|
: `Resolved after ${formatDuration(span)}`}
|
|
</dd>
|
|
<dt {...stylex.props(styles.term)}>First seen</dt>
|
|
<dd {...stylex.props(styles.value)}>{formatTime(data.first_seen)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Last seen</dt>
|
|
<dd {...stylex.props(styles.value)}>{formatTime(data.last_seen)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Occurrences</dt>
|
|
<dd {...stylex.props(styles.value, shared.tabularNums)}>{data.occurrences}</dd>
|
|
<dt {...stylex.props(styles.term)}>Resolved</dt>
|
|
<dd {...stylex.props(styles.value)}>
|
|
{data.resolved_at === null ? "Not yet — still failing" : formatTime(data.resolved_at)}
|
|
</dd>
|
|
<dt {...stylex.props(styles.term)}>Component</dt>
|
|
<dd {...stylex.props(styles.value)}>{componentLabel(data.component)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Code</dt>
|
|
<dd {...stylex.props(styles.value, shared.mono)}>{data.code}</dd>
|
|
</dl>
|
|
</div>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>Impact</h2>
|
|
<p {...stylex.props(styles.prose)}>{copy.impact}</p>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>What to do</h2>
|
|
<p {...stylex.props(styles.prose)}>{copy.remediation}</p>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>Last error</h2>
|
|
{data.detail === "" ? (
|
|
<p {...stylex.props(styles.prose)}>The server recorded no error text for this event.</p>
|
|
) : (
|
|
<pre {...stylex.props(styles.detail, shared.mono)}>{data.detail}</pre>
|
|
)}
|
|
|
|
{copy.link !== undefined && (
|
|
<p {...stylex.props(styles.links)}>
|
|
{copy.link.to === "/configuration/protection" ? (
|
|
<Link
|
|
to="/configuration/protection"
|
|
search={{ tab: "sources" }}
|
|
{...stylex.props(styles.link, shared.focusRing)}
|
|
>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
) : copy.link.to === "/configuration/resolution" ? (
|
|
<Link
|
|
to="/configuration/resolution"
|
|
search={{ tab: "upstreams" }}
|
|
{...stylex.props(styles.link, shared.focusRing)}
|
|
>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
) : (
|
|
<Link to={copy.link.to} {...stylex.props(styles.link, shared.focusRing)}>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
)}
|
|
</p>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
isOpen={confirming}
|
|
title="Purge event"
|
|
message="Purge this resolved event? Its history is gone for good."
|
|
confirmLabel="Purge"
|
|
onConfirm={confirmPurge}
|
|
onCancel={() => setConfirming(false)}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|