@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,142 @@
|
|
|
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('does not offer a live drawer for confirmed absence with unknown health', () => {
|
|
90
|
+
const data = insight(false)
|
|
91
|
+
data.changes = [{ ...change, missing: true, hasLive: false, health: 'Unknown', drift: undefined }]
|
|
92
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={data} onOpenResource={() => {}} />)
|
|
93
|
+
expect(html).not.toContain('Open Deployment')
|
|
94
|
+
expect(html).toContain('No live resource')
|
|
95
|
+
expect(html).toContain('Not present in the cluster')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('keeps unknown-health live objects navigable', () => {
|
|
99
|
+
const data = insight(false)
|
|
100
|
+
data.changes = [{ ...change, health: 'Unknown', drift: undefined }]
|
|
101
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={data} onOpenResource={() => {}} />)
|
|
102
|
+
expect(html).toContain('Open Deployment')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('does not report confirmed absence as healthy or tell a Synced resource to sync', () => {
|
|
106
|
+
const data = insight(false)
|
|
107
|
+
data.changes = [{ ...change, missing: true, hasLive: false, health: 'Healthy', sync: 'Synced', category: 'Synced', message: 'Controller diagnostic detail', drift: undefined }]
|
|
108
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={data} onOpenResource={() => {}} />)
|
|
109
|
+
expect(html).not.toContain('1/1 resources healthy')
|
|
110
|
+
expect(html).toContain('Missing')
|
|
111
|
+
expect(html).toContain('Refresh the application')
|
|
112
|
+
expect(html).toContain('Controller diagnostic detail')
|
|
113
|
+
expect(html).not.toContain('Sync the application to create it')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("hands the last-applied list to the Argo diff instead of rendering it alongside", () => {
|
|
117
|
+
let passed: GitOpsDrift | undefined
|
|
118
|
+
const html = renderToStaticMarkup(
|
|
119
|
+
<GitOpsChangesView
|
|
120
|
+
insight={insight(true)}
|
|
121
|
+
focusKey="Deployment/web/web"
|
|
122
|
+
renderResourceDiff={(_ref, fallbackDrift) => {
|
|
123
|
+
passed = fallbackDrift
|
|
124
|
+
return <div data-testid="argo-diff" />
|
|
125
|
+
}}
|
|
126
|
+
/>,
|
|
127
|
+
)
|
|
128
|
+
expect(html).toContain('argo-diff')
|
|
129
|
+
expect(html).not.toContain('Field diff')
|
|
130
|
+
expect(passed).toEqual(lastApplied)
|
|
131
|
+
// The row's "N diff" badge counts the last-applied list, which can
|
|
132
|
+
// undercount here, so it isn't shown.
|
|
133
|
+
expect(html).not.toContain('1 diff')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('without the Argo diff, the row still shows the last-applied list', () => {
|
|
137
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={insight(false)} focusKey="Deployment/web/web" />)
|
|
138
|
+
expect(html).toContain('Field diff')
|
|
139
|
+
expect(html).toContain('spec.replicas')
|
|
140
|
+
expect(html).toContain('1 diff')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -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'
|
|
@@ -23,13 +23,14 @@ import {
|
|
|
23
23
|
healthStatusRank,
|
|
24
24
|
isArgoResourceSyncEligible,
|
|
25
25
|
messageToPhase,
|
|
26
|
-
|
|
26
|
+
missingResourceMessage,
|
|
27
27
|
normalizeSyncStatus,
|
|
28
28
|
resourceStatusCounts,
|
|
29
29
|
syncStatusRank,
|
|
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
|
|
@@ -233,7 +237,8 @@ function buildHealthSummary(changes: GitOpsChange[]): { text: string; tone: stri
|
|
|
233
237
|
// Count HEALTH, not sync — a Synced resource can still be Degraded, so check
|
|
234
238
|
// degraded/missing FIRST and never let a Synced sync-state mask it. A Synced
|
|
235
239
|
// resource with no assessed health (ConfigMaps etc.) still reads as fine.
|
|
236
|
-
if (c.
|
|
240
|
+
if (c.missing) missing++
|
|
241
|
+
else if (c.health === 'Degraded' || c.category === 'Degraded') degraded++
|
|
237
242
|
else if (c.health === 'Missing' || c.category === 'Missing') missing++
|
|
238
243
|
else if (c.health === 'Healthy' || c.category === 'Synced') healthy++
|
|
239
244
|
else if (c.category === 'OutOfSync') outOfSync++
|
|
@@ -735,7 +740,10 @@ interface GitOpsChangesViewProps {
|
|
|
735
740
|
// returns the loaded <ArgoResourceDiff/>. When present AND
|
|
736
741
|
// insight.capabilities.argoDiffAvailable, resolvable rows expose a "Full
|
|
737
742
|
// diff" toggle. Library consumers that don't wire it get no such affordance.
|
|
738
|
-
|
|
743
|
+
// fallbackDrift is the row's last-applied field list. The loaded diff shows
|
|
744
|
+
// Argo's own field list instead, and falls back to this one when the diff
|
|
745
|
+
// can't be loaded.
|
|
746
|
+
renderResourceDiff?: (ref: GitOpsInsightRef, fallbackDrift?: GitOpsDrift) => ReactNode
|
|
739
747
|
// Opens the global Settings dialog. Backs the "Connect Argo CD" hint shown
|
|
740
748
|
// when the root is an Argo Application without the diff integration.
|
|
741
749
|
onOpenSettings?: () => void
|
|
@@ -1005,7 +1013,7 @@ export function GitOpsChangesView({ insight, error, onOpenResource, onSyncResour
|
|
|
1005
1013
|
)}
|
|
1006
1014
|
</div>
|
|
1007
1015
|
)}
|
|
1008
|
-
{totalCount > 0 && isArgoRoot && !insight.capabilities?.argoDiffAvailable && onOpenSettings && (
|
|
1016
|
+
{totalCount > 0 && isArgoRoot && !insight.capabilities?.argoDiffAvailable && !insight.capabilities?.argoConnecting && onOpenSettings && (
|
|
1009
1017
|
<div className="border-b border-theme-border bg-theme-base/40 px-4 py-2 text-[11px] text-theme-text-tertiary">
|
|
1010
1018
|
{insight.capabilities?.argoConfigured ? (
|
|
1011
1019
|
<>
|
|
@@ -1089,9 +1097,11 @@ export function GitOpsChangesView({ insight, error, onOpenResource, onSyncResour
|
|
|
1089
1097
|
const waveSet = !!planItem?.waveSet
|
|
1090
1098
|
const rowKey = refKey(change.ref)
|
|
1091
1099
|
const focused = focusKey === rowKey
|
|
1092
|
-
const explanation =
|
|
1093
|
-
?
|
|
1094
|
-
:
|
|
1100
|
+
const explanation = change.missing
|
|
1101
|
+
? missingResourceMessage(change.sync)
|
|
1102
|
+
: !change.syncError && !change.message
|
|
1103
|
+
? explainChangeStatus(change.sync, change.health, insight.summary)
|
|
1104
|
+
: ''
|
|
1095
1105
|
const hasInlineDetail = !!(
|
|
1096
1106
|
(change.drift && change.drift.entries.length > 0) ||
|
|
1097
1107
|
(change.recentEvents && change.recentEvents.length > 0)
|
|
@@ -1238,7 +1248,8 @@ function buildTreeExtras(nodes: GitOpsTreeNode[], declared: GitOpsChange[]): Git
|
|
|
1238
1248
|
// had a controller verdict to be distinguished from.
|
|
1239
1249
|
message: n.healthMessage,
|
|
1240
1250
|
hasDesired: false,
|
|
1241
|
-
hasLive:
|
|
1251
|
+
hasLive: !n.missing,
|
|
1252
|
+
missing: n.missing,
|
|
1242
1253
|
partial: true,
|
|
1243
1254
|
partialNote: 'Generated resource — not directly tracked by the GitOps controller (no drift / event diagnostics).',
|
|
1244
1255
|
})
|
|
@@ -1330,7 +1341,7 @@ function ChangeRow({
|
|
|
1330
1341
|
// When true, the expanded panel offers a "Full diff" toggle that mounts the
|
|
1331
1342
|
// host-wired Argo CD diff loader for this resource.
|
|
1332
1343
|
canFullDiff: boolean
|
|
1333
|
-
renderResourceDiff?: (ref: GitOpsInsightRef) => ReactNode
|
|
1344
|
+
renderResourceDiff?: (ref: GitOpsInsightRef, fallbackDrift?: GitOpsDrift) => ReactNode
|
|
1334
1345
|
onOpenResource?: (ref: GitOpsChange['ref']) => void
|
|
1335
1346
|
onSyncResource?: (ref: GitOpsChange['ref']) => void
|
|
1336
1347
|
syncResourceDisabledReason?: string
|
|
@@ -1358,8 +1369,10 @@ function ChangeRow({
|
|
|
1358
1369
|
// differently: hide the Open pill, suppress the drawer-open click path,
|
|
1359
1370
|
// and offer "View in Git →" instead so the operator can read the
|
|
1360
1371
|
// declared source instead of a non-existent live object.
|
|
1361
|
-
const isAbsent = change.health === 'Missing' && !change.hasLive
|
|
1362
|
-
const radarNote =
|
|
1372
|
+
const isAbsent = change.missing || (change.health === 'Missing' && !change.hasLive)
|
|
1373
|
+
const radarNote = change.missing
|
|
1374
|
+
? 'Radar confirmed that this resource is not present in the cluster.'
|
|
1375
|
+
: radarHealthNote({ health: change.health, healthSource: change.healthSource, healthMessage: change.message })
|
|
1363
1376
|
const handleRowClick = () => {
|
|
1364
1377
|
if (expandable) {
|
|
1365
1378
|
setExpanded((v) => !v)
|
|
@@ -1422,8 +1435,11 @@ function ChangeRow({
|
|
|
1422
1435
|
)}
|
|
1423
1436
|
{/* Detail badges: surface that there's something to see in
|
|
1424
1437
|
the expanded panel. Without these the user has no signal
|
|
1425
|
-
that clicking will reveal anything useful.
|
|
1426
|
-
|
|
1438
|
+
that clicking will reveal anything useful. The field count
|
|
1439
|
+
comes from the last-applied list, which can undercount on an
|
|
1440
|
+
Argo row (Argo's own list loads only on expand), so it is
|
|
1441
|
+
left off there. The OutOfSync badge already marks the row. */}
|
|
1442
|
+
{!canFullDiff && driftEntries.length > 0 && (
|
|
1427
1443
|
<Tooltip content={`${driftEntries.length} field${driftEntries.length === 1 ? '' : 's'} differ from Git`} delay={200} wrapperClassName="shrink-0">
|
|
1428
1444
|
<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
1445
|
{driftEntries.length} diff
|
|
@@ -1453,7 +1469,7 @@ function ChangeRow({
|
|
|
1453
1469
|
</Tooltip>
|
|
1454
1470
|
)}
|
|
1455
1471
|
{change.message && !change.syncError && <div className="ml-[18px] mt-1 line-clamp-2 text-xs text-theme-text-secondary">{change.message}</div>}
|
|
1456
|
-
{!change.syncError && !change.message && explanation && (
|
|
1472
|
+
{!change.syncError && (change.missing || !change.message) && explanation && (
|
|
1457
1473
|
<div className="ml-[18px] mt-1 text-xs text-theme-text-tertiary">{explanation}</div>
|
|
1458
1474
|
)}
|
|
1459
1475
|
</button>
|
|
@@ -1467,7 +1483,7 @@ function ChangeRow({
|
|
|
1467
1483
|
)}
|
|
1468
1484
|
</div>
|
|
1469
1485
|
<div className="flex flex-col items-start gap-1 self-start">
|
|
1470
|
-
<HealthStatusBadge health={
|
|
1486
|
+
<HealthStatusBadge health={changeHealth(change)} />
|
|
1471
1487
|
{radarNote && (
|
|
1472
1488
|
<Tooltip content={radarNote} delay={200} wrapperClassName="inline-flex">
|
|
1473
1489
|
<span className="cursor-help select-none rounded border border-theme-border bg-theme-elevated/70 px-1.5 py-0.5 text-[10px] leading-3 text-theme-text-tertiary">
|
|
@@ -1523,15 +1539,19 @@ function ChangeRow({
|
|
|
1523
1539
|
{expandable && (
|
|
1524
1540
|
<Collapse open={expanded} mountLazily>
|
|
1525
1541
|
<div className="border-t border-theme-border bg-theme-base/40 px-4 py-3">
|
|
1526
|
-
{
|
|
1542
|
+
{/* The last-applied list only knows what was applied at the last
|
|
1543
|
+
sync, so it misses a Git change that hasn't synced yet. When
|
|
1544
|
+
Argo's diff can load, it owns the field list and shows this
|
|
1545
|
+
one only if that load fails. */}
|
|
1546
|
+
{!canFullDiff && driftEntries.length > 0 && <DriftPanel drift={change.drift!} />}
|
|
1527
1547
|
{events.length > 0 && <RecentEventsPanel events={events} />}
|
|
1528
1548
|
{/* Expanding a row IS the request to see its diff — render it inline
|
|
1529
1549
|
rather than behind a second "Full diff" click. The capability is
|
|
1530
1550
|
already gated on a live Argo connection, so this only mounts (and
|
|
1531
1551
|
fetches) when the diff can actually be served. */}
|
|
1532
1552
|
{canFullDiff && (
|
|
1533
|
-
<div className={clsx(
|
|
1534
|
-
{renderResourceDiff?.(change.ref)}
|
|
1553
|
+
<div className={clsx(events.length > 0 && 'mt-3')}>
|
|
1554
|
+
{renderResourceDiff?.(change.ref, change.drift)}
|
|
1535
1555
|
</div>
|
|
1536
1556
|
)}
|
|
1537
1557
|
</div>
|
|
@@ -1541,67 +1561,6 @@ function ChangeRow({
|
|
|
1541
1561
|
)
|
|
1542
1562
|
}
|
|
1543
1563
|
|
|
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
1564
|
// RecentEventsPanel surfaces the last few events involving this resource.
|
|
1606
1565
|
// Warning events get a red bar so the eye lands on them; normals are
|
|
1607
1566
|
// muted. Aggregation count (when present) is a critical signal — "this
|
|
@@ -3,6 +3,7 @@ import { SEVERITY_DOT } from '../../../utils/badge-colors'
|
|
|
3
3
|
import type { GitOpsChange } from '../../../types'
|
|
4
4
|
import {
|
|
5
5
|
changeMatchesFacets,
|
|
6
|
+
changeHealth,
|
|
6
7
|
changeMatchesSearch,
|
|
7
8
|
compactSource,
|
|
8
9
|
entryTone,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
healthStatusRank,
|
|
11
12
|
isArgoResourceSyncEligible,
|
|
12
13
|
messageToPhase,
|
|
14
|
+
missingResourceMessage,
|
|
13
15
|
phaseToTone,
|
|
14
16
|
resourceStatusCounts,
|
|
15
17
|
syncStatusRank,
|
|
@@ -19,6 +21,22 @@ function change(partial: Partial<GitOpsChange> & { ref: GitOpsChange['ref'] }):
|
|
|
19
21
|
return { category: 'Unknown', hasDesired: true, hasLive: true, partial: false, ...partial }
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
describe('confirmed missing resources', () => {
|
|
25
|
+
it('counts, filters and enables sync independently of stale health and sync', () => {
|
|
26
|
+
const item = change({ ref: { kind: 'Secret', name: 'values' }, missing: true, hasLive: false, health: 'Healthy', sync: 'Synced' })
|
|
27
|
+
expect(changeHealth(item)).toBe('Missing')
|
|
28
|
+
expect(resourceStatusCounts([item]).missing).toBe(1)
|
|
29
|
+
expect(changeMatchesFacets(item, new Set(['missing']))).toBe(true)
|
|
30
|
+
expect(isArgoResourceSyncEligible(item, true)).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('asks for a refresh when Argo still reports Synced', () => {
|
|
34
|
+
expect(missingResourceMessage('Synced')).toContain('Refresh')
|
|
35
|
+
expect(missingResourceMessage('Synced')).not.toContain('Sync the application')
|
|
36
|
+
expect(missingResourceMessage('OutOfSync')).toContain('Sync the application')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
22
40
|
describe('gitopsToSeverity', () => {
|
|
23
41
|
it.each([
|
|
24
42
|
['critical', 'error'],
|
|
@@ -43,9 +43,16 @@ export function changeSync(change: GitOpsChange): SyncStatus {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function changeHealth(change: GitOpsChange): GitOpsHealthStatus {
|
|
46
|
+
if (change.missing) return 'Missing'
|
|
46
47
|
return normalizeHealthStatus(change.health)
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export function missingResourceMessage(sync?: string): string {
|
|
51
|
+
return sync === 'Synced'
|
|
52
|
+
? 'Argo reports this resource as synced, but it is not present in the cluster. Refresh the application to check its status.'
|
|
53
|
+
: 'Not present in the cluster. Sync the application to create it.'
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export function isArgoResourceSyncEligible(change: GitOpsChange, declared: boolean, hook?: string): boolean {
|
|
50
57
|
if (!declared || hook || change.hookPhase) return false
|
|
51
58
|
return changeSync(change) === 'OutOfSync' || changeHealth(change) === 'Missing' || change.category === 'Missing'
|