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
describereads it, or a group's rule label. Children replace it: what a group shares —totalCost— is yours to say. actionis aFindingAction,{ 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. Withpressedit 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 itsdetail— 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
FindingsRootis Ark's Popover, holding thetally.undefinedmeans nothing checked — no rules, no run — and is not clean.open,defaultOpenandonOpenChangeare the machine's.FindingsBadgeis the trigger: aBadgethat is a button in every state, named by its children. It wears the worst severity found,successwhen the check found nothing andoutlinewhen nothing was checked.sizeandpillareBadge's.FindingsContentis the popover, at a fixed width: aFindingsHeader, then aFindingsList.FindingsHeaderis 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.FindingsListis the children in a vertical-only ScrollArea, oremptyinstead when nothing was found or checked.FindingsGroupputs items under a heading — a<section>named by its title — withFindingsTallybeside the title, a count per severity rather than the paths it holds, and oneactionat 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.