Kanzo UI
Data display

Findings

What a check found — one result or many sharing a rule — as items, grouped under a heading, behind a badge that tallies them and a header that says what was checked.

Usage

import {
  FindingsActions,
  FindingsBadge,
  FindingsContent,
  FindingsGroup,
  FindingsHeader,
  FindingsItem,
  FindingsList,
  FindingsRoot,
  FindingsSource,
  groupFindings,
  PopoverDescription,
  tallyFindings,
} from "@kanzo-tech/ui";
const groups = groupFindings(findings, (f) => `${f.rule.id}|${f.place.path}`);
const tally = tallyFindings(groups);

<FindingsRoot tally={tally}>
  <FindingsBadge>{(t) => (t ? `${t.violation} violations · ${t.warning} warnings` : "No rules")}</FindingsBadge>
  <FindingsContent>
    <FindingsHeader>
      <FindingsSource>flights-shapes.ttl · 4 rules</FindingsSource>
      <FindingsActions>
        <Button aria-label="Check again" size="icon-sm" variant="ghost"><RotateCwIcon /></Button>
      </FindingsActions>
      <PopoverDescription>1,204 of 3,310 nodes checked · 1,199 conform</PopoverDescription>
    </FindingsHeader>
    <FindingsList empty={<ChooseRules />}>
      <FindingsGroup title="ProjectShape" tally={tally} action={{ label: "Show 44", run: showAll }}>
        {groups.map((group) => (
          <FindingsItem
            key={group.rule.id}
            finding={group}
            describe={describe}
            action={{ label: `Show ${group.count}`, run: () => show(group.places) }}
          >
            {group.places[0].path}
          </FindingsItem>
        ))}
      </FindingsGroup>
    </FindingsList>
  </FindingsContent>
</FindingsRoot>

A SHACL report and a compiler's diagnostics are the same list: one result, or many that share a rule, each pointing at a place. The place is yours — a focus node and a path, a line of a program — and the library reaches it only through describe. Why it is shaped so is on the design page.

The model

type FindingSeverity = "violation" | "warning" | "info";

interface Finding<Place> {
  severity: FindingSeverity;
  message: string;
  rule: { id: string; label: string };
  place: Place;
  help?: string;
}

interface FindingGroup<Place> {
  rule: FindingRule;
  severity: FindingSeverity;
  message: string;
  count: number;
  places: Place[];
  sample: Finding<Place>[];
}

A Finding is always one result. A FindingGroup is many: its count, every place, and a sample of the first few to open. groupFindings(findings, keyOf, sample = 3) folds findings by your key — usually the rule and the path — worst severity first and in your order within one. tallyFindings(items) counts findings and groups alike, a group by its count, into a FindingTally: { violation, warning, info, total }. Both are plain functions, callable from a Server Component.

A place, described

const describe: DescribePlace<Node> = (node) => ({
  where: `${node.path} · ${node.focus}`,
  detail: node.value,
});

where is what line 1 shows; detail is what opening the item shows. A compiler's place is { where: "Line 12" }. A FindingPlace says where a place is, never what to do there: the action is the item's.

The item

FindingsItem draws one finding or a group, from finding. Line 1 is the severity — with the count on the badge, for a group — where, and the item's one action; line 2 is the message, on its own line, and it wraps. Nothing is held to one line but the severity badge and the buttons — a long IRI breaks rather than widening the list.

  • Where is one result's place as describe reads it, or a group's rule label. Children replace it: what a group shares — totalCost — is yours to say.
  • action is a FindingAction, { label, run, pressed? }: Go to field, Go to line, Show 362. One per item, and inside a findings popover the item closes the popover before it runs the action, so focus lands where the action sends it. With pressed it is Ark's Toggle — Show puts something on the page and pressing again takes it off.
  • Opening it shows one result's detail, or a group's sample — each place where it is, its message when it differs, and its detail — then the rule (label and id) and the help. A sampled place is read, not acted on.

FindingsItem is a Diagnostic, so it goes in a DiagnosticList or a FindingsGroup, and takes Diagnostic's open, defaultOpen and onOpenChange.

The words are yours

Nothing tallies in English. The badge's words are its children, a function of the tally, because 362 violations and Valid are a product's vocabulary and a plural rule is a locale's. The words the parts draw — the header's title, the severity and the disclosure's name — are FindingsTranslations, English by default (Findings, Violation, Details), given once on FindingsRoot or per item as translations, the one prop every component here takes its words through. Counts are formatted in the LocaleProvider's locale.

The badge and its popover

  • FindingsRoot is Ark's Popover, holding the tally. undefined means nothing checked — no rules, no run — and is not clean. open, defaultOpen and onOpenChange are the machine's.
  • FindingsBadge is the trigger: a Badge that is a button in every state, named by its children. It wears the worst severity found, success when the check found nothing and outline when nothing was checked. size and pill are Badge's.
  • FindingsContent is the popover, at a fixed width: a FindingsHeader, then a FindingsList.
  • FindingsHeader is above the list and does not scroll with it. Its title is the translations' Findings, Ark's popover title; then your parts, in order: FindingsSource, what was checked — flights-shapes.ttl · 4 rules, graph.fossil, a form's name — muted and monospace; FindingsActions, at the end of the line, for icon buttons (Check again, Stop, a menu) and at most one text toggle; and a PopoverDescription for what the check covered — 1,204 of 3,310 nodes checked, 2 errors · 1 warning — on a line of its own.
  • FindingsList is the children in a vertical-only ScrollArea, or empty instead when nothing was found or checked.
  • FindingsGroup puts items under a heading — a <section> named by its title — with FindingsTally beside the title, a count per severity rather than the paths it holds, and one action at the end of the heading — Show all 44 — that works as an item's does.

The props types — FindingsItemProps, FindingsRootProps, FindingsBadgeProps, FindingsListProps, FindingsGroupProps, FindingsTallyProps — and FindingAction, FindingPlace, FindingRule are exported for wrappers.

Testing

FindingsBadgeHarness and FindingsItemHarness from @kanzo-tech/testing drive these.

On this page