@skyhook-io/k8s-ui 1.16.3 → 1.17.1

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 (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. package/src/utils/view-transition.ts +16 -1
package/README.md CHANGED
@@ -29,3 +29,44 @@ command remains pending for the next connection.
29
29
  `YamlEditor` and `YamlDiffEditor` bundle Monaco, its editor worker, and the YAML language worker into the consuming application. They make no runtime CDN or internet requests, so they work in air-gapped environments.
30
30
 
31
31
  Consumers must use a bundler that supports module workers created with `new Worker(new URL(..., import.meta.url), { type: 'module' })`. The source package is compatible with Vite and webpack 5. Monaco and `monaco-yaml` are intentionally pinned as a compatible pair because their worker-factory APIs must move together. The package declares that exact Monaco version as a peer so the host and YAML runtime share one Monaco instance.
32
+
33
+ The YAML language worker comes from `monaco-yaml`, which imports Monaco's worker entry as `monaco-editor/esm/vs/editor/editor.worker.js`. Monaco's `exports` map does not serve that path, so the host bundler must alias it to the path Monaco does export. In Vite:
34
+
35
+ ```ts
36
+ resolve: {
37
+ alias: {
38
+ 'monaco-editor/esm/vs/editor/editor.worker.js': 'monaco-editor/editor/editor.worker.js',
39
+ },
40
+ },
41
+ ```
42
+
43
+ webpack 5 hosts need the same mapping in `resolve.alias`. Without it the build fails to resolve the worker import.
44
+
45
+ ## Resource kind views and certainty
46
+
47
+ `ResourcesView.renderKindView(kind)` optionally replaces a kind's table while
48
+ keeping the sidebar and resource drawer. Return `null` or `false` to use the table. Hosts
49
+ own data fetching; row/search/bulk shortcuts and column preferences are inactive
50
+ while a replacement is shown. Sidebar kind navigation remains available.
51
+
52
+ `CertaintyGlyph` displays `=`, `≥`, `≤` or `?` for an exact, lower-bound,
53
+ upper-bound or unknown value, with an accessible label and optional evidence
54
+ tooltip. `certaintyGlyph` and `certaintyValueLabel` expose the same vocabulary.
55
+
56
+ ## Optional flow controls
57
+
58
+ `CreateResourceDialog` defaults to Apply. Guided flows can set `initialMode="create"` and `lockMode` to require creation, and use `onBack(draftYaml)` to retain edits when returning to setup. Cancel closes the dialog. After a successful write, `onCreated(result, submittedYaml)` receives the first result and the exact submitted manifest; dry runs do not invoke it. `result.uid`, when supplied by Radar, identifies the persisted object. Partial creates name the objects already created. Unlocked flows switch to Apply for review when preview is available; direct creation keeps Create selected so a retry cannot silently update existing objects.
59
+
60
+ `useOpenTerminal` accepts `shell` (one executable name, without arguments), `title`, and `sessionNote`. Dock hosts must forward these fields to their terminal renderer; Radar does so. The shared `TerminalTab` accepts `command` to describe that executable, pin the container, and suppress shell debug advice, plus `note` for session context. `createSession` remains responsible for launching the command. Omitted options retain the normal shell session.
61
+
62
+ `AreaChart` supports optional `shadedRanges`, controlled `selection`, and `onSelectRange`. Range timestamps use Unix seconds. Dragging selects a range; with positive `stepSeconds`, clicking selects a step-wide window centered on the pointer, clipped at the chart edges. Arrow keys plus Enter select a window from the keyboard. Shaded range labels appear on hover. `AlertBanner.action` places a host-supplied control alongside the title. Omitting these props preserves existing charts and banners.
63
+
64
+ ## Facts, problems and reviewed actions
65
+
66
+ `FactValue`, `FactSource`, `FactGrid` and `FactRow` show an observed value and its source. Hosts supply explicit unknown or partial text; these components never infer completeness. `at` is a past event by default; `atMeaning="since"` describes a continuing state. `FoldSection` opens when its `attention` becomes true. `RefLink` is navigable only when the host supplies `onNavigate`; `ManagedByText` reuses the GitOps owner mapper for a plain relationship label.
67
+
68
+ `WorkspaceProblemCallout`, `ProblemList` and `ProblemMeta` show host-classified findings using non-healthy `HealthLevel` tones. `source` selects issue, audit or measurement provenance; source-specific fields cannot be combined. `rootKind` identifies the workspace's root; `subjectIsSelf` suppresses a link back to the current object. The host supplies `OpenIssueContext` only when it can open the matching Issues entry. CNPG maps its critical, warning and posture classifications to shared tones. `formatGrant` formats a permission for display; it performs no authorization.
69
+
70
+ `ActionConfirmDialog` adds context, reviewed writes and optional typed acknowledgement to the simpler `ConfirmDialog`. Hosts own loading, permission checks, stale-fact validation and result classification. Its `result` binds the state (`error`, `unknown` or `partial`), message and optional heading; unknown and partial results lock another submit. Retain that result outside the dialog across dismissal and remount, and clear it only after explicitly reviewing current state. The component does not infer outcomes from HTTP status. Remount it when changing the reviewed target or action, so acknowledgement and disclosure state start fresh. The default error heading makes no claim about whether a write occurred; use a stronger result title only when the producer proves it.
71
+
72
+ Existing CNPG primitive exports remain available and delegate to these shared implementations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.16.3",
3
+ "version": "1.17.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -83,7 +83,7 @@
83
83
  "diff": ">=5.0.0",
84
84
  "elkjs": ">=0.9.0",
85
85
  "lucide-react": ">=0.400.0",
86
- "monaco-editor": "0.55.1",
86
+ "monaco-editor": "0.57.0",
87
87
  "react": ">=18.0.0",
88
88
  "react-dom": ">=18.0.0",
89
89
  "yaml": ">=2.0.0"
@@ -101,7 +101,7 @@
101
101
  "elkjs": "^0.11.1",
102
102
  "jsdom": "^29.1.1",
103
103
  "lucide-react": "^1.48.0",
104
- "monaco-editor": "0.55.1",
104
+ "monaco-editor": "0.57.0",
105
105
  "react": "^19.3.0",
106
106
  "react-dom": "^19.3.0",
107
107
  "typescript": "^6.0.2",
@@ -377,6 +377,21 @@ describe("ApplicationDetail shell", () => {
377
377
  expect(local).toContain('ring is full, so the oldest activity is not loaded')
378
378
  })
379
379
 
380
+ it('shows the host history-limit note beside the history, and nothing without one', () => {
381
+ const base = {
382
+ selectedView: 'history' as const,
383
+ onSelectView: () => {},
384
+ selectedWorkloadKey: null,
385
+ onSelectWorkload: () => {},
386
+ historyMode: 'local' as const,
387
+ historyRange: 'all' as const,
388
+ historyRangeOptions: [{ value: 'all' as const, label: 'All observed' }],
389
+ historyItems: [],
390
+ }
391
+ expect(renderDetail({ ...base, historyRetentionNote: <span>Host limit note</span> })).toContain('<span>Host limit note</span>')
392
+ expect(renderDetail(base)).not.toContain('Host limit note')
393
+ })
394
+
380
395
  it('shows a single honest error state when runtime history fails without source history', () => {
381
396
  const html = renderDetail({
382
397
  selectedView: 'history',
@@ -89,7 +89,7 @@ import {
89
89
  overlayProvenance,
90
90
  } from "../../utils/applications";
91
91
  import { PaneLoader } from "../ui/PaneLoader";
92
- import { midTruncate } from "../../utils/format";
92
+ import { formatRelativeAgeTime, midTruncate } from "../../utils/format";
93
93
  import { VersionTooltip, AppIdentityTooltip, RolloutSummaryTooltip } from "./AppTooltips";
94
94
  import {
95
95
  ProvenanceBadge,
@@ -253,6 +253,9 @@ export type ApplicationDetailProps = {
253
253
  // The retained ring itself was row-capped: the OLDEST part of the retention
254
254
  // window is not loaded, independent of the per-query event limit.
255
255
  historyRingTruncated?: boolean;
256
+ // The host's note about how far back history goes, or the stretch before
257
+ // the limit reads as "no history".
258
+ historyRetentionNote?: ReactNode;
256
259
  onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
257
260
  onOpenTimeline?: (timestamp?: string) => void;
258
261
  onOpenSource?: (source: AppSourceRef) => void;
@@ -346,6 +349,7 @@ export function ApplicationDetail({
346
349
  historyCoverageRecordCount,
347
350
  historyRuntimeLimited,
348
351
  historyRingTruncated,
352
+ historyRetentionNote,
349
353
  onHistoryRangeChange,
350
354
  onOpenTimeline,
351
355
  onOpenSource,
@@ -784,6 +788,7 @@ export function ApplicationDetail({
784
788
  historyCoverageRecordCount={historyCoverageRecordCount}
785
789
  historyRuntimeLimited={historyRuntimeLimited}
786
790
  historyRingTruncated={historyRingTruncated}
791
+ historyRetentionNote={historyRetentionNote}
787
792
  onHistoryRangeChange={onHistoryRangeChange}
788
793
  onOpenTimeline={onOpenTimeline}
789
794
  onOpenSource={onOpenSource}
@@ -832,6 +837,7 @@ function ApplicationWorkspace({
832
837
  historyCoverageRecordCount,
833
838
  historyRuntimeLimited,
834
839
  historyRingTruncated,
840
+ historyRetentionNote,
835
841
  onHistoryRangeChange,
836
842
  onOpenTimeline,
837
843
  onOpenSource,
@@ -884,6 +890,9 @@ function ApplicationWorkspace({
884
890
  historyCoverageRecordCount?: number;
885
891
  historyRuntimeLimited?: boolean;
886
892
  historyRingTruncated?: boolean;
893
+ // The host's note about how far back history goes, or the stretch before
894
+ // the limit reads as "no history".
895
+ historyRetentionNote?: ReactNode;
887
896
  onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
888
897
  onOpenTimeline?: (timestamp?: string) => void;
889
898
  onOpenSource?: (source: AppSourceRef) => void;
@@ -953,6 +962,7 @@ function ApplicationWorkspace({
953
962
  coverageRecordCount={historyCoverageRecordCount}
954
963
  runtimeLimited={historyRuntimeLimited}
955
964
  ringTruncated={historyRingTruncated}
965
+ retentionNote={historyRetentionNote}
956
966
  workloads={workloads}
957
967
  onSelectWorkload={onSelectWorkload}
958
968
  onNavigateToResource={onNavigateToResource}
@@ -1827,7 +1837,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
1827
1837
  return {
1828
1838
  latestValue: latest?.latestRunPhase || latest?.label || "None",
1829
1839
  latestDetail: latest
1830
- ? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${relativeTime(latestRunTimestamp(latest)!)} ` : ""}`.trim()
1840
+ ? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${formatRelativeAgeTime(latestRunTimestamp(latest)!)} ` : ""}`.trim()
1831
1841
  : "No retained runs",
1832
1842
  retainedValue:
1833
1843
  retained > 0 ? `${succeeded} succeeded / ${failed} failed` : "0 retained",
@@ -1842,7 +1852,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
1842
1852
  : `${schedules.length} schedules`,
1843
1853
  scheduleDetail:
1844
1854
  schedules.length === 1 && latest?.lastSuccessfulAt
1845
- ? `last success ${relativeTime(latest.lastSuccessfulAt)}`
1855
+ ? `last success ${formatRelativeAgeTime(latest.lastSuccessfulAt)}`
1846
1856
  : undefined,
1847
1857
  scheduleMono: schedules.length === 1,
1848
1858
  };
@@ -1856,17 +1866,6 @@ function activeWorkloadNames(activity: AppBatchActivity[]): string {
1856
1866
  return `${active.slice(0, 2).join(", ")} +${active.length - 2} more`;
1857
1867
  }
1858
1868
 
1859
- function relativeTime(value: string): string {
1860
- const t = Date.parse(value);
1861
- if (Number.isNaN(t)) return value;
1862
- const diff = Date.now() - t;
1863
- if (diff < 60_000) return "just now";
1864
- const minutes = Math.floor(diff / 60_000);
1865
- if (minutes < 60) return `${minutes}m ago`;
1866
- const hours = Math.floor(minutes / 60);
1867
- if (hours < 48) return `${hours}h ago`;
1868
- return `${Math.floor(hours / 24)}d ago`;
1869
- }
1870
1869
 
1871
1870
  function ApplicationLatestHistory({
1872
1871
  history,
@@ -2093,7 +2092,7 @@ function workloadRuntimeDetail(workload: AppWorkload): string {
2093
2092
  const at =
2094
2093
  batch.latestStartedAt || batch.latestFinishedAt || batch.lastScheduledAt;
2095
2094
  if (batch.latestRunName)
2096
- return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${relativeTime(at)}` : ""}`;
2095
+ return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${formatRelativeAgeTime(at)}` : ""}`;
2097
2096
  if (batch.schedule) return `Schedule ${batch.schedule}`;
2098
2097
  return "No retained runs";
2099
2098
  }
@@ -2201,6 +2200,7 @@ function ApplicationHistoryView({
2201
2200
  coverageRecordCount,
2202
2201
  runtimeLimited,
2203
2202
  ringTruncated,
2203
+ retentionNote,
2204
2204
  workloads,
2205
2205
  onSelectWorkload,
2206
2206
  onNavigateToResource,
@@ -2219,6 +2219,7 @@ function ApplicationHistoryView({
2219
2219
  coverageRecordCount?: number;
2220
2220
  runtimeLimited?: boolean;
2221
2221
  ringTruncated?: boolean;
2222
+ retentionNote?: ReactNode;
2222
2223
  workloads: AppWorkload[];
2223
2224
  onSelectWorkload: (workload: AppWorkload) => void;
2224
2225
  onNavigateToResource?: (resource: ResourceRef) => void;
@@ -2428,6 +2429,7 @@ function ApplicationHistoryView({
2428
2429
  application activity in this range may not be included.
2429
2430
  </div>
2430
2431
  )}
2432
+ {retentionNote && <div>{retentionNote}</div>}
2431
2433
  {ringTruncated && (
2432
2434
  <div>
2433
2435
  {mode === "retained"
@@ -0,0 +1,118 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import { beforeEach, afterEach, expect, it, vi } from 'vitest'
5
+ import { AreaChart } from './AreaChart'
6
+
7
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
8
+ let root: Root, host: HTMLDivElement
9
+ const series = [{ labels: {}, dataPoints: [{ timestamp: 1000, value: 1 }, { timestamp: 1600, value: 2 }] }]
10
+ beforeEach(() => {
11
+ vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} })
12
+ vi.stubGlobal('requestAnimationFrame', () => 0)
13
+ Object.defineProperty(SVGElement.prototype, 'getScreenCTM', { configurable: true, value: () => ({ a: 2, e: 10 }) })
14
+ Object.defineProperty(SVGElement.prototype, 'setPointerCapture', { configurable: true, value: vi.fn() })
15
+ Object.defineProperty(SVGElement.prototype, 'hasPointerCapture', { configurable: true, value: () => true })
16
+ Object.defineProperty(SVGElement.prototype, 'releasePointerCapture', { configurable: true, value: vi.fn() })
17
+ host = document.createElement('div'); document.body.appendChild(host); root = createRoot(host)
18
+ })
19
+ afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.unstubAllGlobals() })
20
+ async function render(props: Partial<Parameters<typeof AreaChart>[0]> = {}) {
21
+ await act(async () => root.render(<AreaChart series={series} color="currentColor" fillColor="currentColor" unit="count" {...props} />))
22
+ return host.querySelector<SVGRectElement>('rect[fill="transparent"]')!
23
+ }
24
+ async function pointer(plot: SVGRectElement, type: string, x: number, button = 0, pointerType = 'mouse') {
25
+ await act(async () => {
26
+ const event = new MouseEvent(type, { bubbles: true, clientX: x * 2 + 10, button })
27
+ Object.defineProperty(event, 'pointerId', { value: 1 })
28
+ Object.defineProperty(event, 'pointerType', { value: pointerType })
29
+ plot.dispatchEvent(event)
30
+ })
31
+ }
32
+ it.each([[84, 960], [960, 84]])('uses the release position and orders forward/reverse ranges (%s,%s)', async (from, to) => {
33
+ const selected = vi.fn(); const plot = await render({ onSelectRange: selected })
34
+ await pointer(plot, 'pointerdown', from); await pointer(plot, 'pointerup', to)
35
+ expect(selected).toHaveBeenCalledWith({ start: 1000, end: 1600 })
36
+ })
37
+ it('clamps drag release beyond the plot without canceling and clears canceled gestures', async () => {
38
+ const selected = vi.fn(); const plot = await render({ onSelectRange: selected })
39
+ await pointer(plot, 'pointerdown', 84); await pointer(plot, 'pointermove', 1500); await pointer(plot, 'pointerup', 1500)
40
+ expect(selected).toHaveBeenLastCalledWith({ start: 1000, end: 1600 })
41
+ selected.mockClear()
42
+ await pointer(plot, 'pointerdown', 200); await pointer(plot, 'pointercancel', 800); await pointer(plot, 'pointerup', 800)
43
+ expect(selected).not.toHaveBeenCalled(); expect(host.querySelector('[data-chart-selection]')).toBeNull()
44
+ })
45
+ it('selects a bounded evaluation interval by click and keyboard', async () => {
46
+ const selected = vi.fn(); const plot = await render({ onSelectRange: selected, stepSeconds: 60 })
47
+ await pointer(plot, 'pointerdown', 84); await pointer(plot, 'pointerup', 84)
48
+ expect(selected).toHaveBeenLastCalledWith({ start: 1000, end: 1030 })
49
+ await act(async () => { plot.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' })) })
50
+ await act(async () => { plot.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })) })
51
+ expect(selected).toHaveBeenLastCalledWith({ start: 1030, end: 1090 })
52
+ })
53
+ it('keeps default hover and exposes a gap reason even beside a nearby sample', async () => {
54
+ const plot = await render({ shadedRanges: [{ start: 1000, end: 1100, label: 'Metrics unavailable' }], stepSeconds: 600 })
55
+ expect(plot.style.cursor).toBe('crosshair'); expect(plot.getAttribute('tabindex')).toBeNull()
56
+ await pointer(plot, 'pointermove', 84)
57
+ expect(host.textContent).toContain('Metrics unavailable'); expect(host.textContent).toContain('1')
58
+ })
59
+ it('does not treat secondary pointer buttons as a selection', async () => {
60
+ const selected = vi.fn(); const plot = await render({ onSelectRange: selected })
61
+ await pointer(plot, 'pointerdown', 84, 2); await pointer(plot, 'pointerup', 960, 2)
62
+ expect(selected).not.toHaveBeenCalled()
63
+ })
64
+
65
+ it('shows keyboard focus and clears its tooltip after blur or Escape', async () => {
66
+ const plot = await render({ onSelectRange: vi.fn(), stepSeconds: 60 })
67
+ await act(async () => plot.dispatchEvent(new FocusEvent('focusin', { bubbles: true })))
68
+ expect(plot.getAttribute('stroke')).toBe('currentColor')
69
+ await act(async () => plot.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' })))
70
+ expect(host.querySelector('.pointer-events-none.absolute')).not.toBeNull()
71
+ await act(async () => plot.dispatchEvent(new FocusEvent('focusout', { bubbles: true })))
72
+ expect(plot.getAttribute('stroke')).toBeNull()
73
+ expect(host.querySelector('.pointer-events-none.absolute')).toBeNull()
74
+ await act(async () => plot.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' })))
75
+ await act(async () => plot.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' })))
76
+ expect(host.querySelector('.pointer-events-none.absolute')).toBeNull()
77
+ })
78
+
79
+ it('retains a default-chart touch tooltip after a tap without pointermove, then clears on mouse leave', async () => {
80
+ const plot = await render()
81
+ await pointer(plot, 'pointerdown', 84, 0, 'touch')
82
+ await pointer(plot, 'pointerup', 84, 0, 'touch')
83
+ await pointer(plot, 'pointerout', 84, 0, 'touch')
84
+ expect(host.querySelector('.pointer-events-none.absolute')).not.toBeNull()
85
+ expect(host.textContent).toContain('1')
86
+ await act(async () => plot.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })))
87
+ expect(host.querySelector('.pointer-events-none.absolute')).toBeNull()
88
+ })
89
+
90
+ it('paints selected, shaded and dragged ranges above opaque fills and below chart lines', async () => {
91
+ const plot = await render({ fillColor: '#123456', selection: { start: 1000, end: 1100 }, shadedRanges: [{ start: 1100, end: 1200 }], onSelectRange: vi.fn() })
92
+ await pointer(plot, 'pointerdown', 84)
93
+ await pointer(plot, 'pointermove', 500)
94
+ const fill = host.querySelector('path[fill="#123456"]')!
95
+ const line = host.querySelector('path[fill="none"]')!
96
+ const bands = [...host.querySelectorAll('[data-chart-gap], [data-chart-selection], rect[opacity="0.2"]')]
97
+ expect(bands).toHaveLength(3)
98
+ for (const band of bands) {
99
+ expect(fill.compareDocumentPosition(band) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
100
+ expect(band.compareDocumentPosition(line) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
101
+ }
102
+ })
103
+
104
+ it('does not render a selectable empty chart without a finite timestamp domain', async () => {
105
+ const selected = vi.fn()
106
+ await render({ series: [{ labels: {}, dataPoints: [] }], onSelectRange: selected, stepSeconds: 60 })
107
+ expect(host.querySelector('svg')).toBeNull()
108
+ expect(host.querySelector('[role="button"]')).toBeNull()
109
+ expect(selected).not.toHaveBeenCalled()
110
+ })
111
+
112
+ it('keeps an empty chart selectable when an explicit valid domain supplies its bounds', async () => {
113
+ const selected = vi.fn()
114
+ const plot = await render({ series: [{ labels: {}, dataPoints: [] }], domain: { start: 1000, end: 1600 }, onSelectRange: selected, stepSeconds: 60 })
115
+ await pointer(plot, 'pointerdown', 84)
116
+ await pointer(plot, 'pointerup', 960)
117
+ expect(selected).toHaveBeenCalledWith({ start: 1000, end: 1600 })
118
+ })
@@ -171,3 +171,28 @@ describe('AreaChart compact layout and count axes', () => {
171
171
  expect(compactCount).not.toContain('>10<')
172
172
  })
173
173
  })
174
+
175
+ describe('AreaChart gaps and selection', () => {
176
+ const base = [series([[0, 1], [60, null], [120, 3], [180, 2]])]
177
+
178
+ it('hatches shaded ranges and draws the selection band inside the plot', () => {
179
+ const html = render({
180
+ series: base,
181
+ stepSeconds: 60,
182
+ shadedRanges: [{ start: t0 + 30, end: t0 + 90, label: 'No sample' }],
183
+ selection: { start: t0 + 120, end: t0 + 180 },
184
+ onSelectRange: () => {},
185
+ })
186
+ expect(html.match(/data-chart-gap/g)).toHaveLength(1)
187
+ expect(html).toMatch(/fill="url\(#hatch-[^"]+\)"/)
188
+ expect(html).toContain('data-chart-selection')
189
+ expect(html).toContain('cursor:col-resize')
190
+ })
191
+
192
+ it('draws nothing extra without the new props', () => {
193
+ const html = render({ series: base })
194
+ expect(html).not.toContain('data-chart-gap')
195
+ expect(html).not.toContain('data-chart-selection')
196
+ expect(html).toContain('cursor:crosshair')
197
+ })
198
+ })
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
2
2
  import type * as React from 'react'
3
3
  import { seriesColor, seriesFill, computeShortLabels, seriesDisplayLabels } from './colors'
4
4
  import { formatMetricValue, formatTimestamp } from './format'
@@ -13,7 +13,13 @@ import { nearestSample } from './nearestSample'
13
13
  export const ANNOTATION_LABEL_MIN_WIDTH_PX = 420
14
14
  const ANNOTATION_HOVER_TOLERANCE = 8
15
15
 
16
- export function AreaChart({ series, color, fillColor, unit, referenceLines, annotations, domain, seriesLabels, stepSeconds, layout = 'full' }: {
16
+ /** A time range on the chart's X axis, in unix seconds. */
17
+ export interface ChartTimeRange {
18
+ start: number
19
+ end: number
20
+ }
21
+
22
+ export function AreaChart({ series, color, fillColor, unit, referenceLines, annotations, domain, seriesLabels, stepSeconds, layout = 'full', shadedRanges, selection, onSelectRange }: {
17
23
  series: TimeSeries[]
18
24
  color: string
19
25
  fillColor: string
@@ -39,10 +45,23 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
39
45
  * keeps text and plot height stable as its container resizes.
40
46
  */
41
47
  layout?: 'full' | 'auto' | 'compact' | 'dashboard'
48
+ /** Ranges drawn hatched, e.g. where no sample exists; `label` says why and shows on hover. */
49
+ shadedRanges?: (ChartTimeRange & { label?: string })[]
50
+ /** A selected range, drawn as a band. */
51
+ selection?: ChartTimeRange | null
52
+ /**
53
+ * Enables range selection: drag across the plot, or click to pick the one
54
+ * step-wide window around the pointer (needs `stepSeconds`; clipped at the domain edges).
55
+ */
56
+ onSelectRange?: (range: ChartTimeRange) => void
42
57
  }) {
58
+ const hatchId = `hatch-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`
59
+ const [drag, setDrag] = useState<{ from: number; to: number } | null>(null)
60
+ const dragRef = useRef<{ from: number; to: number } | null>(null)
43
61
  const svgRef = useRef<SVGSVGElement>(null)
44
62
  const wrapperRef = useRef<HTMLDivElement>(null)
45
63
  const [hoverX, setHoverX] = useState<number | null>(null)
64
+ const [keyboardFocus, setKeyboardFocus] = useState(false)
46
65
  const [labelsFit, setLabelsFit] = useState(true)
47
66
  const [renderedWidth, setRenderedWidth] = useState(0)
48
67
  const multiSeries = series.length > 1
@@ -70,6 +89,7 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
70
89
  minTs = domain.start
71
90
  maxTs = domain.end
72
91
  }
92
+ if (!Number.isFinite(minTs) || !Number.isFinite(maxTs)) return null
73
93
  if (minTs === maxTs) maxTs = minTs + 60
74
94
  if (maxVal === 0 && minVal === 0) {
75
95
  // Unit-appropriate floor so the Y-axis isn't misleadingly large.
@@ -264,17 +284,77 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
264
284
  p => Math.abs(p.x - clampedX) <= ANNOTATION_HOVER_TOLERANCE,
265
285
  )
266
286
 
267
- return { ts, x: clampedX, points, nearbyAnnotations }
268
- }, [hoverX, chartData, placedAnnotations, seriesLabels, marginLeft, plotWidth, marginTop, plotHeight, multiSeries, color, stepSeconds])
287
+ const shaded = shadedRanges?.find(r => ts >= r.start && ts <= r.end && r.label)
288
+
289
+ return { ts, x: clampedX, points, nearbyAnnotations, shaded }
290
+ }, [hoverX, chartData, placedAnnotations, seriesLabels, marginLeft, plotWidth, marginTop, plotHeight, multiSeries, color, stepSeconds, shadedRanges])
269
291
 
270
- const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
271
- const svg = svgRef.current
272
- if (!svg) return
273
- const ctm = svg.getScreenCTM()
274
- if (!ctm) return
275
- setHoverX((e.clientX - ctm.e) / ctm.a)
292
+ const pointerX = useCallback((e: { clientX: number }) => {
293
+ const ctm = svgRef.current?.getScreenCTM()
294
+ return ctm ? (e.clientX - ctm.e) / ctm.a : null
276
295
  }, [])
277
296
 
297
+ const clampX = (x: number) => Math.max(marginLeft, Math.min(marginLeft + plotWidth, x))
298
+ const tsAt = (x: number) => {
299
+ if (!chartData) return 0
300
+ return chartData.minTs + ((clampX(x) - marginLeft) / plotWidth) * (chartData.maxTs - chartData.minTs)
301
+ }
302
+ const selectAt = (from: number, to: number) => {
303
+ if (!onSelectRange || !chartData) return
304
+ const a = tsAt(Math.min(from, to))
305
+ const b = tsAt(Math.max(from, to))
306
+ if (Math.abs(to - from) >= 6) {
307
+ onSelectRange({ start: Math.round(a), end: Math.round(b) })
308
+ } else if (stepSeconds && stepSeconds > 0) {
309
+ const half = stepSeconds / 2
310
+ onSelectRange({ start: Math.round(Math.max(chartData.minTs, a - half)), end: Math.round(Math.min(chartData.maxTs, a + half)) })
311
+ }
312
+ }
313
+ const cancelDrag = () => {
314
+ dragRef.current = null
315
+ setDrag(null)
316
+ }
317
+ const handlePointerMove = (e: React.PointerEvent<SVGRectElement>) => {
318
+ const x = pointerX(e)
319
+ if (x === null) return
320
+ setHoverX(x)
321
+ if (dragRef.current) {
322
+ dragRef.current = { ...dragRef.current, to: clampX(x) }
323
+ setDrag(dragRef.current)
324
+ }
325
+ }
326
+ const handlePointerDown = (e: React.PointerEvent<SVGRectElement>) => {
327
+ if (e.button !== 0) return
328
+ const x = pointerX(e)
329
+ if (x === null) return
330
+ setHoverX(x)
331
+ if (!onSelectRange) return
332
+ e.currentTarget.setPointerCapture(e.pointerId)
333
+ dragRef.current = { from: clampX(x), to: clampX(x) }
334
+ setDrag(dragRef.current)
335
+ }
336
+ const handlePointerUp = (e: React.PointerEvent<SVGRectElement>) => {
337
+ const current = dragRef.current
338
+ const x = pointerX(e)
339
+ cancelDrag()
340
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId)
341
+ if (current && x !== null) selectAt(current.from, clampX(x))
342
+ }
343
+ const handleKeyDown = (e: React.KeyboardEvent<SVGRectElement>) => {
344
+ if (!chartData || !onSelectRange) return
345
+ if (e.key === 'Escape') { cancelDrag(); setHoverX(null); return }
346
+ if (!stepSeconds || stepSeconds <= 0) return
347
+ const x = hoverX ?? marginLeft
348
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
349
+ e.preventDefault()
350
+ const step = (stepSeconds / (chartData.maxTs - chartData.minTs)) * plotWidth
351
+ setHoverX(clampX(x + (e.key === 'ArrowRight' ? step : -step)))
352
+ } else if (e.key === 'Enter' || e.key === ' ') {
353
+ e.preventDefault()
354
+ selectAt(x, x)
355
+ }
356
+ }
357
+
278
358
  // Hook calls above run unconditionally; bail out of rendering only after
279
359
  // every hook has been invoked (Rules of Hooks).
280
360
  if (!chartData) return null
@@ -292,6 +372,12 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
292
372
  preserveAspectRatio="xMidYMid meet"
293
373
  data-chart-layout={compact ? 'compact' : 'full'}
294
374
  >
375
+ <defs>
376
+ <pattern id={hatchId} width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
377
+ <line x1="0" y1="0" x2="0" y2="6" stroke="currentColor" strokeWidth="2" className="text-theme-border" />
378
+ </pattern>
379
+ </defs>
380
+
295
381
  {/* Grid lines */}
296
382
  {yTicks.map((tick, i) => (
297
383
  <line
@@ -359,6 +445,32 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
359
445
  />
360
446
  ))}
361
447
 
448
+ {shadedRanges?.map((r, i) => {
449
+ const x1 = Math.max(marginLeft, toX(r.start))
450
+ const x2 = Math.min(marginLeft + plotWidth, toX(r.end))
451
+ if (x2 <= x1) return null
452
+ return <rect key={`shade-${i}`} data-chart-gap x={x1} y={marginTop} width={x2 - x1} height={plotHeight} fill={`url(#${hatchId})`} opacity="0.7" />
453
+ })}
454
+
455
+ {selection && (() => {
456
+ const x1 = Math.max(marginLeft, toX(selection.start))
457
+ const x2 = Math.min(marginLeft + plotWidth, toX(selection.end))
458
+ if (x2 < x1) return null
459
+ return <rect data-chart-selection x={x1} y={marginTop} width={Math.max(2, x2 - x1)} height={plotHeight} fill="currentColor" className="text-accent" opacity="0.15" />
460
+ })()}
461
+
462
+ {drag && Math.abs(drag.to - drag.from) >= 2 && (
463
+ <rect
464
+ x={Math.max(marginLeft, Math.min(drag.from, drag.to))}
465
+ y={marginTop}
466
+ width={Math.abs(drag.to - drag.from)}
467
+ height={plotHeight}
468
+ fill="currentColor"
469
+ className="text-accent"
470
+ opacity="0.2"
471
+ />
472
+ )}
473
+
362
474
  {/* Lines */}
363
475
  {paths.map(p => p && (
364
476
  <path
@@ -510,9 +622,23 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
510
622
  x={marginLeft} y={marginTop}
511
623
  width={plotWidth} height={plotHeight}
512
624
  fill="transparent"
513
- style={{ cursor: 'crosshair' }}
514
- onMouseMove={handleMouseMove}
625
+ onPointerMove={handlePointerMove}
626
+ onPointerDown={handlePointerDown}
627
+ onPointerUp={handlePointerUp}
628
+ onPointerCancel={cancelDrag}
629
+ onLostPointerCapture={cancelDrag}
630
+ onPointerLeave={(e) => { if (e.pointerType !== 'touch') setHoverX(null) }}
515
631
  onMouseLeave={() => setHoverX(null)}
632
+ tabIndex={onSelectRange && stepSeconds && stepSeconds > 0 ? 0 : undefined}
633
+ role={onSelectRange && stepSeconds && stepSeconds > 0 ? 'button' : undefined}
634
+ aria-label={onSelectRange && stepSeconds && stepSeconds > 0 ? 'Select chart interval: use arrow keys to move, then Enter to select' : undefined}
635
+ onKeyDown={handleKeyDown}
636
+ onFocus={() => setKeyboardFocus(true)}
637
+ onBlur={() => { setKeyboardFocus(false); setHoverX(null); cancelDrag() }}
638
+ stroke={keyboardFocus ? 'currentColor' : undefined}
639
+ strokeWidth={keyboardFocus ? 2 : undefined}
640
+ className={keyboardFocus ? 'text-accent' : undefined}
641
+ style={{ cursor: onSelectRange ? 'col-resize' : 'crosshair', touchAction: onSelectRange ? 'none' : undefined }}
516
642
  />
517
643
  </svg>
518
644
 
@@ -538,6 +664,9 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines, anno
538
664
  </span>
539
665
  </div>
540
666
  ))}
667
+ {hoverData.shaded && (
668
+ <div className="py-0.5 text-theme-text-secondary">{hoverData.shaded.label}</div>
669
+ )}
541
670
  {hoverData.points.map((p, i) => (
542
671
  <div key={i} className="flex items-center gap-2 py-0.5">
543
672
  <div
@@ -1,4 +1,5 @@
1
1
  export { AreaChart } from './AreaChart'
2
+ export type { ChartTimeRange } from './AreaChart'
2
3
  export { MetricsSummary } from './MetricsSummary'
3
4
  export { SeriesLegend } from './SeriesLegend'
4
5
  export {