@elabs-ai/components-process 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/core/index.d.ts +801 -3
- package/dist/core/index.js +1334 -0
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +1889 -34
- package/dist/index.js +5512 -196
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.ts +223 -5
- package/dist/test/index.js +346 -191
- package/dist/test/index.js.map +1 -1
- package/package.json +14 -13
- package/src/__contract__/case-table.contract.test.tsx +49 -0
- package/src/__contract__/compare-kpi-strip.contract.test.tsx +49 -0
- package/src/__contract__/conformance-overlay.contract.test.tsx +49 -0
- package/src/__contract__/happy-path-editor.contract.test.tsx +49 -0
- package/src/__contract__/process-kpi-strip.contract.test.tsx +49 -0
- package/src/__contract__/violation-list.contract.test.tsx +49 -0
- package/src/abstraction-controls/abstraction-controls-per-type.test.tsx +80 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +43 -1
- package/src/abstraction-controls/abstraction-controls.tsx +198 -5
- package/src/case-table/case-table.stories.tsx +89 -0
- package/src/case-table/case-table.test.tsx +148 -0
- package/src/case-table/case-table.tsx +144 -0
- package/src/case-table/columns.ts +116 -0
- package/src/case-table/index.ts +11 -0
- package/src/case-timeline/case-timeline-model.test.ts +72 -0
- package/src/case-timeline/case-timeline-model.ts +112 -0
- package/src/case-timeline/case-timeline.stories.tsx +94 -0
- package/src/case-timeline/case-timeline.test.tsx +51 -0
- package/src/case-timeline/case-timeline.tsx +109 -0
- package/src/case-timeline/index.ts +9 -0
- package/src/conformance-overlay/conformance-fixture.ts +59 -0
- package/src/conformance-overlay/conformance-legend.tsx +109 -0
- package/src/conformance-overlay/conformance-overlay.stories.tsx +116 -0
- package/src/conformance-overlay/conformance-overlay.test.tsx +88 -0
- package/src/conformance-overlay/conformance-overlay.tsx +107 -0
- package/src/conformance-overlay/conformance-state.test.ts +79 -0
- package/src/conformance-overlay/conformance-state.ts +220 -0
- package/src/conformance-overlay/index.ts +4 -0
- package/src/core/activity-color-scale.test.ts +107 -0
- package/src/core/activity-color-scale.ts +133 -0
- package/src/core/adapters/ocel.test.ts +112 -0
- package/src/core/adapters/ocel.ts +359 -0
- package/src/core/adapters/xes.test.ts +293 -0
- package/src/core/adapters/xes.ts +384 -0
- package/src/core/cases-from-log.test.ts +72 -0
- package/src/core/cases-from-log.ts +85 -0
- package/src/core/conformance.test.ts +80 -0
- package/src/core/conformance.ts +91 -0
- package/src/core/diff-graphs.test.ts +151 -0
- package/src/core/diff-graphs.ts +118 -0
- package/src/core/discover-object-centric-graph.test.ts +94 -0
- package/src/core/discover-object-centric-graph.ts +296 -0
- package/src/core/fixtures/ocel-sample.ts +82 -0
- package/src/core/fixtures/sample.xes +68 -0
- package/src/core/index.ts +113 -0
- package/src/core/reference-model.test.ts +43 -0
- package/src/core/reference-model.ts +116 -0
- package/src/core/replay-timeline.test.ts +161 -0
- package/src/core/replay-timeline.ts +260 -0
- package/src/core/segments.test.ts +185 -0
- package/src/core/segments.ts +153 -0
- package/src/core/token-replay.test.ts +218 -0
- package/src/core/token-replay.ts +456 -0
- package/src/core/types.ts +2 -2
- package/src/dotted-chart/compute-dots.test.ts +176 -0
- package/src/dotted-chart/compute-dots.ts +241 -0
- package/src/dotted-chart/dotted-chart-labels.ts +93 -0
- package/src/dotted-chart/dotted-chart.stories.tsx +182 -0
- package/src/dotted-chart/dotted-chart.test.tsx +135 -0
- package/src/dotted-chart/dotted-chart.tsx +841 -0
- package/src/dotted-chart/index.ts +23 -0
- package/src/dotted-chart/use-element-size.ts +33 -0
- package/src/happy-path-editor/happy-path-editor-context.ts +81 -0
- package/src/happy-path-editor/happy-path-editor.stories.tsx +116 -0
- package/src/happy-path-editor/happy-path-editor.test.tsx +142 -0
- package/src/happy-path-editor/happy-path-editor.tsx +239 -0
- package/src/happy-path-editor/happy-path-step-node.tsx +175 -0
- package/src/happy-path-editor/index.ts +4 -0
- package/src/index.ts +51 -1
- package/src/performance-spectrum/aggregate-segments.test.ts +107 -0
- package/src/performance-spectrum/aggregate-segments.ts +174 -0
- package/src/performance-spectrum/index.ts +25 -0
- package/src/performance-spectrum/performance-spectrum-context.tsx +116 -0
- package/src/performance-spectrum/performance-spectrum.stories.tsx +128 -0
- package/src/performance-spectrum/performance-spectrum.test.tsx +190 -0
- package/src/performance-spectrum/performance-spectrum.tsx +870 -0
- package/src/process-compare/compare-kpi-strip.stories.tsx +48 -0
- package/src/process-compare/compare-kpi-strip.tsx +94 -0
- package/src/process-compare/compare-model.ts +83 -0
- package/src/process-compare/compare-side.tsx +42 -0
- package/src/process-compare/diff-to-graph.ts +104 -0
- package/src/process-compare/index.ts +23 -0
- package/src/process-compare/process-compare.stories.tsx +184 -0
- package/src/process-compare/process-compare.test.tsx +224 -0
- package/src/process-compare/process-compare.tsx +251 -0
- package/src/process-explorer.stories.tsx +1 -1
- package/src/process-filter-bar/index.ts +2 -0
- package/src/process-filter-bar/process-filter-bar.stories.tsx +156 -0
- package/src/process-filter-bar/process-filter-bar.test.tsx +201 -0
- package/src/process-filter-bar/process-filter-bar.tsx +167 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +47 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +67 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +148 -8
- package/src/process-map/activity-accent.ts +25 -0
- package/src/process-map/index.ts +1 -0
- package/src/process-map/map-model.test.ts +16 -0
- package/src/process-map/map-model.ts +323 -1
- package/src/process-map/object-centric-map.test.tsx +132 -0
- package/src/process-map/process-activity-node.tsx +152 -14
- package/src/process-map/process-map-object-centric.stories.tsx +219 -0
- package/src/process-map/process-map.stories.tsx +64 -0
- package/src/process-map/process-map.tsx +240 -16
- package/src/process-map/process-transition-edge.test.tsx +47 -0
- package/src/process-map/process-transition-edge.tsx +134 -7
- package/src/process-map/use-process-layout.ts +30 -9
- package/src/process-replay/congestion-heat.tsx +107 -0
- package/src/process-replay/index.ts +14 -0
- package/src/process-replay/process-replay.stories.tsx +168 -0
- package/src/process-replay/process-replay.test.tsx +170 -0
- package/src/process-replay/process-replay.tsx +285 -0
- package/src/process-replay/replay-controls.tsx +147 -0
- package/src/process-replay/replay-format.ts +83 -0
- package/src/process-replay/replay-tokens-context.ts +30 -0
- package/src/process-replay/use-controllable-value.ts +30 -0
- package/src/templates-process-explorer.stories.tsx +1304 -0
- package/src/test/contract.test.ts +66 -0
- package/src/test/contract.ts +107 -6
- package/src/test/doubles.test.tsx +87 -1
- package/src/test/doubles.tsx +174 -3
- package/src/test/index.ts +25 -1
- package/src/use-process-explorer/use-process-explorer.test.ts +44 -0
- package/src/use-process-explorer/use-process-explorer.ts +34 -2
- package/src/variant-explorer/coverage-bar.tsx +36 -0
- package/src/variant-explorer/index.ts +16 -0
- package/src/variant-explorer/sequence-chips.tsx +103 -0
- package/src/variant-explorer/variant-explorer-model.ts +42 -0
- package/src/variant-explorer/variant-explorer.stories.tsx +226 -0
- package/src/variant-explorer/variant-explorer.test.tsx +302 -0
- package/src/variant-explorer/variant-explorer.tsx +567 -0
- package/src/variant-explorer/variant-row.tsx +137 -0
- package/src/violation-list/index.ts +2 -0
- package/src/violation-list/violation-list.stories.tsx +73 -0
- package/src/violation-list/violation-list.test.tsx +84 -0
- package/src/violation-list/violation-list.tsx +259 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, fn, userEvent, within } from "storybook/test";
|
|
3
|
+
import { liftHappyPath } from "../core/reference-model";
|
|
4
|
+
import { emptyDeviationCounts, tokenReplay } from "../core/token-replay";
|
|
5
|
+
import {
|
|
6
|
+
CONFORMANCE_FIXTURE_LOG,
|
|
7
|
+
CONFORMANCE_FIXTURE_PATH,
|
|
8
|
+
} from "../conformance-overlay/conformance-fixture";
|
|
9
|
+
import { ViolationList } from "./violation-list";
|
|
10
|
+
|
|
11
|
+
const conformance = tokenReplay(CONFORMANCE_FIXTURE_LOG, liftHappyPath(CONFORMANCE_FIXTURE_PATH));
|
|
12
|
+
|
|
13
|
+
const meta = {
|
|
14
|
+
title: "Process/ViolationList",
|
|
15
|
+
component: ViolationList,
|
|
16
|
+
tags: ["autodocs"],
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: "padded",
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
"Deviations from a token-replay result, grouped by type and ranked by count, with " +
|
|
23
|
+
"each type’s share of cases and a final conforming row. Choosing a row emits a " +
|
|
24
|
+
'`{ kind: "cases", ids }` filter intent for exactly the cases that deviated that ' +
|
|
25
|
+
"way — the component never filters anything itself.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
args: { conformance, onFilterIntent: fn() },
|
|
30
|
+
} satisfies Meta<typeof ViolationList>;
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof meta>;
|
|
33
|
+
|
|
34
|
+
/** Ranked deviation types; a row click or Enter emits that row’s case ids. */
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
play: async ({ canvasElement, args }) => {
|
|
37
|
+
const canvas = within(canvasElement);
|
|
38
|
+
await userEvent.click(canvas.getByRole("button", { name: /Skipped step/ }));
|
|
39
|
+
await expect(args.onFilterIntent).toHaveBeenLastCalledWith({
|
|
40
|
+
kind: "cases",
|
|
41
|
+
ids: ["c07", "c08"],
|
|
42
|
+
});
|
|
43
|
+
canvas.getByRole("button", { name: /Undesired activity/ }).focus();
|
|
44
|
+
await userEvent.keyboard("{Enter}");
|
|
45
|
+
await expect(args.onFilterIntent).toHaveBeenLastCalledWith({ kind: "cases", ids: ["c09"] });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Without `onFilterIntent` the rows are read-only — no buttons. */
|
|
50
|
+
export const ReadOnly: Story = {
|
|
51
|
+
args: { onFilterIntent: undefined },
|
|
52
|
+
play: async ({ canvasElement }) => {
|
|
53
|
+
await expect(within(canvasElement).queryByRole("button")).toBeNull();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** A replay over zero cases. */
|
|
58
|
+
export const Empty: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
conformance: {
|
|
61
|
+
overallFitness: 0,
|
|
62
|
+
traces: [],
|
|
63
|
+
deviationCounts: emptyDeviationCounts(),
|
|
64
|
+
perActivity: {},
|
|
65
|
+
perEdge: {},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Replay still running. */
|
|
71
|
+
export const Loading: Story = {
|
|
72
|
+
args: { loading: true },
|
|
73
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { liftHappyPath } from "../core/reference-model";
|
|
5
|
+
import { emptyDeviationCounts, tokenReplay } from "../core/token-replay";
|
|
6
|
+
import {
|
|
7
|
+
CONFORMANCE_FIXTURE_LOG,
|
|
8
|
+
CONFORMANCE_FIXTURE_PATH,
|
|
9
|
+
} from "../conformance-overlay/conformance-fixture";
|
|
10
|
+
import { ViolationList, violationRows } from "./violation-list";
|
|
11
|
+
|
|
12
|
+
afterEach(cleanup);
|
|
13
|
+
|
|
14
|
+
const conformance = tokenReplay(CONFORMANCE_FIXTURE_LOG, liftHappyPath(CONFORMANCE_FIXTURE_PATH));
|
|
15
|
+
|
|
16
|
+
describe("violationRows", () => {
|
|
17
|
+
it("orders types by deviation count descending, then the conforming row", () => {
|
|
18
|
+
expect(violationRows(conformance).map((row) => row.kind)).toEqual([
|
|
19
|
+
"skipped",
|
|
20
|
+
"undesired",
|
|
21
|
+
"incomplete",
|
|
22
|
+
"conforming",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("row case counts sum to the number of replayed cases (one type per case here)", () => {
|
|
27
|
+
const sum = violationRows(conformance).reduce((total, row) => total + row.caseIds.length, 0);
|
|
28
|
+
expect(sum).toBe(conformance.traces.length);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("resolves exactly the case ids whose replay found that type", () => {
|
|
32
|
+
const rows = violationRows(conformance);
|
|
33
|
+
const expected = (type: string) =>
|
|
34
|
+
conformance.traces
|
|
35
|
+
.filter((trace) => trace.deviations.some((d) => d.type === type))
|
|
36
|
+
.map((trace) => trace.caseId);
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
if (row.kind === "conforming") continue;
|
|
39
|
+
expect(row.caseIds).toEqual(expected(row.kind));
|
|
40
|
+
}
|
|
41
|
+
expect(rows.find((row) => row.kind === "skipped")?.caseIds).toEqual(["c07", "c08"]);
|
|
42
|
+
expect(rows.find((row) => row.kind === "skipped")?.share).toBeCloseTo(0.2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("ViolationList", () => {
|
|
47
|
+
it("emits a cases filter intent with that row's ids, from pointer and keyboard", async () => {
|
|
48
|
+
const onFilterIntent = vi.fn();
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
render(<ViolationList conformance={conformance} onFilterIntent={onFilterIntent} />);
|
|
51
|
+
|
|
52
|
+
await user.click(screen.getByRole("button", { name: /Skipped step/ }));
|
|
53
|
+
expect(onFilterIntent).toHaveBeenLastCalledWith({ kind: "cases", ids: ["c07", "c08"] });
|
|
54
|
+
|
|
55
|
+
screen.getByRole("button", { name: /Undesired activity/ }).focus();
|
|
56
|
+
await user.keyboard("{Enter}");
|
|
57
|
+
expect(onFilterIntent).toHaveBeenLastCalledWith({ kind: "cases", ids: ["c09"] });
|
|
58
|
+
expect(onFilterIntent).toHaveBeenCalledTimes(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("prints share and case count per row, and the conforming total in the caption", () => {
|
|
62
|
+
render(<ViolationList conformance={conformance} />);
|
|
63
|
+
const row = screen.getByRole("row", { name: /Skipped step/ });
|
|
64
|
+
expect(within(row).getByText("20%")).toBeInTheDocument();
|
|
65
|
+
expect(within(row).getByText("2")).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText(/6 of 10 cases conform/)).toBeInTheDocument();
|
|
67
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows an empty panel for an empty replay and a loading panel while loading", () => {
|
|
71
|
+
const empty = {
|
|
72
|
+
overallFitness: 0,
|
|
73
|
+
traces: [],
|
|
74
|
+
deviationCounts: emptyDeviationCounts(),
|
|
75
|
+
perActivity: {},
|
|
76
|
+
perEdge: {},
|
|
77
|
+
};
|
|
78
|
+
const { unmount } = render(<ViolationList conformance={empty} />);
|
|
79
|
+
expect(screen.getByText("No cases replayed")).toBeInTheDocument();
|
|
80
|
+
unmount();
|
|
81
|
+
render(<ViolationList conformance={conformance} loading />);
|
|
82
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ViolationList — why cases lost fitness, one row per deviation type (RM-062).
|
|
5
|
+
*
|
|
6
|
+
* A `Table` (`@elabs-ai/components-ui`) over a `ConformanceResult` (RM-061): the deviation
|
|
7
|
+
* type, the share of cases carrying it and the number of those cases. Rows with at least
|
|
8
|
+
* one deviation are sorted by `deviationCounts` descending (ties keep `DEVIATION_TYPES`
|
|
9
|
+
* order); a final "Conforming" row counts the cases with no deviation at all, so the
|
|
10
|
+
* conforming vs non-conforming pair analysis §2 asks for reads off the same table.
|
|
11
|
+
*
|
|
12
|
+
* Choosing a row emits `{ kind: "cases", ids }` — exactly the case ids whose replay found
|
|
13
|
+
* that type — which a host hands straight to `filterLog`. The row's type cell is a real
|
|
14
|
+
* `<button>`, so the same action is keyboard-operable; the component never filters itself.
|
|
15
|
+
*/
|
|
16
|
+
import { forwardRef, useCallback, useMemo, type HTMLAttributes } from "react";
|
|
17
|
+
import { ListFilter } from "lucide-react";
|
|
18
|
+
import {
|
|
19
|
+
Button,
|
|
20
|
+
StatePanel,
|
|
21
|
+
Table,
|
|
22
|
+
TableBody,
|
|
23
|
+
TableCaption,
|
|
24
|
+
TableCell,
|
|
25
|
+
TableHead,
|
|
26
|
+
TableHeader,
|
|
27
|
+
TableRow,
|
|
28
|
+
useLocale,
|
|
29
|
+
} from "@elabs-ai/components-ui";
|
|
30
|
+
import type { ConformanceResult } from "../core/conformance";
|
|
31
|
+
import type { FilterSpec } from "../core/filter-log";
|
|
32
|
+
import { DEVIATION_TYPES, type DeviationType } from "../core/token-replay";
|
|
33
|
+
import { fillLabel } from "../variant-explorer/variant-explorer-model";
|
|
34
|
+
|
|
35
|
+
/** What a row stands for: a deviation type, or the cases with none. */
|
|
36
|
+
export type ViolationRowKind = DeviationType | "conforming";
|
|
37
|
+
|
|
38
|
+
/** One row of {@link ViolationList}. */
|
|
39
|
+
export interface ViolationRow {
|
|
40
|
+
kind: ViolationRowKind;
|
|
41
|
+
/** Deviations of this type across all cases (`0` for `"conforming"`). */
|
|
42
|
+
deviations: number;
|
|
43
|
+
/** Ids of the cases carrying at least one deviation of this type, in log order. */
|
|
44
|
+
caseIds: string[];
|
|
45
|
+
/** `caseIds.length / traces.length`; `0` for an empty log. */
|
|
46
|
+
share: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The filter a row emits — a strict subset of `/core`'s `FilterSpec`. */
|
|
50
|
+
export type ViolationFilterIntent = Extract<FilterSpec, { kind: "cases" }>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The rows {@link ViolationList} renders, in order: every type with deviations, by count
|
|
54
|
+
* descending, then `"conforming"`.
|
|
55
|
+
*/
|
|
56
|
+
export function violationRows(conformance: ConformanceResult): ViolationRow[] {
|
|
57
|
+
const total = conformance.traces.length;
|
|
58
|
+
const byType = new Map<DeviationType, string[]>(DEVIATION_TYPES.map((type) => [type, []]));
|
|
59
|
+
const conforming: string[] = [];
|
|
60
|
+
for (const trace of conformance.traces) {
|
|
61
|
+
if (trace.deviations.length === 0) {
|
|
62
|
+
conforming.push(trace.caseId);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const seen = new Set<DeviationType>();
|
|
66
|
+
for (const deviation of trace.deviations) {
|
|
67
|
+
if (seen.has(deviation.type)) continue;
|
|
68
|
+
seen.add(deviation.type);
|
|
69
|
+
byType.get(deviation.type)?.push(trace.caseId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const share = (count: number) => (total === 0 ? 0 : count / total);
|
|
73
|
+
const rows = DEVIATION_TYPES.filter((type) => conformance.deviationCounts[type] > 0)
|
|
74
|
+
.map((type, order) => ({ type, order }))
|
|
75
|
+
.sort(
|
|
76
|
+
(a, b) =>
|
|
77
|
+
conformance.deviationCounts[b.type] - conformance.deviationCounts[a.type] ||
|
|
78
|
+
a.order - b.order,
|
|
79
|
+
)
|
|
80
|
+
.map(({ type }): ViolationRow => {
|
|
81
|
+
const caseIds = byType.get(type) ?? [];
|
|
82
|
+
return {
|
|
83
|
+
kind: type,
|
|
84
|
+
deviations: conformance.deviationCounts[type],
|
|
85
|
+
caseIds,
|
|
86
|
+
share: share(caseIds.length),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
rows.push({
|
|
90
|
+
kind: "conforming",
|
|
91
|
+
deviations: 0,
|
|
92
|
+
caseIds: conforming,
|
|
93
|
+
share: share(conforming.length),
|
|
94
|
+
});
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every user-visible string. `{name}` placeholders are filled at render. */
|
|
99
|
+
export interface ViolationListLabels {
|
|
100
|
+
undesired: string;
|
|
101
|
+
skipped: string;
|
|
102
|
+
wrongOrder: string;
|
|
103
|
+
wrongStart: string;
|
|
104
|
+
incomplete: string;
|
|
105
|
+
conforming: string;
|
|
106
|
+
columnType: string;
|
|
107
|
+
columnShare: string;
|
|
108
|
+
columnCases: string;
|
|
109
|
+
/** `{conforming}`, `{total}`. */
|
|
110
|
+
caption: string;
|
|
111
|
+
/** Appended to the caption when rows can be chosen. */
|
|
112
|
+
captionFilter: string;
|
|
113
|
+
loading: string;
|
|
114
|
+
empty: string;
|
|
115
|
+
emptyBody: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The shipped English labels. */
|
|
119
|
+
export const VIOLATION_LIST_DEFAULT_LABELS: Readonly<ViolationListLabels> = Object.freeze({
|
|
120
|
+
undesired: "Undesired activity",
|
|
121
|
+
skipped: "Skipped step",
|
|
122
|
+
wrongOrder: "Wrong order",
|
|
123
|
+
wrongStart: "Wrong start",
|
|
124
|
+
incomplete: "Incomplete case",
|
|
125
|
+
conforming: "Conforming",
|
|
126
|
+
columnType: "Deviation",
|
|
127
|
+
columnShare: "Share of cases",
|
|
128
|
+
columnCases: "Cases",
|
|
129
|
+
caption: "Deviations by type — {conforming} of {total} cases conform.",
|
|
130
|
+
captionFilter: "Choose a row to filter the log to its cases.",
|
|
131
|
+
loading: "Replaying cases…",
|
|
132
|
+
empty: "No cases replayed",
|
|
133
|
+
emptyBody: "Replay an event log against a happy path to list its deviations.",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/** Props for {@link ViolationList}. */
|
|
137
|
+
export interface ViolationListProps extends HTMLAttributes<HTMLDivElement> {
|
|
138
|
+
/** The replay result (`tokenReplay`). */
|
|
139
|
+
conformance: ConformanceResult;
|
|
140
|
+
/** When given, each row emits `{ kind: "cases", ids }` for its cases. */
|
|
141
|
+
onFilterIntent?: (intent: ViolationFilterIntent) => void;
|
|
142
|
+
/** No replay yet. Renders the loading panel rather than an empty table. */
|
|
143
|
+
loading?: boolean;
|
|
144
|
+
/** Override any user-visible string. */
|
|
145
|
+
labels?: Partial<ViolationListLabels>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The violation list.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* <ViolationList
|
|
154
|
+
* conformance={tokenReplay(log, liftHappyPath(path))}
|
|
155
|
+
* onFilterIntent={(intent) => explorer.applyIntent(intent)}
|
|
156
|
+
* />
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export const ViolationList = forwardRef<HTMLDivElement, ViolationListProps>(function ViolationList(
|
|
160
|
+
{ conformance, onFilterIntent, loading = false, labels: labelOverrides, className, ...props },
|
|
161
|
+
ref,
|
|
162
|
+
) {
|
|
163
|
+
const { formatNumber } = useLocale();
|
|
164
|
+
const labels = useMemo<ViolationListLabels>(
|
|
165
|
+
() => ({ ...VIOLATION_LIST_DEFAULT_LABELS, ...labelOverrides }),
|
|
166
|
+
[labelOverrides],
|
|
167
|
+
);
|
|
168
|
+
const rows = useMemo(() => violationRows(conformance), [conformance]);
|
|
169
|
+
const emit = useCallback(
|
|
170
|
+
(row: ViolationRow) => onFilterIntent?.({ kind: "cases", ids: row.caseIds }),
|
|
171
|
+
[onFilterIntent],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (loading) {
|
|
175
|
+
return (
|
|
176
|
+
<div
|
|
177
|
+
ref={ref}
|
|
178
|
+
data-slot="violation-list"
|
|
179
|
+
data-state="loading"
|
|
180
|
+
className={className}
|
|
181
|
+
{...props}
|
|
182
|
+
>
|
|
183
|
+
<StatePanel kind="loading" title={labels.loading} />
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const total = conformance.traces.length;
|
|
189
|
+
if (total === 0) {
|
|
190
|
+
return (
|
|
191
|
+
<div ref={ref} data-slot="violation-list" data-state="empty" className={className} {...props}>
|
|
192
|
+
<StatePanel kind="empty" title={labels.empty} description={labels.emptyBody} />
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const conformingCount = rows[rows.length - 1]?.caseIds.length ?? 0;
|
|
198
|
+
const interactive = onFilterIntent !== undefined;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div ref={ref} data-slot="violation-list" className={className} {...props}>
|
|
202
|
+
<Table data-slot="violation-list-table">
|
|
203
|
+
<TableCaption>
|
|
204
|
+
{fillLabel(labels.caption, {
|
|
205
|
+
conforming: formatNumber(conformingCount),
|
|
206
|
+
total: formatNumber(total),
|
|
207
|
+
})}
|
|
208
|
+
{interactive ? ` ${labels.captionFilter}` : null}
|
|
209
|
+
</TableCaption>
|
|
210
|
+
<TableHeader>
|
|
211
|
+
<TableRow>
|
|
212
|
+
<TableHead scope="col">{labels.columnType}</TableHead>
|
|
213
|
+
<TableHead scope="col" className="text-end">
|
|
214
|
+
{labels.columnShare}
|
|
215
|
+
</TableHead>
|
|
216
|
+
<TableHead scope="col" className="text-end">
|
|
217
|
+
{labels.columnCases}
|
|
218
|
+
</TableHead>
|
|
219
|
+
</TableRow>
|
|
220
|
+
</TableHeader>
|
|
221
|
+
<TableBody>
|
|
222
|
+
{rows.map((row) => (
|
|
223
|
+
<TableRow
|
|
224
|
+
key={row.kind}
|
|
225
|
+
data-slot="violation-list-row"
|
|
226
|
+
data-kind={row.kind}
|
|
227
|
+
onClick={interactive ? () => emit(row) : undefined}
|
|
228
|
+
>
|
|
229
|
+
<TableHead scope="row" className="font-normal">
|
|
230
|
+
{interactive ? (
|
|
231
|
+
// The row's click handler receives this button's click (keyboard Enter/Space
|
|
232
|
+
// included) — one action, one handler, reachable without a pointer.
|
|
233
|
+
<Button
|
|
234
|
+
type="button"
|
|
235
|
+
variant="ghost"
|
|
236
|
+
size="sm"
|
|
237
|
+
data-slot="violation-list-filter"
|
|
238
|
+
className="-ms-3 gap-1.5"
|
|
239
|
+
>
|
|
240
|
+
<ListFilter aria-hidden="true" className="size-3.5" />
|
|
241
|
+
{labels[row.kind]}
|
|
242
|
+
</Button>
|
|
243
|
+
) : (
|
|
244
|
+
labels[row.kind]
|
|
245
|
+
)}
|
|
246
|
+
</TableHead>
|
|
247
|
+
<TableCell className="text-end tabular-nums">
|
|
248
|
+
{formatNumber(row.share, { style: "percent", maximumFractionDigits: 1 })}
|
|
249
|
+
</TableCell>
|
|
250
|
+
<TableCell className="text-end tabular-nums">
|
|
251
|
+
{formatNumber(row.caseIds.length)}
|
|
252
|
+
</TableCell>
|
|
253
|
+
</TableRow>
|
|
254
|
+
))}
|
|
255
|
+
</TableBody>
|
|
256
|
+
</Table>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
});
|