@skyhook-io/k8s-ui 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +6 -6
- 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/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 +7 -3
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -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 +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- 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 +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- package/src/components/logs/useLogStream.ts +17 -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/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -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 +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- 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 +5 -7
- 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 +7 -15
- 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/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- 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/KedaScaledObjectRenderer.tsx +13 -11
- 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/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- 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 -20
- 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-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -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.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- 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 +3 -71
- 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/Toast.tsx +15 -4
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +4 -2
- 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 +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +193 -71
- 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 +11 -2
- package/src/types/core.ts +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -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 +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -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 +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
import { IssueRow } from './IssuesView'
|
|
6
|
+
import type { Issue } from './types'
|
|
7
|
+
|
|
8
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
9
|
+
|
|
10
|
+
const condition =
|
|
11
|
+
'If victoria-metrics-operator was uninstalled on purpose (and nothing else should finalize this object), remove the finalizer'
|
|
12
|
+
const kubectl = `kubectl patch 'vmagents.operator.victoriametrics.com' 'stuck' -n 'team' --type=json -p '[]' --dry-run=server`
|
|
13
|
+
|
|
14
|
+
const issue: Issue = {
|
|
15
|
+
id: 'vmagent-stuck',
|
|
16
|
+
severity: 'warning',
|
|
17
|
+
source: 'problem',
|
|
18
|
+
category: 'termination_stuck',
|
|
19
|
+
category_group: 'control_plane',
|
|
20
|
+
grouping_scope: 'unknown',
|
|
21
|
+
group: 'operator.victoriametrics.com',
|
|
22
|
+
kind: 'VMAgent',
|
|
23
|
+
namespace: 'team',
|
|
24
|
+
name: 'stuck',
|
|
25
|
+
reason: 'Terminating stuck',
|
|
26
|
+
cause: 'Deletion has been pending for 2h.',
|
|
27
|
+
action:
|
|
28
|
+
'No recognized controller found for finalizer "apps.victoriametrics.com/finalizer". If victoria-metrics-operator should be running, restore it so it can resume cleanup.',
|
|
29
|
+
conditional_cleanup: {
|
|
30
|
+
condition,
|
|
31
|
+
agent_guidance: 'Not the next step.',
|
|
32
|
+
caution: 'Removing a finalizer skips the cleanup its controller would have done.',
|
|
33
|
+
removals: [
|
|
34
|
+
{
|
|
35
|
+
finalizer: 'apps.victoriametrics.com/finalizer',
|
|
36
|
+
patch_resource: { kind: 'VMAgent', name: 'stuck', dry_run: true },
|
|
37
|
+
kubectl,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('keeps a conditional cleanup closed behind its condition, apart from the next step', async () => {
|
|
44
|
+
const element = document.createElement('div')
|
|
45
|
+
const root = createRoot(element)
|
|
46
|
+
await act(async () => {
|
|
47
|
+
root.render(<IssueRow issue={issue} open onToggle={() => {}} />)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const toggle = [...element.querySelectorAll('button')].find((button) => button.textContent?.includes(condition))
|
|
51
|
+
expect(toggle, 'the toggle is labeled with the condition').toBeTruthy()
|
|
52
|
+
expect(toggle!.getAttribute('aria-expanded')).toBe('false')
|
|
53
|
+
const panel = [...element.querySelectorAll<HTMLElement>('[id]')].find((node) => node.id === toggle!.getAttribute('aria-controls'))!
|
|
54
|
+
expect(panel.style.gridTemplateRows).toBe('0fr')
|
|
55
|
+
expect(panel.firstElementChild!.hasAttribute('inert')).toBe(true)
|
|
56
|
+
expect(panel.textContent).toContain(kubectl)
|
|
57
|
+
|
|
58
|
+
const nextStep = [...element.querySelectorAll('section')].find((section) => section.querySelector('h4')?.textContent === 'Next step')
|
|
59
|
+
expect(nextStep?.textContent).toContain('restore it')
|
|
60
|
+
expect(nextStep?.textContent).not.toContain('kubectl')
|
|
61
|
+
expect(nextStep?.textContent).not.toContain('patch_resource')
|
|
62
|
+
expect(nextStep?.contains(toggle!)).toBe(false)
|
|
63
|
+
|
|
64
|
+
await act(async () => {
|
|
65
|
+
toggle!.click()
|
|
66
|
+
})
|
|
67
|
+
expect(toggle!.getAttribute('aria-expanded')).toBe('true')
|
|
68
|
+
expect(panel.style.gridTemplateRows).toBe('1fr')
|
|
69
|
+
expect(panel.firstElementChild!.hasAttribute('inert')).toBe(false)
|
|
70
|
+
|
|
71
|
+
await act(async () => root.unmount())
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('renders no cleanup section when the issue has none', async () => {
|
|
75
|
+
const element = document.createElement('div')
|
|
76
|
+
const root = createRoot(element)
|
|
77
|
+
await act(async () => {
|
|
78
|
+
root.render(<IssueRow issue={{ ...issue, conditional_cleanup: undefined }} open onToggle={() => {}} />)
|
|
79
|
+
})
|
|
80
|
+
expect(element.textContent).not.toContain('Conditional cleanup')
|
|
81
|
+
await act(async () => root.unmount())
|
|
82
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { summarizeSchedulerMessage } from '../resources/resource-utils'
|
|
1
2
|
import { useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
|
2
|
-
import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
|
|
3
|
-
import { CardBody, CardSection, ClusterName, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
|
|
3
|
+
import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, Eraser, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
|
|
4
|
+
import { CardBody, CardSection, ClusterName, Disclosure, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
|
|
4
5
|
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse';
|
|
5
6
|
import { Tooltip } from '../ui/Tooltip';
|
|
6
7
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
ISSUE_SEVERITY_RAIL_CLASS,
|
|
14
15
|
ISSUE_SEVERITY_SOLID_CLASS,
|
|
15
16
|
ISSUE_SEVERITY_TEXT_CLASS,
|
|
16
|
-
|
|
17
|
+
issueTitle,
|
|
17
18
|
groupBadgeClass,
|
|
18
19
|
groupLabel,
|
|
19
20
|
} from './severity';
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
subjectRef,
|
|
25
26
|
type Issue,
|
|
26
27
|
type IssueAffected,
|
|
28
|
+
type IssueConditionalCleanup,
|
|
27
29
|
type IssueResourceRef,
|
|
28
30
|
type IssueSeverity,
|
|
29
31
|
} from './types';
|
|
@@ -168,6 +170,7 @@ export interface IssueRowProps {
|
|
|
168
170
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
169
171
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
170
172
|
as?: 'li' | 'div';
|
|
173
|
+
compact?: boolean;
|
|
171
174
|
className?: string;
|
|
172
175
|
dimmed?: boolean;
|
|
173
176
|
/** Suppress the "Subject" deep-link in the expanded body — set by hosts that
|
|
@@ -195,6 +198,7 @@ export function IssueRow({
|
|
|
195
198
|
resourceHref,
|
|
196
199
|
onResourceClick,
|
|
197
200
|
as = 'li',
|
|
201
|
+
compact = false,
|
|
198
202
|
className,
|
|
199
203
|
dimmed,
|
|
200
204
|
hideSubject,
|
|
@@ -208,6 +212,7 @@ export function IssueRow({
|
|
|
208
212
|
const cluster = clusterLabel?.(issue);
|
|
209
213
|
const affected = affectedSummary(issue.affected);
|
|
210
214
|
const { headline } = issueMessageParts(issue);
|
|
215
|
+
const schedulingCause = compact && issue.reason === 'Unschedulable' ? summarizeSchedulerMessage(issue.cause || issue.message || headline || '', { plain: true }) : undefined;
|
|
211
216
|
const { panelId, buttonProps } = useDisclosure(open);
|
|
212
217
|
const Container = as;
|
|
213
218
|
const severity = normalizeIssueSeverity(issue.severity);
|
|
@@ -289,7 +294,7 @@ export function IssueRow({
|
|
|
289
294
|
|
|
290
295
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
291
296
|
<div className="flex min-w-0 items-baseline gap-2">
|
|
292
|
-
<span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{
|
|
297
|
+
<span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{issueTitle(issue)}</span>
|
|
293
298
|
<span className={`shrink-0 self-center ${groupBadgeClass(issue.category_group)}`}>{groupLabel(issue.category_group)}</span>
|
|
294
299
|
{renderBadges?.(slotCtx)}
|
|
295
300
|
{/* The detector reason rides the title row while COLLAPSED so the
|
|
@@ -297,7 +302,7 @@ export function IssueRow({
|
|
|
297
302
|
cause lives in the WHAT'S WRONG section below, so it fades out
|
|
298
303
|
here (stays mounted — it's the flex-1 filler, so unmounting
|
|
299
304
|
wouldn't reflow anything, but fading avoids the pop). */}
|
|
300
|
-
{issue.reason ? (
|
|
305
|
+
{issue.reason && !schedulingCause ? (
|
|
301
306
|
<span
|
|
302
307
|
aria-hidden={open || undefined}
|
|
303
308
|
className={`min-w-0 flex-1 truncate text-xs text-theme-text-tertiary transition-opacity duration-200 ${open ? 'opacity-0' : 'opacity-100'}`}
|
|
@@ -307,6 +312,7 @@ export function IssueRow({
|
|
|
307
312
|
</span>
|
|
308
313
|
) : null}
|
|
309
314
|
</div>
|
|
315
|
+
{schedulingCause && <div className="break-words text-xs text-theme-text-secondary">{schedulingCause}</div>}
|
|
310
316
|
<div className="flex min-w-0 items-center gap-1.5 text-xs text-theme-text-tertiary">
|
|
311
317
|
<span className={KIND_CHIP_CLASS}>{issue.kind}</span>
|
|
312
318
|
<span className="min-w-0 truncate font-medium text-theme-text-secondary">
|
|
@@ -429,9 +435,12 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
429
435
|
const partialUnknown = issue.onset_coverage?.unknown ?? 0;
|
|
430
436
|
const partialOnset = Boolean(issue.first_seen && partialUnknown > 0);
|
|
431
437
|
const timing = issueTimingForDisplay(issue);
|
|
438
|
+
// A message Radar wrote itself is not raw output, so it never lands in the
|
|
439
|
+
// terminal block; its cause carries the same facts.
|
|
440
|
+
const messageIsRaw = issue.message_source !== 'radar';
|
|
432
441
|
const rawError = [
|
|
433
442
|
...new Set(
|
|
434
|
-
[issue.raw_message ?? (issue.cause ? issue.message : undefined), detail].filter(
|
|
443
|
+
[issue.raw_message ?? (issue.cause && messageIsRaw ? issue.message : undefined), detail].filter(
|
|
435
444
|
(s): s is string => !!s && s !== shownText,
|
|
436
445
|
),
|
|
437
446
|
),
|
|
@@ -544,6 +553,8 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
544
553
|
</CardSection>
|
|
545
554
|
) : null}
|
|
546
555
|
|
|
556
|
+
{issue.conditional_cleanup ? <ConditionalCleanup cleanup={issue.conditional_cleanup} /> : null}
|
|
557
|
+
|
|
547
558
|
{rawError ? (
|
|
548
559
|
<CardSection icon={Terminal} label="Raw error">
|
|
549
560
|
<TerminalBlock>{rawError}</TerminalBlock>
|
|
@@ -553,6 +564,33 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
553
564
|
);
|
|
554
565
|
}
|
|
555
566
|
|
|
567
|
+
// A step that is right only under its condition. It stays closed behind the
|
|
568
|
+
// condition so the row's next step remains the default path, and it is not
|
|
569
|
+
// toned as a fix.
|
|
570
|
+
function ConditionalCleanup({ cleanup }: { cleanup: IssueConditionalCleanup }) {
|
|
571
|
+
return (
|
|
572
|
+
<CardSection icon={Eraser} label="Conditional cleanup">
|
|
573
|
+
<Disclosure
|
|
574
|
+
summary={<span className="text-sm leading-relaxed">{cleanup.condition}</span>}
|
|
575
|
+
summaryClassName="items-baseline text-theme-text-primary hover:text-theme-text-secondary"
|
|
576
|
+
>
|
|
577
|
+
<div className="flex flex-col gap-2 pt-2">
|
|
578
|
+
{cleanup.caution ? <p className="text-xs leading-relaxed text-theme-text-secondary">{cleanup.caution}</p> : null}
|
|
579
|
+
{cleanup.removals.map((removal) => (
|
|
580
|
+
<div key={removal.finalizer} className="flex flex-col gap-1.5">
|
|
581
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
582
|
+
Finalizer <code className="inline-code">{removal.finalizer}</code>
|
|
583
|
+
</p>
|
|
584
|
+
<TerminalBlock label="patch_resource · dry run">{JSON.stringify(removal.patch_resource)}</TerminalBlock>
|
|
585
|
+
<TerminalBlock label="kubectl · server dry run">{removal.kubectl}</TerminalBlock>
|
|
586
|
+
</div>
|
|
587
|
+
))}
|
|
588
|
+
</div>
|
|
589
|
+
</Disclosure>
|
|
590
|
+
</CardSection>
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
556
594
|
function changeContextText(change: NonNullable<Issue['change_context']>): string {
|
|
557
595
|
const parts = [change.when ? `Changed ${change.when} ago` : 'Changed', change.what ? change.what.replace(/_/g, ' ') : null, change.evidence].filter(Boolean);
|
|
558
596
|
return parts.join(' · ');
|
|
@@ -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
|
+
})
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react'
|
|
1
|
+
import { useMemo, useState, type ReactNode } from 'react'
|
|
2
2
|
import { AlertTriangle } from 'lucide-react'
|
|
3
3
|
import { kindToPluralWithGroup } from '../../utils/navigation'
|
|
4
|
-
import { IssueRow } from './IssuesView'
|
|
4
|
+
import { IssueRow, type IssueRowSlotContext } from './IssuesView'
|
|
5
5
|
import { compareIssues, subjectRef, type Issue, type IssueResourceRef } from './types'
|
|
6
6
|
|
|
7
7
|
export function ResourceIssuesSection({
|
|
8
8
|
issues,
|
|
9
9
|
onResourceClick,
|
|
10
10
|
subjectResource,
|
|
11
|
+
compact,
|
|
12
|
+
renderDetailSection,
|
|
11
13
|
}: {
|
|
14
|
+
compact?: boolean
|
|
15
|
+
renderDetailSection?: (ctx: IssueRowSlotContext) => ReactNode
|
|
12
16
|
issues: Issue[] | undefined
|
|
13
17
|
/** When provided, related resources in a causal link become clickable. */
|
|
14
18
|
onResourceClick?: (ref: IssueResourceRef) => void
|
|
@@ -36,6 +40,8 @@ export function ResourceIssuesSection({
|
|
|
36
40
|
<IssueRow
|
|
37
41
|
key={key}
|
|
38
42
|
issue={issue}
|
|
43
|
+
compact={compact}
|
|
44
|
+
renderDetailSection={renderDetailSection}
|
|
39
45
|
open={openId === key}
|
|
40
46
|
onToggle={() => setOpenId((cur) => (cur === key ? null : key))}
|
|
41
47
|
onResourceClick={onResourceClick}
|
|
@@ -52,6 +52,8 @@ export function diagnosticFactLabel(type: string): string {
|
|
|
52
52
|
return 'Dependent pods';
|
|
53
53
|
case 'admission_webhook_backend':
|
|
54
54
|
return 'Admission dependency';
|
|
55
|
+
case 'network_policy_isolates':
|
|
56
|
+
return 'Affected resources';
|
|
55
57
|
default:
|
|
56
58
|
return type.replace(/_/g, ' ');
|
|
57
59
|
}
|
|
@@ -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, IssueRunningPreviousValue, IssueWithheld } 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') {
|
|
@@ -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,6 +139,33 @@ 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
171
|
issue: mk({ category_group: 'control_plane', kind: 'Node' }),
|
|
@@ -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',
|
|
@@ -489,4 +537,30 @@ describe('IssueRow diagnosis raw messages', () => {
|
|
|
489
537
|
expect(html).toContain('app path does not exist')
|
|
490
538
|
expect(html).toContain('rpc error: code = Unknown desc = app path does not exist')
|
|
491
539
|
})
|
|
540
|
+
|
|
541
|
+
it("doesn't present Radar's own summary as a raw error", () => {
|
|
542
|
+
const base = {
|
|
543
|
+
category: 'network_policy_denied',
|
|
544
|
+
category_group: 'networking',
|
|
545
|
+
severity: 'warning' as const,
|
|
546
|
+
reason: 'NetworkPolicy denies all ingress',
|
|
547
|
+
message: 'Would block all inbound traffic to the 2 pods behind Service recommendation',
|
|
548
|
+
cause: 'NetworkPolicy deny-all leaves the 2 pods behind Service recommendation with no allowed inbound traffic.',
|
|
549
|
+
}
|
|
550
|
+
const radar = renderToString(createElement(IssueRow, {
|
|
551
|
+
issue: mk({
|
|
552
|
+
...base,
|
|
553
|
+
message_source: 'radar',
|
|
554
|
+
diagnostic_context: { role: 'candidate', facts: [{ type: 'network_policy_isolates', confidence: 'high', refs: [{ kind: 'Service', namespace: 'shop', name: 'recommendation' }] }] },
|
|
555
|
+
}),
|
|
556
|
+
open: true, onToggle: () => undefined, as: 'div',
|
|
557
|
+
}))
|
|
558
|
+
expect(radar).not.toContain('Raw error')
|
|
559
|
+
expect(radar).toContain('leaves the 2 pods behind Service recommendation')
|
|
560
|
+
expect(radar).toContain('Affected resources')
|
|
561
|
+
|
|
562
|
+
// Without the marker the message is still treated as detector output.
|
|
563
|
+
const unmarked = renderToString(createElement(IssueRow, { issue: mk(base), open: true, onToggle: () => undefined, as: 'div' }))
|
|
564
|
+
expect(unmarked).toContain('Raw error')
|
|
565
|
+
})
|
|
492
566
|
})
|
|
@@ -70,13 +70,14 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
70
70
|
pdb_blocks_evictions: 'Evictions blocked',
|
|
71
71
|
secret_sync_failed: 'Secret sync failed',
|
|
72
72
|
service_no_endpoints: 'No endpoints',
|
|
73
|
+
service_mixed_backends: 'Mixed Service backends',
|
|
73
74
|
ingress_backend_missing: 'Ingress backend missing',
|
|
74
75
|
ingress_class_missing: 'Ingress class missing',
|
|
75
76
|
load_balancer_pending: 'Load balancer pending',
|
|
76
77
|
gateway_not_ready: 'Gateway not ready',
|
|
77
78
|
gateway_route_invalid: 'Gateway route invalid',
|
|
78
79
|
dns_failure: 'DNS failure',
|
|
79
|
-
|
|
80
|
+
network_policy_denied: 'Traffic denied by NetworkPolicy',
|
|
80
81
|
pvc_pending: 'PVC pending',
|
|
81
82
|
pvc_lost: 'PVC lost',
|
|
82
83
|
pv_failed: 'PV failed',
|
|
@@ -87,7 +88,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
87
88
|
backup_target_unavailable: 'Backup target unavailable',
|
|
88
89
|
backup_stalled: 'Backup not progressing',
|
|
89
90
|
job_failed: 'Job failed',
|
|
90
|
-
cronjob_failed: 'CronJob
|
|
91
|
+
cronjob_failed: 'CronJob needs attention',
|
|
91
92
|
rollout_stalled: 'Rollout stalled',
|
|
92
93
|
hpa_limited_or_failed: 'Autoscaling limited',
|
|
93
94
|
rbac_forbidden: 'Permission denied',
|
|
@@ -136,6 +137,33 @@ export function categoryLabel(category: string): string {
|
|
|
136
137
|
return CATEGORY_LABEL[category] ?? humanize(category);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
// Reasons filed under a category whose label names a different operation:
|
|
141
|
+
// CNPG archiving and an unanswered scheduled run sit under backup_failed.
|
|
142
|
+
const REASON_TITLE: Record<string, string> = {
|
|
143
|
+
InvalidSchedule: 'Scheduling error reported',
|
|
144
|
+
UnparseableSchedule: 'Scheduling error reported',
|
|
145
|
+
UnParseableCronJobSchedule: 'Scheduling error reported',
|
|
146
|
+
UnknownTimeZone: 'Time zone error reported',
|
|
147
|
+
MissSchedule: 'Missed schedule reported',
|
|
148
|
+
TooManyMissedTimes: 'Missed schedules reported',
|
|
149
|
+
CNPGWALArchivingFailing: "WAL archiving failing",
|
|
150
|
+
CNPGLastBackupFailed: "Latest backup failed",
|
|
151
|
+
CNPGScheduleDestinationMissing: "Backup schedule has no destination",
|
|
152
|
+
CNPGInstanceReadinessMismatch: "Instance Pods contradict Cluster readiness",
|
|
153
|
+
CNPGPrimaryLabelMismatch: "Primary Pod label contradicts Cluster status",
|
|
154
|
+
CNPGScheduledRunNoBackup: "No successful backup since a scheduled run",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The title a reason has wherever it is shown, when its category's label would name the wrong operation. */
|
|
158
|
+
export function issueReasonTitle(reason: string | undefined): string | undefined {
|
|
159
|
+
return reason ? REASON_TITLE[reason] : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** An issue row's title: its category, unless the reason names it better. */
|
|
163
|
+
export function issueTitle(issue: { category: string; reason?: string }): string {
|
|
164
|
+
return issueReasonTitle(issue.reason) || categoryLabel(issue.category);
|
|
165
|
+
}
|
|
166
|
+
|
|
139
167
|
export function groupLabel(group: string): string {
|
|
140
168
|
return GROUP_LABEL[group] ?? humanize(group);
|
|
141
169
|
}
|
|
@@ -118,6 +118,23 @@ export interface IssueRecentChangeField {
|
|
|
118
118
|
newValue?: unknown;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** Names the caller's access kept out of a list, counted but never named. */
|
|
122
|
+
export interface IssueWithheld {
|
|
123
|
+
count: number;
|
|
124
|
+
/** A readreason code; always "user_forbidden" today. */
|
|
125
|
+
reason: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface IssueRunningPreviousValue {
|
|
129
|
+
/** Owners the caller may read, capped at 5. */
|
|
130
|
+
workloads: string[] | null;
|
|
131
|
+
/** Owners left out because the caller may not read them. */
|
|
132
|
+
withheld?: IssueWithheld;
|
|
133
|
+
pods: number;
|
|
134
|
+
keys: string[];
|
|
135
|
+
note: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
export interface IssueRecentChange {
|
|
122
139
|
source?: string;
|
|
123
140
|
kind: string;
|
|
@@ -130,15 +147,39 @@ export interface IssueRecentChange {
|
|
|
130
147
|
timestamp: string;
|
|
131
148
|
change_category?: 'spec_config' | 'lifecycle' | 'runtime_status' | string;
|
|
132
149
|
rank_reason?: string;
|
|
133
|
-
/** Ranking hint for workload runtime configuration (including the image)
|
|
150
|
+
/** Ranking hint for workload runtime configuration (including the image), directly referenced ConfigMap data, or Secret writes with likely-stale env readers; not a causal claim. */
|
|
134
151
|
application_configuration_change?: boolean;
|
|
135
152
|
/** Set only on top-level recent_changes in an eligible, unfiltered issues response with complete linkage evidence. */
|
|
136
153
|
not_linked_to_returned_issues?: boolean;
|
|
137
154
|
fields?: IssueRecentChangeField[];
|
|
138
|
-
/** Workloads
|
|
139
|
-
* Direct spec references only
|
|
140
|
-
* service are not captured. */
|
|
155
|
+
/** Workloads or Pods this ConfigMap or Secret is referenced by ("Deployment/flagd"), capped at 5.
|
|
156
|
+
* Direct spec references only; references do not establish runtime use. */
|
|
141
157
|
consumed_by?: string[];
|
|
158
|
+
/** Referencing workloads or Pods left out of consumed_by because the caller may not read them. */
|
|
159
|
+
consumed_by_withheld?: IssueWithheld;
|
|
160
|
+
running_previous_value?: IssueRunningPreviousValue;
|
|
161
|
+
updates_in_window?: number;
|
|
162
|
+
first_update_at?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One finalizer removal: a JSON Patch that tests the object UID and the
|
|
166
|
+
* finalizer at its index before removing it, previewed as dry runs. */
|
|
167
|
+
export interface IssueFinalizerRemoval {
|
|
168
|
+
finalizer: string;
|
|
169
|
+
/** patch_resource tool input, with dry_run=true. */
|
|
170
|
+
patch_resource: Record<string, unknown>;
|
|
171
|
+
/** The same patch as a kubectl server-side dry run. */
|
|
172
|
+
kubectl: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface IssueConditionalCleanup {
|
|
176
|
+
/** Lead-in the reader must confirm, e.g. "If victoria-metrics-operator was
|
|
177
|
+
* uninstalled on purpose (…), remove the finalizer". */
|
|
178
|
+
condition: string;
|
|
179
|
+
/** For agents: when the cleanup may be proposed. */
|
|
180
|
+
agent_guidance: string;
|
|
181
|
+
caution: string;
|
|
182
|
+
removals: IssueFinalizerRemoval[];
|
|
142
183
|
}
|
|
143
184
|
|
|
144
185
|
/** "No tracked non-status changes in the window" — the claim is scoped by
|
|
@@ -181,6 +222,10 @@ export interface Issue {
|
|
|
181
222
|
reason: string;
|
|
182
223
|
message?: string;
|
|
183
224
|
raw_message?: string;
|
|
225
|
+
/** 'radar' when `message` is Radar's own summary rather than text from
|
|
226
|
+
* Kubernetes or a controller. Such a message is never shown as raw output;
|
|
227
|
+
* the server writes a `cause` that stands without it. */
|
|
228
|
+
message_source?: 'radar';
|
|
184
229
|
/** Parsed domain diagnosis: plain-English cause, suggested next step, and
|
|
185
230
|
* an optional structured one-click fix.
|
|
186
231
|
* Server-emitted (omitempty); empty for issues without a parser. */
|
|
@@ -188,6 +233,9 @@ export interface Issue {
|
|
|
188
233
|
action?: string;
|
|
189
234
|
remediation_kind?: string;
|
|
190
235
|
remediation_target?: string;
|
|
236
|
+
/** A cleanup that is right only when its condition holds — never part of
|
|
237
|
+
* `action`. Shown collapsed, labeled with the condition. */
|
|
238
|
+
conditional_cleanup?: IssueConditionalCleanup;
|
|
191
239
|
/** Controller-operation retry count (e.g. Argo's "(retried N times)") —
|
|
192
240
|
* distinct from restart_count (pod restarts). stuck = not expected to
|
|
193
241
|
* self-recover. */
|
|
@@ -260,6 +308,9 @@ export interface Issue {
|
|
|
260
308
|
* NEITHER correlation field was not checked — absence must not be read as
|
|
261
309
|
* "no changes". MCP-only, like correlated_changes. */
|
|
262
310
|
no_recent_changes?: IssueNoRecentChanges;
|
|
311
|
+
/** The subject's change history in the window was larger than Radar reads,
|
|
312
|
+
* so correlated_changes are leads, not its full history. MCP-only. */
|
|
313
|
+
correlated_changes_incomplete?: boolean;
|
|
263
314
|
}
|
|
264
315
|
|
|
265
316
|
/** subjectRef builds a deep-linkable ref for an issue's subject — the row's
|