@skyhook-io/k8s-ui 1.17.0 → 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/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 +6 -2
- 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 +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 +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- 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 +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- 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.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- 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/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 +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- 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/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- 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.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- 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 +82 -6
- 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/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- 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/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 +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 +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 +180 -64
- 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 +29 -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 +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,115 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { GitOpsChange, GitOpsDrift, GitOpsInsight, GitOpsResourceDiff } from '../../../types'
|
|
5
|
+
import { ArgoResourceDiff } from './ArgoResourceDiff'
|
|
6
|
+
import { GitOpsChangesView } from './GitOpsInsightViews'
|
|
7
|
+
|
|
8
|
+
// Shape seen live on Argo CD 3.5.2: the Deployment was scaled by hand (1 -> 3)
|
|
9
|
+
// and a Git commit bumping the image hasn't synced yet. last-applied holds the
|
|
10
|
+
// manifest from the last sync, so it only sees the scale.
|
|
11
|
+
const lastApplied: GitOpsDrift = {
|
|
12
|
+
source: 'lastAppliedAnnotation',
|
|
13
|
+
entries: [{ path: 'spec.replicas', op: 'changed', desired: '1', live: '3' }],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const argoDiff: GitOpsResourceDiff = {
|
|
17
|
+
source: 'argocd-api',
|
|
18
|
+
desired: 'spec:\n replicas: 1\n image: nginx:1.28-alpine\n',
|
|
19
|
+
live: 'spec:\n replicas: 3\n image: nginx:1.27-alpine\n',
|
|
20
|
+
fieldEntries: [
|
|
21
|
+
{ path: 'spec.replicas', op: 'changed', desired: '1', live: '3' },
|
|
22
|
+
{ path: 'spec.template.spec.containers.[0].image', op: 'changed', desired: '"nginx:1.28-alpine"', live: '"nginx:1.27-alpine"' },
|
|
23
|
+
],
|
|
24
|
+
redacted: false,
|
|
25
|
+
hook: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const IMAGE_PATH = 'spec.template.spec.containers.[0].image'
|
|
29
|
+
|
|
30
|
+
const count = (html: string, needle: string) => html.split(needle).length - 1
|
|
31
|
+
|
|
32
|
+
describe('ArgoResourceDiff field list', () => {
|
|
33
|
+
test("shows Argo's field list above the YAML diff, including the unsynced Git change", () => {
|
|
34
|
+
const html = renderToStaticMarkup(<ArgoResourceDiff diff={argoDiff} loading={false} fallbackDrift={lastApplied} />)
|
|
35
|
+
expect(html).toContain('Field diff')
|
|
36
|
+
expect(html).toContain(IMAGE_PATH)
|
|
37
|
+
expect(html).toContain('via Argo CD API')
|
|
38
|
+
expect(html.indexOf('Field diff')).toBeLessThan(html.indexOf('desired (Git-rendered)'))
|
|
39
|
+
// One list, not Argo's plus the last-applied fallback.
|
|
40
|
+
expect(count(html, 'Field diff')).toBe(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('renders no field list box when Argo reports no field differences', () => {
|
|
44
|
+
const html = renderToStaticMarkup(<ArgoResourceDiff diff={{ ...argoDiff, fieldEntries: [] }} loading={false} fallbackDrift={lastApplied} />)
|
|
45
|
+
expect(html).not.toContain('Field diff')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('treats a null field list (sent by older Radars) as empty instead of crashing', () => {
|
|
49
|
+
const html = renderToStaticMarkup(<ArgoResourceDiff diff={{ ...argoDiff, fieldEntries: null }} loading={false} fallbackDrift={lastApplied} />)
|
|
50
|
+
expect(html).not.toContain('Field diff')
|
|
51
|
+
expect(html).toContain('desired (Git-rendered)')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('says the list was cut short instead of presenting the cap as the count', () => {
|
|
55
|
+
const html = renderToStaticMarkup(<ArgoResourceDiff diff={{ ...argoDiff, fieldEntriesTruncated: true }} loading={false} />)
|
|
56
|
+
expect(html).toContain('showing first 50 entries')
|
|
57
|
+
expect(html).not.toContain('2 fields')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("falls back to the last-applied list when Argo's diff fails to load", () => {
|
|
61
|
+
const html = renderToStaticMarkup(<ArgoResourceDiff loading={false} error="Failed to fetch the diff from the Argo CD API server." fallbackDrift={lastApplied} />)
|
|
62
|
+
expect(html).toContain('Field diff')
|
|
63
|
+
expect(html).toContain('spec.replicas')
|
|
64
|
+
expect(html).not.toContain('via Argo CD API')
|
|
65
|
+
expect(html).toContain('Failed to fetch the diff from the Argo CD API server.')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('GitOpsChangesView expanded Argo row', () => {
|
|
70
|
+
const change: GitOpsChange = {
|
|
71
|
+
ref: { group: 'apps', kind: 'Deployment', namespace: 'web', name: 'web' },
|
|
72
|
+
category: 'OutOfSync',
|
|
73
|
+
sync: 'OutOfSync',
|
|
74
|
+
health: 'Healthy',
|
|
75
|
+
hasDesired: true,
|
|
76
|
+
hasLive: true,
|
|
77
|
+
drift: lastApplied,
|
|
78
|
+
partial: false,
|
|
79
|
+
}
|
|
80
|
+
const insight = (argoDiffAvailable: boolean): GitOpsInsight => ({
|
|
81
|
+
summary: { tool: 'argocd', kind: 'Application', namespace: 'argocd', name: 'web', sync: 'OutOfSync', health: 'Healthy' },
|
|
82
|
+
changes: [change],
|
|
83
|
+
capabilities: {
|
|
84
|
+
sync: true, refresh: true, terminate: false, suspend: false, resume: false,
|
|
85
|
+
syncWithSource: false, selectiveSync: true, rollback: false, argoDiffAvailable,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("hands the last-applied list to the Argo diff instead of rendering it alongside", () => {
|
|
90
|
+
let passed: GitOpsDrift | undefined
|
|
91
|
+
const html = renderToStaticMarkup(
|
|
92
|
+
<GitOpsChangesView
|
|
93
|
+
insight={insight(true)}
|
|
94
|
+
focusKey="Deployment/web/web"
|
|
95
|
+
renderResourceDiff={(_ref, fallbackDrift) => {
|
|
96
|
+
passed = fallbackDrift
|
|
97
|
+
return <div data-testid="argo-diff" />
|
|
98
|
+
}}
|
|
99
|
+
/>,
|
|
100
|
+
)
|
|
101
|
+
expect(html).toContain('argo-diff')
|
|
102
|
+
expect(html).not.toContain('Field diff')
|
|
103
|
+
expect(passed).toEqual(lastApplied)
|
|
104
|
+
// The row's "N diff" badge counts the last-applied list, which can
|
|
105
|
+
// undercount here, so it isn't shown.
|
|
106
|
+
expect(html).not.toContain('1 diff')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('without the Argo diff, the row still shows the last-applied list', () => {
|
|
110
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={insight(false)} focusKey="Deployment/web/web" />)
|
|
111
|
+
expect(html).toContain('Field diff')
|
|
112
|
+
expect(html).toContain('spec.replicas')
|
|
113
|
+
expect(html).toContain('1 diff')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -2,18 +2,23 @@ import { useState } from 'react'
|
|
|
2
2
|
import { createTwoFilesPatch } from 'diff'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { FileText, GitCompare, Maximize2, Rows, ShieldOff, X } from 'lucide-react'
|
|
5
|
-
import type { GitOpsResourceDiff } from '../../../types'
|
|
5
|
+
import type { GitOpsDrift, GitOpsResourceDiff } from '../../../types'
|
|
6
6
|
import { DiffLine, hasDiffBodyChange } from '../../shared/UnifiedDiff'
|
|
7
7
|
import { CodeViewer } from '../../ui/CodeViewer'
|
|
8
8
|
import { DialogPortal } from '../../ui/DialogPortal'
|
|
9
9
|
import { YamlDiffEditor } from '../../ui/YamlEditor'
|
|
10
10
|
import { Tooltip } from '../../ui/Tooltip'
|
|
11
|
+
import { DriftPanel } from './DriftPanel'
|
|
11
12
|
|
|
12
13
|
interface ArgoResourceDiffProps {
|
|
13
14
|
diff?: GitOpsResourceDiff | null
|
|
14
15
|
loading: boolean
|
|
15
16
|
// Server error string ({"error"} body), surfaced inline — not a toast.
|
|
16
17
|
error?: string | null
|
|
18
|
+
// The row's last-applied field list, shown only when Argo's diff can't be
|
|
19
|
+
// loaded. A loaded diff carries Argo's own field list, which also covers Git
|
|
20
|
+
// changes that haven't synced yet.
|
|
21
|
+
fallbackDrift?: GitOpsDrift | null
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
// ArgoResourceDiff renders the full Git-rendered desired-vs-live diff for a
|
|
@@ -21,7 +26,8 @@ interface ArgoResourceDiffProps {
|
|
|
21
26
|
// fetch and passes data / loading / error. The inline view is a compact unified
|
|
22
27
|
// preview; the maximize control opens the full view (Diff / Live manifest /
|
|
23
28
|
// Desired manifest, with the Monaco-based YamlDiffEditor powering the Diff tab).
|
|
24
|
-
|
|
29
|
+
// Argo's per-field list sits above the preview, and only when it has entries.
|
|
30
|
+
export function ArgoResourceDiff({ diff, loading, error, fallbackDrift }: ArgoResourceDiffProps) {
|
|
25
31
|
const [maximized, setMaximized] = useState(false)
|
|
26
32
|
|
|
27
33
|
if (loading) {
|
|
@@ -42,19 +48,30 @@ export function ArgoResourceDiff({ diff, loading, error }: ArgoResourceDiffProps
|
|
|
42
48
|
</div>
|
|
43
49
|
)
|
|
44
50
|
}
|
|
51
|
+
const fallbackPanel = fallbackDrift && fallbackDrift.entries.length > 0 ? <DriftPanel drift={fallbackDrift} /> : null
|
|
45
52
|
if (error) {
|
|
46
53
|
return (
|
|
47
|
-
<div className="
|
|
48
|
-
{
|
|
54
|
+
<div className="space-y-3">
|
|
55
|
+
{fallbackPanel}
|
|
56
|
+
<div className="rounded-md border border-red-500/40 bg-red-500/5 px-3 py-2 text-xs text-red-700 dark:text-red-400">
|
|
57
|
+
{error}
|
|
58
|
+
</div>
|
|
49
59
|
</div>
|
|
50
60
|
)
|
|
51
61
|
}
|
|
52
|
-
if (!diff) return
|
|
62
|
+
if (!diff) return fallbackPanel
|
|
53
63
|
|
|
54
64
|
const unchanged = !docsDiffer(diff.desired, diff.live)
|
|
65
|
+
// Older Radars send null for an empty list.
|
|
66
|
+
const fieldEntries = diff.fieldEntries ?? []
|
|
55
67
|
|
|
56
68
|
return (
|
|
57
69
|
<div>
|
|
70
|
+
{fieldEntries.length > 0 && (
|
|
71
|
+
<div className="mb-3">
|
|
72
|
+
<DriftPanel drift={{ entries: fieldEntries, source: diff.source, truncated: diff.fieldEntriesTruncated }} />
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
58
75
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
59
76
|
<div className="flex items-center gap-2">
|
|
60
77
|
<GitCompare className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { GitOpsDrift, GitOpsDriftEntry } from '../../../types'
|
|
2
|
+
|
|
3
|
+
// DriftPanel renders the structured per-field diff. Format mimics a
|
|
4
|
+
// `diff -u` summary: removed paths in red, added in green, changed shown
|
|
5
|
+
// inline as "old → new". Path is monospace; values are JSON-encoded and
|
|
6
|
+
// pre-wrapped so structured values (objects, arrays) render readably.
|
|
7
|
+
export function DriftPanel({ drift }: { drift: GitOpsDrift }) {
|
|
8
|
+
const sourceLabel = driftSourceLabel(drift.source)
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<div className="mb-2 flex items-baseline justify-between gap-2">
|
|
12
|
+
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Field diff</h4>
|
|
13
|
+
<span className="text-[10px] text-theme-text-tertiary">
|
|
14
|
+
desired (Git) → live ·
|
|
15
|
+
{drift.truncated ? ' showing first 50 entries' : ` ${drift.entries.length} field${drift.entries.length === 1 ? '' : 's'}`}
|
|
16
|
+
{sourceLabel && ` · ${sourceLabel}`}
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="space-y-1 font-mono text-[11px]">
|
|
20
|
+
{drift.entries.map((entry, i) => (
|
|
21
|
+
<DriftEntryRow key={`${entry.path}-${i}`} entry={entry} />
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Provenance label for a Drift's field entries. Only Argo-sourced drift gets an
|
|
29
|
+
// explicit label; the built-in last-applied path renders without one (the
|
|
30
|
+
// "desired (Git) → live" copy already implies it). Unknown sources (a library
|
|
31
|
+
// consumer lagging the backend) fall through to no label rather than surfacing
|
|
32
|
+
// a raw enum string.
|
|
33
|
+
function driftSourceLabel(source: GitOpsDrift['source']): string {
|
|
34
|
+
return source === 'argocd-api' ? 'via Argo CD API' : ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function DriftEntryRow({ entry }: { entry: GitOpsDriftEntry }) {
|
|
38
|
+
if (entry.op === 'removed') {
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
<span className="text-red-600 dark:text-red-400">- {entry.path}</span>
|
|
42
|
+
{entry.desired && <span className="ml-2 text-theme-text-secondary">{entry.desired}</span>}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
if (entry.op === 'added') {
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<span className="text-emerald-700 dark:text-emerald-400">+ {entry.path}</span>
|
|
50
|
+
{entry.live && <span className="ml-2 text-theme-text-secondary">{entry.live}</span>}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<span className="text-amber-700 dark:text-amber-400">~ {entry.path}</span>
|
|
57
|
+
<span className="ml-2 text-theme-text-tertiary">{entry.desired}</span>
|
|
58
|
+
<span className="mx-1 text-theme-text-tertiary">→</span>
|
|
59
|
+
<span className="text-theme-text-primary">{entry.live}</span>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -2,7 +2,7 @@ import { AlertTriangle, ArrowUpDown, ChevronDown, ChevronRight, ChevronUp, Circl
|
|
|
2
2
|
import { PaneLoader } from '../../ui/PaneLoader'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
5
|
-
import type { GitOpsChange, GitOpsHistoryItem, GitOpsInsight, GitOpsInsightRef, GitOpsIssue, GitOpsPlanItem, GitOpsRemediation, GitOpsResourceTree, GitOpsTreeNode } from '../../../types'
|
|
5
|
+
import type { GitOpsChange, GitOpsDrift, GitOpsHistoryItem, GitOpsInsight, GitOpsInsightRef, GitOpsIssue, GitOpsPlanItem, GitOpsRemediation, GitOpsResourceTree, GitOpsTreeNode } from '../../../types'
|
|
6
6
|
import { HealthStatusBadge, SyncStatusBadge } from '../GitOpsStatusBadge'
|
|
7
7
|
import { radarHealthNote } from '../health-provenance'
|
|
8
8
|
import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../../utils/badge-colors'
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type ResourceSortKey,
|
|
31
31
|
type ResourceStatusFacet,
|
|
32
32
|
} from './insights-helpers'
|
|
33
|
+
import { DriftPanel } from './DriftPanel'
|
|
33
34
|
|
|
34
35
|
interface GitOpsStatusStripProps {
|
|
35
36
|
insight?: GitOpsInsight | null
|
|
@@ -222,7 +223,10 @@ function commitURLForRepo(source: string | undefined, revision: string): string
|
|
|
222
223
|
// "did everything come up?" summary. All-healthy reads green and explicit
|
|
223
224
|
// ("4/4 resources healthy"); mixed states list the unhealthy buckets so
|
|
224
225
|
// the user sees at a glance what didn't make it.
|
|
225
|
-
function buildHealthSummary(
|
|
226
|
+
function buildHealthSummary(allChanges: GitOpsChange[]): { text: string; tone: string } | null {
|
|
227
|
+
// Dependencies are consumed, not deployed, so they don't count toward
|
|
228
|
+
// "did everything come up"; a failing one has its own Issue.
|
|
229
|
+
const changes = allChanges.filter((c) => !c.dependency)
|
|
226
230
|
if (changes.length === 0) return null
|
|
227
231
|
let healthy = 0
|
|
228
232
|
let degraded = 0
|
|
@@ -735,7 +739,10 @@ interface GitOpsChangesViewProps {
|
|
|
735
739
|
// returns the loaded <ArgoResourceDiff/>. When present AND
|
|
736
740
|
// insight.capabilities.argoDiffAvailable, resolvable rows expose a "Full
|
|
737
741
|
// diff" toggle. Library consumers that don't wire it get no such affordance.
|
|
738
|
-
|
|
742
|
+
// fallbackDrift is the row's last-applied field list. The loaded diff shows
|
|
743
|
+
// Argo's own field list instead, and falls back to this one when the diff
|
|
744
|
+
// can't be loaded.
|
|
745
|
+
renderResourceDiff?: (ref: GitOpsInsightRef, fallbackDrift?: GitOpsDrift) => ReactNode
|
|
739
746
|
// Opens the global Settings dialog. Backs the "Connect Argo CD" hint shown
|
|
740
747
|
// when the root is an Argo Application without the diff integration.
|
|
741
748
|
onOpenSettings?: () => void
|
|
@@ -1005,7 +1012,7 @@ export function GitOpsChangesView({ insight, error, onOpenResource, onSyncResour
|
|
|
1005
1012
|
)}
|
|
1006
1013
|
</div>
|
|
1007
1014
|
)}
|
|
1008
|
-
{totalCount > 0 && isArgoRoot && !insight.capabilities?.argoDiffAvailable && onOpenSettings && (
|
|
1015
|
+
{totalCount > 0 && isArgoRoot && !insight.capabilities?.argoDiffAvailable && !insight.capabilities?.argoConnecting && onOpenSettings && (
|
|
1009
1016
|
<div className="border-b border-theme-border bg-theme-base/40 px-4 py-2 text-[11px] text-theme-text-tertiary">
|
|
1010
1017
|
{insight.capabilities?.argoConfigured ? (
|
|
1011
1018
|
<>
|
|
@@ -1330,7 +1337,7 @@ function ChangeRow({
|
|
|
1330
1337
|
// When true, the expanded panel offers a "Full diff" toggle that mounts the
|
|
1331
1338
|
// host-wired Argo CD diff loader for this resource.
|
|
1332
1339
|
canFullDiff: boolean
|
|
1333
|
-
renderResourceDiff?: (ref: GitOpsInsightRef) => ReactNode
|
|
1340
|
+
renderResourceDiff?: (ref: GitOpsInsightRef, fallbackDrift?: GitOpsDrift) => ReactNode
|
|
1334
1341
|
onOpenResource?: (ref: GitOpsChange['ref']) => void
|
|
1335
1342
|
onSyncResource?: (ref: GitOpsChange['ref']) => void
|
|
1336
1343
|
syncResourceDisabledReason?: string
|
|
@@ -1422,8 +1429,11 @@ function ChangeRow({
|
|
|
1422
1429
|
)}
|
|
1423
1430
|
{/* Detail badges: surface that there's something to see in
|
|
1424
1431
|
the expanded panel. Without these the user has no signal
|
|
1425
|
-
that clicking will reveal anything useful.
|
|
1426
|
-
|
|
1432
|
+
that clicking will reveal anything useful. The field count
|
|
1433
|
+
comes from the last-applied list, which can undercount on an
|
|
1434
|
+
Argo row (Argo's own list loads only on expand), so it is
|
|
1435
|
+
left off there. The OutOfSync badge already marks the row. */}
|
|
1436
|
+
{!canFullDiff && driftEntries.length > 0 && (
|
|
1427
1437
|
<Tooltip content={`${driftEntries.length} field${driftEntries.length === 1 ? '' : 's'} differ from Git`} delay={200} wrapperClassName="shrink-0">
|
|
1428
1438
|
<span className="rounded border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-400">
|
|
1429
1439
|
{driftEntries.length} diff
|
|
@@ -1523,15 +1533,19 @@ function ChangeRow({
|
|
|
1523
1533
|
{expandable && (
|
|
1524
1534
|
<Collapse open={expanded} mountLazily>
|
|
1525
1535
|
<div className="border-t border-theme-border bg-theme-base/40 px-4 py-3">
|
|
1526
|
-
{
|
|
1536
|
+
{/* The last-applied list only knows what was applied at the last
|
|
1537
|
+
sync, so it misses a Git change that hasn't synced yet. When
|
|
1538
|
+
Argo's diff can load, it owns the field list and shows this
|
|
1539
|
+
one only if that load fails. */}
|
|
1540
|
+
{!canFullDiff && driftEntries.length > 0 && <DriftPanel drift={change.drift!} />}
|
|
1527
1541
|
{events.length > 0 && <RecentEventsPanel events={events} />}
|
|
1528
1542
|
{/* Expanding a row IS the request to see its diff — render it inline
|
|
1529
1543
|
rather than behind a second "Full diff" click. The capability is
|
|
1530
1544
|
already gated on a live Argo connection, so this only mounts (and
|
|
1531
1545
|
fetches) when the diff can actually be served. */}
|
|
1532
1546
|
{canFullDiff && (
|
|
1533
|
-
<div className={clsx(
|
|
1534
|
-
{renderResourceDiff?.(change.ref)}
|
|
1547
|
+
<div className={clsx(events.length > 0 && 'mt-3')}>
|
|
1548
|
+
{renderResourceDiff?.(change.ref, change.drift)}
|
|
1535
1549
|
</div>
|
|
1536
1550
|
)}
|
|
1537
1551
|
</div>
|
|
@@ -1541,67 +1555,6 @@ function ChangeRow({
|
|
|
1541
1555
|
)
|
|
1542
1556
|
}
|
|
1543
1557
|
|
|
1544
|
-
// DriftPanel renders the structured per-field diff. Format mimics a
|
|
1545
|
-
// `diff -u` summary: removed paths in red, added in green, changed shown
|
|
1546
|
-
// inline as "old → new". Path is monospace; values are JSON-encoded and
|
|
1547
|
-
// pre-wrapped so structured values (objects, arrays) render readably.
|
|
1548
|
-
function DriftPanel({ drift }: { drift: NonNullable<GitOpsChange['drift']> }) {
|
|
1549
|
-
const sourceLabel = driftSourceLabel(drift.source)
|
|
1550
|
-
return (
|
|
1551
|
-
<div>
|
|
1552
|
-
<div className="mb-2 flex items-baseline justify-between gap-2">
|
|
1553
|
-
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Field diff</h4>
|
|
1554
|
-
<span className="text-[10px] text-theme-text-tertiary">
|
|
1555
|
-
desired (Git) → live ·
|
|
1556
|
-
{drift.truncated ? ' showing first 50 entries' : ` ${drift.entries.length} field${drift.entries.length === 1 ? '' : 's'}`}
|
|
1557
|
-
{sourceLabel && ` · ${sourceLabel}`}
|
|
1558
|
-
</span>
|
|
1559
|
-
</div>
|
|
1560
|
-
<div className="space-y-1 font-mono text-[11px]">
|
|
1561
|
-
{drift.entries.map((entry, i) => (
|
|
1562
|
-
<DriftEntryRow key={`${entry.path}-${i}`} entry={entry} />
|
|
1563
|
-
))}
|
|
1564
|
-
</div>
|
|
1565
|
-
</div>
|
|
1566
|
-
)
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// Provenance label for a Drift's field entries. Only Argo-sourced drift gets an
|
|
1570
|
-
// explicit label; the built-in last-applied path renders without one (the
|
|
1571
|
-
// "desired (Git) → live" copy already implies it). Unknown sources (a library
|
|
1572
|
-
// consumer lagging the backend) fall through to no label rather than surfacing
|
|
1573
|
-
// a raw enum string.
|
|
1574
|
-
function driftSourceLabel(source: NonNullable<GitOpsChange['drift']>['source']): string {
|
|
1575
|
-
return source === 'argocd-api' ? 'via Argo CD API' : ''
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
function DriftEntryRow({ entry }: { entry: NonNullable<GitOpsChange['drift']>['entries'][number] }) {
|
|
1579
|
-
if (entry.op === 'removed') {
|
|
1580
|
-
return (
|
|
1581
|
-
<div>
|
|
1582
|
-
<span className="text-red-600 dark:text-red-400">- {entry.path}</span>
|
|
1583
|
-
{entry.desired && <span className="ml-2 text-theme-text-secondary">{entry.desired}</span>}
|
|
1584
|
-
</div>
|
|
1585
|
-
)
|
|
1586
|
-
}
|
|
1587
|
-
if (entry.op === 'added') {
|
|
1588
|
-
return (
|
|
1589
|
-
<div>
|
|
1590
|
-
<span className="text-emerald-700 dark:text-emerald-400">+ {entry.path}</span>
|
|
1591
|
-
{entry.live && <span className="ml-2 text-theme-text-secondary">{entry.live}</span>}
|
|
1592
|
-
</div>
|
|
1593
|
-
)
|
|
1594
|
-
}
|
|
1595
|
-
return (
|
|
1596
|
-
<div>
|
|
1597
|
-
<span className="text-amber-700 dark:text-amber-400">~ {entry.path}</span>
|
|
1598
|
-
<span className="ml-2 text-theme-text-tertiary">{entry.desired}</span>
|
|
1599
|
-
<span className="mx-1 text-theme-text-tertiary">→</span>
|
|
1600
|
-
<span className="text-theme-text-primary">{entry.live}</span>
|
|
1601
|
-
</div>
|
|
1602
|
-
)
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
1558
|
// RecentEventsPanel surfaces the last few events involving this resource.
|
|
1606
1559
|
// Warning events get a red bar so the eye lands on them; normals are
|
|
1607
1560
|
// muted. Aggregation count (when present) is a critical signal — "this
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { getSubtitle } from './GitOpsTreeGraph'
|
|
3
|
+
import { getSubtitle, nodeCause } from './GitOpsTreeGraph'
|
|
4
4
|
import type { GitOpsTreeNode } from '../../../types'
|
|
5
5
|
|
|
6
6
|
function node(extras: Partial<GitOpsTreeNode> = {}): GitOpsTreeNode {
|
|
@@ -90,3 +90,28 @@ describe('getSubtitle', () => {
|
|
|
90
90
|
expect(getSubtitle(node({ ref: { kind: 'ClusterRole', name: 'podinfo', namespace: '' } }))).toBe('')
|
|
91
91
|
})
|
|
92
92
|
})
|
|
93
|
+
|
|
94
|
+
describe('nodeCause', () => {
|
|
95
|
+
// Shapes as the tree API sends them: the Go side maps Argo's Degraded and
|
|
96
|
+
// Missing to topologyStatus 'unhealthy', Progressing and Suspended to
|
|
97
|
+
// 'degraded' (pkg/gitops/tree healthToTopology).
|
|
98
|
+
const CAUSE = 'Back-off restarting failed container main'
|
|
99
|
+
|
|
100
|
+
it('shows the cause on a node the controller calls Degraded', () => {
|
|
101
|
+
expect(nodeCause(node({ health: 'Degraded', healthSource: 'controller', topologyStatus: 'unhealthy' }), CAUSE)).toBe(CAUSE)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('hides it on a Progressing node, even when an issue matched it', () => {
|
|
105
|
+
// A rollout in flight is not broken; a cause tooltip on it says it is.
|
|
106
|
+
expect(nodeCause(node({ health: 'Progressing', healthSource: 'controller', topologyStatus: 'degraded' }), CAUSE)).toBeUndefined()
|
|
107
|
+
expect(nodeCause(node({ health: 'Progressing', healthSource: 'radar', topologyStatus: 'degraded' }), CAUSE)).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('hides it on a Suspended node, even when an issue matched it', () => {
|
|
111
|
+
expect(nodeCause(node({ health: 'Suspended', healthSource: 'controller', topologyStatus: 'degraded' }), CAUSE)).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('never shows it on a healthy node', () => {
|
|
115
|
+
expect(nodeCause(node({ health: 'Healthy', healthSource: 'controller', topologyStatus: 'healthy' }), CAUSE)).toBeUndefined()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -29,6 +29,7 @@ import { servicePortsTooltip, type ServicePortEntry } from '../../topology/K8sRe
|
|
|
29
29
|
import { getTopologyIcon } from '../../../utils/resource-icons'
|
|
30
30
|
import { Tooltip } from '../../ui/Tooltip'
|
|
31
31
|
import { hasGitOpsTreeFilters, matchesGitOpsTreeFilters, type GitOpsTreeFilters } from './tree-helpers'
|
|
32
|
+
import { rankTree, renderedEdge, shortcutEdges, upstreamClosure, upstreamEdges } from './layout'
|
|
32
33
|
|
|
33
34
|
export type GitOpsTreePreset = 'full' | 'compact' | 'workloads' | 'app'
|
|
34
35
|
|
|
@@ -231,7 +232,7 @@ function GitOpsTreeGraphInner({
|
|
|
231
232
|
function EdgeLegend({ edges }: { edges: Edge[] }) {
|
|
232
233
|
const types = new Set(edges.map((e) => e.data?.type as string).filter(Boolean))
|
|
233
234
|
const rows: Array<{ key: string; color: string; label: string }> = []
|
|
234
|
-
if (types.has('source')) rows.push({ key: 'source', color: getEdgeColor('source'), label: 'Source
|
|
235
|
+
if (types.has('source')) rows.push({ key: 'source', color: getEdgeColor('source'), label: 'Source artifact' })
|
|
235
236
|
if (types.has('dependsOn')) rows.push({ key: 'dependsOn', color: getEdgeColor('dependsOn'), label: 'Depends on' })
|
|
236
237
|
// Always show the default ownership color when there are 2+ edge kinds —
|
|
237
238
|
// operators need to compare against it. Hide when "owns" is the only kind.
|
|
@@ -333,16 +334,10 @@ function buildFlowGraph(tree: GitOpsResourceTree | null, preset: GitOpsTreePrese
|
|
|
333
334
|
if (!tree) return { nodes: [], edges: [] }
|
|
334
335
|
const visibleTree = applyGraphFilters(applyPreset(tree, preset, expandedGroups), filters)
|
|
335
336
|
const byID = new Map(visibleTree.nodes.map(node => [node.id, node]))
|
|
336
|
-
const
|
|
337
|
-
const incoming = new Map<string, number>()
|
|
338
|
-
for (const edge of visibleTree.edges) {
|
|
339
|
-
if (!byID.has(edge.source) || !byID.has(edge.target)) continue
|
|
340
|
-
children.set(edge.source, [...(children.get(edge.source) ?? []), edge.target])
|
|
341
|
-
incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const ranks = assignRanks(visibleTree.root.id, byID, children, incoming)
|
|
337
|
+
const ranks = groupRanks(rankTree(visibleTree), byID)
|
|
345
338
|
const positioned = positionRanks(ranks, byID)
|
|
339
|
+
const upstreamTargets = new Set(upstreamEdges(visibleTree).map(edge => edge.target))
|
|
340
|
+
const shortcuts = shortcutEdges(visibleTree)
|
|
346
341
|
const normalizedQuery = query.trim().toLowerCase()
|
|
347
342
|
const nodes = Array.from(positioned.entries()).map(([id, position]) => {
|
|
348
343
|
const node = byID.get(id)!
|
|
@@ -359,11 +354,10 @@ function buildFlowGraph(tree: GitOpsResourceTree | null, preset: GitOpsTreePrese
|
|
|
359
354
|
})
|
|
360
355
|
|
|
361
356
|
const edges = visibleTree.edges
|
|
362
|
-
.filter(edge => positioned.has(edge.source) && positioned.has(edge.target))
|
|
357
|
+
.filter(edge => positioned.has(edge.source) && positioned.has(edge.target) && !shortcuts.has(edge))
|
|
363
358
|
.map(edge => ({
|
|
364
359
|
id: `${edge.source}->${edge.target}`,
|
|
365
|
-
|
|
366
|
-
target: edge.target,
|
|
360
|
+
...renderedEdge(edge, upstreamTargets),
|
|
367
361
|
type: 'smoothstep',
|
|
368
362
|
// The legend reads back edge.data?.type to decide which rows to render —
|
|
369
363
|
// ReactFlow doesn't preserve our custom `type` field at the top level
|
|
@@ -394,7 +388,9 @@ function applyPreset(tree: GitOpsResourceTree, preset: GitOpsTreePreset, expande
|
|
|
394
388
|
children.set(edge.source, [...(children.get(edge.source) ?? []), edge.target])
|
|
395
389
|
}
|
|
396
390
|
|
|
397
|
-
|
|
391
|
+
// Dependencies answer "where does this come from" and are few, so every
|
|
392
|
+
// preset keeps them.
|
|
393
|
+
const keep = new Set<string>([tree.root.id, ...upstreamClosure(tree)])
|
|
398
394
|
if (preset === 'app') {
|
|
399
395
|
for (const edge of tree.edges) {
|
|
400
396
|
if (edge.source === tree.root.id) keep.add(edge.target)
|
|
@@ -438,7 +434,7 @@ function applyGraphFilters(tree: GitOpsResourceTree, filters?: GitOpsTreeFilters
|
|
|
438
434
|
if (node.id === tree.root.id) continue
|
|
439
435
|
if (matchesGitOpsTreeFilters(node, filters)) {
|
|
440
436
|
let current: string | undefined = node.id
|
|
441
|
-
while (current) {
|
|
437
|
+
while (current && !keep.has(current)) {
|
|
442
438
|
keep.add(current)
|
|
443
439
|
current = parent.get(current)
|
|
444
440
|
}
|
|
@@ -506,43 +502,10 @@ function compactInfra(tree: GitOpsResourceTree, expandedGroups?: Set<string>): G
|
|
|
506
502
|
}
|
|
507
503
|
}
|
|
508
504
|
|
|
509
|
-
function
|
|
510
|
-
rootID: string,
|
|
511
|
-
nodes: Map<string, GitOpsTreeNode>,
|
|
512
|
-
children: Map<string, string[]>,
|
|
513
|
-
incoming: Map<string, number>
|
|
514
|
-
): Map<number, string[]> {
|
|
515
|
-
const rankByID = new Map<string, number>()
|
|
516
|
-
const queue = [{ id: rootID, rank: 0 }]
|
|
517
|
-
// Cycle/explosion guard: GitOps trees are owner-ref DAGs in practice, but
|
|
518
|
-
// the EdgeManages set we feed in could pathologically include a cycle
|
|
519
|
-
// (e.g. a CRD whose owner ref points back at one of its own children, or
|
|
520
|
-
// a malformed inventory). Without this cap, the "previous >= rank" skip
|
|
521
|
-
// never fires for cyclic edges and the loop runs forever, freezing the
|
|
522
|
-
// UI thread. Cap at a large multiple of node count so legitimate trees
|
|
523
|
-
// never hit it; cycles bail with the partial layout we have so far.
|
|
524
|
-
let iterations = 0
|
|
525
|
-
const maxIterations = nodes.size * 8 + 64
|
|
526
|
-
|
|
527
|
-
while (queue.length > 0) {
|
|
528
|
-
if (++iterations > maxIterations) break
|
|
529
|
-
const current = queue.shift()!
|
|
530
|
-
const previous = rankByID.get(current.id)
|
|
531
|
-
if (previous !== undefined && previous >= current.rank) continue
|
|
532
|
-
rankByID.set(current.id, current.rank)
|
|
533
|
-
for (const child of children.get(current.id) ?? []) {
|
|
534
|
-
queue.push({ id: child, rank: current.rank + 1 })
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
for (const id of nodes.keys()) {
|
|
539
|
-
if (!rankByID.has(id)) {
|
|
540
|
-
rankByID.set(id, incoming.get(id) ? 1 : 0)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
505
|
+
function groupRanks(rankByID: Map<string, number>, nodes: Map<string, GitOpsTreeNode>): Map<number, string[]> {
|
|
544
506
|
const ranks = new Map<number, string[]>()
|
|
545
507
|
for (const [id, rank] of rankByID.entries()) {
|
|
508
|
+
if (!nodes.has(id)) continue
|
|
546
509
|
ranks.set(rank, [...(ranks.get(rank) ?? []), id])
|
|
547
510
|
}
|
|
548
511
|
for (const ids of ranks.values()) {
|
|
@@ -565,12 +528,23 @@ function positionRanks(ranks: Map<number, string[]>, nodes: Map<string, GitOpsTr
|
|
|
565
528
|
const rootRank = ranks.get(0) ?? []
|
|
566
529
|
if (rootRank.length === 1) {
|
|
567
530
|
const rootID = rootRank[0]
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
531
|
+
const rootHeight = getNodeDimensions(nodes.get(rootID)!).height
|
|
532
|
+
const tallest = Math.max((ranks.get(1)?.length ?? 0), (ranks.get(-1)?.length ?? 0))
|
|
533
|
+
if (tallest > 0) {
|
|
534
|
+
const maxY = (tallest - 1) * ROW_GAP
|
|
572
535
|
positioned.set(rootID, { x: 0, y: Math.max(0, (maxY - rootHeight) / 2 - ROOT_Y_OFFSET) })
|
|
573
536
|
}
|
|
537
|
+
// Dependency columns are short; center them on the root so the chain
|
|
538
|
+
// reads as one line into it.
|
|
539
|
+
const rootCenter = positioned.get(rootID)!.y + rootHeight / 2
|
|
540
|
+
for (const rank of sortedRanks) {
|
|
541
|
+
if (rank >= 0) continue
|
|
542
|
+
const ids = ranks.get(rank) ?? []
|
|
543
|
+
const columnCenter = ((ids.length - 1) * ROW_GAP + rootHeight) / 2
|
|
544
|
+
ids.forEach((id, row) => {
|
|
545
|
+
positioned.set(id, { x: rank * RANK_GAP, y: row * ROW_GAP + rootCenter - columnCenter })
|
|
546
|
+
})
|
|
547
|
+
}
|
|
574
548
|
}
|
|
575
549
|
|
|
576
550
|
return positioned
|
|
@@ -587,7 +561,7 @@ const GitOpsResourceNode = memo(function GitOpsResourceNode({ data }: NodeProps<
|
|
|
587
561
|
const KindIcon = getTopologyIcon(kind)
|
|
588
562
|
// Only unhealthy nodes carry a cause worth surfacing — a healthy node
|
|
589
563
|
// matched by a stale/unrelated issue ref shouldn't show a tooltip.
|
|
590
|
-
const cause =
|
|
564
|
+
const cause = nodeCause(node, data.cause)
|
|
591
565
|
// Only declared resources can carry the controller's verdict, so only
|
|
592
566
|
// there does "this one is Radar's" mean anything. Generated children
|
|
593
567
|
// (ReplicaSets, Pods) are always Radar's read; marking each would be
|
|
@@ -771,6 +745,15 @@ function buildChips(node: GitOpsTreeNode): Array<{ label?: string; value: string
|
|
|
771
745
|
return chips
|
|
772
746
|
}
|
|
773
747
|
|
|
748
|
+
// nodeCause returns the matched issue's cause when the node's hover should
|
|
749
|
+
// show it: only on a node that is actually failing (Degraded or Missing,
|
|
750
|
+
// topology 'unhealthy'). 'degraded' is also where Progressing and Suspended
|
|
751
|
+
// land, and a cause there reads as "this is broken" on a rollout in flight
|
|
752
|
+
// or a deliberate pause.
|
|
753
|
+
export function nodeCause(node: GitOpsTreeNode, cause?: string): string | undefined {
|
|
754
|
+
return normalizeHealth(node.topologyStatus) === 'unhealthy' ? cause : undefined
|
|
755
|
+
}
|
|
756
|
+
|
|
774
757
|
// The subtitle is the node's one line of prose, and sync, health and the
|
|
775
758
|
// backend's info line all want it. It can't be won on precedence: the backend
|
|
776
759
|
// derives a health for every node it can build an info line for, so a rule that
|