@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.
Files changed (357) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -3
  3. package/dist/bpmn-name-lookup.d.ts +72 -0
  4. package/dist/bpmn-name-lookup.d.ts.map +1 -0
  5. package/dist/bpmn-name-lookup.js +123 -0
  6. package/dist/bpmn-name-lookup.js.map +1 -0
  7. package/dist/bpmn-name-lookup.test.d.ts +2 -0
  8. package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
  9. package/dist/bpmn-name-lookup.test.js +287 -0
  10. package/dist/bpmn-name-lookup.test.js.map +1 -0
  11. package/dist/bpmn-names.d.ts +38 -0
  12. package/dist/bpmn-names.d.ts.map +1 -0
  13. package/dist/bpmn-names.js +22 -0
  14. package/dist/bpmn-names.js.map +1 -0
  15. package/dist/brand-gates.test.d.ts +2 -0
  16. package/dist/brand-gates.test.d.ts.map +1 -0
  17. package/dist/brand-gates.test.js +58 -0
  18. package/dist/brand-gates.test.js.map +1 -0
  19. package/dist/builder-texts.d.ts +4 -0
  20. package/dist/builder-texts.d.ts.map +1 -0
  21. package/dist/builder-texts.js +14 -0
  22. package/dist/builder-texts.js.map +1 -0
  23. package/dist/builder-texts.test.d.ts +2 -0
  24. package/dist/builder-texts.test.d.ts.map +1 -0
  25. package/dist/builder-texts.test.js +32 -0
  26. package/dist/builder-texts.test.js.map +1 -0
  27. package/dist/definition.d.ts +3 -0
  28. package/dist/definition.d.ts.map +1 -0
  29. package/dist/definition.js +151 -0
  30. package/dist/definition.js.map +1 -0
  31. package/dist/engine-ids.d.ts +42 -0
  32. package/dist/engine-ids.d.ts.map +1 -0
  33. package/dist/engine-ids.js +50 -0
  34. package/dist/engine-ids.js.map +1 -0
  35. package/dist/engine-scope.test.d.ts +2 -0
  36. package/dist/engine-scope.test.d.ts.map +1 -0
  37. package/dist/engine-scope.test.js +260 -0
  38. package/dist/engine-scope.test.js.map +1 -0
  39. package/dist/heatmap-data.d.ts +15 -0
  40. package/dist/heatmap-data.d.ts.map +1 -0
  41. package/dist/heatmap-data.js +2 -0
  42. package/dist/heatmap-data.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/instructions.d.ts +8 -0
  48. package/dist/instructions.d.ts.map +1 -0
  49. package/dist/instructions.js +33 -0
  50. package/dist/instructions.js.map +1 -0
  51. package/dist/messages/de.ask-ai.d.ts +9 -0
  52. package/dist/messages/de.ask-ai.d.ts.map +1 -0
  53. package/dist/messages/de.ask-ai.js +18 -0
  54. package/dist/messages/de.ask-ai.js.map +1 -0
  55. package/dist/messages/de.builder.d.ts +12 -0
  56. package/dist/messages/de.builder.d.ts.map +1 -0
  57. package/dist/messages/de.builder.js +53 -0
  58. package/dist/messages/de.builder.js.map +1 -0
  59. package/dist/messages/de.server.d.ts +8 -0
  60. package/dist/messages/de.server.d.ts.map +1 -0
  61. package/dist/messages/de.server.js +26 -0
  62. package/dist/messages/de.server.js.map +1 -0
  63. package/dist/messages/de.sweep.d.ts +10 -0
  64. package/dist/messages/de.sweep.d.ts.map +1 -0
  65. package/dist/messages/de.sweep.js +203 -0
  66. package/dist/messages/de.sweep.js.map +1 -0
  67. package/dist/messages/en.ask-ai.d.ts +25 -0
  68. package/dist/messages/en.ask-ai.d.ts.map +1 -0
  69. package/dist/messages/en.ask-ai.js +24 -0
  70. package/dist/messages/en.ask-ai.js.map +1 -0
  71. package/dist/messages/en.builder.d.ts +12 -0
  72. package/dist/messages/en.builder.d.ts.map +1 -0
  73. package/dist/messages/en.builder.js +53 -0
  74. package/dist/messages/en.builder.js.map +1 -0
  75. package/dist/messages/en.server.d.ts +8 -0
  76. package/dist/messages/en.server.d.ts.map +1 -0
  77. package/dist/messages/en.server.js +26 -0
  78. package/dist/messages/en.server.js.map +1 -0
  79. package/dist/messages/en.sweep.d.ts +11 -0
  80. package/dist/messages/en.sweep.d.ts.map +1 -0
  81. package/dist/messages/en.sweep.js +204 -0
  82. package/dist/messages/en.sweep.js.map +1 -0
  83. package/dist/messages/glossary.d.ts +26 -0
  84. package/dist/messages/glossary.d.ts.map +1 -0
  85. package/dist/messages/glossary.js +37 -0
  86. package/dist/messages/glossary.js.map +1 -0
  87. package/dist/messages/glossary.test.d.ts +2 -0
  88. package/dist/messages/glossary.test.d.ts.map +1 -0
  89. package/dist/messages/glossary.test.js +109 -0
  90. package/dist/messages/glossary.test.js.map +1 -0
  91. package/dist/messages/index.d.ts +12 -0
  92. package/dist/messages/index.d.ts.map +1 -0
  93. package/dist/messages/index.js +23 -0
  94. package/dist/messages/index.js.map +1 -0
  95. package/dist/messages/plural.d.ts +10 -0
  96. package/dist/messages/plural.d.ts.map +1 -0
  97. package/dist/messages/plural.js +17 -0
  98. package/dist/messages/plural.js.map +1 -0
  99. package/dist/messages/use-t.d.ts +10 -0
  100. package/dist/messages/use-t.d.ts.map +1 -0
  101. package/dist/messages/use-t.js +10 -0
  102. package/dist/messages/use-t.js.map +1 -0
  103. package/dist/module.d.ts +48 -0
  104. package/dist/module.d.ts.map +1 -0
  105. package/dist/module.js +174 -0
  106. package/dist/module.js.map +1 -0
  107. package/dist/module.test.d.ts +2 -0
  108. package/dist/module.test.d.ts.map +1 -0
  109. package/dist/module.test.js +230 -0
  110. package/dist/module.test.js.map +1 -0
  111. package/dist/named-views.d.ts +27 -0
  112. package/dist/named-views.d.ts.map +1 -0
  113. package/dist/named-views.js +73 -0
  114. package/dist/named-views.js.map +1 -0
  115. package/dist/named-views.test.d.ts +2 -0
  116. package/dist/named-views.test.d.ts.map +1 -0
  117. package/dist/named-views.test.js +110 -0
  118. package/dist/named-views.test.js.map +1 -0
  119. package/dist/plugin.d.ts +44 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +52 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/prometheus-wiring.test.d.ts +2 -0
  124. package/dist/prometheus-wiring.test.d.ts.map +1 -0
  125. package/dist/prometheus-wiring.test.js +142 -0
  126. package/dist/prometheus-wiring.test.js.map +1 -0
  127. package/dist/server-locale.d.ts +48 -0
  128. package/dist/server-locale.d.ts.map +1 -0
  129. package/dist/server-locale.js +33 -0
  130. package/dist/server-locale.js.map +1 -0
  131. package/dist/settings-tools.d.ts +23 -0
  132. package/dist/settings-tools.d.ts.map +1 -0
  133. package/dist/settings-tools.js +100 -0
  134. package/dist/settings-tools.js.map +1 -0
  135. package/dist/settings.d.ts +77 -0
  136. package/dist/settings.d.ts.map +1 -0
  137. package/dist/settings.js +99 -0
  138. package/dist/settings.js.map +1 -0
  139. package/dist/settings.test.d.ts +2 -0
  140. package/dist/settings.test.d.ts.map +1 -0
  141. package/dist/settings.test.js +299 -0
  142. package/dist/settings.test.js.map +1 -0
  143. package/dist/steps/app-config.d.ts +30 -0
  144. package/dist/steps/app-config.d.ts.map +1 -0
  145. package/dist/steps/app-config.js +46 -0
  146. package/dist/steps/app-config.js.map +1 -0
  147. package/dist/steps/dashboard.d.ts +12 -0
  148. package/dist/steps/dashboard.d.ts.map +1 -0
  149. package/dist/steps/dashboard.js +58 -0
  150. package/dist/steps/dashboard.js.map +1 -0
  151. package/dist/steps/failure-dashboard.d.ts +10 -0
  152. package/dist/steps/failure-dashboard.d.ts.map +1 -0
  153. package/dist/steps/failure-dashboard.js +32 -0
  154. package/dist/steps/failure-dashboard.js.map +1 -0
  155. package/dist/steps/index.d.ts +3 -0
  156. package/dist/steps/index.d.ts.map +1 -0
  157. package/dist/steps/index.js +3 -0
  158. package/dist/steps/index.js.map +1 -0
  159. package/dist/steps/steps.test.d.ts +2 -0
  160. package/dist/steps/steps.test.d.ts.map +1 -0
  161. package/dist/steps/steps.test.js +153 -0
  162. package/dist/steps/steps.test.js.map +1 -0
  163. package/dist/tool-names.d.ts +13 -0
  164. package/dist/tool-names.d.ts.map +1 -0
  165. package/dist/tool-names.js +27 -0
  166. package/dist/tool-names.js.map +1 -0
  167. package/dist/tools/cluster-compare.d.ts +8 -0
  168. package/dist/tools/cluster-compare.d.ts.map +1 -0
  169. package/dist/tools/cluster-compare.js +17 -0
  170. package/dist/tools/cluster-compare.js.map +1 -0
  171. package/dist/tools/element.d.ts +8 -0
  172. package/dist/tools/element.d.ts.map +1 -0
  173. package/dist/tools/element.js +25 -0
  174. package/dist/tools/element.js.map +1 -0
  175. package/dist/tools/engine-compare.d.ts +8 -0
  176. package/dist/tools/engine-compare.d.ts.map +1 -0
  177. package/dist/tools/engine-compare.js +18 -0
  178. package/dist/tools/engine-compare.js.map +1 -0
  179. package/dist/tools/engine-compare.test.d.ts +2 -0
  180. package/dist/tools/engine-compare.test.d.ts.map +1 -0
  181. package/dist/tools/engine-compare.test.js +166 -0
  182. package/dist/tools/engine-compare.test.js.map +1 -0
  183. package/dist/tools/engine-landscape.d.ts +7 -0
  184. package/dist/tools/engine-landscape.d.ts.map +1 -0
  185. package/dist/tools/engine-landscape.js +13 -0
  186. package/dist/tools/engine-landscape.js.map +1 -0
  187. package/dist/tools/engine-landscape.test.d.ts +2 -0
  188. package/dist/tools/engine-landscape.test.d.ts.map +1 -0
  189. package/dist/tools/engine-landscape.test.js +110 -0
  190. package/dist/tools/engine-landscape.test.js.map +1 -0
  191. package/dist/tools/failures.d.ts +7 -0
  192. package/dist/tools/failures.d.ts.map +1 -0
  193. package/dist/tools/failures.js +13 -0
  194. package/dist/tools/failures.js.map +1 -0
  195. package/dist/tools/health.d.ts +7 -0
  196. package/dist/tools/health.d.ts.map +1 -0
  197. package/dist/tools/health.js +23 -0
  198. package/dist/tools/health.js.map +1 -0
  199. package/dist/tools/health.test.d.ts +2 -0
  200. package/dist/tools/health.test.d.ts.map +1 -0
  201. package/dist/tools/health.test.js +119 -0
  202. package/dist/tools/health.test.js.map +1 -0
  203. package/dist/tools/index.d.ts +18 -0
  204. package/dist/tools/index.d.ts.map +1 -0
  205. package/dist/tools/index.js +35 -0
  206. package/dist/tools/index.js.map +1 -0
  207. package/dist/tools/performance.d.ts +8 -0
  208. package/dist/tools/performance.d.ts.map +1 -0
  209. package/dist/tools/performance.js +25 -0
  210. package/dist/tools/performance.js.map +1 -0
  211. package/dist/tools/performance.test.d.ts +2 -0
  212. package/dist/tools/performance.test.d.ts.map +1 -0
  213. package/dist/tools/performance.test.js +176 -0
  214. package/dist/tools/performance.test.js.map +1 -0
  215. package/dist/tools/version-compare.d.ts +8 -0
  216. package/dist/tools/version-compare.d.ts.map +1 -0
  217. package/dist/tools/version-compare.js +17 -0
  218. package/dist/tools/version-compare.js.map +1 -0
  219. package/dist/tools/version-compare.test.d.ts +2 -0
  220. package/dist/tools/version-compare.test.d.ts.map +1 -0
  221. package/dist/tools/version-compare.test.js +82 -0
  222. package/dist/tools/version-compare.test.js.map +1 -0
  223. package/dist/toolsets.d.ts +17 -0
  224. package/dist/toolsets.d.ts.map +1 -0
  225. package/dist/toolsets.js +47 -0
  226. package/dist/toolsets.js.map +1 -0
  227. package/dist/toolsets.test.d.ts +2 -0
  228. package/dist/toolsets.test.d.ts.map +1 -0
  229. package/dist/toolsets.test.js +91 -0
  230. package/dist/toolsets.test.js.map +1 -0
  231. package/dist/view-title.test.d.ts +2 -0
  232. package/dist/view-title.test.d.ts.map +1 -0
  233. package/dist/view-title.test.js +91 -0
  234. package/dist/view-title.test.js.map +1 -0
  235. package/dist/widget-tools/comparisons.d.ts +13 -0
  236. package/dist/widget-tools/comparisons.d.ts.map +1 -0
  237. package/dist/widget-tools/comparisons.js +177 -0
  238. package/dist/widget-tools/comparisons.js.map +1 -0
  239. package/dist/widget-tools/comparisons.test.d.ts +2 -0
  240. package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
  241. package/dist/widget-tools/comparisons.test.js +123 -0
  242. package/dist/widget-tools/comparisons.test.js.map +1 -0
  243. package/dist/widget-tools/shared.d.ts +36 -0
  244. package/dist/widget-tools/shared.d.ts.map +1 -0
  245. package/dist/widget-tools/shared.js +39 -0
  246. package/dist/widget-tools/shared.js.map +1 -0
  247. package/dist/widget-tools.d.ts +25 -0
  248. package/dist/widget-tools.d.ts.map +1 -0
  249. package/dist/widget-tools.js +269 -0
  250. package/dist/widget-tools.js.map +1 -0
  251. package/dist/widget-tools.test.d.ts +2 -0
  252. package/dist/widget-tools.test.d.ts.map +1 -0
  253. package/dist/widget-tools.test.js +272 -0
  254. package/dist/widget-tools.test.js.map +1 -0
  255. package/package.json +91 -5
  256. package/src/bpmn-name-lookup.test.ts +333 -0
  257. package/src/bpmn-name-lookup.ts +197 -0
  258. package/src/bpmn-names.ts +51 -0
  259. package/src/brand-gates.test.ts +69 -0
  260. package/src/builder-texts.test.ts +40 -0
  261. package/src/builder-texts.ts +15 -0
  262. package/src/definition.ts +181 -0
  263. package/src/engine-ids.ts +85 -0
  264. package/src/engine-scope.test.ts +324 -0
  265. package/src/heatmap-data.ts +14 -0
  266. package/src/index.ts +4 -0
  267. package/src/instructions.ts +33 -0
  268. package/src/messages/GLOSSARY.md +88 -0
  269. package/src/messages/de.ask-ai.ts +28 -0
  270. package/src/messages/de.builder.ts +77 -0
  271. package/src/messages/de.server.ts +39 -0
  272. package/src/messages/de.sweep.ts +244 -0
  273. package/src/messages/en.ask-ai.ts +34 -0
  274. package/src/messages/en.builder.ts +76 -0
  275. package/src/messages/en.server.ts +39 -0
  276. package/src/messages/en.sweep.ts +240 -0
  277. package/src/messages/glossary.test.ts +122 -0
  278. package/src/messages/glossary.ts +62 -0
  279. package/src/messages/index.ts +24 -0
  280. package/src/messages/plural.ts +18 -0
  281. package/src/messages/use-t.ts +12 -0
  282. package/src/module.test.ts +319 -0
  283. package/src/module.ts +211 -0
  284. package/src/named-views.test.ts +141 -0
  285. package/src/named-views.ts +97 -0
  286. package/src/plugin.ts +94 -0
  287. package/src/prometheus-wiring.test.ts +174 -0
  288. package/src/server-locale.ts +59 -0
  289. package/src/settings-tools.ts +170 -0
  290. package/src/settings.test.ts +402 -0
  291. package/src/settings.ts +114 -0
  292. package/src/steps/app-config.ts +75 -0
  293. package/src/steps/dashboard.ts +73 -0
  294. package/src/steps/failure-dashboard.ts +38 -0
  295. package/src/steps/index.ts +2 -0
  296. package/src/steps/steps.test.ts +216 -0
  297. package/src/tool-names.ts +26 -0
  298. package/src/tools/cluster-compare.ts +28 -0
  299. package/src/tools/element.ts +35 -0
  300. package/src/tools/engine-compare.test.ts +210 -0
  301. package/src/tools/engine-compare.ts +30 -0
  302. package/src/tools/engine-landscape.test.ts +138 -0
  303. package/src/tools/engine-landscape.ts +21 -0
  304. package/src/tools/failures.ts +19 -0
  305. package/src/tools/health.test.ts +156 -0
  306. package/src/tools/health.ts +29 -0
  307. package/src/tools/index.ts +44 -0
  308. package/src/tools/performance.test.ts +210 -0
  309. package/src/tools/performance.ts +38 -0
  310. package/src/tools/version-compare.test.ts +97 -0
  311. package/src/tools/version-compare.ts +28 -0
  312. package/src/toolsets.test.ts +107 -0
  313. package/src/toolsets.ts +56 -0
  314. package/src/view-title.test.ts +123 -0
  315. package/src/widget-tools/comparisons.test.ts +170 -0
  316. package/src/widget-tools/comparisons.ts +229 -0
  317. package/src/widget-tools/shared.ts +67 -0
  318. package/src/widget-tools.test.ts +306 -0
  319. package/src/widget-tools.ts +375 -0
  320. package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
  321. package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
  322. package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
  323. package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
  324. package/src/widgets/analytics-dashboard/lib.ts +43 -0
  325. package/src/widgets/bpmn-heatmap.test.tsx +75 -0
  326. package/src/widgets/bpmn-heatmap.tsx +127 -0
  327. package/src/widgets/catalogue-sync.test.ts +16 -0
  328. package/src/widgets/cluster-compare.tsx +148 -0
  329. package/src/widgets/comparison-shared.test.ts +237 -0
  330. package/src/widgets/comparison-shared.tsx +371 -0
  331. package/src/widgets/comparison-widgets.test.tsx +307 -0
  332. package/src/widgets/dashboard-widgets.test.tsx +397 -0
  333. package/src/widgets/engine-compare.tsx +87 -0
  334. package/src/widgets/engine-landscape.tsx +352 -0
  335. package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
  336. package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
  337. package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
  338. package/src/widgets/failure-dashboard/lib.ts +32 -0
  339. package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
  340. package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
  341. package/src/widgets/figure.tsx +79 -0
  342. package/src/widgets/hand-off.test.ts +126 -0
  343. package/src/widgets/hand-off.ts +102 -0
  344. package/src/widgets/index.ts +124 -0
  345. package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
  346. package/src/widgets/model-descriptions.test.ts +251 -0
  347. package/src/widgets/model-descriptions.ts +428 -0
  348. package/src/widgets/names.test.ts +32 -0
  349. package/src/widgets/names.ts +39 -0
  350. package/src/widgets/query-gate.test.tsx +57 -0
  351. package/src/widgets/query-gate.tsx +50 -0
  352. package/src/widgets/settings-section.test.tsx +174 -0
  353. package/src/widgets/settings-section.tsx +244 -0
  354. package/src/widgets/version-compare.test.ts +68 -0
  355. package/src/widgets/version-compare.tsx +96 -0
  356. package/src/widgets/view-meta.ts +18 -0
  357. 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
+ }