@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
|
@@ -106,3 +106,31 @@ it('keeps pod/template evidence in expanded neutral context with navigable witne
|
|
|
106
106
|
expect(expanded).not.toContain('confidence')
|
|
107
107
|
expect(expanded).not.toContain('Caused by')
|
|
108
108
|
})
|
|
109
|
+
|
|
110
|
+
it('wraps a compact Pod scheduling cause separately and retains the full message in details', () => {
|
|
111
|
+
const raw = '0/2 nodes are available: 2 Too many pods. preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.'
|
|
112
|
+
const pod: Issue = { ...issue, kind: 'Pod', reason: 'Unschedulable', cause: raw, message: raw }
|
|
113
|
+
const html = renderToString(<IssueRow issue={pod} compact open onToggle={() => {}} />)
|
|
114
|
+
expect(html).toContain('break-words text-xs text-theme-text-secondary')
|
|
115
|
+
expect(html).toContain('both nodes have reached their Pod limit')
|
|
116
|
+
expect(html).toContain(raw)
|
|
117
|
+
const producerMessage = '2 node(s) insufficient pods (0/2 nodes available)'
|
|
118
|
+
const reported = renderToString(<IssueRow issue={{ ...pod, cause: undefined, message: producerMessage }} compact open onToggle={() => {}} />)
|
|
119
|
+
expect(reported).toContain('both nodes have reached their Pod limit')
|
|
120
|
+
expect(reported).toContain(producerMessage)
|
|
121
|
+
const regular = renderToString(<IssueRow issue={pod} open={false} onToggle={() => {}} />)
|
|
122
|
+
expect(regular).not.toContain('both nodes have reached their Pod limit')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it.each(['Cluster', 'Deployment'])('wraps compact scheduling causes aggregated under %s', (kind) => {
|
|
126
|
+
const raw = '2 node(s) insufficient pods (0/2 nodes available)'
|
|
127
|
+
const aggregated: Issue = { ...issue, kind, reason: 'Unschedulable', cause: raw, message: raw }
|
|
128
|
+
const collapsed = renderToString(<IssueRow issue={aggregated} compact open={false} onToggle={() => {}} />)
|
|
129
|
+
expect(collapsed).toContain('<div class="break-words text-xs text-theme-text-secondary">both nodes have reached their Pod limit</div>')
|
|
130
|
+
expect(collapsed).not.toContain(raw)
|
|
131
|
+
const expanded = renderToString(<IssueRow issue={aggregated} compact open onToggle={() => {}} />)
|
|
132
|
+
expect(expanded).toContain(raw)
|
|
133
|
+
const regular = renderToString(<IssueRow issue={aggregated} open={false} onToggle={() => {}} />)
|
|
134
|
+
expect(regular).toContain('Unschedulable')
|
|
135
|
+
expect(regular).not.toContain('both nodes have reached their Pod limit')
|
|
136
|
+
})
|
|
@@ -8,7 +8,9 @@ export function ResourceIssuesSection({
|
|
|
8
8
|
issues,
|
|
9
9
|
onResourceClick,
|
|
10
10
|
subjectResource,
|
|
11
|
+
compact,
|
|
11
12
|
}: {
|
|
13
|
+
compact?: boolean
|
|
12
14
|
issues: Issue[] | undefined
|
|
13
15
|
/** When provided, related resources in a causal link become clickable. */
|
|
14
16
|
onResourceClick?: (ref: IssueResourceRef) => void
|
|
@@ -36,6 +38,7 @@ export function ResourceIssuesSection({
|
|
|
36
38
|
<IssueRow
|
|
37
39
|
key={key}
|
|
38
40
|
issue={issue}
|
|
41
|
+
compact={compact}
|
|
39
42
|
open={openId === key}
|
|
40
43
|
onToggle={() => setOpenId((cur) => (cur === key ? null : key))}
|
|
41
44
|
onResourceClick={onResourceClick}
|
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
issueSortAnchor,
|
|
17
17
|
compareIssueSortAnchors,
|
|
18
18
|
} from './types';
|
|
19
|
-
export type { Issue, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
|
|
19
|
+
export type { Issue, IssueConditionalCleanup, IssueFinalizerRemoval, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
|
|
20
20
|
export {
|
|
21
21
|
ISSUE_SEVERITY_LABEL,
|
|
22
22
|
ISSUE_SEVERITY_BADGE_CLASS,
|
|
@@ -24,5 +24,7 @@ export {
|
|
|
24
24
|
ISSUE_SEVERITY_FILL_CLASS,
|
|
25
25
|
ISSUE_SEVERITY_RAIL_CLASS,
|
|
26
26
|
categoryLabel,
|
|
27
|
+
issueTitle,
|
|
28
|
+
issueReasonTitle,
|
|
27
29
|
groupLabel,
|
|
28
30
|
} from './severity';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeploymentLikeWorkloadKind } from '../../types';
|
|
2
2
|
import type { Issue } from './types';
|
|
3
3
|
|
|
4
|
-
export type IssueTimingDisplayKind = 'creation' | 'regression';
|
|
4
|
+
export type IssueTimingDisplayKind = 'creation' | 'regression' | 'deletion';
|
|
5
5
|
|
|
6
6
|
export interface IssueTimingDisplay {
|
|
7
7
|
kind: IssueTimingDisplayKind;
|
|
@@ -77,6 +77,17 @@ export function issueTiming(issue: Issue): IssueTimingDisplay | null {
|
|
|
77
77
|
const grouped = (coverage?.known ?? 0) + (coverage?.unknown ?? 0) > 1;
|
|
78
78
|
const uncertaintyMeta = onsetUncertaintyMeta(issue);
|
|
79
79
|
const uncertaintyTooltip = onsetUncertaintyTooltip(issue);
|
|
80
|
+
// A stuck deletion is timed from its deletionTimestamp. The creation-vs-healthy
|
|
81
|
+
// classification only says whether the object was deleted soon after it was
|
|
82
|
+
// created, which is not what the age beside this chip measures.
|
|
83
|
+
if (issue.issue_timing_basis === 'deletion' && issue.issue_timing) {
|
|
84
|
+
return {
|
|
85
|
+
kind: 'deletion',
|
|
86
|
+
chip: 'since deletion',
|
|
87
|
+
meta: 'pending since deletion was requested',
|
|
88
|
+
tooltip: 'Timed from when deletion was requested; the object is still present.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
80
91
|
switch (issue.issue_timing) {
|
|
81
92
|
case 'started_after_resource_was_healthy': {
|
|
82
93
|
if (issue.issue_timing_basis === 'owner_condition') {
|
|
@@ -113,9 +124,9 @@ export function issueTiming(issue: Issue): IssueTimingDisplay | null {
|
|
|
113
124
|
}
|
|
114
125
|
return {
|
|
115
126
|
kind: 'regression',
|
|
116
|
-
chip: 'after healthy',
|
|
127
|
+
chip: issue.kind === 'Node' && issue.reason === 'NotReady' ? 'NotReady' : 'after healthy',
|
|
117
128
|
meta: 'failing evidence followed a healthy period',
|
|
118
|
-
tooltip: 'A healthy period preceded this failing evidence.',
|
|
129
|
+
tooltip: issue.kind === 'Node' && issue.reason === 'NotReady' ? 'The Ready condition stopped reporting healthy at this time.' : 'A healthy period preceded this failing evidence.',
|
|
119
130
|
};
|
|
120
131
|
}
|
|
121
132
|
case 'started_at_resource_creation': {
|
|
@@ -2,7 +2,7 @@ import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
|
2
2
|
import { createElement } from 'react'
|
|
3
3
|
import { renderToString } from 'react-dom/server'
|
|
4
4
|
import { compareIssues, issueSortAnchor, subjectRef, memberRef, normalizeImagePullMessage, issueMessageParts, type Issue } from './types'
|
|
5
|
-
import { categoryLabel, groupBadgeClass, groupLabel } from './severity'
|
|
5
|
+
import { categoryLabel, groupBadgeClass, groupLabel, issueTitle } from './severity'
|
|
6
6
|
import { IssueRow } from './IssuesView'
|
|
7
7
|
import { issueFirstSeenTitle, issueResourceCreatedTitle, issueTiming } from './issue-timing'
|
|
8
8
|
|
|
@@ -83,6 +83,8 @@ describe('category/group label fallbacks', () => {
|
|
|
83
83
|
it('returns the mapped label, else humanizes (server-added category needs no frontend deploy)', () => {
|
|
84
84
|
expect(categoryLabel('crashloop')).toBe('Crash loop')
|
|
85
85
|
expect(categoryLabel('some_new_future_category')).toBe('Some new future category')
|
|
86
|
+
expect(issueTitle({ category: 'backup_failed', reason: 'CNPGWALArchivingFailing' })).toBe('WAL archiving failing')
|
|
87
|
+
expect(issueTitle({ category: 'backup_failed', reason: 'BackupFailed' })).toBe('Backup failed')
|
|
86
88
|
})
|
|
87
89
|
it('humanizes an unmapped group', () => {
|
|
88
90
|
expect(groupLabel('runtime')).toBe('Runtime')
|
|
@@ -137,9 +139,36 @@ describe('IssueRow', () => {
|
|
|
137
139
|
expect(html).toContain('after healthy')
|
|
138
140
|
})
|
|
139
141
|
|
|
142
|
+
it('labels a stuck deletion by how long deletion has been pending', () => {
|
|
143
|
+
vi.useFakeTimers()
|
|
144
|
+
vi.setSystemTime(new Date('2026-06-30T12:00:00Z'))
|
|
145
|
+
|
|
146
|
+
const html = renderToString(createElement(IssueRow, {
|
|
147
|
+
issue: mk({
|
|
148
|
+
id: 'terminating',
|
|
149
|
+
kind: 'VMAgent',
|
|
150
|
+
group: 'operator.victoriametrics.com',
|
|
151
|
+
category: 'termination_stuck',
|
|
152
|
+
category_group: 'control_plane',
|
|
153
|
+
reason: 'Terminating stuck',
|
|
154
|
+
first_seen: '2026-06-30T11:30:00Z',
|
|
155
|
+
issue_timing: 'started_at_resource_creation',
|
|
156
|
+
issue_timing_basis: 'deletion',
|
|
157
|
+
}),
|
|
158
|
+
open: true,
|
|
159
|
+
onToggle: () => undefined,
|
|
160
|
+
}))
|
|
161
|
+
|
|
162
|
+
expect(html).toContain('30m')
|
|
163
|
+
expect(html).toContain('since deletion')
|
|
164
|
+
expect(html).toContain('pending since deletion was requested')
|
|
165
|
+
expect(html).not.toContain('since creation')
|
|
166
|
+
expect(html).not.toContain('first reconciliation')
|
|
167
|
+
})
|
|
168
|
+
|
|
140
169
|
it('uses the category-group chip class in the queue row', () => {
|
|
141
170
|
const html = renderToString(createElement(IssueRow, {
|
|
142
|
-
issue: mk({ category_group: 'control_plane' }),
|
|
171
|
+
issue: mk({ category_group: 'control_plane', kind: 'Node' }),
|
|
143
172
|
open: false,
|
|
144
173
|
onToggle: () => undefined,
|
|
145
174
|
}))
|
|
@@ -394,6 +423,25 @@ describe('issueTiming', () => {
|
|
|
394
423
|
})
|
|
395
424
|
})
|
|
396
425
|
|
|
426
|
+
it('times stuck deletions from the deletion request, however soon after creation it came', () => {
|
|
427
|
+
for (const timing of ['started_at_resource_creation', 'started_after_resource_was_healthy'] as const) {
|
|
428
|
+
for (const kind of ['Pod', 'VMAgent']) {
|
|
429
|
+
const display = issueTiming(mk({
|
|
430
|
+
kind,
|
|
431
|
+
category: 'termination_stuck',
|
|
432
|
+
issue_timing: timing,
|
|
433
|
+
issue_timing_basis: 'deletion',
|
|
434
|
+
}))
|
|
435
|
+
expect(display).toMatchObject({
|
|
436
|
+
kind: 'deletion',
|
|
437
|
+
chip: 'since deletion',
|
|
438
|
+
meta: 'pending since deletion was requested',
|
|
439
|
+
})
|
|
440
|
+
expect(`${display?.chip} ${display?.meta} ${display?.tooltip}`).not.toMatch(/creation|healthy|deploy/i)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
397
445
|
it('describes direct after-healthy timing without coupling it to first_seen', () => {
|
|
398
446
|
const display = issueTiming(mk({
|
|
399
447
|
first_seen: '2026-06-30T10:00:00Z',
|
|
@@ -136,6 +136,27 @@ export function categoryLabel(category: string): string {
|
|
|
136
136
|
return CATEGORY_LABEL[category] ?? humanize(category);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Reasons filed under a category whose label names a different operation:
|
|
140
|
+
// CNPG archiving and an unanswered scheduled run sit under backup_failed.
|
|
141
|
+
const REASON_TITLE: Record<string, string> = {
|
|
142
|
+
CNPGWALArchivingFailing: "WAL archiving failing",
|
|
143
|
+
CNPGLastBackupFailed: "Latest backup failed",
|
|
144
|
+
CNPGScheduleDestinationMissing: "Backup schedule has no destination",
|
|
145
|
+
CNPGInstanceReadinessMismatch: "Instance Pods contradict Cluster readiness",
|
|
146
|
+
CNPGPrimaryLabelMismatch: "Primary Pod label contradicts Cluster status",
|
|
147
|
+
CNPGScheduledRunNoBackup: "No successful backup since a scheduled run",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** The title a reason has wherever it is shown, when its category's label would name the wrong operation. */
|
|
151
|
+
export function issueReasonTitle(reason: string | undefined): string | undefined {
|
|
152
|
+
return reason ? REASON_TITLE[reason] : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** An issue row's title: its category, unless the reason names it better. */
|
|
156
|
+
export function issueTitle(issue: { category: string; reason?: string }): string {
|
|
157
|
+
return issueReasonTitle(issue.reason) || categoryLabel(issue.category);
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
export function groupLabel(group: string): string {
|
|
140
161
|
return GROUP_LABEL[group] ?? humanize(group);
|
|
141
162
|
}
|
|
@@ -141,6 +141,26 @@ export interface IssueRecentChange {
|
|
|
141
141
|
consumed_by?: string[];
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** One finalizer removal: a JSON Patch that tests the object UID and the
|
|
145
|
+
* finalizer at its index before removing it, previewed as dry runs. */
|
|
146
|
+
export interface IssueFinalizerRemoval {
|
|
147
|
+
finalizer: string;
|
|
148
|
+
/** patch_resource tool input, with dry_run=true. */
|
|
149
|
+
patch_resource: Record<string, unknown>;
|
|
150
|
+
/** The same patch as a kubectl server-side dry run. */
|
|
151
|
+
kubectl: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface IssueConditionalCleanup {
|
|
155
|
+
/** Lead-in the reader must confirm, e.g. "If victoria-metrics-operator was
|
|
156
|
+
* uninstalled on purpose (…), remove the finalizer". */
|
|
157
|
+
condition: string;
|
|
158
|
+
/** For agents: when the cleanup may be proposed. */
|
|
159
|
+
agent_guidance: string;
|
|
160
|
+
caution: string;
|
|
161
|
+
removals: IssueFinalizerRemoval[];
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
/** "No tracked non-status changes in the window" — the claim is scoped by
|
|
145
165
|
* window_seconds so a change just outside it can't be misread as absent. */
|
|
146
166
|
export interface IssueNoRecentChanges {
|
|
@@ -188,6 +208,9 @@ export interface Issue {
|
|
|
188
208
|
action?: string;
|
|
189
209
|
remediation_kind?: string;
|
|
190
210
|
remediation_target?: string;
|
|
211
|
+
/** A cleanup that is right only when its condition holds — never part of
|
|
212
|
+
* `action`. Shown collapsed, labeled with the condition. */
|
|
213
|
+
conditional_cleanup?: IssueConditionalCleanup;
|
|
191
214
|
/** Controller-operation retry count (e.g. Argo's "(retried N times)") —
|
|
192
215
|
* distinct from restart_count (pod restarts). stuck = not expected to
|
|
193
216
|
* self-recover. */
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { act } from 'react'
|
|
2
|
+
import { act, type ComponentProps, type ReactNode } from 'react'
|
|
3
3
|
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
5
|
import { LogCore } from './LogCore'
|
|
6
6
|
|
|
7
|
+
vi.mock('react-virtuoso', () => ({ Virtuoso: ({ data, itemContent }: { data: unknown[]; itemContent: (index: number, value: unknown) => ReactNode }) => <div>{data.map((value, index) => <div key={index}>{itemContent(index, value)}</div>)}</div> }))
|
|
8
|
+
|
|
7
9
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
8
10
|
|
|
9
11
|
let root: Root
|
|
@@ -20,7 +22,7 @@ afterEach(async () => {
|
|
|
20
22
|
})
|
|
21
23
|
|
|
22
24
|
const noop = () => {}
|
|
23
|
-
async function render(props:
|
|
25
|
+
async function render(props: Partial<ComponentProps<typeof LogCore>>) {
|
|
24
26
|
await act(async () =>
|
|
25
27
|
root.render(
|
|
26
28
|
<LogCore
|
|
@@ -75,3 +77,79 @@ describe('LogCore theme', () => {
|
|
|
75
77
|
expect(toggle()).toBeNull()
|
|
76
78
|
})
|
|
77
79
|
})
|
|
80
|
+
|
|
81
|
+
it('dims unavailable streaming, explains why, suppresses its hint and keyboard action', async () => {
|
|
82
|
+
vi.useFakeTimers()
|
|
83
|
+
const start = vi.fn()
|
|
84
|
+
await render({ onStartStream: start, sourceUnavailable: true })
|
|
85
|
+
const stream = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Stream')!
|
|
86
|
+
expect(stream.disabled).toBe(true)
|
|
87
|
+
expect(stream.classList.contains('disabled:opacity-50')).toBe(true)
|
|
88
|
+
expect([...element.querySelectorAll('kbd')].some((k) => k.textContent === 'S')).toBe(false)
|
|
89
|
+
act(() => { stream.parentElement!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); vi.advanceTimersByTime(500) })
|
|
90
|
+
expect(document.body.textContent).toContain('Available once an instance is running')
|
|
91
|
+
act(() => window.dispatchEvent(new KeyboardEvent('keydown', { key: 's' })))
|
|
92
|
+
expect(start).not.toHaveBeenCalled()
|
|
93
|
+
vi.useRealTimers()
|
|
94
|
+
})
|
|
95
|
+
it('keeps a non-CNPG caller streamable without Pods and wraps its utilities together', async () => {
|
|
96
|
+
const start = vi.fn()
|
|
97
|
+
await render({ onStartStream: start, onClear: noop, toolbarExtra: <span>Deployment nginx sources</span>, entries: [{ id: 1, content: '{"msg":"nginx started","level":"info"}', timestamp: '', container: 'nginx', pod: 'nginx-1', isJson: true, isLogfmt: false, level: 'info', levelSource: 'structured' }] })
|
|
98
|
+
const stream = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Stream')!
|
|
99
|
+
expect(stream.disabled).toBe(false)
|
|
100
|
+
expect([...element.querySelectorAll('kbd')].some((k) => k.textContent === 'S')).toBe(true)
|
|
101
|
+
act(() => stream.click())
|
|
102
|
+
expect(start).toHaveBeenCalledOnce()
|
|
103
|
+
const utilities = element.querySelector('[aria-label="Log display and utilities"]')!
|
|
104
|
+
expect(utilities.classList.contains('flex-nowrap')).toBe(true)
|
|
105
|
+
expect(utilities.classList.contains('shrink-0')).toBe(true)
|
|
106
|
+
for (const icon of ['lucide-trash-2', 'lucide-download', 'lucide-search', 'lucide-clock', 'lucide-braces']) expect(utilities.querySelector(`.${icon}`)).not.toBeNull()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('still stops an existing stream when its source becomes unavailable', async () => {
|
|
110
|
+
const stop = vi.fn()
|
|
111
|
+
await render({ onStartStream: noop, onStopStream: stop, isStreaming: true, sourceUnavailable: true })
|
|
112
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Stop')!
|
|
113
|
+
expect(button.disabled).toBe(false)
|
|
114
|
+
act(() => window.dispatchEvent(new KeyboardEvent('keydown', { key: 's' })))
|
|
115
|
+
expect(stop).toHaveBeenCalledOnce()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it.each([true, false])('wraps non-CNPG plain logs at words with ANSI enabled=%s', async (ansi) => {
|
|
119
|
+
localStorage.setItem('radar-logs-ansi', String(ansi))
|
|
120
|
+
await render({ entries: [{ id: 1, timestamp: '', content: 'nginx serving requests', container: 'nginx', isJson: false, isLogfmt: false, level: 'info', levelSource: 'keyword' }] })
|
|
121
|
+
const line = [...element.querySelectorAll('span')].find((el) => el.textContent === 'nginx serving requests' && el.classList.contains('whitespace-pre-wrap'))!
|
|
122
|
+
expect(line.classList.contains('[overflow-wrap:anywhere]')).toBe(true)
|
|
123
|
+
expect(line.classList.contains('break-all')).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
it('keeps normal word wrapping when searching a structured log', async () => {
|
|
126
|
+
await render({ entries: [{ id: 1, timestamp: '', content: '{"msg":"nginx serving requests"}', container: 'nginx', isJson: true, isLogfmt: false, level: 'info', levelSource: 'structured' }] })
|
|
127
|
+
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true })))
|
|
128
|
+
const input = element.querySelector<HTMLInputElement>('input[placeholder="Search logs..."]')!
|
|
129
|
+
await act(async () => {
|
|
130
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(input, 'nginx')
|
|
131
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
132
|
+
})
|
|
133
|
+
const highlight = element.querySelector('mark')!
|
|
134
|
+
expect(highlight).not.toBeNull()
|
|
135
|
+
expect(highlight.parentElement!.classList.contains('[overflow-wrap:anywhere]')).toBe(true)
|
|
136
|
+
expect(highlight.parentElement!.classList.contains('break-all')).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('disables empty Export and Clear while preserving Refresh and enables them after logs load', async () => {
|
|
140
|
+
await render({ onClear: noop })
|
|
141
|
+
const exportButton = () => element.querySelector<HTMLButtonElement>('[aria-label="Export logs"]')!
|
|
142
|
+
const clear = () => element.querySelector<HTMLButtonElement>('[aria-label="Clear logs"]')!
|
|
143
|
+
expect(exportButton().disabled).toBe(true)
|
|
144
|
+
expect(clear().disabled).toBe(true)
|
|
145
|
+
expect(element.querySelector<HTMLButtonElement>('[aria-label="Refresh logs"]')?.disabled).toBe(false)
|
|
146
|
+
await render({ onClear: noop, entries: [{ id: 1, container: 'postgres', level: 'info', levelSource: 'keyword', isJson: false, isLogfmt: false, content: 'hello', timestamp: '2026-10-01T00:00:00Z' }] })
|
|
147
|
+
expect(exportButton().disabled).toBe(false)
|
|
148
|
+
expect(clear().disabled).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('keeps Export and Clear usable when a Pod filter hides a nonempty buffer', async () => {
|
|
152
|
+
await render({ entries: [], allEntries: [{ id: 1, container: 'postgres', level: 'info', levelSource: 'keyword', isJson: false, isLogfmt: false, content: 'hello', timestamp: '' }], onClear: noop })
|
|
153
|
+
expect(element.querySelector<HTMLButtonElement>('[aria-label="Export logs"]')!.disabled).toBe(false)
|
|
154
|
+
expect(element.querySelector<HTMLButtonElement>('[aria-label="Clear logs"]')!.disabled).toBe(false)
|
|
155
|
+
})
|
|
@@ -58,7 +58,8 @@ interface LogCoreProps {
|
|
|
58
58
|
onClear?: () => void
|
|
59
59
|
toolbarExtra?: ToolbarExtraRenderer
|
|
60
60
|
showPodName?: boolean
|
|
61
|
-
emptyMessage?:
|
|
61
|
+
emptyMessage?: ReactNode
|
|
62
|
+
sourceUnavailable?: boolean
|
|
62
63
|
emptyCommand?: string | null
|
|
63
64
|
errorMessage?: string | null
|
|
64
65
|
/**
|
|
@@ -146,6 +147,7 @@ export function LogCore({
|
|
|
146
147
|
toolbarExtra,
|
|
147
148
|
showPodName = false,
|
|
148
149
|
emptyMessage = 'No logs available',
|
|
150
|
+
sourceUnavailable = false,
|
|
149
151
|
emptyCommand,
|
|
150
152
|
errorMessage,
|
|
151
153
|
forceDark,
|
|
@@ -422,7 +424,7 @@ export function LogCore({
|
|
|
422
424
|
search.open()
|
|
423
425
|
return
|
|
424
426
|
}
|
|
425
|
-
if (e.key === 's' && !e.ctrlKey && !e.metaKey && !e.altKey && onStartStream) {
|
|
427
|
+
if (e.key === 's' && !e.ctrlKey && !e.metaKey && !e.altKey && onStartStream && (isStreaming || !sourceUnavailable)) {
|
|
426
428
|
const target = e.target as HTMLElement | null
|
|
427
429
|
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable)) return
|
|
428
430
|
e.preventDefault()
|
|
@@ -432,7 +434,7 @@ export function LogCore({
|
|
|
432
434
|
}
|
|
433
435
|
window.addEventListener('keydown', handleKeyDown)
|
|
434
436
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
435
|
-
}, [search.open, onStartStream, onStopStream, isStreaming])
|
|
437
|
+
}, [search.open, onStartStream, onStopStream, isStreaming, sourceUnavailable])
|
|
436
438
|
|
|
437
439
|
const handleFollowOutput = useCallback((isAtBottom: boolean) => {
|
|
438
440
|
if (isAtBottom) return 'smooth' as const
|
|
@@ -568,15 +570,16 @@ export function LogCore({
|
|
|
568
570
|
style={{ colorScheme: isDark ? 'dark' : 'light', fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}
|
|
569
571
|
>
|
|
570
572
|
{/* Toolbar */}
|
|
571
|
-
<div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
|
|
573
|
+
<div className={`flex flex-wrap items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
|
|
572
574
|
{toolbarExtraNode}
|
|
573
575
|
|
|
574
576
|
{/* Stream / Stop toggle — only shown when streaming is supported */}
|
|
575
577
|
{onStartStream && (
|
|
576
|
-
<Tooltip content={isStreaming ? 'Stop streaming' : 'Start streaming'} delay={TIP_DELAY} position="bottom">
|
|
578
|
+
<Tooltip content={isStreaming ? 'Stop streaming' : sourceUnavailable ? 'Available once an instance is running' : 'Start streaming'} delay={TIP_DELAY} position="bottom">
|
|
577
579
|
<button
|
|
578
580
|
onClick={isStreaming ? onStopStream : onStartStream}
|
|
579
|
-
|
|
581
|
+
disabled={sourceUnavailable && !isStreaming}
|
|
582
|
+
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
580
583
|
isStreaming
|
|
581
584
|
? 'bg-green-600 text-white hover:bg-green-700'
|
|
582
585
|
: `${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg}`
|
|
@@ -592,6 +595,7 @@ export function LogCore({
|
|
|
592
595
|
<Tooltip content="Refresh logs" delay={TIP_DELAY} position="bottom">
|
|
593
596
|
<button
|
|
594
597
|
onClick={onRefresh}
|
|
598
|
+
aria-label="Refresh logs"
|
|
595
599
|
disabled={isLoading || isStreaming}
|
|
596
600
|
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded ${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
597
601
|
>
|
|
@@ -608,7 +612,7 @@ export function LogCore({
|
|
|
608
612
|
<Tooltip key={opt.level} content={`${active ? 'Hide' : 'Show'} ${opt.noun}`} delay={TIP_DELAY} position="bottom">
|
|
609
613
|
<button
|
|
610
614
|
onClick={() => toggleLevel(opt.level)}
|
|
611
|
-
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border transition-colors ${
|
|
615
|
+
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border whitespace-nowrap transition-colors ${
|
|
612
616
|
active
|
|
613
617
|
? getLevelActiveColor(opt.level, palette)
|
|
614
618
|
: levelChipInactive
|
|
@@ -621,7 +625,7 @@ export function LogCore({
|
|
|
621
625
|
})}
|
|
622
626
|
</div>
|
|
623
627
|
|
|
624
|
-
<div className="flex-
|
|
628
|
+
<div role="group" aria-label="Log display and utilities" className="ml-auto flex shrink-0 flex-nowrap items-center gap-2">
|
|
625
629
|
|
|
626
630
|
{/* Structured-log display mode: icon cycles compact→expanded→raw, chevron picks explicitly. */}
|
|
627
631
|
{hasStructuredEntries && (
|
|
@@ -801,14 +805,15 @@ export function LogCore({
|
|
|
801
805
|
|
|
802
806
|
{/* Export */}
|
|
803
807
|
<div className="relative flex items-center" ref={downloadMenuRef}>
|
|
804
|
-
<Tooltip content=
|
|
808
|
+
<Tooltip content={bufferEntries.length === 0 ? 'No logs loaded' : 'Export logs'} delay={TIP_DELAY} position="bottom" disabled={showDownloadMenu} preserveWrapperWhenDisabled>
|
|
805
809
|
<button
|
|
806
810
|
ref={exportTriggerRef}
|
|
811
|
+
disabled={bufferEntries.length === 0}
|
|
807
812
|
onClick={toggleExportMenu}
|
|
808
813
|
aria-label="Export logs"
|
|
809
814
|
aria-haspopup="dialog"
|
|
810
815
|
aria-expanded={showDownloadMenu}
|
|
811
|
-
className={iconBtnInactive}
|
|
816
|
+
className={`${iconBtnInactive} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
812
817
|
>
|
|
813
818
|
<Download className="w-4 h-4" />
|
|
814
819
|
</button>
|
|
@@ -884,15 +889,18 @@ export function LogCore({
|
|
|
884
889
|
|
|
885
890
|
{/* Clear */}
|
|
886
891
|
{onClear && (
|
|
887
|
-
<Tooltip content=
|
|
892
|
+
<Tooltip content={bufferEntries.length === 0 ? 'No logs loaded' : 'Clear logs'} delay={TIP_DELAY} position="bottom">
|
|
888
893
|
<button
|
|
889
894
|
onClick={onClear}
|
|
890
|
-
|
|
895
|
+
aria-label="Clear logs"
|
|
896
|
+
disabled={bufferEntries.length === 0}
|
|
897
|
+
className={`${iconBtnInactive} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
891
898
|
>
|
|
892
899
|
<Trash2 className="w-4 h-4" />
|
|
893
900
|
</button>
|
|
894
901
|
</Tooltip>
|
|
895
902
|
)}
|
|
903
|
+
</div>
|
|
896
904
|
</div>
|
|
897
905
|
|
|
898
906
|
{/* Search bar */}
|
|
@@ -1007,7 +1015,7 @@ export function LogCore({
|
|
|
1007
1015
|
) : groupedEntries.length === 0 ? (
|
|
1008
1016
|
<div className={`${EMPTY_STATE_CLASS} ${palette.textTertiary}`}>
|
|
1009
1017
|
<Terminal className="w-8 h-8" />
|
|
1010
|
-
<
|
|
1018
|
+
<div>{entries.length > 0 ? `Filters hide all ${entries.length.toLocaleString()} loaded lines` : emptyMessage}</div>
|
|
1011
1019
|
{entries.length === 0 && emptyCommand && (
|
|
1012
1020
|
<button type="button" onClick={() => { void copyText(emptyCommand) }} className={`mt-2 inline-flex max-w-[80%] items-center gap-2 rounded border px-3 py-2 font-mono text-xs ${palette.border} ${palette.toolbarBg}`} title="Copy recovery command">
|
|
1013
1021
|
<code className="truncate">{emptyCommand}</code>
|
|
@@ -1062,7 +1070,7 @@ export function LogCore({
|
|
|
1062
1070
|
|
|
1063
1071
|
{/* Keyboard shortcut hints */}
|
|
1064
1072
|
<div className={`flex items-center gap-4 px-3 py-1 border-t ${palette.border} ${palette.toolbarBg} text-[10px] ${palette.textDisabled}`}>
|
|
1065
|
-
{onStartStream && <Shortcut keys="S" label={isStreaming ? 'Stop stream' : 'Stream'} palette={palette} />}
|
|
1073
|
+
{onStartStream && (!sourceUnavailable || isStreaming) && <Shortcut keys="S" label={isStreaming ? 'Stop stream' : 'Stream'} palette={palette} />}
|
|
1066
1074
|
<Shortcut keys="Ctrl+F" label="Search" palette={palette} />
|
|
1067
1075
|
{search.mode !== 'hide' && (
|
|
1068
1076
|
<>
|
|
@@ -1189,7 +1197,7 @@ function LogLine({
|
|
|
1189
1197
|
const highlighted = highlightSearchMatches(plain, searchQuery, searchIsRegex, searchIsCaseSensitive)
|
|
1190
1198
|
contentElement = (
|
|
1191
1199
|
<span
|
|
1192
|
-
className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1200
|
+
className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}
|
|
1193
1201
|
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
1194
1202
|
/>
|
|
1195
1203
|
)
|
|
@@ -1209,13 +1217,13 @@ function LogLine({
|
|
|
1209
1217
|
const html = ansiToHtml(entry.content)
|
|
1210
1218
|
contentElement = (
|
|
1211
1219
|
<span
|
|
1212
|
-
className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1220
|
+
className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}
|
|
1213
1221
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
1214
1222
|
/>
|
|
1215
1223
|
)
|
|
1216
1224
|
} else {
|
|
1217
1225
|
contentElement = (
|
|
1218
|
-
<span className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1226
|
+
<span className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}>
|
|
1219
1227
|
{stripAnsi(entry.content)}
|
|
1220
1228
|
</span>
|
|
1221
1229
|
)
|
|
@@ -50,6 +50,7 @@ interface LogRangeSelectProps {
|
|
|
50
50
|
isDark?: boolean
|
|
51
51
|
/** When true, the control is greyed out and non-interactive. */
|
|
52
52
|
disabled?: boolean
|
|
53
|
+
disabledReason?: string
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function LogRangeSelect({
|
|
@@ -59,10 +60,11 @@ export function LogRangeSelect({
|
|
|
59
60
|
tooltip = 'How many logs to load — by line count or time range',
|
|
60
61
|
isDark = true,
|
|
61
62
|
disabled = false,
|
|
63
|
+
disabledReason = 'Stop streaming to change the log range',
|
|
62
64
|
}: LogRangeSelectProps) {
|
|
63
65
|
const palette = getLogPalette(isDark)
|
|
64
66
|
return (
|
|
65
|
-
<Tooltip content={disabled ?
|
|
67
|
+
<Tooltip content={disabled ? disabledReason : tooltip} position="bottom">
|
|
66
68
|
<select
|
|
67
69
|
value={value}
|
|
68
70
|
onChange={(e) => onChange(e.target.value)}
|