@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,94 @@
|
|
|
1
|
+
import { WidgetShell, formatDuration, formatNumber } from "@miragon/pulse-widget-shell/widgets"
|
|
2
|
+
import { KpiGrid, KpiGridSkeleton, type KpiCell } from "@miragon/mcp-toolkit-ui"
|
|
3
|
+
import { useDashboardSelfFetch, type DashboardScopeProps, type DashboardView } from "./lib.js"
|
|
4
|
+
import { QueryGate } from "../query-gate.js"
|
|
5
|
+
import { useT, type T } from "../../messages/use-t.js"
|
|
6
|
+
import { RATE_UP_IS_WORSE, deltaVerdict, type DeltaVerdict } from "../comparison-shared.js"
|
|
7
|
+
|
|
8
|
+
/** Incidents per 100 starts, one decimal: "3,4" / "3.4" (no %: a rate may pass 100). */
|
|
9
|
+
const rate = (value: number | null) => formatNumber(value, { maximumFractionDigits: 1 })
|
|
10
|
+
|
|
11
|
+
const VERDICT_KEY: Record<DeltaVerdict, string> = {
|
|
12
|
+
worse: "aComparison.deltaWorse",
|
|
13
|
+
better: "aComparison.deltaBetter",
|
|
14
|
+
notReliable: "aComparison.deltaNotReliable",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The incident rate judged against the period before (U14/U17): a rate on its
|
|
19
|
+
* own is no verdict, so the cell itself carries no tone. Its trend line names
|
|
20
|
+
* the previous period's rate; only a change from the comparisons' threshold
|
|
21
|
+
* on (1 incident per 100 starts) is "worse" (danger arrow) or "better"
|
|
22
|
+
* (success arrow), a smaller one is flat and neutral, and over fewer starts
|
|
23
|
+
* than the comparison floor it reads "not reliable" without a colour.
|
|
24
|
+
*
|
|
25
|
+
* Only with the process held fixed: over several processes the rate is a
|
|
26
|
+
* total over their mix, so it moves when the mix moves while no process
|
|
27
|
+
* changed, and can even invert the per-process truth (Simpson's paradox, the
|
|
28
|
+
* reason every comparison requires a `processDefinitionKey`). An unscoped
|
|
29
|
+
* dashboard names the previous rate as a plain reference: no verdict, no
|
|
30
|
+
* direction, no colour.
|
|
31
|
+
*/
|
|
32
|
+
export function incidentRateTrend(
|
|
33
|
+
t: T,
|
|
34
|
+
data: Pick<DashboardView, "incidentRatePct" | "previous" | "processDefinitionKey">,
|
|
35
|
+
): Pick<KpiCell, "trend" | "trendDirection" | "trendTone"> {
|
|
36
|
+
const previous = data.previous
|
|
37
|
+
if (!previous || data.incidentRatePct === null) return {}
|
|
38
|
+
if (previous.incidentRatePct === null) return { trend: t("aExecPerf.previousNoStarts") }
|
|
39
|
+
const value = rate(previous.incidentRatePct)
|
|
40
|
+
if (!data.processDefinitionKey) return { trend: t("aExecPerf.previous", { value }) }
|
|
41
|
+
const change = data.incidentRatePct - previous.incidentRatePct
|
|
42
|
+
const verdict = deltaVerdict(change, RATE_UP_IS_WORSE, previous.suppressed)
|
|
43
|
+
if (!verdict) {
|
|
44
|
+
return {
|
|
45
|
+
trend: t("aExecPerf.previous", { value }),
|
|
46
|
+
trendDirection: "flat",
|
|
47
|
+
trendTone: "neutral",
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
trend: t("aExecPerf.previousJudged", { value, verdict: t(VERDICT_KEY[verdict]) }),
|
|
52
|
+
trendDirection: change > 0 ? "up" : change < 0 ? "down" : "flat",
|
|
53
|
+
trendTone: verdict === "worse" ? "danger" : verdict === "better" ? "success" : "neutral",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ExecutionPerformanceKpi({
|
|
58
|
+
data: initialData,
|
|
59
|
+
processDefinitionKey,
|
|
60
|
+
period,
|
|
61
|
+
engine,
|
|
62
|
+
}: { data: DashboardView | null } & DashboardScopeProps) {
|
|
63
|
+
const t = useT()
|
|
64
|
+
const fallbackQuery = useDashboardSelfFetch(initialData, { processDefinitionKey, period, engine })
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<QueryGate
|
|
68
|
+
initialData={initialData}
|
|
69
|
+
query={fallbackQuery}
|
|
70
|
+
skeleton={<KpiGridSkeleton cells={4} boxed />}
|
|
71
|
+
>
|
|
72
|
+
{(data) => (
|
|
73
|
+
<WidgetShell>
|
|
74
|
+
<KpiGrid
|
|
75
|
+
boxed
|
|
76
|
+
header={{ label: t("aExecPerf.headerLabel") }}
|
|
77
|
+
cells={[
|
|
78
|
+
{ label: t("aExecPerf.avgDuration"), value: formatDuration(data.avgDurationMs) },
|
|
79
|
+
{ label: t("aExecPerf.median"), value: formatDuration(data.medianDurationMs) },
|
|
80
|
+
{ label: t("aExecPerf.p95"), value: formatDuration(data.p95DurationMs) },
|
|
81
|
+
{
|
|
82
|
+
label: t("aExecPerf.incidentRate"),
|
|
83
|
+
// Incidents per 100 starts: one instance may carry several, so
|
|
84
|
+
// the rate can pass 100 and reads as a number, not a share.
|
|
85
|
+
value: rate(data.incidentRatePct),
|
|
86
|
+
...incidentRateTrend(t, data),
|
|
87
|
+
},
|
|
88
|
+
]}
|
|
89
|
+
/>
|
|
90
|
+
</WidgetShell>
|
|
91
|
+
)}
|
|
92
|
+
</QueryGate>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HandOffButton,
|
|
3
|
+
ViewMeta,
|
|
4
|
+
WidgetShell,
|
|
5
|
+
formatLookback,
|
|
6
|
+
formatNumber,
|
|
7
|
+
} from "@miragon/pulse-widget-shell/widgets"
|
|
8
|
+
import type { AnalyticsDashboardData } from "@miragon/pulse-analytics-client"
|
|
9
|
+
import { KpiGrid, KpiGridSkeleton, WidgetHeader } from "@miragon/mcp-toolkit-ui"
|
|
10
|
+
import { useDashboardSelfFetch, type DashboardScopeProps, type DashboardView } from "./lib.js"
|
|
11
|
+
import { QueryGate } from "../query-gate.js"
|
|
12
|
+
import { useT } from "../../messages/use-t.js"
|
|
13
|
+
import { engineIdsOf, useHandOff, type HandOff } from "../hand-off.js"
|
|
14
|
+
import { enginesMeta } from "../view-meta.js"
|
|
15
|
+
import { processNameOf } from "../../bpmn-names.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Assess the dashboard on screen — its echoed scope (process, period,
|
|
19
|
+
* engines) as ids, its headline numbers as facts named for what they measure:
|
|
20
|
+
* window flows (`…InWindow`; incidents, not failed instances) apart from the
|
|
21
|
+
* live gauges (`…Now`). A figure that was not measured is null and left out,
|
|
22
|
+
* never stated as 0 (#336).
|
|
23
|
+
*/
|
|
24
|
+
export function executionSummaryHandOff(data: AnalyticsDashboardData): HandOff {
|
|
25
|
+
return {
|
|
26
|
+
intent: "askAi.executionSummary",
|
|
27
|
+
ids: {
|
|
28
|
+
engine: engineIdsOf(data.engines),
|
|
29
|
+
processDefinitionKey: data.processDefinitionKey ?? undefined,
|
|
30
|
+
period: data.period,
|
|
31
|
+
},
|
|
32
|
+
facts: {
|
|
33
|
+
startedInWindow: data.totalCount,
|
|
34
|
+
completedInWindow: data.completedCount,
|
|
35
|
+
incidentsCreatedInWindow: data.incidentsCreated,
|
|
36
|
+
incidentsResolvedInWindow: data.incidentsResolved,
|
|
37
|
+
incidentRatePct: data.incidentRatePct,
|
|
38
|
+
avgDurationMs: data.avgDurationMs,
|
|
39
|
+
p95DurationMs: data.p95DurationMs,
|
|
40
|
+
runningNow: data.runningNow,
|
|
41
|
+
openIncidentsNow: data.openIncidentsNow,
|
|
42
|
+
},
|
|
43
|
+
tools: ["analytics_analyze_process_performance", "analytics_find_failed_instances"],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ExecutionSummaryKpi({
|
|
48
|
+
data: initialData,
|
|
49
|
+
processDefinitionKey,
|
|
50
|
+
period,
|
|
51
|
+
engine,
|
|
52
|
+
}: { data: DashboardView | null } & DashboardScopeProps) {
|
|
53
|
+
const fallbackQuery = useDashboardSelfFetch(initialData, { processDefinitionKey, period, engine })
|
|
54
|
+
const t = useT()
|
|
55
|
+
const { ask } = useHandOff()
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<QueryGate
|
|
59
|
+
initialData={initialData}
|
|
60
|
+
query={fallbackQuery}
|
|
61
|
+
header={<WidgetHeader title={t("aExecSummary.title")} />}
|
|
62
|
+
skeleton={<KpiGridSkeleton cells={5} boxed />}
|
|
63
|
+
>
|
|
64
|
+
{(data) => (
|
|
65
|
+
<WidgetShell>
|
|
66
|
+
<WidgetHeader
|
|
67
|
+
title={t("aExecSummary.title")}
|
|
68
|
+
sub={
|
|
69
|
+
<ViewMeta
|
|
70
|
+
subject={
|
|
71
|
+
data.processDefinitionKey &&
|
|
72
|
+
(processNameOf(data.names, data.processDefinitionKey) ??
|
|
73
|
+
data.processDefinitionKey)
|
|
74
|
+
}
|
|
75
|
+
period={formatLookback(data.period)}
|
|
76
|
+
engines={enginesMeta(data.engines, data.reportingEngines)}
|
|
77
|
+
asOf={data.asOf}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
actions={
|
|
81
|
+
<HandOffButton
|
|
82
|
+
action="assess"
|
|
83
|
+
variant="primary"
|
|
84
|
+
prompt={ask(executionSummaryHandOff(data))}
|
|
85
|
+
/>
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
{/* Counts of what happened stay neutral (CI §3.3, U14): starts,
|
|
89
|
+
completions, new incidents and what runs now are volumes. The
|
|
90
|
+
one state is an incident open right now, its danger dot next to
|
|
91
|
+
the label; the digit itself is never coloured. */}
|
|
92
|
+
<KpiGrid
|
|
93
|
+
boxed
|
|
94
|
+
header={{ label: t("aExecSummary.headerExecutionSummary") }}
|
|
95
|
+
cells={[
|
|
96
|
+
{ label: t("aExecSummary.cellStarted"), value: formatNumber(data.totalCount) },
|
|
97
|
+
{
|
|
98
|
+
label: t("aExecSummary.cellCompleted"),
|
|
99
|
+
value: formatNumber(data.completedCount),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: t("aExecSummary.cellIncidentsCreated"),
|
|
103
|
+
value: formatNumber(data.incidentsCreated),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// A live gauge without a series is "not reported": formatNumber
|
|
107
|
+
// renders it as an em-dash, never as a plausible 0.
|
|
108
|
+
label: t("aExecSummary.cellRunningNow"),
|
|
109
|
+
value: formatNumber(data.runningNow),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: t("aExecSummary.cellOpenIncidentsNow"),
|
|
113
|
+
value: formatNumber(data.openIncidentsNow),
|
|
114
|
+
tone: (data.openIncidentsNow ?? 0) > 0 ? "danger" : undefined,
|
|
115
|
+
},
|
|
116
|
+
]}
|
|
117
|
+
/>
|
|
118
|
+
</WidgetShell>
|
|
119
|
+
)}
|
|
120
|
+
</QueryGate>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useToolQuery } from "@miragon/mcp-toolkit-ui"
|
|
2
|
+
import type {
|
|
3
|
+
AnalyticsDashboardData,
|
|
4
|
+
EngineFilterInput,
|
|
5
|
+
Period,
|
|
6
|
+
} from "@miragon/pulse-analytics-client"
|
|
7
|
+
import { ANALYTICS_DASHBOARD_DATA } from "../../tool-names.js"
|
|
8
|
+
import type { Named } from "../../bpmn-names.js"
|
|
9
|
+
|
|
10
|
+
/** The dashboard payload the widgets read: the figures plus the names behind their ids. */
|
|
11
|
+
export type DashboardView = Named<AnalyticsDashboardData>
|
|
12
|
+
|
|
13
|
+
/** The cell props every split dashboard widget takes — the scope its self-fetch reads. */
|
|
14
|
+
export interface DashboardScopeProps {
|
|
15
|
+
processDefinitionKey?: string
|
|
16
|
+
period?: Period
|
|
17
|
+
/** Configured engine id(s); omitted = every configured engine (the feed resolves it). */
|
|
18
|
+
engine?: EngineFilterInput
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Centralised so all four split dashboard widgets share one self-fetch contract.
|
|
22
|
+
// The cache key includes every scope prop so per-cell instances (e.g. one tab
|
|
23
|
+
// per period or per engine) don't collide on a single shared cache entry — and
|
|
24
|
+
// an engine-scoped cell never reads a fleet-wide entry. Self-fetches the
|
|
25
|
+
// app-only *_data feed — calling the show_* tool from inside the iframe is
|
|
26
|
+
// host-defined behavior (hosts honoring resultCanProduceWidget may render a
|
|
27
|
+
// second widget per refresh).
|
|
28
|
+
export function useDashboardSelfFetch(
|
|
29
|
+
initialData: DashboardView | null,
|
|
30
|
+
props: DashboardScopeProps,
|
|
31
|
+
) {
|
|
32
|
+
const { processDefinitionKey, period, engine } = props
|
|
33
|
+
const queryArgs: DashboardScopeProps = {}
|
|
34
|
+
if (processDefinitionKey) queryArgs.processDefinitionKey = processDefinitionKey
|
|
35
|
+
if (period) queryArgs.period = period
|
|
36
|
+
if (engine !== undefined) queryArgs.engine = engine
|
|
37
|
+
return useToolQuery<DashboardView>(
|
|
38
|
+
["analytics:dashboard", processDefinitionKey ?? null, period ?? null, engine ?? null],
|
|
39
|
+
ANALYTICS_DASHBOARD_DATA,
|
|
40
|
+
queryArgs,
|
|
41
|
+
{ enabled: !initialData },
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
3
|
+
import type { ComponentType } from "react"
|
|
4
|
+
import { cleanup, render, screen } from "@testing-library/react"
|
|
5
|
+
import { LocaleProvider, queryClient } from "@miragon/mcp-toolkit-ui"
|
|
6
|
+
import { WidgetFixtureHost } from "@miragon/mcp-toolkit-ui/app"
|
|
7
|
+
import { AnalyticsBpmnHeatmap, type AnalyticsBpmnHeatmapData } from "./bpmn-heatmap.js"
|
|
8
|
+
|
|
9
|
+
// The kit's viewer meets a diagram bpmn-js cannot import: the import-error
|
|
10
|
+
// alert is what this suite reads.
|
|
11
|
+
const viewer = vi.hoisted(() => ({
|
|
12
|
+
MockViewer: class {
|
|
13
|
+
importXML(): Promise<never> {
|
|
14
|
+
return Promise.reject(new Error("unparsable content <bpmn:foo> detected"))
|
|
15
|
+
}
|
|
16
|
+
get(): unknown {
|
|
17
|
+
return { zoom: () => 1 }
|
|
18
|
+
}
|
|
19
|
+
destroy() {}
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
22
|
+
vi.mock("bpmn-js/lib/NavigatedViewer", () => ({ default: viewer.MockViewer }))
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup()
|
|
26
|
+
queryClient.clear()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const HEATMAP: AnalyticsBpmnHeatmapData = {
|
|
30
|
+
processDefinitionKey: "order",
|
|
31
|
+
period: "14d",
|
|
32
|
+
engines: ["prod-a"],
|
|
33
|
+
frequency: { Task_A: 12 },
|
|
34
|
+
durationSec: { Task_A: 3.5 },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderIn(language: "de" | "en") {
|
|
38
|
+
return render(
|
|
39
|
+
<LocaleProvider locale={language}>
|
|
40
|
+
<WidgetFixtureHost
|
|
41
|
+
widget={AnalyticsBpmnHeatmap as unknown as ComponentType<Record<string, unknown>>}
|
|
42
|
+
data={HEATMAP as unknown as Record<string, unknown>}
|
|
43
|
+
// The view reads its diagram from the feed, never from its payload.
|
|
44
|
+
tools={{ analytics_bpmn_xml_data: { bpmnXml: "<broken/>" } }}
|
|
45
|
+
/>
|
|
46
|
+
</LocaleProvider>,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The kit's inline error state (U1): what happened as its title, what you
|
|
52
|
+
* can do under it (the kit's `errorHint` label), and the parser's raw cause
|
|
53
|
+
* collapsed under "Details" — never the raw text as the headline.
|
|
54
|
+
*/
|
|
55
|
+
describe("the BPMN heatmap — a diagram bpmn-js cannot import", () => {
|
|
56
|
+
it.each([
|
|
57
|
+
[
|
|
58
|
+
"de" as const,
|
|
59
|
+
"Das Diagramm konnte nicht gezeichnet werden.",
|
|
60
|
+
"Frag im Chat noch mal nach der Heatmap.",
|
|
61
|
+
],
|
|
62
|
+
["en" as const, "Could not draw the diagram.", "Ask in the chat to show the heatmap again."],
|
|
63
|
+
])(
|
|
64
|
+
"says what happened and what you can do, the cause under Details (%s)",
|
|
65
|
+
async (language, title, hint) => {
|
|
66
|
+
renderIn(language)
|
|
67
|
+
const alert = await screen.findByRole("alert")
|
|
68
|
+
expect(alert.getAttribute("data-slot")).toBe("error-state")
|
|
69
|
+
expect(alert.textContent?.startsWith(title + hint)).toBe(true)
|
|
70
|
+
expect(
|
|
71
|
+
screen.getByText("unparsable content <bpmn:foo> detected").closest("details"),
|
|
72
|
+
).toBeTruthy()
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Skeleton, useToolQuery } from "@miragon/mcp-toolkit-ui"
|
|
2
|
+
import {
|
|
3
|
+
BpmnHeatmapWidget,
|
|
4
|
+
QueryFallback,
|
|
5
|
+
ViewMeta,
|
|
6
|
+
formatLookback,
|
|
7
|
+
type BpmnHeatmapData,
|
|
8
|
+
} from "@miragon/pulse-widget-shell/widgets"
|
|
9
|
+
|
|
10
|
+
import type { BpmnMissingReason } from "../heatmap-data.js"
|
|
11
|
+
import { useT } from "../messages/use-t.js"
|
|
12
|
+
import { ANALYTICS_BPMN_XML_DATA } from "../tool-names.js"
|
|
13
|
+
import { enginesMeta } from "./view-meta.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The analytics heatmap payload (`analytics_show_bpmn_heatmap`): the heat
|
|
17
|
+
* values plus the configured engines they add up, so the model description
|
|
18
|
+
* and the meta line name the scope like every other analytics view, and when
|
|
19
|
+
* the values were read. The diagram is NOT in it: the widget reads it from
|
|
20
|
+
* `analytics_bpmn_xml_data`, so the model never sees the XML.
|
|
21
|
+
*/
|
|
22
|
+
export interface AnalyticsBpmnHeatmapData extends Omit<BpmnHeatmapData, "bpmnXml"> {
|
|
23
|
+
engines: string[]
|
|
24
|
+
/** When the heat values were read (ISO); absent in payloads recorded before it existed. */
|
|
25
|
+
asOf?: string
|
|
26
|
+
/**
|
|
27
|
+
* Why there is no diagram, as the server knows it (`heatmap-data.ts`).
|
|
28
|
+
* Absent when a diagram exists (the widget then reads it).
|
|
29
|
+
*/
|
|
30
|
+
bpmnMissing?: BpmnMissingReason
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `analytics_bpmn_xml_data`: the key's latest diagram, or why there is none. */
|
|
34
|
+
interface HeatmapDiagram {
|
|
35
|
+
bpmnXml: string | null
|
|
36
|
+
bpmnMissing?: BpmnMissingReason
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The "no diagram" text per known cause. An absent or unknown cause gets the
|
|
41
|
+
* text that names none: the widget never guesses why the diagram is missing.
|
|
42
|
+
*/
|
|
43
|
+
const BPMN_MISSING_KEY: Record<BpmnMissingReason, string> = {
|
|
44
|
+
"no-camunda7": "aHeatmap.bpmnNoCamunda7",
|
|
45
|
+
"not-loaded": "aHeatmap.bpmnNotLoaded",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The catalog key of the "no diagram" text for a payload. */
|
|
49
|
+
function bpmnMissingKey(data: Pick<AnalyticsBpmnHeatmapData, "bpmnMissing"> | null) {
|
|
50
|
+
const reason = data?.bpmnMissing
|
|
51
|
+
return reason && Object.hasOwn(BPMN_MISSING_KEY, reason)
|
|
52
|
+
? BPMN_MISSING_KEY[reason]
|
|
53
|
+
: "aHeatmap.bpmnUnavailable"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Analytics-side wrapper that localizes the shared shell heatmap. The shell
|
|
58
|
+
* carries no module i18n — every string arrives via `labels` — so this binds
|
|
59
|
+
* `useT()` to the component. Registered under `analytics:bpmn-heatmap` in
|
|
60
|
+
* `widgets/index.ts` in place of the raw shell widget.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* The heatmap's data with its diagram: read from `analytics_bpmn_xml_data`
|
|
64
|
+
* unless the server said why there is none, scoped to the engines whose heat
|
|
65
|
+
* the view shows. `pending` while the read is out (or failed).
|
|
66
|
+
*/
|
|
67
|
+
function useHeatmapWithDiagram(data: AnalyticsBpmnHeatmapData | null) {
|
|
68
|
+
const hasDiagram = data !== null && data.bpmnMissing === undefined
|
|
69
|
+
const diagram = useToolQuery<HeatmapDiagram>(
|
|
70
|
+
["analytics:bpmn-xml", data?.processDefinitionKey ?? null, data?.engines ?? null],
|
|
71
|
+
ANALYTICS_BPMN_XML_DATA,
|
|
72
|
+
{ processDefinitionKey: data?.processDefinitionKey ?? "", engine: data?.engines },
|
|
73
|
+
{ enabled: hasDiagram },
|
|
74
|
+
)
|
|
75
|
+
const full: (BpmnHeatmapData & Pick<AnalyticsBpmnHeatmapData, "bpmnMissing">) | null = data && {
|
|
76
|
+
...data,
|
|
77
|
+
bpmnXml: diagram.data?.bpmnXml ?? null,
|
|
78
|
+
bpmnMissing: data.bpmnMissing ?? diagram.data?.bpmnMissing,
|
|
79
|
+
}
|
|
80
|
+
return { diagram, pending: hasDiagram && !diagram.data, full }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function AnalyticsBpmnHeatmap({ data }: { data: AnalyticsBpmnHeatmapData | null }) {
|
|
84
|
+
const t = useT()
|
|
85
|
+
const { diagram, pending, full } = useHeatmapWithDiagram(data)
|
|
86
|
+
if (pending) {
|
|
87
|
+
return (
|
|
88
|
+
<QueryFallback
|
|
89
|
+
isError={diagram.isError}
|
|
90
|
+
error={diagram.error}
|
|
91
|
+
onRetry={() => void diagram.refetch()}
|
|
92
|
+
errorTitle={t("aHeatmap.errorTitle")}
|
|
93
|
+
skeleton={<Skeleton className="h-60 w-full rounded-md" />}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
<BpmnHeatmapWidget
|
|
99
|
+
data={full}
|
|
100
|
+
meta={
|
|
101
|
+
data && (
|
|
102
|
+
<ViewMeta
|
|
103
|
+
period={formatLookback(data.period)}
|
|
104
|
+
engines={enginesMeta(data.engines)}
|
|
105
|
+
asOf={data.asOf}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
labels={{
|
|
110
|
+
title: t("aHeatmap.title"),
|
|
111
|
+
window: t("aHeatmap.window"),
|
|
112
|
+
frequency: t("aHeatmap.frequency"),
|
|
113
|
+
duration: t("aHeatmap.duration"),
|
|
114
|
+
frequencyLegend: t("aHeatmap.frequencyLegend"),
|
|
115
|
+
durationLegend: t("aHeatmap.durationLegend"),
|
|
116
|
+
noData: t("aHeatmap.noData"),
|
|
117
|
+
noHeat: t("aHeatmap.noHeat"),
|
|
118
|
+
bpmnUnavailable: t(bpmnMissingKey(full)),
|
|
119
|
+
less: t("aHeatmap.less"),
|
|
120
|
+
more: t("aHeatmap.more"),
|
|
121
|
+
diagramAriaLabel: t("aHeatmap.diagramAria"),
|
|
122
|
+
errorTitle: t("aHeatmap.errorTitle"),
|
|
123
|
+
errorHint: t("aHeatmap.errorHint"),
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { catalogueSyncIssues } from "@miragon/pulse-widget-shell/server"
|
|
3
|
+
import { definition } from "../definition.js"
|
|
4
|
+
import { analyticsWidgets } from "./index.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guard against widget-catalogue drift (invariant 3, links 1↔2), mirroring the
|
|
8
|
+
* camunda7 module's test: every widget registered in the host bundle map
|
|
9
|
+
* (`widgets/index.ts`) must be catalogued in `definition.ts` and vice versa —
|
|
10
|
+
* the shared checker lists every drifted id.
|
|
11
|
+
*/
|
|
12
|
+
describe("analytics widget catalogue ↔ registry sync", () => {
|
|
13
|
+
it("definition.widgets matches the registered widget components exactly", () => {
|
|
14
|
+
expect(catalogueSyncIssues(definition, analyticsWidgets)).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Badge } from "@miragon/mcp-toolkit-ui"
|
|
2
|
+
import type { ClusterCompareKpi, ClusterCompareResult } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import {
|
|
4
|
+
HandOffButton,
|
|
5
|
+
ViewMeta,
|
|
6
|
+
formatNumber,
|
|
7
|
+
formatTimestamp,
|
|
8
|
+
} from "@miragon/pulse-widget-shell/widgets"
|
|
9
|
+
import { useT, type T } from "../messages/use-t.js"
|
|
10
|
+
import {
|
|
11
|
+
ComparisonCard,
|
|
12
|
+
ComparisonEmptyState,
|
|
13
|
+
SuppressedBadge,
|
|
14
|
+
buildComparisonMetrics,
|
|
15
|
+
deltaFacts,
|
|
16
|
+
} from "./comparison-shared.js"
|
|
17
|
+
import { engineIdsOf, useHandOff, type HandOff } from "./hand-off.js"
|
|
18
|
+
import { enginesMeta } from "./view-meta.js"
|
|
19
|
+
|
|
20
|
+
export type ClusterCompareData = ClusterCompareResult | null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interpret a before/after-deployment comparison — confirm it, then find the
|
|
24
|
+
* driving element. The ids re-run the comparison as asked (the requested
|
|
25
|
+
* whole-day windows, the echoed engines); the windows actually measured — cut
|
|
26
|
+
* short at now or the retention — are facts (#336).
|
|
27
|
+
*/
|
|
28
|
+
export function clusterCompareHandOff(data: ClusterCompareResult): HandOff {
|
|
29
|
+
return {
|
|
30
|
+
intent: "askAi.clusterCompare",
|
|
31
|
+
ids: {
|
|
32
|
+
engine: engineIdsOf(data.engines),
|
|
33
|
+
deploymentTimestamp: data.deploymentTimestamp,
|
|
34
|
+
windowBeforeDays: data.requestedWindowDays.before,
|
|
35
|
+
windowAfterDays: data.requestedWindowDays.after,
|
|
36
|
+
processDefinitionKey: data.processDefinitionKey,
|
|
37
|
+
activityId: data.activityId,
|
|
38
|
+
},
|
|
39
|
+
facts: {
|
|
40
|
+
measuredBeforeDays: data.windowDays.before,
|
|
41
|
+
measuredAfterDays: data.windowDays.after,
|
|
42
|
+
partial: data.partial || undefined,
|
|
43
|
+
...deltaFacts(data.delta, data.suppressed),
|
|
44
|
+
},
|
|
45
|
+
tools: ["analytics_cluster_compare", "analytics_element_bottleneck"],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MINUTE = 60
|
|
50
|
+
const HOUR = 60 * MINUTE
|
|
51
|
+
const DAY = 24 * HOUR
|
|
52
|
+
|
|
53
|
+
type MeasuredWindow = Pick<ClusterCompareKpi, "window_from" | "window_to" | "window_days">
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A measured window's length in seconds, from its exact bounds. `window_days`
|
|
57
|
+
* is rounded to 0.01 d (about 14 minutes), too coarse for a window cut short
|
|
58
|
+
* at now; it stands in only when the bounds do not parse.
|
|
59
|
+
*/
|
|
60
|
+
function measuredSeconds(measured: MeasuredWindow) {
|
|
61
|
+
const seconds = (Date.parse(measured.window_to) - Date.parse(measured.window_from)) / 1000
|
|
62
|
+
return Number.isFinite(seconds) ? seconds : measured.window_days * DAY
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A measured window's length in words, in the largest unit it fills: days
|
|
67
|
+
* and tenths ("7 Tage", "1.5 days"), else whole hours, else whole minutes,
|
|
68
|
+
* else "unter 1 Minute". A window cut short at now reads "2 Stunden" or
|
|
69
|
+
* "10 Minuten", never "0,1 Tage" and never a minute window as "1 Stunde".
|
|
70
|
+
*/
|
|
71
|
+
export function windowLength(t: T, measured: MeasuredWindow): string {
|
|
72
|
+
const seconds = measuredSeconds(measured)
|
|
73
|
+
if (seconds >= DAY) {
|
|
74
|
+
return t("aCommon.days", { count: formatNumber(seconds / DAY, { maximumFractionDigits: 1 }) })
|
|
75
|
+
}
|
|
76
|
+
if (seconds >= HOUR) {
|
|
77
|
+
return t("aCommon.hours", { count: formatNumber(Math.round(seconds / HOUR)) })
|
|
78
|
+
}
|
|
79
|
+
if (seconds >= MINUTE) {
|
|
80
|
+
return t("aCommon.minutes", { count: formatNumber(Math.round(seconds / MINUTE)) })
|
|
81
|
+
}
|
|
82
|
+
return t("aCommon.underOneMinute")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ClusterCompareWidget({ data }: { data: ClusterCompareData }) {
|
|
86
|
+
const t = useT()
|
|
87
|
+
const { ask } = useHandOff()
|
|
88
|
+
if (!data) return <ComparisonEmptyState>{t("aClusterCompare.noData")}</ComparisonEmptyState>
|
|
89
|
+
|
|
90
|
+
const before = data.kpis.find((k) => k.period === "before")
|
|
91
|
+
const after = data.kpis.find((k) => k.period === "after")
|
|
92
|
+
if (!before || !after) {
|
|
93
|
+
return <ComparisonEmptyState>{t("aClusterCompare.incompleteData")}</ComparisonEmptyState>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const metrics = buildComparisonMetrics(t, before, after, data.delta)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ComparisonCard
|
|
100
|
+
title={t("aClusterCompare.title")}
|
|
101
|
+
tableLabel={t("aClusterCompare.tableLabel")}
|
|
102
|
+
beforeLabel={t("aClusterCompare.beforeLabel")}
|
|
103
|
+
afterLabel={t("aClusterCompare.afterLabel")}
|
|
104
|
+
metrics={metrics}
|
|
105
|
+
suppressed={data.suppressed}
|
|
106
|
+
actions={
|
|
107
|
+
<HandOffButton
|
|
108
|
+
action="assess"
|
|
109
|
+
prompt={ask(clusterCompareHandOff(data))}
|
|
110
|
+
variant="primary"
|
|
111
|
+
/>
|
|
112
|
+
}
|
|
113
|
+
meta={
|
|
114
|
+
<ViewMeta
|
|
115
|
+
period={t("aClusterCompare.period", {
|
|
116
|
+
before: windowLength(t, before),
|
|
117
|
+
after: windowLength(t, after),
|
|
118
|
+
})}
|
|
119
|
+
engines={enginesMeta(data.engines)}
|
|
120
|
+
asOf={data.asOf}
|
|
121
|
+
/>
|
|
122
|
+
}
|
|
123
|
+
badges={
|
|
124
|
+
<>
|
|
125
|
+
<Badge variant="secondary">
|
|
126
|
+
{t("aClusterCompare.deployBadge", {
|
|
127
|
+
timestamp: formatTimestamp(data.deploymentTimestamp),
|
|
128
|
+
})}
|
|
129
|
+
</Badge>
|
|
130
|
+
{data.partial && <Badge variant="outline">{t("aClusterCompare.partialBadge")}</Badge>}
|
|
131
|
+
{data.processDefinitionKey && <Badge>{data.processDefinitionKey}</Badge>}
|
|
132
|
+
{data.activityId && (
|
|
133
|
+
<Badge variant="outline">
|
|
134
|
+
{t("aClusterCompare.elementBadge", { activityId: data.activityId })}
|
|
135
|
+
</Badge>
|
|
136
|
+
)}
|
|
137
|
+
{data.suppressed && (
|
|
138
|
+
<SuppressedBadge>
|
|
139
|
+
{t("aClusterCompare.insufficientSignal", {
|
|
140
|
+
minBucketSize: formatNumber(data.minBucketSize),
|
|
141
|
+
})}
|
|
142
|
+
</SuppressedBadge>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
)
|
|
148
|
+
}
|