@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.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +57 -17
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -24
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +99 -39
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +8 -74
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +21 -2
- package/src/types/core.ts +60 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- 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.
|
|
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.
|
|
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.
|
|
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) ? ` · ${
|
|
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 ${
|
|
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 ? ` · ${
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
271
|
-
const
|
|
272
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|