@miragon/pulse-analytics-connector 0.0.0-bootstrap.0 → 0.21.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/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/bpmn-name-lookup.d.ts +72 -0
- package/dist/bpmn-name-lookup.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.js +123 -0
- package/dist/bpmn-name-lookup.js.map +1 -0
- package/dist/bpmn-name-lookup.test.d.ts +2 -0
- package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.test.js +287 -0
- package/dist/bpmn-name-lookup.test.js.map +1 -0
- package/dist/bpmn-names.d.ts +38 -0
- package/dist/bpmn-names.d.ts.map +1 -0
- package/dist/bpmn-names.js +22 -0
- package/dist/bpmn-names.js.map +1 -0
- package/dist/brand-gates.test.d.ts +2 -0
- package/dist/brand-gates.test.d.ts.map +1 -0
- package/dist/brand-gates.test.js +58 -0
- package/dist/brand-gates.test.js.map +1 -0
- package/dist/builder-texts.d.ts +4 -0
- package/dist/builder-texts.d.ts.map +1 -0
- package/dist/builder-texts.js +14 -0
- package/dist/builder-texts.js.map +1 -0
- package/dist/builder-texts.test.d.ts +2 -0
- package/dist/builder-texts.test.d.ts.map +1 -0
- package/dist/builder-texts.test.js +32 -0
- package/dist/builder-texts.test.js.map +1 -0
- package/dist/definition.d.ts +3 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +151 -0
- package/dist/definition.js.map +1 -0
- package/dist/engine-ids.d.ts +42 -0
- package/dist/engine-ids.d.ts.map +1 -0
- package/dist/engine-ids.js +50 -0
- package/dist/engine-ids.js.map +1 -0
- package/dist/engine-scope.test.d.ts +2 -0
- package/dist/engine-scope.test.d.ts.map +1 -0
- package/dist/engine-scope.test.js +260 -0
- package/dist/engine-scope.test.js.map +1 -0
- package/dist/heatmap-data.d.ts +15 -0
- package/dist/heatmap-data.d.ts.map +1 -0
- package/dist/heatmap-data.js +2 -0
- package/dist/heatmap-data.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +8 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -0
- package/dist/messages/de.ask-ai.d.ts +9 -0
- package/dist/messages/de.ask-ai.d.ts.map +1 -0
- package/dist/messages/de.ask-ai.js +18 -0
- package/dist/messages/de.ask-ai.js.map +1 -0
- package/dist/messages/de.builder.d.ts +12 -0
- package/dist/messages/de.builder.d.ts.map +1 -0
- package/dist/messages/de.builder.js +53 -0
- package/dist/messages/de.builder.js.map +1 -0
- package/dist/messages/de.server.d.ts +8 -0
- package/dist/messages/de.server.d.ts.map +1 -0
- package/dist/messages/de.server.js +26 -0
- package/dist/messages/de.server.js.map +1 -0
- package/dist/messages/de.sweep.d.ts +10 -0
- package/dist/messages/de.sweep.d.ts.map +1 -0
- package/dist/messages/de.sweep.js +203 -0
- package/dist/messages/de.sweep.js.map +1 -0
- package/dist/messages/en.ask-ai.d.ts +25 -0
- package/dist/messages/en.ask-ai.d.ts.map +1 -0
- package/dist/messages/en.ask-ai.js +24 -0
- package/dist/messages/en.ask-ai.js.map +1 -0
- package/dist/messages/en.builder.d.ts +12 -0
- package/dist/messages/en.builder.d.ts.map +1 -0
- package/dist/messages/en.builder.js +53 -0
- package/dist/messages/en.builder.js.map +1 -0
- package/dist/messages/en.server.d.ts +8 -0
- package/dist/messages/en.server.d.ts.map +1 -0
- package/dist/messages/en.server.js +26 -0
- package/dist/messages/en.server.js.map +1 -0
- package/dist/messages/en.sweep.d.ts +11 -0
- package/dist/messages/en.sweep.d.ts.map +1 -0
- package/dist/messages/en.sweep.js +204 -0
- package/dist/messages/en.sweep.js.map +1 -0
- package/dist/messages/glossary.d.ts +26 -0
- package/dist/messages/glossary.d.ts.map +1 -0
- package/dist/messages/glossary.js +37 -0
- package/dist/messages/glossary.js.map +1 -0
- package/dist/messages/glossary.test.d.ts +2 -0
- package/dist/messages/glossary.test.d.ts.map +1 -0
- package/dist/messages/glossary.test.js +109 -0
- package/dist/messages/glossary.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +23 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/plural.d.ts +10 -0
- package/dist/messages/plural.d.ts.map +1 -0
- package/dist/messages/plural.js +17 -0
- package/dist/messages/plural.js.map +1 -0
- package/dist/messages/use-t.d.ts +10 -0
- package/dist/messages/use-t.d.ts.map +1 -0
- package/dist/messages/use-t.js +10 -0
- package/dist/messages/use-t.js.map +1 -0
- package/dist/module.d.ts +48 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +174 -0
- package/dist/module.js.map +1 -0
- package/dist/module.test.d.ts +2 -0
- package/dist/module.test.d.ts.map +1 -0
- package/dist/module.test.js +230 -0
- package/dist/module.test.js.map +1 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.d.ts.map +1 -0
- package/dist/named-views.js +73 -0
- package/dist/named-views.js.map +1 -0
- package/dist/named-views.test.d.ts +2 -0
- package/dist/named-views.test.d.ts.map +1 -0
- package/dist/named-views.test.js +110 -0
- package/dist/named-views.test.js.map +1 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +52 -0
- package/dist/plugin.js.map +1 -0
- package/dist/prometheus-wiring.test.d.ts +2 -0
- package/dist/prometheus-wiring.test.d.ts.map +1 -0
- package/dist/prometheus-wiring.test.js +142 -0
- package/dist/prometheus-wiring.test.js.map +1 -0
- package/dist/server-locale.d.ts +48 -0
- package/dist/server-locale.d.ts.map +1 -0
- package/dist/server-locale.js +33 -0
- package/dist/server-locale.js.map +1 -0
- package/dist/settings-tools.d.ts +23 -0
- package/dist/settings-tools.d.ts.map +1 -0
- package/dist/settings-tools.js +100 -0
- package/dist/settings-tools.js.map +1 -0
- package/dist/settings.d.ts +77 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +99 -0
- package/dist/settings.js.map +1 -0
- package/dist/settings.test.d.ts +2 -0
- package/dist/settings.test.d.ts.map +1 -0
- package/dist/settings.test.js +299 -0
- package/dist/settings.test.js.map +1 -0
- package/dist/steps/app-config.d.ts +30 -0
- package/dist/steps/app-config.d.ts.map +1 -0
- package/dist/steps/app-config.js +46 -0
- package/dist/steps/app-config.js.map +1 -0
- package/dist/steps/dashboard.d.ts +12 -0
- package/dist/steps/dashboard.d.ts.map +1 -0
- package/dist/steps/dashboard.js +58 -0
- package/dist/steps/dashboard.js.map +1 -0
- package/dist/steps/failure-dashboard.d.ts +10 -0
- package/dist/steps/failure-dashboard.d.ts.map +1 -0
- package/dist/steps/failure-dashboard.js +32 -0
- package/dist/steps/failure-dashboard.js.map +1 -0
- package/dist/steps/index.d.ts +3 -0
- package/dist/steps/index.d.ts.map +1 -0
- package/dist/steps/index.js +3 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/steps.test.d.ts +2 -0
- package/dist/steps/steps.test.d.ts.map +1 -0
- package/dist/steps/steps.test.js +153 -0
- package/dist/steps/steps.test.js.map +1 -0
- package/dist/tool-names.d.ts +13 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +27 -0
- package/dist/tool-names.js.map +1 -0
- package/dist/tools/cluster-compare.d.ts +8 -0
- package/dist/tools/cluster-compare.d.ts.map +1 -0
- package/dist/tools/cluster-compare.js +17 -0
- package/dist/tools/cluster-compare.js.map +1 -0
- package/dist/tools/element.d.ts +8 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +25 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/engine-compare.d.ts +8 -0
- package/dist/tools/engine-compare.d.ts.map +1 -0
- package/dist/tools/engine-compare.js +18 -0
- package/dist/tools/engine-compare.js.map +1 -0
- package/dist/tools/engine-compare.test.d.ts +2 -0
- package/dist/tools/engine-compare.test.d.ts.map +1 -0
- package/dist/tools/engine-compare.test.js +166 -0
- package/dist/tools/engine-compare.test.js.map +1 -0
- package/dist/tools/engine-landscape.d.ts +7 -0
- package/dist/tools/engine-landscape.d.ts.map +1 -0
- package/dist/tools/engine-landscape.js +13 -0
- package/dist/tools/engine-landscape.js.map +1 -0
- package/dist/tools/engine-landscape.test.d.ts +2 -0
- package/dist/tools/engine-landscape.test.d.ts.map +1 -0
- package/dist/tools/engine-landscape.test.js +110 -0
- package/dist/tools/engine-landscape.test.js.map +1 -0
- package/dist/tools/failures.d.ts +7 -0
- package/dist/tools/failures.d.ts.map +1 -0
- package/dist/tools/failures.js +13 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/health.d.ts +7 -0
- package/dist/tools/health.d.ts.map +1 -0
- package/dist/tools/health.js +23 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/health.test.d.ts +2 -0
- package/dist/tools/health.test.d.ts.map +1 -0
- package/dist/tools/health.test.js +119 -0
- package/dist/tools/health.test.js.map +1 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +35 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance.d.ts +8 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +25 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/performance.test.d.ts +2 -0
- package/dist/tools/performance.test.d.ts.map +1 -0
- package/dist/tools/performance.test.js +176 -0
- package/dist/tools/performance.test.js.map +1 -0
- package/dist/tools/version-compare.d.ts +8 -0
- package/dist/tools/version-compare.d.ts.map +1 -0
- package/dist/tools/version-compare.js +17 -0
- package/dist/tools/version-compare.js.map +1 -0
- package/dist/tools/version-compare.test.d.ts +2 -0
- package/dist/tools/version-compare.test.d.ts.map +1 -0
- package/dist/tools/version-compare.test.js +82 -0
- package/dist/tools/version-compare.test.js.map +1 -0
- package/dist/toolsets.d.ts +17 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +47 -0
- package/dist/toolsets.js.map +1 -0
- package/dist/toolsets.test.d.ts +2 -0
- package/dist/toolsets.test.d.ts.map +1 -0
- package/dist/toolsets.test.js +91 -0
- package/dist/toolsets.test.js.map +1 -0
- package/dist/view-title.test.d.ts +2 -0
- package/dist/view-title.test.d.ts.map +1 -0
- package/dist/view-title.test.js +91 -0
- package/dist/view-title.test.js.map +1 -0
- package/dist/widget-tools/comparisons.d.ts +13 -0
- package/dist/widget-tools/comparisons.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.js +177 -0
- package/dist/widget-tools/comparisons.js.map +1 -0
- package/dist/widget-tools/comparisons.test.d.ts +2 -0
- package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.test.js +123 -0
- package/dist/widget-tools/comparisons.test.js.map +1 -0
- package/dist/widget-tools/shared.d.ts +36 -0
- package/dist/widget-tools/shared.d.ts.map +1 -0
- package/dist/widget-tools/shared.js +39 -0
- package/dist/widget-tools/shared.js.map +1 -0
- package/dist/widget-tools.d.ts +25 -0
- package/dist/widget-tools.d.ts.map +1 -0
- package/dist/widget-tools.js +269 -0
- package/dist/widget-tools.js.map +1 -0
- package/dist/widget-tools.test.d.ts +2 -0
- package/dist/widget-tools.test.d.ts.map +1 -0
- package/dist/widget-tools.test.js +272 -0
- package/dist/widget-tools.test.js.map +1 -0
- package/package.json +91 -5
- package/src/bpmn-name-lookup.test.ts +333 -0
- package/src/bpmn-name-lookup.ts +197 -0
- package/src/bpmn-names.ts +51 -0
- package/src/brand-gates.test.ts +69 -0
- package/src/builder-texts.test.ts +40 -0
- package/src/builder-texts.ts +15 -0
- package/src/definition.ts +181 -0
- package/src/engine-ids.ts +85 -0
- package/src/engine-scope.test.ts +324 -0
- package/src/heatmap-data.ts +14 -0
- package/src/index.ts +4 -0
- package/src/instructions.ts +33 -0
- package/src/messages/GLOSSARY.md +88 -0
- package/src/messages/de.ask-ai.ts +28 -0
- package/src/messages/de.builder.ts +77 -0
- package/src/messages/de.server.ts +39 -0
- package/src/messages/de.sweep.ts +244 -0
- package/src/messages/en.ask-ai.ts +34 -0
- package/src/messages/en.builder.ts +76 -0
- package/src/messages/en.server.ts +39 -0
- package/src/messages/en.sweep.ts +240 -0
- package/src/messages/glossary.test.ts +122 -0
- package/src/messages/glossary.ts +62 -0
- package/src/messages/index.ts +24 -0
- package/src/messages/plural.ts +18 -0
- package/src/messages/use-t.ts +12 -0
- package/src/module.test.ts +319 -0
- package/src/module.ts +211 -0
- package/src/named-views.test.ts +141 -0
- package/src/named-views.ts +97 -0
- package/src/plugin.ts +94 -0
- package/src/prometheus-wiring.test.ts +174 -0
- package/src/server-locale.ts +59 -0
- package/src/settings-tools.ts +170 -0
- package/src/settings.test.ts +402 -0
- package/src/settings.ts +114 -0
- package/src/steps/app-config.ts +75 -0
- package/src/steps/dashboard.ts +73 -0
- package/src/steps/failure-dashboard.ts +38 -0
- package/src/steps/index.ts +2 -0
- package/src/steps/steps.test.ts +216 -0
- package/src/tool-names.ts +26 -0
- package/src/tools/cluster-compare.ts +28 -0
- package/src/tools/element.ts +35 -0
- package/src/tools/engine-compare.test.ts +210 -0
- package/src/tools/engine-compare.ts +30 -0
- package/src/tools/engine-landscape.test.ts +138 -0
- package/src/tools/engine-landscape.ts +21 -0
- package/src/tools/failures.ts +19 -0
- package/src/tools/health.test.ts +156 -0
- package/src/tools/health.ts +29 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/performance.test.ts +210 -0
- package/src/tools/performance.ts +38 -0
- package/src/tools/version-compare.test.ts +97 -0
- package/src/tools/version-compare.ts +28 -0
- package/src/toolsets.test.ts +107 -0
- package/src/toolsets.ts +56 -0
- package/src/view-title.test.ts +123 -0
- package/src/widget-tools/comparisons.test.ts +170 -0
- package/src/widget-tools/comparisons.ts +229 -0
- package/src/widget-tools/shared.ts +67 -0
- package/src/widget-tools.test.ts +306 -0
- package/src/widget-tools.ts +375 -0
- package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
- package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
- package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
- package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
- package/src/widgets/analytics-dashboard/lib.ts +43 -0
- package/src/widgets/bpmn-heatmap.test.tsx +75 -0
- package/src/widgets/bpmn-heatmap.tsx +127 -0
- package/src/widgets/catalogue-sync.test.ts +16 -0
- package/src/widgets/cluster-compare.tsx +148 -0
- package/src/widgets/comparison-shared.test.ts +237 -0
- package/src/widgets/comparison-shared.tsx +371 -0
- package/src/widgets/comparison-widgets.test.tsx +307 -0
- package/src/widgets/dashboard-widgets.test.tsx +397 -0
- package/src/widgets/engine-compare.tsx +87 -0
- package/src/widgets/engine-landscape.tsx +352 -0
- package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
- package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
- package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
- package/src/widgets/failure-dashboard/lib.ts +32 -0
- package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
- package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
- package/src/widgets/figure.tsx +79 -0
- package/src/widgets/hand-off.test.ts +126 -0
- package/src/widgets/hand-off.ts +102 -0
- package/src/widgets/index.ts +124 -0
- package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
- package/src/widgets/model-descriptions.test.ts +251 -0
- package/src/widgets/model-descriptions.ts +428 -0
- package/src/widgets/names.test.ts +32 -0
- package/src/widgets/names.ts +39 -0
- package/src/widgets/query-gate.test.tsx +57 -0
- package/src/widgets/query-gate.tsx +50 -0
- package/src/widgets/settings-section.test.tsx +174 -0
- package/src/widgets/settings-section.tsx +244 -0
- package/src/widgets/version-compare.test.ts +68 -0
- package/src/widgets/version-compare.tsx +96 -0
- package/src/widgets/view-meta.ts +18 -0
- package/src/widgets/widget-render.test-support.tsx +103 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import type { CompareKpiDelta } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import { setFormatLocale } from "@miragon/pulse-widget-shell/testing"
|
|
4
|
+
import { translator } from "../messages/index.js"
|
|
5
|
+
import type { T } from "../messages/use-t.js"
|
|
6
|
+
import {
|
|
7
|
+
DURATION_MIN_CHANGE_PCT,
|
|
8
|
+
RATE_MIN_CHANGE,
|
|
9
|
+
buildComparisonMetrics,
|
|
10
|
+
deltaFacts,
|
|
11
|
+
deltaVerdict,
|
|
12
|
+
formatDelta,
|
|
13
|
+
type DeltaRule,
|
|
14
|
+
} from "./comparison-shared.js"
|
|
15
|
+
|
|
16
|
+
afterEach(() => setFormatLocale(undefined))
|
|
17
|
+
|
|
18
|
+
const english = () => setFormatLocale({ language: "en", locale: "en-US" })
|
|
19
|
+
const german = () => setFormatLocale({ language: "de", locale: "de-DE" })
|
|
20
|
+
const en: T = (key, params) => translator("en", key, params)
|
|
21
|
+
const de: T = (key, params) => translator("de", key, params)
|
|
22
|
+
|
|
23
|
+
/** The formatters keep a number and its unit together with a non-breaking space. */
|
|
24
|
+
const plain = (s: string) => s.replace(/\u00a0/g, " ")
|
|
25
|
+
|
|
26
|
+
type Kpis = Parameters<typeof buildComparisonMetrics>[1]
|
|
27
|
+
|
|
28
|
+
const kpis = (over: Partial<Kpis> = {}): Kpis => ({
|
|
29
|
+
instance_count: 1240,
|
|
30
|
+
incident_rate_pct: 5,
|
|
31
|
+
element_incident_count: null,
|
|
32
|
+
element_incident_rate_pct: null,
|
|
33
|
+
avg_duration_sec: 60,
|
|
34
|
+
p95_duration_sec: 187,
|
|
35
|
+
...over,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const delta: CompareKpiDelta = {
|
|
39
|
+
started_per_day_delta_pct: 10,
|
|
40
|
+
incident_rate_delta_pp: -2,
|
|
41
|
+
element_incident_rate_delta_pp: null,
|
|
42
|
+
avg_duration_delta_pct: 5,
|
|
43
|
+
p95_duration_delta_pct: null,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const row = (t: T, metrics: ReturnType<typeof buildComparisonMetrics>, labelKey: string) =>
|
|
47
|
+
metrics.find((m) => m.label === t(labelKey))
|
|
48
|
+
|
|
49
|
+
const VOLUME: DeltaRule = { kind: "volume" }
|
|
50
|
+
const DURATION: DeltaRule = { kind: "quality", worseIfUp: true, minChange: DURATION_MIN_CHANGE_PCT }
|
|
51
|
+
const RATE: DeltaRule = { kind: "quality", worseIfUp: true, minChange: RATE_MIN_CHANGE }
|
|
52
|
+
|
|
53
|
+
describe("buildComparisonMetrics", () => {
|
|
54
|
+
it("formats the values in the view's locale and keeps the raw deltas with their rule", () => {
|
|
55
|
+
english()
|
|
56
|
+
const metrics = buildComparisonMetrics(en, kpis(), kpis({ incident_rate_pct: 3 }), delta)
|
|
57
|
+
|
|
58
|
+
expect(row(en, metrics, "aComparison.metricIncidentRate")).toEqual({
|
|
59
|
+
label: "Incidents per 100 starts",
|
|
60
|
+
before: "5.0",
|
|
61
|
+
after: "3.0",
|
|
62
|
+
delta: { value: -2, unit: "rate", rule: RATE },
|
|
63
|
+
})
|
|
64
|
+
// The count per window, the delta per day: fair across unequal windows,
|
|
65
|
+
// and a volume, so it carries no verdict rule.
|
|
66
|
+
expect(row(en, metrics, "aComparison.metricStarted")).toMatchObject({
|
|
67
|
+
label: "Starts (change per day)",
|
|
68
|
+
before: "1,240",
|
|
69
|
+
delta: { value: 10, unit: "pct", rule: VOLUME },
|
|
70
|
+
})
|
|
71
|
+
expect(row(en, metrics, "aComparison.metricAvgDuration")).toMatchObject({
|
|
72
|
+
before: "1m 0s",
|
|
73
|
+
delta: { value: 5, unit: "pct", rule: DURATION },
|
|
74
|
+
})
|
|
75
|
+
expect(row(en, metrics, "aComparison.metricP95Duration")).toMatchObject({
|
|
76
|
+
before: "3m 7s",
|
|
77
|
+
delta: { value: null },
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("speaks German: grouped digits, decimal comma, German units and labels", () => {
|
|
82
|
+
german()
|
|
83
|
+
const metrics = buildComparisonMetrics(de, kpis(), kpis({ incident_rate_pct: 3.25 }), delta)
|
|
84
|
+
expect(row(de, metrics, "aComparison.metricStarted")).toMatchObject({
|
|
85
|
+
label: "Starts (Änderung je Tag)",
|
|
86
|
+
before: "1.240",
|
|
87
|
+
})
|
|
88
|
+
expect(row(de, metrics, "aComparison.metricIncidentRate")).toMatchObject({
|
|
89
|
+
label: "Incidents je 100 Starts",
|
|
90
|
+
before: "5,0",
|
|
91
|
+
after: "3,3",
|
|
92
|
+
})
|
|
93
|
+
expect(plain(row(de, metrics, "aComparison.metricP95Duration")!.before)).toBe("3 Min. 7 s")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("renders unmeasured rates as 'not measured' with no delta, never as 0 (#327)", () => {
|
|
97
|
+
english()
|
|
98
|
+
const unmeasured = kpis({ incident_rate_pct: null })
|
|
99
|
+
const metrics = buildComparisonMetrics(en, unmeasured, unmeasured, {
|
|
100
|
+
...delta,
|
|
101
|
+
incident_rate_delta_pp: null,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(row(en, metrics, "aComparison.metricIncidentRate")).toMatchObject({
|
|
105
|
+
before: "not measured",
|
|
106
|
+
after: "not measured",
|
|
107
|
+
delta: { value: null },
|
|
108
|
+
})
|
|
109
|
+
expect(JSON.stringify(metrics)).not.toContain('"0.0"')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("renders 'nothing ended' as an unmeasured duration, never 0 ms (N78)", () => {
|
|
113
|
+
german()
|
|
114
|
+
const noneEnded = kpis({ avg_duration_sec: null, p95_duration_sec: null })
|
|
115
|
+
const metrics = buildComparisonMetrics(de, kpis(), noneEnded, {
|
|
116
|
+
...delta,
|
|
117
|
+
avg_duration_delta_pct: null,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
for (const labelKey of ["aComparison.metricAvgDuration", "aComparison.metricP95Duration"]) {
|
|
121
|
+
expect(row(de, metrics, labelKey)).toMatchObject({
|
|
122
|
+
after: "nicht gemessen",
|
|
123
|
+
delta: { value: null },
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
expect(JSON.stringify(metrics)).not.toContain("0 ms")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("shows the element incident rate only when the comparison was scoped to an element", () => {
|
|
130
|
+
english()
|
|
131
|
+
expect(
|
|
132
|
+
row(
|
|
133
|
+
en,
|
|
134
|
+
buildComparisonMetrics(en, kpis(), kpis(), delta),
|
|
135
|
+
"aComparison.metricElementIncidentRate",
|
|
136
|
+
),
|
|
137
|
+
).toBeUndefined()
|
|
138
|
+
|
|
139
|
+
const scoped = kpis({ element_incident_count: 2, element_incident_rate_pct: 5 })
|
|
140
|
+
expect(
|
|
141
|
+
row(
|
|
142
|
+
en,
|
|
143
|
+
buildComparisonMetrics(
|
|
144
|
+
en,
|
|
145
|
+
scoped,
|
|
146
|
+
kpis({ element_incident_count: 0, element_incident_rate_pct: 0 }),
|
|
147
|
+
{ ...delta, element_incident_rate_delta_pp: -5 },
|
|
148
|
+
),
|
|
149
|
+
"aComparison.metricElementIncidentRate",
|
|
150
|
+
),
|
|
151
|
+
).toMatchObject({ before: "5.0", after: "0.0", delta: { value: -5, unit: "rate", rule: RATE } })
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe("deltaVerdict — U6: volume neutral, thresholds per metric, suppressed not reliable", () => {
|
|
156
|
+
it("never judges a volume: fewer starts on a test engine are no regression", () => {
|
|
157
|
+
for (const value of [-84.7, 0, 427.4]) {
|
|
158
|
+
expect(deltaVerdict(value, VOLUME, false)).toBeUndefined()
|
|
159
|
+
expect(deltaVerdict(value, VOLUME, true)).toBeUndefined()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("judges a duration only from 5 % on, in either direction", () => {
|
|
164
|
+
expect(deltaVerdict(4.9, DURATION, false)).toBeUndefined()
|
|
165
|
+
expect(deltaVerdict(-4.9, DURATION, false)).toBeUndefined()
|
|
166
|
+
expect(deltaVerdict(5, DURATION, false)).toBe("worse")
|
|
167
|
+
expect(deltaVerdict(-5, DURATION, false)).toBe("better")
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("judges an incident rate only from 1 incident per 100 starts on", () => {
|
|
171
|
+
expect(deltaVerdict(0.2, RATE, false)).toBeUndefined()
|
|
172
|
+
expect(deltaVerdict(-0.94, RATE, false)).toBeUndefined()
|
|
173
|
+
expect(deltaVerdict(1, RATE, false)).toBe("worse")
|
|
174
|
+
expect(deltaVerdict(-6.5, RATE, false)).toBe("better")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("judges the delta as the cell shows it, so one number never gets two readings", () => {
|
|
178
|
+
german()
|
|
179
|
+
// 4.96 shows as "+5 %": judged like 5. 4.94 shows as "+4,9 %": neutral.
|
|
180
|
+
expect(plain(formatDelta(4.96, "pct"))).toBe("+5 %")
|
|
181
|
+
expect(deltaVerdict(4.96, DURATION, false)).toBe("worse")
|
|
182
|
+
expect(deltaVerdict(-4.96, DURATION, false)).toBe("better")
|
|
183
|
+
expect(plain(formatDelta(4.94, "pct"))).toBe("+4,9 %")
|
|
184
|
+
expect(deltaVerdict(4.94, DURATION, false)).toBeUndefined()
|
|
185
|
+
// Half away from zero, like Intl: -0.95 shows as "-1" and is judged.
|
|
186
|
+
expect(formatDelta(-0.95, "rate")).toBe("-1")
|
|
187
|
+
expect(deltaVerdict(-0.95, RATE, false)).toBe("better")
|
|
188
|
+
expect(formatDelta(0.96, "rate")).toBe("+1")
|
|
189
|
+
expect(deltaVerdict(0.96, RATE, false)).toBe("worse")
|
|
190
|
+
// A change that rounds to zero shows no sign and no verdict.
|
|
191
|
+
expect(formatDelta(-0.04, "rate")).toBe("0")
|
|
192
|
+
expect(deltaVerdict(-0.04, RATE, false)).toBeUndefined()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("follows the metric's direction: a rule where up is good flips the verdict", () => {
|
|
196
|
+
const upIsGood: DeltaRule = { kind: "quality", worseIfUp: false, minChange: 1 }
|
|
197
|
+
expect(deltaVerdict(3, upIsGood, false)).toBe("better")
|
|
198
|
+
expect(deltaVerdict(-3, upIsGood, false)).toBe("worse")
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("calls every quality delta of a suppressed comparison not reliable, below threshold too", () => {
|
|
202
|
+
expect(deltaVerdict(-100, DURATION, true)).toBe("notReliable")
|
|
203
|
+
expect(deltaVerdict(0.1, RATE, true)).toBe("notReliable")
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("has nothing to judge for a missing delta (a zero baseline)", () => {
|
|
207
|
+
expect(deltaVerdict(null, DURATION, false)).toBeUndefined()
|
|
208
|
+
expect(deltaVerdict(null, RATE, true)).toBeUndefined()
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe("formatDelta", () => {
|
|
213
|
+
it("signs relative changes as percent and rate changes in the rate's own unit", () => {
|
|
214
|
+
english()
|
|
215
|
+
expect(formatDelta(12.5, "pct")).toBe("+12.5%")
|
|
216
|
+
expect(formatDelta(-84.66, "pct")).toBe("-84.7%")
|
|
217
|
+
expect(formatDelta(0.24, "rate")).toBe("+0.2")
|
|
218
|
+
expect(formatDelta(0, "rate")).toBe("0")
|
|
219
|
+
expect(formatDelta(null, "pct")).toBe("—")
|
|
220
|
+
german()
|
|
221
|
+
expect(plain(formatDelta(12.5, "pct"))).toBe("+12,5 %")
|
|
222
|
+
expect(formatDelta(-2, "rate")).toBe("-2")
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe("deltaFacts", () => {
|
|
227
|
+
it("names starts per day and leaves a never-measured delta null — never a 0", () => {
|
|
228
|
+
expect(deltaFacts(delta, false)).toEqual({
|
|
229
|
+
startedPerDayDeltaPct: 10,
|
|
230
|
+
incidentRateDeltaPp: -2,
|
|
231
|
+
elementIncidentRateDeltaPp: null,
|
|
232
|
+
avgDurationDeltaPct: 5,
|
|
233
|
+
p95DurationDeltaPct: null,
|
|
234
|
+
suppressed: false,
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
AlertDescription,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
cn,
|
|
14
|
+
TONE_ICON,
|
|
15
|
+
} from "@miragon/mcp-toolkit-ui"
|
|
16
|
+
import { CircleAlert, TrendingDown, TrendingUp } from "lucide-react"
|
|
17
|
+
import {
|
|
18
|
+
Icon,
|
|
19
|
+
formatDuration,
|
|
20
|
+
formatNumber,
|
|
21
|
+
formatPercent,
|
|
22
|
+
} from "@miragon/pulse-widget-shell/widgets"
|
|
23
|
+
import type { CompareKpiDelta, CompareKpis } from "@miragon/pulse-analytics-client"
|
|
24
|
+
import { useT, type T } from "../messages/use-t.js"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The on-screen deltas as hand-off facts (#338) — null deltas (a zero
|
|
28
|
+
* baseline) are left out, like the em-dash on screen, never sent as 0.
|
|
29
|
+
*/
|
|
30
|
+
export function deltaFacts(delta: CompareKpiDelta, suppressed: boolean) {
|
|
31
|
+
return {
|
|
32
|
+
// Starts compare PER DAY, so a clamped (shorter) window compares fairly.
|
|
33
|
+
startedPerDayDeltaPct: delta.started_per_day_delta_pct,
|
|
34
|
+
incidentRateDeltaPp: delta.incident_rate_delta_pp,
|
|
35
|
+
// Non-null only when the comparison was scoped to an element.
|
|
36
|
+
elementIncidentRateDeltaPp: delta.element_incident_rate_delta_pp,
|
|
37
|
+
avgDurationDeltaPct: delta.avg_duration_delta_pct,
|
|
38
|
+
p95DurationDeltaPct: delta.p95_duration_delta_pct,
|
|
39
|
+
suppressed,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How a metric's change is judged (U6). Only a QUALITY metric earns a verdict,
|
|
45
|
+
* and only from its own threshold on, so rounding jitter stays neutral:
|
|
46
|
+
*
|
|
47
|
+
* - `volume` (starts): never better or worse. Fewer starts on a test engine,
|
|
48
|
+
* or traffic moving to a new version, is no regression.
|
|
49
|
+
* - `quality`: "worse" or "better" once the change reaches `minChange`, in
|
|
50
|
+
* the delta's own unit (percent for durations, incidents per 100 starts
|
|
51
|
+
* for the rates).
|
|
52
|
+
*/
|
|
53
|
+
export type DeltaRule =
|
|
54
|
+
{ kind: "volume" } | { kind: "quality"; worseIfUp: boolean; minChange: number }
|
|
55
|
+
|
|
56
|
+
/** A duration changes noticeably from 5 % on (relative). */
|
|
57
|
+
export const DURATION_MIN_CHANGE_PCT = 5
|
|
58
|
+
/** An incident rate changes noticeably from 1 incident per 100 starts on (absolute). */
|
|
59
|
+
export const RATE_MIN_CHANGE = 1
|
|
60
|
+
|
|
61
|
+
export type DeltaVerdict = "worse" | "better" | "notReliable"
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A delta as its cell shows it: one decimal, rounded half away from zero
|
|
65
|
+
* like `Intl.NumberFormat`. The verdict judges this number, so the same
|
|
66
|
+
* "+5 %" on screen is never neutral in one row and "schlechter" in another.
|
|
67
|
+
*/
|
|
68
|
+
function shownDelta(value: number): number {
|
|
69
|
+
return Math.sign(value) * (Math.round(Math.abs(value) * 10) / 10)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The verdict on one delta, or `undefined` for none: a missing (null) delta,
|
|
74
|
+
* a volume, or a change below the metric's threshold. The threshold applies
|
|
75
|
+
* to the delta as shown ({@link shownDelta}). In a suppressed comparison (a
|
|
76
|
+
* side below `minBucketSize`) every quality delta is "not reliable": the
|
|
77
|
+
* screen must not judge what the model is told is noise.
|
|
78
|
+
*/
|
|
79
|
+
export function deltaVerdict(
|
|
80
|
+
value: number | null,
|
|
81
|
+
rule: DeltaRule,
|
|
82
|
+
suppressed: boolean,
|
|
83
|
+
): DeltaVerdict | undefined {
|
|
84
|
+
if (value === null || rule.kind === "volume") return undefined
|
|
85
|
+
if (suppressed) return "notReliable"
|
|
86
|
+
const shown = shownDelta(value)
|
|
87
|
+
if (Math.abs(shown) < rule.minChange) return undefined
|
|
88
|
+
const bad = rule.worseIfUp ? shown > 0 : shown < 0
|
|
89
|
+
return bad ? "worse" : "better"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `pct`: a relative change in percent (starts per day, durations);
|
|
94
|
+
* `rate`: an absolute change of incidents per 100 starts, the unit of the
|
|
95
|
+
* values themselves.
|
|
96
|
+
*/
|
|
97
|
+
type DeltaUnit = "pct" | "rate"
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A signed delta in the view's locale ("+12,5 %", "−0,4"), or an em-dash when
|
|
101
|
+
* null. It formats {@link shownDelta}, the number the verdict judges.
|
|
102
|
+
*/
|
|
103
|
+
export function formatDelta(value: number | null, unit: DeltaUnit): string {
|
|
104
|
+
const shown = value === null ? null : shownDelta(value)
|
|
105
|
+
if (unit === "pct") return formatPercent(shown, { signed: true })
|
|
106
|
+
return formatNumber(shown, { signDisplay: "exceptZero", maximumFractionDigits: 1 })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The compare KPIs the table reads. Every rate and duration is nullable — a
|
|
111
|
+
* window in which nothing ended has no duration, one in which nothing started
|
|
112
|
+
* no rate — and a null renders "not measured", never "0,0" or "0 ms".
|
|
113
|
+
*/
|
|
114
|
+
type ComparableKpis = Pick<CompareKpis, "instance_count"> & {
|
|
115
|
+
incident_rate_pct: number | null
|
|
116
|
+
/** Non-null exactly when the comparison was scoped to an element. */
|
|
117
|
+
element_incident_count: number | null
|
|
118
|
+
element_incident_rate_pct: number | null
|
|
119
|
+
avg_duration_sec: number | null
|
|
120
|
+
p95_duration_sec: number | null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Incidents per 100 starts, one decimal: "6,6" / "6.6" (no %: a rate may pass 100). */
|
|
124
|
+
const rateValue = (n: number | null) =>
|
|
125
|
+
n === null ? null : formatNumber(n, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
|
|
126
|
+
const durationValue = (sec: number | null) => (sec === null ? null : formatDuration(sec * 1000))
|
|
127
|
+
|
|
128
|
+
/** A single metric row: label, the two compared values, and the RAW delta with its rule. */
|
|
129
|
+
export type ComparisonMetric = {
|
|
130
|
+
label: string
|
|
131
|
+
before: string
|
|
132
|
+
after: string
|
|
133
|
+
delta: { value: number | null; unit: DeltaUnit; rule: DeltaRule }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const VOLUME: DeltaRule = { kind: "volume" }
|
|
137
|
+
/** Incidents per 100 starts: up is worse, from 1 per 100 starts on (also the dashboard's trend). */
|
|
138
|
+
export const RATE_UP_IS_WORSE: DeltaRule = {
|
|
139
|
+
kind: "quality",
|
|
140
|
+
worseIfUp: true,
|
|
141
|
+
minChange: RATE_MIN_CHANGE,
|
|
142
|
+
}
|
|
143
|
+
const DURATION_UP_IS_WORSE: DeltaRule = {
|
|
144
|
+
kind: "quality",
|
|
145
|
+
worseIfUp: true,
|
|
146
|
+
minChange: DURATION_MIN_CHANGE_PCT,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The one metric table shared by the cluster / version / engine compare
|
|
151
|
+
* widgets — same `CompareKpis`/`CompareKpiDelta` core, so the rows are
|
|
152
|
+
* defined once. Deltas stay numeric here; formatting and the verdict happen
|
|
153
|
+
* at render time in {@link ComparisonCard}.
|
|
154
|
+
*/
|
|
155
|
+
const COMPARE_METRICS: Array<{
|
|
156
|
+
labelKey: string
|
|
157
|
+
/** Formatted value, or null when the KPI is not measured for this comparison. */
|
|
158
|
+
value: (k: ComparableKpis) => string | null
|
|
159
|
+
delta: (d: CompareKpiDelta) => number | null
|
|
160
|
+
unit: DeltaUnit
|
|
161
|
+
rule: DeltaRule
|
|
162
|
+
/** Shown only when the comparison was scoped to an element. */
|
|
163
|
+
elementOnly?: boolean
|
|
164
|
+
}> = [
|
|
165
|
+
{
|
|
166
|
+
// The count per window; the delta compares starts PER DAY, so a clamped
|
|
167
|
+
// (shorter) window still compares fairly. A volume: no verdict.
|
|
168
|
+
labelKey: "aComparison.metricStarted",
|
|
169
|
+
value: (k) => formatNumber(k.instance_count),
|
|
170
|
+
delta: (d) => d.started_per_day_delta_pct,
|
|
171
|
+
unit: "pct",
|
|
172
|
+
rule: VOLUME,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
labelKey: "aComparison.metricIncidentRate",
|
|
176
|
+
value: (k) => rateValue(k.incident_rate_pct),
|
|
177
|
+
delta: (d) => d.incident_rate_delta_pp,
|
|
178
|
+
unit: "rate",
|
|
179
|
+
rule: RATE_UP_IS_WORSE,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
labelKey: "aComparison.metricElementIncidentRate",
|
|
183
|
+
value: (k) => rateValue(k.element_incident_rate_pct),
|
|
184
|
+
delta: (d) => d.element_incident_rate_delta_pp,
|
|
185
|
+
unit: "rate",
|
|
186
|
+
rule: RATE_UP_IS_WORSE,
|
|
187
|
+
elementOnly: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
labelKey: "aComparison.metricAvgDuration",
|
|
191
|
+
value: (k) => durationValue(k.avg_duration_sec),
|
|
192
|
+
delta: (d) => d.avg_duration_delta_pct,
|
|
193
|
+
unit: "pct",
|
|
194
|
+
rule: DURATION_UP_IS_WORSE,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
labelKey: "aComparison.metricP95Duration",
|
|
198
|
+
value: (k) => durationValue(k.p95_duration_sec),
|
|
199
|
+
delta: (d) => d.p95_duration_delta_pct,
|
|
200
|
+
unit: "pct",
|
|
201
|
+
rule: DURATION_UP_IS_WORSE,
|
|
202
|
+
},
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
/** Resolve the shared metric rows for one baseline/other KPI pair. */
|
|
206
|
+
export function buildComparisonMetrics(
|
|
207
|
+
t: T,
|
|
208
|
+
before: ComparableKpis,
|
|
209
|
+
after: ComparableKpis,
|
|
210
|
+
delta: CompareKpiDelta,
|
|
211
|
+
): ComparisonMetric[] {
|
|
212
|
+
const unavailable = t("aComparison.valueUnavailable")
|
|
213
|
+
const elementScoped =
|
|
214
|
+
before.element_incident_count !== null || after.element_incident_count !== null
|
|
215
|
+
return COMPARE_METRICS.filter((m) => !m.elementOnly || elementScoped).map((m) => ({
|
|
216
|
+
label: t(m.labelKey),
|
|
217
|
+
before: m.value(before) ?? unavailable,
|
|
218
|
+
after: m.value(after) ?? unavailable,
|
|
219
|
+
delta: { value: m.delta(delta), unit: m.unit, rule: m.rule },
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const VERDICT_KEY: Record<DeltaVerdict, string> = {
|
|
224
|
+
worse: "aComparison.deltaWorse",
|
|
225
|
+
better: "aComparison.deltaBetter",
|
|
226
|
+
notReliable: "aComparison.deltaNotReliable",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A single delta cell. The number stays neutral (CI: a state is an icon next
|
|
231
|
+
* to black text, never a coloured digit); a verdict shows as a direction
|
|
232
|
+
* icon in its tone plus the word ("schlechter", "besser"), so it never rests
|
|
233
|
+
* on colour alone. A volume or a change below the threshold shows the signed
|
|
234
|
+
* number only; a suppressed comparison mutes it with "nicht belastbar".
|
|
235
|
+
*/
|
|
236
|
+
function DeltaCell({
|
|
237
|
+
delta,
|
|
238
|
+
suppressed,
|
|
239
|
+
t,
|
|
240
|
+
}: {
|
|
241
|
+
delta: ComparisonMetric["delta"]
|
|
242
|
+
suppressed: boolean
|
|
243
|
+
t: T
|
|
244
|
+
}) {
|
|
245
|
+
const verdict = deltaVerdict(delta.value, delta.rule, suppressed)
|
|
246
|
+
const word = verdict ? t(VERDICT_KEY[verdict]) : undefined
|
|
247
|
+
const judged = verdict === "worse" || verdict === "better"
|
|
248
|
+
return (
|
|
249
|
+
<TableCell data-verdict={verdict ?? "none"}>
|
|
250
|
+
<span
|
|
251
|
+
className={cn(
|
|
252
|
+
"inline-flex items-center gap-1.5 tabular-nums",
|
|
253
|
+
verdict === "notReliable" && "text-muted-foreground",
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
{judged && (
|
|
257
|
+
<Icon
|
|
258
|
+
icon={(delta.value ?? 0) > 0 ? TrendingUp : TrendingDown}
|
|
259
|
+
dense
|
|
260
|
+
className={TONE_ICON[verdict === "worse" ? "danger" : "success"]}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
{formatDelta(delta.value, delta.unit)}
|
|
264
|
+
{word && <span className="text-muted-foreground text-small">{word}</span>}
|
|
265
|
+
</span>
|
|
266
|
+
</TableCell>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Neutral "no data" / "incomplete" treatment — destructive is reserved for errors. */
|
|
271
|
+
export function ComparisonEmptyState({ children }: { children: ReactNode }) {
|
|
272
|
+
return (
|
|
273
|
+
<Alert>
|
|
274
|
+
<AlertDescription>{children}</AlertDescription>
|
|
275
|
+
</Alert>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The "too little data" chip of a suppressed comparison: a data-quality note,
|
|
281
|
+
* not an error, so it stays neutral (outline + icon) like the deltas it
|
|
282
|
+
* explains. A plain outline chip, not the toolkit `Badge`: that one forces
|
|
283
|
+
* its icons to 12 px, and the icon keeps the kit's 16 px like every other
|
|
284
|
+
* icon in the view (CI §11, `scanIconSizes`).
|
|
285
|
+
*/
|
|
286
|
+
export function SuppressedBadge({ children }: { children: ReactNode }) {
|
|
287
|
+
return (
|
|
288
|
+
<span
|
|
289
|
+
className="text-foreground text-small inline-flex w-fit shrink-0 items-center gap-1 rounded-sm border px-2 py-0.5 font-medium whitespace-nowrap"
|
|
290
|
+
data-suppressed=""
|
|
291
|
+
>
|
|
292
|
+
<Icon icon={CircleAlert} dense />
|
|
293
|
+
{children}
|
|
294
|
+
</span>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Shared comparison card used by the cluster / version / engine compare widgets.
|
|
300
|
+
*
|
|
301
|
+
* Callers resolve their own KPI buckets (`buildComparisonMetrics`) and pass in
|
|
302
|
+
* the metric rows plus the heading, badges, the reference frame (`meta`, a
|
|
303
|
+
* `ViewMeta`) and the two comparison column labels.
|
|
304
|
+
*/
|
|
305
|
+
export function ComparisonCard({
|
|
306
|
+
title,
|
|
307
|
+
badges,
|
|
308
|
+
meta,
|
|
309
|
+
beforeLabel,
|
|
310
|
+
afterLabel,
|
|
311
|
+
tableLabel,
|
|
312
|
+
metrics,
|
|
313
|
+
suppressed,
|
|
314
|
+
actions,
|
|
315
|
+
note,
|
|
316
|
+
}: {
|
|
317
|
+
title: string
|
|
318
|
+
badges: ReactNode
|
|
319
|
+
/** The quiet line under the title: period, engines, as-of time. */
|
|
320
|
+
meta?: ReactNode
|
|
321
|
+
beforeLabel: ReactNode
|
|
322
|
+
afterLabel: ReactNode
|
|
323
|
+
tableLabel: string
|
|
324
|
+
metrics: ComparisonMetric[]
|
|
325
|
+
/** A side is below `minBucketSize`: every quality delta reads "not reliable". */
|
|
326
|
+
suppressed: boolean
|
|
327
|
+
/** Optional header action slot (e.g. an AI affordance), right-aligned. */
|
|
328
|
+
actions?: ReactNode
|
|
329
|
+
/** Optional caveat under the table, e.g. why a metric reads "not measured". */
|
|
330
|
+
note?: ReactNode
|
|
331
|
+
}) {
|
|
332
|
+
const t = useT()
|
|
333
|
+
return (
|
|
334
|
+
<Card>
|
|
335
|
+
<CardContent>
|
|
336
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
337
|
+
<h2 className="text-foreground text-section">{title}</h2>
|
|
338
|
+
{badges}
|
|
339
|
+
{actions && <div className="ml-auto flex items-center gap-2">{actions}</div>}
|
|
340
|
+
</div>
|
|
341
|
+
{meta && <div className="mt-1">{meta}</div>}
|
|
342
|
+
|
|
343
|
+
<Table className="mt-4" aria-label={tableLabel}>
|
|
344
|
+
<TableHeader>
|
|
345
|
+
<TableRow>
|
|
346
|
+
<TableHead scope="col">{t("aComparison.metricColumnHeader")}</TableHead>
|
|
347
|
+
<TableHead scope="col">{beforeLabel}</TableHead>
|
|
348
|
+
<TableHead scope="col">{afterLabel}</TableHead>
|
|
349
|
+
<TableHead scope="col">{t("aComparison.deltaColumnHeader")}</TableHead>
|
|
350
|
+
</TableRow>
|
|
351
|
+
</TableHeader>
|
|
352
|
+
<TableBody>
|
|
353
|
+
{metrics.map((m) => (
|
|
354
|
+
<TableRow key={m.label}>
|
|
355
|
+
<TableCell>{m.label}</TableCell>
|
|
356
|
+
<TableCell className="tabular-nums">{m.before}</TableCell>
|
|
357
|
+
<TableCell className="tabular-nums">{m.after}</TableCell>
|
|
358
|
+
<DeltaCell delta={m.delta} suppressed={suppressed} t={t} />
|
|
359
|
+
</TableRow>
|
|
360
|
+
))}
|
|
361
|
+
</TableBody>
|
|
362
|
+
</Table>
|
|
363
|
+
{note && (
|
|
364
|
+
<Alert className="mt-4">
|
|
365
|
+
<AlertDescription>{note}</AlertDescription>
|
|
366
|
+
</Alert>
|
|
367
|
+
)}
|
|
368
|
+
</CardContent>
|
|
369
|
+
</Card>
|
|
370
|
+
)
|
|
371
|
+
}
|