UI recipesUI recipes
Risk heatmap
The open risks on the default matrix, one cell per likelihood and impact with the count in the band's color.
It loads its data on the server through the block's client; the risks block must be installed (npx better-supabase add risks). Its page on bettersupabase.com is /recipes/risk-heatmap.
Web, with shadcn components:
npx shadcn add alert badge button empty skeleton table @reui/frame// oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await
async function retry() {
"use server";
refresh();
}
const TONE_VARIANT = {
low: "success-light",
medium: "warning-light",
high: "destructive-light",
critical: "destructive",
} as const;
/**
* Render inside `<Suspense>`: the open risks on the default matrix, one cell
* per likelihood and impact, colored by the cell's band. The highest likelihood
* is the top row.
*/
export async function RiskHeatmap() {
const [state, t] = await Promise.all([
getRiskHeatmap(),
getExtracted("risks"),
]);
if (state.kind === "no-organization") {
return null;
}
if (state.kind === "error") {
return (
<Alert variant="destructive" data-recipe="risk-heatmap">
<TriangleAlertIcon />
<AlertTitle>{t("The heatmap didn't load")}</AlertTitle>
<AlertDescription>{t("Try again in a moment.")}</AlertDescription>
<AlertAction>
<form action={retry}>
<Button size="sm" variant="outline" type="submit">
{t("Retry")}
</Button>
</form>
</AlertAction>
</Alert>
);
}
const { matrixName, likelihoods, impacts, cells, total } = state.data;
const cellAt = new Map(
cells.map((cell) => [`${cell.likelihood}:${cell.impact}`, cell]),
);
return (
<Frame spacing="sm" data-recipe="risk-heatmap">
<FrameHeader className="gap-0.5">
<FrameTitle>{t("Heatmap")}</FrameTitle>
<FrameDescription>
{t(
"{total, plural, =0 {No open risks} one {# open risk} other {# open risks}} on the {matrix} matrix, by residual rating.",
{ total, matrix: matrixName },
)}
</FrameDescription>
</FrameHeader>
<FramePanel>
{total === 0 ? (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<GridIcon />
</EmptyMedia>
<EmptyTitle>{t("Nothing assessed yet")}</EmptyTitle>
<EmptyDescription>
{t(
"Assessed risks appear here in the cell of their residual likelihood and impact.",
)}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<a
className="text-sm underline underline-offset-4"
href="https://bettersupabase.com/docs/blocks/risks#assess"
>
{t("Read how assessment works")}
</a>
</EmptyContent>
</Empty>
) : (
<Table aria-label={t("Risks by likelihood and impact")}>
<TableHeader>
<TableRow>
<TableHead>{t("Likelihood / impact")}</TableHead>
{impacts.map((impact) => (
<TableHead key={impact.level} className="text-center">
{impact.label}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{likelihoods.toReversed().map((likelihood) => (
<TableRow key={likelihood.level}>
<TableHead scope="row">{likelihood.label}</TableHead>
{impacts.map((impact) => {
const cell = cellAt.get(
`${likelihood.level}:${impact.level}`,
);
return (
<TableCell key={impact.level} className="text-center">
{cell ? (
<Badge
variant={TONE_VARIANT[cell.tone]}
aria-label={t(
"{band}: {count, plural, one {# risk} other {# risks}}",
{ band: cell.band, count: cell.count },
)}
>
{cell.count}
</Badge>
) : null}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
)}
</FramePanel>
</Frame>
);
}Last updated on