@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.
Files changed (250) hide show
  1. package/README.md +41 -0
  2. package/package.json +6 -6
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
  67. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  68. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  69. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
  70. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
  71. package/src/components/gitops/tree/layout.test.ts +111 -0
  72. package/src/components/gitops/tree/layout.ts +128 -0
  73. package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
  74. package/src/components/gitops/tree/tree-helpers.ts +12 -2
  75. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  76. package/src/components/issues/IssuesView.tsx +44 -6
  77. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  78. package/src/components/issues/ResourceIssuesSection.tsx +8 -2
  79. package/src/components/issues/diagnostic.ts +2 -0
  80. package/src/components/issues/index.ts +3 -1
  81. package/src/components/issues/issue-timing.ts +12 -1
  82. package/src/components/issues/issues.test.ts +75 -1
  83. package/src/components/issues/severity.ts +30 -2
  84. package/src/components/issues/types.ts +55 -4
  85. package/src/components/logs/LogCore.theme.test.tsx +206 -3
  86. package/src/components/logs/LogCore.tsx +180 -53
  87. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  88. package/src/components/logs/LogsViewer.test.tsx +369 -0
  89. package/src/components/logs/LogsViewer.tsx +91 -13
  90. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  91. package/src/components/logs/StructuredLogLine.tsx +23 -31
  92. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  93. package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
  94. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  95. package/src/components/logs/useLogBuffer.test.ts +8 -0
  96. package/src/components/logs/useLogBuffer.ts +48 -18
  97. package/src/components/logs/useLogSearch.ts +3 -1
  98. package/src/components/logs/useLogStream.ts +17 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  105. package/src/components/resources/ColumnPickerList.tsx +174 -0
  106. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  107. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  108. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  109. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  110. package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
  111. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  112. package/src/components/resources/ResourcesView.tsx +392 -181
  113. package/src/components/resources/column-order.test.ts +104 -0
  114. package/src/components/resources/column-order.ts +64 -0
  115. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  116. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  117. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  118. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  119. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  120. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  121. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  122. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  123. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  124. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  127. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  128. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  129. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  130. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  131. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  132. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  133. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  134. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  135. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  136. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  137. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  138. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  139. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  140. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  141. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  142. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  143. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  144. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  145. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  146. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  147. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  148. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  149. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  150. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  151. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  152. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  153. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  154. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  155. package/src/components/resources/renderers/PodRenderer.tsx +81 -40
  156. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  157. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  158. package/src/components/resources/resource-utils-capi.ts +1 -1
  159. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  160. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  161. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  162. package/src/components/resources/resource-utils-contour.ts +16 -0
  163. package/src/components/resources/resource-utils-istio.ts +12 -8
  164. package/src/components/resources/resource-utils-knative.ts +18 -0
  165. package/src/components/resources/resource-utils.ts +96 -7
  166. package/src/components/resources/rules-column-width.test.ts +1 -1
  167. package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
  168. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  169. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  170. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  171. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  172. package/src/components/shared/DetailShell.test.tsx +80 -0
  173. package/src/components/shared/DetailShell.tsx +85 -10
  174. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  175. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  176. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  177. package/src/components/shared/EditableYamlView.tsx +1 -1
  178. package/src/components/shared/ResourceActionsBar.tsx +14 -7
  179. package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
  180. package/src/components/shared/index.ts +2 -0
  181. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  182. package/src/components/timeline/DiffViewer.tsx +113 -4
  183. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  184. package/src/components/timeline/shared.test.tsx +48 -0
  185. package/src/components/timeline/shared.tsx +14 -19
  186. package/src/components/timeline/timeline-filters.ts +1 -0
  187. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  188. package/src/components/topology/K8sResourceNode.tsx +44 -2
  189. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  190. package/src/components/topology/TopologyGraph.tsx +3 -71
  191. package/src/components/topology/TopologySearch.tsx +1 -0
  192. package/src/components/topology/internet-groups.test.ts +35 -0
  193. package/src/components/topology/internet-groups.ts +55 -0
  194. package/src/components/topology/layout.ts +1 -1
  195. package/src/components/ui/Badge.tsx +1 -0
  196. package/src/components/ui/FoldSection.tsx +68 -0
  197. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  198. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  199. package/src/components/ui/RefLink.tsx +20 -0
  200. package/src/components/ui/RowActionMenu.tsx +8 -3
  201. package/src/components/ui/Toast.tsx +15 -4
  202. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  203. package/src/components/ui/drawer-components.test.tsx +110 -1
  204. package/src/components/ui/drawer-components.tsx +62 -9
  205. package/src/components/ui/index.ts +4 -2
  206. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  207. package/src/components/ui/monacoRuntime.ts +14 -19
  208. package/src/components/ui/status-tone.test.ts +10 -1
  209. package/src/components/ui/status-tone.tsx +29 -0
  210. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  211. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  212. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  213. package/src/components/workload/WorkloadView.tsx +193 -71
  214. package/src/index.ts +14 -1
  215. package/src/monaco-deep.d.ts +19 -12
  216. package/src/theme/components.css +13 -0
  217. package/src/types/capacity.ts +11 -2
  218. package/src/types/core.ts +53 -2
  219. package/src/types/gitops-flux-status.test.ts +32 -1
  220. package/src/types/gitops-insights.ts +20 -2
  221. package/src/types/gitops-tree.ts +12 -0
  222. package/src/types/gitops.ts +26 -6
  223. package/src/utils/api-resources.ts +7 -0
  224. package/src/utils/application-history.ts +1 -0
  225. package/src/utils/cnpg-backup.test.ts +26 -0
  226. package/src/utils/cnpg-backup.ts +70 -0
  227. package/src/utils/format.test.ts +9 -0
  228. package/src/utils/format.ts +4 -0
  229. package/src/utils/gitops-owner.ts +3 -2
  230. package/src/utils/grant.test.ts +19 -0
  231. package/src/utils/grant.ts +35 -0
  232. package/src/utils/index.ts +4 -0
  233. package/src/utils/k8s-errors.ts +7 -7
  234. package/src/utils/log-format.test.ts +41 -0
  235. package/src/utils/log-format.ts +71 -15
  236. package/src/utils/log-level.ts +12 -8
  237. package/src/utils/navigation.test.ts +57 -0
  238. package/src/utils/navigation.ts +86 -8
  239. package/src/utils/object-reference-navigation.test.ts +43 -0
  240. package/src/utils/resource-hierarchy.test.ts +37 -0
  241. package/src/utils/resource-hierarchy.ts +15 -7
  242. package/src/utils/resource-icons.ts +2 -0
  243. package/src/utils/skeleton-yaml.test.ts +21 -0
  244. package/src/utils/skeleton-yaml.ts +16 -0
  245. package/src/utils/url-path.test.ts +35 -0
  246. package/src/utils/url-path.ts +21 -0
  247. package/src/utils/validators.test.ts +4 -13
  248. package/src/utils/validators.ts +5 -19
  249. package/src/utils/view-transition.test.ts +35 -1
  250. 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
- export function ArgoResourceDiff({ diff, loading, error }: ArgoResourceDiffProps) {
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="rounded-md border border-red-500/40 bg-red-500/5 px-3 py-2 text-xs text-red-700 dark:text-red-400">
48
- {error}
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 null
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
- normalizeHealthStatus,
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(changes: GitOpsChange[]): { text: string; tone: string } | null {
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.health === 'Degraded' || c.category === 'Degraded') degraded++
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
- renderResourceDiff?: (ref: GitOpsInsightRef) => ReactNode
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 = !change.syncError && !change.message
1093
- ? explainChangeStatus(change.sync, change.health, insight.summary)
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: true,
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 = radarHealthNote({ health: change.health, healthSource: change.healthSource, healthMessage: change.message })
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
- {driftEntries.length > 0 && (
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={normalizeHealthStatus(change.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
- {driftEntries.length > 0 && <DriftPanel drift={change.drift!} />}
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((driftEntries.length > 0 || events.length > 0) && 'mt-3')}>
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'