# Risk heatmap

> The open risks on the default matrix, one cell per likelihood and impact with the count in the band's color.

- Block: [risks](https://bettersupabase.com/blocks/risks.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/risk-heatmap

## Install the block

```bash
npx better-supabase add risks
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button empty skeleton table @reui/frame
```

From `apps/examples/nextjs/src/features/risks/components/risk-heatmap.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/risks/components/risk-heatmap.tsx):

```tsx
// 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>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Risk heatmap" in a React web app that uses shadcn/ui components. The open risks on the default matrix, one cell per likelihood and impact with the count in the band's color.

It uses the better-supabase `risks` block (Risks). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add risks` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button empty skeleton table @reui/frame`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/risks/components/risk-heatmap.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/risk-heatmap.md
- Block docs: https://bettersupabase.com/docs/blocks/risks
```
