@skyhook-io/k8s-ui 1.18.2 → 1.20.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/package.json +9 -2
- package/src/components/applications/ApplicationDetail.tsx +8 -2
- package/src/components/charts/AreaChart.test.tsx +39 -1
- package/src/components/charts/AreaChart.tsx +32 -13
- package/src/components/charts/axis.test.ts +17 -1
- package/src/components/charts/axis.ts +13 -0
- package/src/components/charts/format.test.ts +159 -2
- package/src/components/charts/format.ts +118 -0
- package/src/components/cluster-switcher/ClusterSwitcher.test.tsx +54 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +7 -3
- package/src/components/compare/ResourceCompareView.test.tsx +54 -0
- package/src/components/compare/ResourceCompareView.tsx +35 -12
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +16 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +54 -7
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +1 -1
- package/src/components/gitops/GitOpsTableView.tsx +26 -7
- package/src/components/gitops/RollbackDialog.test.tsx +80 -0
- package/src/components/gitops/RollbackDialog.tsx +25 -6
- package/src/components/gitops/SyncOptionsDialog.render.test.tsx +374 -0
- package/src/components/gitops/SyncOptionsDialog.test.ts +54 -1
- package/src/components/gitops/SyncOptionsDialog.tsx +179 -34
- package/src/components/gitops/applicationset-row.test.ts +12 -0
- package/src/components/gitops/argo-radar-upgrade.test.tsx +369 -0
- package/src/components/gitops/detail-helpers.test.ts +8 -2
- package/src/components/gitops/detail-helpers.ts +9 -11
- package/src/components/gitops/index.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +32 -0
- package/src/components/gitops/insights/GitOpsActivityInsightView.test.tsx +55 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +94 -46
- package/src/components/gitops/insights/GitOpsIssuesBand.test.tsx +108 -0
- package/src/components/gitops/insights/GitOpsRollbackInsightView.test.tsx +73 -0
- package/src/components/gitops/insights/GitOpsSyncWindowInsightView.test.tsx +29 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +65 -0
- package/src/components/gitops/insights/insights-helpers.ts +17 -3
- package/src/components/issues/IssueGuidance.test.tsx +190 -0
- package/src/components/issues/IssueGuidance.tsx +48 -0
- package/src/components/issues/IssuesView.tsx +15 -3
- package/src/components/issues/ResourceIssuesSection.tsx +3 -1
- package/src/components/issues/diagnostic.ts +14 -1
- package/src/components/issues/index.ts +1 -0
- package/src/components/issues/severity.ts +15 -0
- package/src/components/issues/types.ts +10 -0
- package/src/components/kueue/AdmissionPath.tsx +223 -0
- package/src/components/kueue/QueueEvidence.tsx +148 -0
- package/src/components/kueue/constants.ts +1 -0
- package/src/components/kueue/eligibility.ts +43 -0
- package/src/components/kueue/flavors-tried.ts +79 -0
- package/src/components/kueue/helpers.test.ts +105 -0
- package/src/components/kueue/policy.ts +100 -0
- package/src/components/kueue/queue-detail.test.tsx +317 -0
- package/src/components/kueue/queue-status.ts +42 -0
- package/src/components/kueue/related-read.ts +48 -0
- package/src/components/kueue/testdata/kueue-v0.18-policy-enums.json +89 -0
- package/src/components/logs/StructuredLogLine.test.tsx +22 -0
- package/src/components/logs/StructuredLogLine.tsx +8 -3
- package/src/components/resources/KueueAdmissionSection.test.tsx +117 -0
- package/src/components/resources/KueueAdmissionSection.tsx +51 -12
- package/src/components/resources/KueuePendingPosition.test.tsx +97 -0
- package/src/components/resources/KueuePendingPosition.tsx +91 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +8 -0
- package/src/components/resources/ResourcesSidebar.tsx +3 -2
- package/src/components/resources/ResourcesView.crd.test.tsx +28 -0
- package/src/components/resources/ResourcesView.node-pods.test.tsx +25 -0
- package/src/components/resources/ResourcesView.strimzi.test.tsx +37 -0
- package/src/components/resources/ResourcesView.tsx +50 -3
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +7 -0
- package/src/components/resources/get-pod-phase-display.test.ts +70 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +54 -1
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +7 -8
- package/src/components/resources/renderers/CRDSchemaBrowser.test.tsx +105 -0
- package/src/components/resources/renderers/CRDSchemaBrowser.tsx +134 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.test.tsx +106 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.tsx +152 -0
- package/src/components/resources/renderers/JobRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/JobRenderer.tsx +7 -2
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +5 -5
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +250 -170
- package/src/components/resources/renderers/KueueQueueWorkloads.test.tsx +189 -0
- package/src/components/resources/renderers/KueueQueueWorkloads.tsx +460 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +23 -52
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +11 -6
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +23 -5
- package/src/components/resources/renderers/ServiceAccountRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +15 -10
- package/src/components/resources/renderers/StrimziConnectorRenderer.tsx +61 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -3
- package/src/components/resources/renderers/crd-cells.test.tsx +30 -0
- package/src/components/resources/renderers/crd-cells.tsx +55 -0
- package/src/components/resources/renderers/crd-schema-parts.test.tsx +59 -0
- package/src/components/resources/renderers/crd-schema-parts.tsx +121 -0
- package/src/components/resources/renderers/index.ts +6 -0
- package/src/components/resources/renderers/kueue-cells.tsx +5 -0
- package/src/components/resources/resource-utils-argo.test.ts +94 -1
- package/src/components/resources/resource-utils-argo.ts +84 -5
- package/src/components/resources/resource-utils-crd.test.ts +32 -0
- package/src/components/resources/resource-utils-crd.ts +26 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +12 -0
- package/src/components/resources/resource-utils-kueue-admission.test.ts +26 -0
- package/src/components/resources/resource-utils-kueue.test.ts +15 -0
- package/src/components/resources/resource-utils-kueue.ts +137 -4
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +9 -1
- package/src/components/resources/resource-utils-opentelemetry.ts +1 -1
- package/src/components/resources/resource-utils-ray.test.ts +42 -1
- package/src/components/resources/resource-utils-ray.ts +38 -0
- package/src/components/resources/resource-utils-strimzi.test.tsx +118 -0
- package/src/components/resources/resource-utils-strimzi.ts +35 -0
- package/src/components/resources/resource-utils.ts +45 -1
- package/src/components/shared/ResourceActionsBar.tsx +83 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +37 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +40 -7
- package/src/components/topology/K8sResourceNode.test.ts +14 -0
- package/src/components/topology/K8sResourceNode.tsx +18 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +96 -94
- package/src/components/topology/layout.ts +3 -1
- package/src/components/topology/pod-group-expansion.test.ts +38 -0
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/RadarUpgradeNote.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/PodList.tsx +35 -9
- package/src/components/workload/WorkloadView.spec.test.tsx +41 -1
- package/src/components/workload/WorkloadView.tsx +61 -8
- package/src/components/workload/workload-logs-availability.test.ts +7 -0
- package/src/types/core.ts +41 -0
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +2 -2
- package/src/types/kueue-queues.ts +84 -0
- package/src/types/read-coverage.ts +2 -1
- package/src/types/scheduling.ts +72 -0
- package/src/utils/animation.test.ts +38 -0
- package/src/utils/animation.ts +34 -0
- package/src/utils/api-resources-crd.test.ts +11 -0
- package/src/utils/api-resources.ts +5 -1
- package/src/utils/applications.test.ts +15 -1
- package/src/utils/applications.ts +26 -1
- package/src/utils/crd-schema.test.ts +107 -0
- package/src/utils/crd-schema.ts +201 -0
- package/src/utils/format.test.ts +37 -1
- package/src/utils/format.ts +28 -80
- package/src/utils/gitops-owner.test.ts +28 -2
- package/src/utils/gitops-owner.ts +19 -0
- package/src/utils/kueue-queue-workloads.ts +19 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +90 -1
- package/src/utils/topology-neighborhood.ts +55 -3
- package/src/utils/workload-history-notes.test.ts +76 -0
- package/src/utils/workload-history-notes.ts +93 -0
|
@@ -47,9 +47,11 @@ export function changeHealth(change: GitOpsChange): GitOpsHealthStatus {
|
|
|
47
47
|
return normalizeHealthStatus(change.health)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function missingResourceMessage(sync?: string): string {
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
export function missingResourceMessage(sync?: string, context?: { hazard?: string; rolledBack?: boolean }): string {
|
|
51
|
+
if (sync === 'Synced') return 'Argo reports this resource as synced, but it is not present in the cluster. Refresh the application to check its status.'
|
|
52
|
+
if (context?.hazard) return 'Not present in the cluster. See the sync warning above before syncing this Application.'
|
|
53
|
+
return context?.rolledBack
|
|
54
|
+
? 'Declared in Git but not present in the cluster after a rollback. Sync can recreate it and undo the rollback.'
|
|
53
55
|
: 'Not present in the cluster. Sync the application to create it.'
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -158,6 +160,18 @@ export function entryTone(item: GitOpsHistoryItem): EntryTone {
|
|
|
158
160
|
return { dot: SEVERITY_DOT.neutral, inferredFrom: 'no phase information' }
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// historyRollbackBlockedReason is why a history row's Rollback button is
|
|
164
|
+
// disabled, or undefined when it can roll back. An entry the backend marks
|
|
165
|
+
// not rollbackable is refused by the rollback API whatever else changes, so
|
|
166
|
+
// its reason wins over the app-wide one (auto-sync), which the operator can
|
|
167
|
+
// fix. A Radar that predates `rollbackable` omits it; the API decides there.
|
|
168
|
+
export function historyRollbackBlockedReason(item: GitOpsHistoryItem, appBlockedReason?: string): string | undefined {
|
|
169
|
+
if (item.rollbackable === false) {
|
|
170
|
+
return "This entry records no source, so Argo CD can't roll back to it. Sync to its revision instead."
|
|
171
|
+
}
|
|
172
|
+
return appBlockedReason
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
// Compact a source string for inline display. Argo emits the full GitHub URL
|
|
162
176
|
// followed by " · path/within/repo", which dominates the timeline row when
|
|
163
177
|
// rendered raw. Strip the protocol+host (full string still shown on hover via
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { GitOpsIssuesBand } from '../gitops/insights/GitOpsInsightViews'
|
|
6
|
+
import { IssueRow } from './IssuesView'
|
|
7
|
+
import { ResourceIssuesSection } from './ResourceIssuesSection'
|
|
8
|
+
import type { GitOpsIssue } from '../../types/gitops-insights'
|
|
9
|
+
import type { Issue } from './types'
|
|
10
|
+
|
|
11
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
12
|
+
|
|
13
|
+
const primary = 'The render includes none of the resources this Application manages. A sync with Prune enabled could delete all of them. Prune=false blocks direct Argo pruning; Prune=confirm requires confirmation. Pruning a rendered Namespace deletes its contents, regardless of their Prune options. Check the source path, targetRevision, Helm values, and Kustomize or directory include/exclude settings first.'
|
|
14
|
+
const secondary = [
|
|
15
|
+
'If this Application should go away: remove it from the ApplicationSet generator if generated; while the generator still emits it, the ApplicationSet recreates it. Check whether the generated Application survives; its ApplicationSet sync policy may keep it. If it survives, delete it explicitly with the intended cascade choice. For a plain Application, delete the Application. Cleanup follows the cascade/resources finalizer choice and Delete=false / Delete=confirm, not Prune options.',
|
|
16
|
+
'If it should render empty: set syncPolicy.automated.allowEmpty: true in the Application or its ApplicationSet template. This turns off the empty-render guard for all future syncs.',
|
|
17
|
+
]
|
|
18
|
+
const title = 'If removing all managed resources is intended'
|
|
19
|
+
const cause = 'Argo CD blocked auto-sync because the render includes none of the resources this Application manages.'
|
|
20
|
+
const guard: GitOpsIssue = {
|
|
21
|
+
severity: 'critical', scope: 'condition', reason: 'AutoSyncBlockedEmpty',
|
|
22
|
+
message: 'Auto-sync blocked: the render includes none of the managed resources', cause,
|
|
23
|
+
action: [primary, ...secondary].join(' '), actionPrimary: primary,
|
|
24
|
+
actionSecondaryTitle: title, actionSecondary: secondary,
|
|
25
|
+
}
|
|
26
|
+
const issue: Issue = {
|
|
27
|
+
id: 'guard', severity: 'critical', source: 'problem',
|
|
28
|
+
category: 'gitops_sync_failed', category_group: 'gitops', grouping_scope: 'unknown',
|
|
29
|
+
kind: 'Application', group: 'argoproj.io', namespace: 'argocd', name: 'empty',
|
|
30
|
+
reason: guard.reason, message: guard.message, cause,
|
|
31
|
+
action: guard.action, action_primary: primary,
|
|
32
|
+
action_secondary_title: title, action_secondary: secondary,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let root: Root
|
|
36
|
+
let element: HTMLDivElement
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
element = document.createElement('div')
|
|
39
|
+
document.body.appendChild(element)
|
|
40
|
+
root = createRoot(element)
|
|
41
|
+
})
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await act(async () => root.unmount())
|
|
44
|
+
element.remove()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function disclosure() {
|
|
48
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent?.includes(title))!
|
|
49
|
+
expect(button).toBeDefined()
|
|
50
|
+
return { button, panel: document.getElementById(button.getAttribute('aria-controls')!)! }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkPrimaryVisible() {
|
|
54
|
+
for (const text of [primary, cause]) {
|
|
55
|
+
const paragraph = [...element.querySelectorAll('p')].find((p) => p.textContent === text)!
|
|
56
|
+
expect(paragraph).toBeDefined()
|
|
57
|
+
expect(paragraph.closest('[inert]')).toBeNull()
|
|
58
|
+
}
|
|
59
|
+
expect(element.querySelector('button button')).toBeNull()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function checkDisclosure() {
|
|
63
|
+
const { button, panel } = disclosure()
|
|
64
|
+
expect(button.getAttribute('aria-expanded')).toBe('false')
|
|
65
|
+
expect(panel.style.gridTemplateRows).toBe('0fr')
|
|
66
|
+
expect(panel.querySelector('[inert]')).not.toBeNull()
|
|
67
|
+
checkPrimaryVisible()
|
|
68
|
+
await act(async () => button.click())
|
|
69
|
+
expect(button.getAttribute('aria-expanded')).toBe('true')
|
|
70
|
+
expect(panel.style.gridTemplateRows).toBe('1fr')
|
|
71
|
+
expect(panel.querySelector('[inert]')).toBeNull()
|
|
72
|
+
for (const paragraph of secondary) expect(panel.textContent).toContain(paragraph)
|
|
73
|
+
await act(async () => button.click())
|
|
74
|
+
expect(button.getAttribute('aria-expanded')).toBe('false')
|
|
75
|
+
expect(panel.querySelector('[inert]')).not.toBeNull()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('empty-render guidance', () => {
|
|
79
|
+
it('shows primary guidance on the detail band and opens both intended-render paths', async () => {
|
|
80
|
+
await act(async () => root.render(<GitOpsIssuesBand issues={[guard]} />))
|
|
81
|
+
await checkDisclosure()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps the headline guidance independent of the collapsed detail stack', async () => {
|
|
85
|
+
const drift: GitOpsIssue = { severity: 'warning', scope: 'condition', reason: 'OtherWarning', message: 'Another condition' }
|
|
86
|
+
await act(async () => root.render(<GitOpsIssuesBand issues={[guard, drift]} />))
|
|
87
|
+
await checkDisclosure()
|
|
88
|
+
const stack = [...element.querySelectorAll('button')].find((b) => b.textContent?.includes('AutoSyncBlockedEmpty'))!
|
|
89
|
+
expect(stack.getAttribute('aria-expanded')).toBe('false')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('opens guidance on a lower detail row after expanding the stack', async () => {
|
|
93
|
+
const first: GitOpsIssue = { severity: 'critical', scope: 'condition', reason: 'ComparisonError', message: 'Cannot render' }
|
|
94
|
+
await act(async () => root.render(<GitOpsIssuesBand issues={[first, guard]} />))
|
|
95
|
+
const stack = [...element.querySelectorAll('button')].find((b) => b.textContent?.includes('ComparisonError'))!
|
|
96
|
+
await act(async () => stack.click())
|
|
97
|
+
await checkDisclosure()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it.each([false, true])('shows split guidance on the cluster Issues row (open=%s)', async (open) => {
|
|
101
|
+
await act(async () => root.render(<IssueRow issue={issue} open={open} onToggle={() => {}} />))
|
|
102
|
+
await checkDisclosure()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('shows primary guidance and disclosure in the resource drawer without expanding its issue', async () => {
|
|
106
|
+
await act(async () => root.render(<ResourceIssuesSection issues={[issue]} />))
|
|
107
|
+
await checkDisclosure()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('shows complete action when presentation fields are absent', async () => {
|
|
111
|
+
const { actionPrimary: _primary, actionSecondaryTitle: _title, actionSecondary: _secondary, ...plain } = guard
|
|
112
|
+
await act(async () => root.render(<GitOpsIssuesBand issues={[plain]} />))
|
|
113
|
+
expect(element.textContent).toContain(guard.action)
|
|
114
|
+
expect(element.querySelectorAll('button[aria-controls]')).toHaveLength(0)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const moveSteps = [
|
|
119
|
+
"Check this Application's sync options for FailOnSharedResource=true. If set, the sync will refuse the takeover. Stop unless taking it over is intended and allowed.",
|
|
120
|
+
"Note the resource's live UID.",
|
|
121
|
+
'Sync this Application.',
|
|
122
|
+
]
|
|
123
|
+
const movePrimary = "To leave it with Application payments, remove it from this Application's source."
|
|
124
|
+
const moveTitle = 'To move it to this Application'
|
|
125
|
+
const move: GitOpsIssue = {
|
|
126
|
+
severity: 'warning', scope: 'condition', reason: 'SharedResourceWarning', message: 'Also tracked by payments',
|
|
127
|
+
action: `${movePrimary} ${moveTitle}: ${moveSteps.join(' ')}`, actionPrimary: movePrimary,
|
|
128
|
+
actionSecondaryTitle: moveTitle, actionSecondary: moveSteps,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('shared-resource move guidance', () => {
|
|
132
|
+
it.each(['detail', 'cluster'] as const)('renders numbered steps on the %s surface', async (surface) => {
|
|
133
|
+
const row: Issue = { ...issue, reason: move.reason, message: move.message, action: move.action,
|
|
134
|
+
action_primary: movePrimary, action_secondary_title: moveTitle, action_secondary: moveSteps }
|
|
135
|
+
await act(async () => root.render(surface === 'detail'
|
|
136
|
+
? <GitOpsIssuesBand issues={[move]} />
|
|
137
|
+
: <IssueRow issue={row} open={true} onToggle={() => {}} />))
|
|
138
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent?.includes(moveTitle))!
|
|
139
|
+
expect(button).toBeDefined()
|
|
140
|
+
await act(async () => button.click())
|
|
141
|
+
const panel = document.getElementById(button.getAttribute('aria-controls')!)!
|
|
142
|
+
expect([...panel.querySelectorAll('ol > li')].map((li) => li.textContent)).toEqual(moveSteps)
|
|
143
|
+
expect(panel.querySelector('[inert]')).toBeNull()
|
|
144
|
+
expect(element.textContent).toContain(movePrimary)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('cluster sync hazards', () => {
|
|
149
|
+
const hazard = 'Resolve shared ownership before syncing: another Application can take tracking back.'
|
|
150
|
+
it.each([undefined, { nextStep: 'Host next step', cause: 'Host cause' }])('keeps a warning ahead of any next-step source (%j)', async (diagnosisSource) => {
|
|
151
|
+
await act(async () => root.render(<IssueRow
|
|
152
|
+
issue={{ ...issue, action_primary: undefined, action_secondary: undefined, action_secondary_title: undefined, hazard }}
|
|
153
|
+
open={true} onToggle={() => {}} diagnosisSource={diagnosisSource} />))
|
|
154
|
+
const warning = [...element.querySelectorAll('p')].find((p) => p.textContent === hazard)!
|
|
155
|
+
expect(warning).toBeDefined()
|
|
156
|
+
expect(warning.closest('.text-warning-text')).not.toBeNull()
|
|
157
|
+
expect(warning.closest('[inert]')).toBeNull()
|
|
158
|
+
expect(element.textContent!.indexOf(hazard)).toBeLessThan(element.textContent!.indexOf(diagnosisSource?.nextStep ?? primary))
|
|
159
|
+
if (diagnosisSource) expect(element.textContent).toContain(diagnosisSource.nextStep)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('shows a hazard on a collapsed row with no split guidance or action', async () => {
|
|
163
|
+
await act(async () => root.render(<IssueRow
|
|
164
|
+
issue={{ ...issue, action: undefined, action_primary: undefined, action_secondary: undefined, action_secondary_title: undefined, hazard }}
|
|
165
|
+
open={false} onToggle={() => {}} diagnosisSource={{ nextStep: 'Host next step' }} />))
|
|
166
|
+
const warning = [...element.querySelectorAll('p')].find((p) => p.textContent === hazard)!
|
|
167
|
+
expect(warning).toBeDefined()
|
|
168
|
+
expect(warning.closest('[inert]')).toBeNull()
|
|
169
|
+
expect(warning.closest('.text-warning-text')).not.toBeNull()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('hazard text retained only once in presentation', () => {
|
|
174
|
+
it.each([false, true])('keeps empty-render guidance and one warning (open=%s)', async (open) => {
|
|
175
|
+
await act(async () => root.render(<IssueRow issue={{ ...issue, hazard: primary }} open={open} onToggle={() => {}} />))
|
|
176
|
+
expect([...element.querySelectorAll('p')].filter((p) => p.textContent === primary)).toHaveLength(1)
|
|
177
|
+
await checkDisclosure()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it.each(['hazard only', 'appended hazard'])('keeps one warning for %s actions', async (mode) => {
|
|
181
|
+
const hazard = 'Sync can take over shared tracking.'
|
|
182
|
+
const nextStep = "Open the application's sync operation details."
|
|
183
|
+
const action = mode === 'hazard only' ? hazard : `${nextStep} ${hazard}`
|
|
184
|
+
await act(async () => root.render(<IssueRow issue={{ ...issue, hazard, action,
|
|
185
|
+
action_primary: undefined, action_secondary: undefined, action_secondary_title: undefined }} open={true} onToggle={() => {}} />))
|
|
186
|
+
expect(element.textContent!.split(hazard)).toHaveLength(2)
|
|
187
|
+
if (mode === 'hazard only') expect(element.textContent).not.toContain('Next step')
|
|
188
|
+
else expect(element.textContent).toContain(nextStep)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AlertTriangle } from 'lucide-react'
|
|
2
|
+
import { Disclosure } from '../ui/Disclosure'
|
|
3
|
+
|
|
4
|
+
export function issueGuidanceSummary(action?: string, primary?: string, hazard?: string) {
|
|
5
|
+
const summary = primary || action
|
|
6
|
+
if (!hazard || !summary) return summary
|
|
7
|
+
if (summary === hazard) return undefined
|
|
8
|
+
return summary.endsWith(` ${hazard}`) ? summary.slice(0, -hazard.length - 1) : summary
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function IssueGuidance({ action, primary, secondaryTitle, secondary, ordered, hazard, className }: {
|
|
12
|
+
action?: string
|
|
13
|
+
/** Replaces a supplied action; primary alone renders no action text. */
|
|
14
|
+
primary?: string
|
|
15
|
+
/** Paired with secondary; no disclosure is shown without both. */
|
|
16
|
+
secondaryTitle?: string
|
|
17
|
+
/** Paired with secondaryTitle; no disclosure is shown without both. */
|
|
18
|
+
secondary?: string[]
|
|
19
|
+
ordered?: boolean
|
|
20
|
+
hazard?: string
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
if (!action && !hazard && !(secondaryTitle && secondary?.length)) return null
|
|
24
|
+
return (
|
|
25
|
+
<div className={className}>
|
|
26
|
+
{hazard && (
|
|
27
|
+
<div className="mb-2 flex items-start gap-2 text-warning-text">
|
|
28
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
|
29
|
+
<p>{hazard}</p>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
{action && <p>{primary || action}</p>}
|
|
33
|
+
{secondaryTitle && secondary && (
|
|
34
|
+
<Disclosure summary={secondaryTitle} className="mt-2" summaryClassName="font-medium">
|
|
35
|
+
{ordered ? (
|
|
36
|
+
<ol className="list-decimal space-y-2 pl-5 pt-2">
|
|
37
|
+
{secondary.map((step) => <li key={step}>{step}</li>)}
|
|
38
|
+
</ol>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="space-y-2 pt-2">
|
|
41
|
+
{secondary.map((paragraph) => <p key={paragraph}>{paragraph}</p>)}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</Disclosure>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { IssueGuidance, issueGuidanceSummary } from './IssueGuidance'
|
|
1
2
|
import { summarizeSchedulerMessage } from '../resources/resource-utils'
|
|
2
3
|
import { useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
|
3
4
|
import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, Eraser, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
|
|
4
|
-
import {
|
|
5
|
+
import { CardSection, ClusterName, Disclosure, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
|
|
5
6
|
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse';
|
|
6
7
|
import { Tooltip } from '../ui/Tooltip';
|
|
7
8
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
|
|
@@ -368,6 +369,15 @@ export function IssueRow({
|
|
|
368
369
|
</div>
|
|
369
370
|
</div>
|
|
370
371
|
|
|
372
|
+
{(issue.action_primary || issue.hazard) && (
|
|
373
|
+
<Collapse open={!open} unmountOnExit>
|
|
374
|
+
<div className="px-4 pb-3 pl-6 text-xs">
|
|
375
|
+
{issue.cause && <p className="mb-1 text-theme-text-primary">{issue.cause}</p>}
|
|
376
|
+
<IssueGuidance hazard={issue.hazard} action={issue.action_primary ? issueGuidanceSummary(issue.action, issue.action_primary, issue.hazard) : undefined} secondaryTitle={issue.action_secondary_title} secondary={issue.action_secondary} ordered={issue.reason === 'SharedResourceWarning'} className="text-theme-text-tertiary" />
|
|
377
|
+
</div>
|
|
378
|
+
</Collapse>
|
|
379
|
+
)}
|
|
380
|
+
|
|
371
381
|
{/* Details unmount after the close transition: a list can hold hundreds
|
|
372
382
|
of rows and the body is the expensive part, so closed rows stay light. */}
|
|
373
383
|
<Collapse open={open} unmountOnExit id={panelId}>
|
|
@@ -509,11 +519,13 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
509
519
|
</>
|
|
510
520
|
) : undefined;
|
|
511
521
|
|
|
522
|
+
const guidanceSummary = issueGuidanceSummary(issue.action, issue.action_primary, issue.hazard);
|
|
512
523
|
const hasNextStep =
|
|
513
|
-
!!issue.
|
|
524
|
+
!!guidanceSummary || !!issue.action_secondary?.length || !!sourceNextStep || (issue.remediation_kind === 'create-namespace' && !!issue.remediation_target);
|
|
514
525
|
|
|
515
526
|
return (
|
|
516
527
|
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
528
|
+
<IssueGuidance hazard={issue.hazard} className="text-sm leading-relaxed" />
|
|
517
529
|
{/* When a parsed cause replaces the detector text, the terse reason code
|
|
518
530
|
(CrashLoopBackOff, FailedMount…) rides the eyebrow — it's the greppable
|
|
519
531
|
identifier operators anchor on, and the collapsed header drops it while
|
|
@@ -547,7 +559,7 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
547
559
|
tone="fix"
|
|
548
560
|
labelExtra={nextStepAttribution ?? undefined}
|
|
549
561
|
>
|
|
550
|
-
{sourceNextStep ??
|
|
562
|
+
{sourceNextStep ?? <IssueGuidance action={guidanceSummary} secondaryTitle={issue.action_secondary_title} secondary={issue.action_secondary} ordered={issue.reason === 'SharedResourceWarning'} className="text-sm leading-relaxed text-theme-text-secondary" />}
|
|
551
563
|
{/* Survives an override: this is a structured one-click fix with an
|
|
552
564
|
apply path, not advice competing with the host's. */}
|
|
553
565
|
{issue.remediation_kind === 'create-namespace' && issue.remediation_target ? (
|
|
@@ -10,10 +10,12 @@ export function ResourceIssuesSection({
|
|
|
10
10
|
subjectResource,
|
|
11
11
|
compact,
|
|
12
12
|
renderDetailSection,
|
|
13
|
+
heading = "Operational issues",
|
|
13
14
|
}: {
|
|
14
15
|
compact?: boolean
|
|
15
16
|
renderDetailSection?: (ctx: IssueRowSlotContext) => ReactNode
|
|
16
17
|
issues: Issue[] | undefined
|
|
18
|
+
heading?: string
|
|
17
19
|
/** When provided, related resources in a causal link become clickable. */
|
|
18
20
|
onResourceClick?: (ref: IssueResourceRef) => void
|
|
19
21
|
/** The resource this section is embedded under. When an issue's subject IS
|
|
@@ -31,7 +33,7 @@ export function ResourceIssuesSection({
|
|
|
31
33
|
<section className="space-y-2">
|
|
32
34
|
<div className="flex items-center gap-2 px-1 text-xs font-semibold uppercase tracking-wide text-theme-text-secondary">
|
|
33
35
|
<AlertTriangle className="h-4 w-4 text-theme-text-tertiary" aria-hidden />
|
|
34
|
-
<span>
|
|
36
|
+
<span>{heading} ({sorted.length})</span>
|
|
35
37
|
</div>
|
|
36
38
|
<ol className="flex flex-col gap-1.5">
|
|
37
39
|
{sorted.map((issue) => {
|
|
@@ -26,8 +26,10 @@ export function diagnosticFactLabel(type: string): string {
|
|
|
26
26
|
return 'Missing reference';
|
|
27
27
|
case 'node_startup_corroboration':
|
|
28
28
|
return 'Same-node failures';
|
|
29
|
+
case 'evicted_from_node':
|
|
30
|
+
return 'Evicted by node';
|
|
29
31
|
case 'owner_rollup':
|
|
30
|
-
return 'Grouped
|
|
32
|
+
return 'Grouped resources';
|
|
31
33
|
case 'selected_backend_issue':
|
|
32
34
|
return 'Backend pods';
|
|
33
35
|
case 'service_config_mismatch':
|
|
@@ -56,6 +58,16 @@ export function diagnosticFactLabel(type: string): string {
|
|
|
56
58
|
return 'Affected resources';
|
|
57
59
|
case 'tls_certificate_consumers':
|
|
58
60
|
return 'Used for TLS by';
|
|
61
|
+
case 'kueue_admission':
|
|
62
|
+
return 'Kueue admission';
|
|
63
|
+
case 'kueue_queue':
|
|
64
|
+
return 'Queue';
|
|
65
|
+
case 'kueue_affected_runs':
|
|
66
|
+
return 'Blocked runs';
|
|
67
|
+
case 'kueue_admission_check':
|
|
68
|
+
return 'Admission check';
|
|
69
|
+
case 'kueue_provisioning_request':
|
|
70
|
+
return 'Provisioning request';
|
|
59
71
|
default:
|
|
60
72
|
return type.replace(/_/g, ' ');
|
|
61
73
|
}
|
|
@@ -70,6 +82,7 @@ export function incidentParentLabel(factType?: string, confidence?: string): str
|
|
|
70
82
|
case 'pvc_blast_radius':
|
|
71
83
|
case 'secret_not_ready':
|
|
72
84
|
case 'admission_webhook_backend':
|
|
85
|
+
case 'kueue_queue':
|
|
73
86
|
return 'Caused by';
|
|
74
87
|
case 'apiservice_hpa':
|
|
75
88
|
return 'Likely cause';
|
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
memberRef,
|
|
16
16
|
issueSortAnchor,
|
|
17
17
|
compareIssueSortAnchors,
|
|
18
|
+
compareIssues,
|
|
18
19
|
} from './types';
|
|
19
20
|
export type { Issue, IssueConditionalCleanup, IssueFinalizerRemoval, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField, IssueRunningPreviousValue, IssueWithheld } from './types';
|
|
20
21
|
export {
|
|
@@ -57,6 +57,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
57
57
|
unschedulable: "Can't be scheduled",
|
|
58
58
|
quota_exceeded: 'Quota exceeded',
|
|
59
59
|
admission_webhook_blocking: 'Admission blocked',
|
|
60
|
+
queue_admission_blocked: 'Queue admission blocked',
|
|
60
61
|
image_pull_failed: 'Image pull failed',
|
|
61
62
|
container_waiting: 'Container waiting',
|
|
62
63
|
init_container_failed: 'Init container failed',
|
|
@@ -66,6 +67,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
66
67
|
readiness_failed: 'Not ready for traffic',
|
|
67
68
|
workload_degraded: 'Workload degraded',
|
|
68
69
|
high_restart: 'High restart count',
|
|
70
|
+
pod_evicted: 'Pod evicted',
|
|
69
71
|
missing_config_ref: 'Missing reference',
|
|
70
72
|
pdb_blocks_evictions: 'Evictions blocked',
|
|
71
73
|
secret_sync_failed: 'Secret sync failed',
|
|
@@ -89,10 +91,12 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
89
91
|
backup_stalled: 'Backup not progressing',
|
|
90
92
|
job_failed: 'Job failed',
|
|
91
93
|
cronjob_failed: 'CronJob needs attention',
|
|
94
|
+
batch_run_stopped: 'Run stopped by Kueue',
|
|
92
95
|
rollout_stalled: 'Rollout stalled',
|
|
93
96
|
hpa_limited_or_failed: 'Autoscaling limited',
|
|
94
97
|
rbac_forbidden: 'Permission denied',
|
|
95
98
|
certificate_not_ready: 'Certificate not ready',
|
|
99
|
+
certificate_renewal_failed: 'Certificate renewal failed',
|
|
96
100
|
pod_security_violation: 'Pod Security blocked',
|
|
97
101
|
node_not_ready: 'Node not ready',
|
|
98
102
|
node_provisioning_failed: 'Node provisioning failed',
|
|
@@ -100,6 +104,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
100
104
|
crossplane_reconcile_failed: 'Crossplane reconcile failed',
|
|
101
105
|
termination_stuck: 'Stuck terminating',
|
|
102
106
|
operator_condition_failed: 'Controller reports a problem',
|
|
107
|
+
gitops_resource_warning: 'GitOps resource warning',
|
|
103
108
|
gitops_sync_failed: 'GitOps sync failed',
|
|
104
109
|
gitops_render_failed: 'GitOps render failed',
|
|
105
110
|
gitops_spec_invalid: 'GitOps spec invalid',
|
|
@@ -153,6 +158,16 @@ const REASON_TITLE: Record<string, string> = {
|
|
|
153
158
|
CNPGInstanceReadinessMismatch: "Instance Pods contradict Cluster readiness",
|
|
154
159
|
CNPGPrimaryLabelMismatch: "Primary Pod label contradicts Cluster status",
|
|
155
160
|
CNPGScheduledRunNoBackup: "No successful backup since a scheduled run",
|
|
161
|
+
KueueAdmissionCheckRejected: 'Admission check rejected the run',
|
|
162
|
+
KueueClusterQueueInactive: 'ClusterQueue admits nothing',
|
|
163
|
+
KueueClusterQueueMissing: 'ClusterQueue missing',
|
|
164
|
+
KueueLocalQueueMissing: 'LocalQueue missing',
|
|
165
|
+
KueueRequeuingLimitExceeded: 'Kueue stopped retrying the run',
|
|
166
|
+
KueueMaximumExecutionTimeExceeded: 'Run hit its maximum execution time',
|
|
167
|
+
KueueDeactivatedByKueue: 'Kueue withdrew the run',
|
|
168
|
+
KueueResourceUnavailable: 'Queue offers no quota for a requested resource',
|
|
169
|
+
KueueNamespaceNotEligible: "Queue doesn't admit this namespace",
|
|
170
|
+
KueueInvalidWorkload: "Kueue can't admit the run's spec",
|
|
156
171
|
};
|
|
157
172
|
|
|
158
173
|
/** The title a reason has wherever it is shown, when its category's label would name the wrong operation. */
|
|
@@ -231,6 +231,16 @@ export interface Issue {
|
|
|
231
231
|
* Server-emitted (omitempty); empty for issues without a parser. */
|
|
232
232
|
cause?: string;
|
|
233
233
|
action?: string;
|
|
234
|
+
/** What the suggested or most obvious action could destroy or take over,
|
|
235
|
+
* such as a GitOps sync that would prune every managed resource. Show it
|
|
236
|
+
* even when replacing cause or action with other guidance; action also
|
|
237
|
+
* contains it. Absence means no warning was supplied, not that the action
|
|
238
|
+
* is safe; older producers omit it. */
|
|
239
|
+
hazard?: string;
|
|
240
|
+
/** Optional presentation split; action always retains the complete guidance. */
|
|
241
|
+
action_primary?: string;
|
|
242
|
+
action_secondary_title?: string;
|
|
243
|
+
action_secondary?: string[];
|
|
234
244
|
remediation_kind?: string;
|
|
235
245
|
remediation_target?: string;
|
|
236
246
|
/** A cleanup that is right only when its condition holds — never part of
|