@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,104 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { applyColumnOrder, moveColumn, pinnedColumnCount, sanitizeColumnOrder } from './column-order'
3
+
4
+ const cols = (...keys: string[]) => keys.map(key => ({ key }))
5
+ const keysOf = (list: { key: string }[]) => list.map(c => c.key)
6
+
7
+ const PODS = cols('name', 'namespace', 'ready', 'status', 'restarts', 'age', 'node')
8
+
9
+ describe('applyColumnOrder', () => {
10
+ it('keeps definition order when no saved order is provided', () => {
11
+ expect(keysOf(applyColumnOrder(PODS, undefined))).toEqual(keysOf(PODS))
12
+ expect(keysOf(applyColumnOrder(PODS, null))).toEqual(keysOf(PODS))
13
+ expect(keysOf(applyColumnOrder(PODS, []))).toEqual(keysOf(PODS))
14
+ })
15
+
16
+ it('applies the saved order and keeps Name first', () => {
17
+ const order = ['age', 'restarts', 'namespace', 'ready', 'status', 'node']
18
+ expect(keysOf(applyColumnOrder(PODS, order)))
19
+ .toEqual(['name', 'age', 'restarts', 'namespace', 'ready', 'status', 'node'])
20
+ })
21
+
22
+ it('ignores a saved Name position', () => {
23
+ const order = ['age', 'name', 'namespace', 'ready', 'status', 'restarts', 'node']
24
+ expect(keysOf(applyColumnOrder(PODS, order))[0]).toBe('name')
25
+ })
26
+
27
+ it('keeps host columns that lead Name in front of it', () => {
28
+ const fleet = cols('cluster', 'name', 'namespace', 'age')
29
+ expect(keysOf(applyColumnOrder(fleet, ['age', 'cluster', 'namespace'])))
30
+ .toEqual(['cluster', 'name', 'age', 'namespace'])
31
+ })
32
+
33
+ it('puts a column added after the save in its default position', () => {
34
+ // 'ip' arrives between status and restarts; the user had moved age up.
35
+ const next = cols('name', 'namespace', 'ready', 'status', 'ip', 'restarts', 'age', 'node')
36
+ const order = ['age', 'namespace', 'ready', 'status', 'restarts', 'node']
37
+ expect(keysOf(applyColumnOrder(next, order)))
38
+ .toEqual(['name', 'age', 'namespace', 'ready', 'status', 'ip', 'restarts', 'node'])
39
+ })
40
+
41
+ it('appends a new trailing column, such as a label column, at the end', () => {
42
+ const next = [...PODS, { key: 'label:app' }]
43
+ const order = ['node', 'age', 'namespace', 'ready', 'status', 'restarts']
44
+ expect(keysOf(applyColumnOrder(next, order)).at(-1)).toBe('label:app')
45
+ })
46
+
47
+ it('keeps several new adjacent columns in their definition order', () => {
48
+ const next = cols('name', 'a', 'b', 'namespace', 'age')
49
+ expect(keysOf(applyColumnOrder(next, ['age', 'namespace'])))
50
+ .toEqual(['name', 'age', 'a', 'b', 'namespace'])
51
+ })
52
+
53
+ it('keeps a hidden column in place: order is independent of visibility', () => {
54
+ const order = ['age', 'restarts', 'namespace', 'ready', 'status', 'node']
55
+ const ordered = applyColumnOrder(PODS, order)
56
+ const visible = new Set(['name', 'age', 'namespace', 'ready', 'status', 'node'])
57
+ expect(keysOf(ordered.filter(c => visible.has(c.key))))
58
+ .toEqual(['name', 'age', 'namespace', 'ready', 'status', 'node'])
59
+ visible.add('restarts')
60
+ expect(keysOf(ordered.filter(c => visible.has(c.key))))
61
+ .toEqual(['name', 'age', 'restarts', 'namespace', 'ready', 'status', 'node'])
62
+ })
63
+
64
+ it('drops saved keys for columns that no longer exist', () => {
65
+ const order = ['gone', 'age', 'namespace', 'ready', 'status', 'restarts', 'node']
66
+ expect(keysOf(applyColumnOrder(PODS, order)))
67
+ .toEqual(['name', 'age', 'namespace', 'ready', 'status', 'restarts', 'node'])
68
+ })
69
+ })
70
+
71
+ describe('sanitizeColumnOrder', () => {
72
+ it('reads anything that is not a key list as no order', () => {
73
+ expect(sanitizeColumnOrder(undefined)).toBeNull()
74
+ expect(sanitizeColumnOrder('age')).toBeNull()
75
+ expect(sanitizeColumnOrder({ 0: 'age' })).toBeNull()
76
+ expect(sanitizeColumnOrder([1, null])).toBeNull()
77
+ })
78
+
79
+ it('keeps string keys, first occurrence wins', () => {
80
+ expect(sanitizeColumnOrder(['age', 3, 'node', 'age'])).toEqual(['age', 'node'])
81
+ })
82
+ })
83
+
84
+ describe('moveColumn', () => {
85
+ it('returns the movable keys with the column moved', () => {
86
+ expect(moveColumn(PODS, 'age', 0)).toEqual(['age', 'namespace', 'ready', 'status', 'restarts', 'node'])
87
+ expect(moveColumn(PODS, 'namespace', 5)).toEqual(['ready', 'status', 'restarts', 'age', 'node', 'namespace'])
88
+ })
89
+
90
+ it('returns null for a no-op, an out-of-range edge move, or a pinned column', () => {
91
+ expect(moveColumn(PODS, 'namespace', -1)).toBeNull()
92
+ expect(moveColumn(PODS, 'node', 99)).toBeNull()
93
+ expect(moveColumn(PODS, 'name', 2)).toBeNull()
94
+ expect(moveColumn(PODS, 'missing', 0)).toBeNull()
95
+ })
96
+ })
97
+
98
+ describe('pinnedColumnCount', () => {
99
+ it('pins through Name, and nothing when there is no Name column', () => {
100
+ expect(pinnedColumnCount(PODS)).toBe(1)
101
+ expect(pinnedColumnCount(cols('cluster', 'name', 'age'))).toBe(2)
102
+ expect(pinnedColumnCount(cols('age'))).toBe(0)
103
+ })
104
+ })
@@ -0,0 +1,64 @@
1
+ // The user's column order for a resource table, kept apart from visibility: it
2
+ // lists every movable column, hidden ones included, so a column hidden and later
3
+ // re-shown comes back where it was.
4
+
5
+ interface Keyed {
6
+ key: string
7
+ }
8
+
9
+ /** Columns up to and including Name stay where the kind defines them. Name is
10
+ * the flexible column and the row link; host-injected leading columns sit in
11
+ * front of it by contract. */
12
+ export function pinnedColumnCount(columns: readonly Keyed[]): number {
13
+ return columns.findIndex(c => c.key === 'name') + 1
14
+ }
15
+
16
+ /** A saved order read from localStorage: only string keys, first occurrence
17
+ * wins. Anything else reads as "no saved order". */
18
+ export function sanitizeColumnOrder(raw: unknown): string[] | null {
19
+ if (!Array.isArray(raw)) return null
20
+ const seen = new Set<string>()
21
+ for (const k of raw) {
22
+ if (typeof k === 'string' && !seen.has(k)) seen.add(k)
23
+ }
24
+ return seen.size ? Array.from(seen) : null
25
+ }
26
+
27
+ /**
28
+ * Columns in the user's saved order.
29
+ *
30
+ * Saved keys that no longer exist are ignored. A column the saved order does not
31
+ * name (a built-in, printer, or custom column) goes
32
+ * where it sits by default: right before the column that follows it in the
33
+ * definition, or at the end when nothing that follows it is in the saved order.
34
+ */
35
+ export function applyColumnOrder<T extends Keyed>(columns: readonly T[], order: readonly string[] | null | undefined): T[] {
36
+ if (!order?.length) return [...columns]
37
+ const pinned = pinnedColumnCount(columns)
38
+ const movable = columns.slice(pinned)
39
+ const rank = new Map(order.map((k, i) => [k, i]))
40
+ const result = movable
41
+ .filter(c => rank.has(c.key))
42
+ .sort((a, b) => rank.get(a.key)! - rank.get(b.key)!)
43
+ movable.forEach((c, i) => {
44
+ if (rank.has(c.key)) return
45
+ const anchor = movable.slice(i + 1).find(n => rank.has(n.key))
46
+ const at = anchor ? result.indexOf(anchor) : -1
47
+ if (at === -1) result.push(c)
48
+ else result.splice(at, 0, c)
49
+ })
50
+ return [...columns.slice(0, pinned), ...result]
51
+ }
52
+
53
+ /** The movable keys of `columns` (already in display order) after moving `key`
54
+ * to `toIndex` within them. Returns null when nothing would change. */
55
+ export function moveColumn(columns: readonly Keyed[], key: string, toIndex: number): string[] | null {
56
+ const keys = columns.slice(pinnedColumnCount(columns)).map(c => c.key)
57
+ const from = keys.indexOf(key)
58
+ if (from === -1) return null
59
+ const to = Math.max(0, Math.min(keys.length - 1, toIndex))
60
+ if (to === from) return null
61
+ keys.splice(from, 1)
62
+ keys.splice(to, 0, key)
63
+ return keys
64
+ }
@@ -108,8 +108,8 @@ describe('curated column ownership', () => {
108
108
  // Brace-counting, not parsing: assert the extraction actually saw the whole
109
109
  // table, or a silently-missed key would let the guard above pass vacuously.
110
110
  it('extracted every curated key', () => {
111
- expect(CURATED.length).toBe(239)
112
- expect(OWNED.size).toBe(201)
111
+ expect(CURATED.length).toBe(240)
112
+ expect(OWNED.size).toBe(202)
113
113
  expect(CURATED).toContain('pods')
114
114
  expect(CURATED).toContain('crossplanemanagedresources')
115
115
  expect([...OWNED]).toContain('awsmanagedcontrolplanes')
@@ -166,7 +166,7 @@ describe('every declaration resolves', () => {
166
166
  .map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
167
167
 
168
168
  it('covers the whole ownership table', () => {
169
- expect(DECLARED.length).toBe(201)
169
+ expect(DECLARED.length).toBe(202)
170
170
  })
171
171
 
172
172
  it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
@@ -0,0 +1,15 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { ArgoApplicationRenderer } from './ArgoApplicationRenderer'
4
+
5
+ describe('Argo Helm value files', () => {
6
+ it.each([true, false, undefined])('shows missing-file behavior for single and multiple sources: %s', optional => {
7
+ const source = { chart: 'cluster', repoURL: 'https://cloudnative-pg.github.io/charts', helm: { valueFiles: ['values.yaml', 'overrides.yaml'], ignoreMissingValueFiles: optional } }
8
+ for (const spec of [{ source }, { sources: [source] }]) {
9
+ const html = renderToStaticMarkup(<ArgoApplicationRenderer data={{ metadata: { name: 'app' }, spec }} />)
10
+ expect(html).toContain('values.yaml, overrides.yaml')
11
+ expect(html).toContain('If a Value File Is Missing')
12
+ expect(html).toContain(optional ? 'Skipped' : 'Render fails')
13
+ }
14
+ })
15
+ })
@@ -81,7 +81,10 @@ function SourceProperties({ source }: { source: any }) {
81
81
  )}
82
82
  {source.chart && <Property label="Helm Chart" value={source.chart} />}
83
83
  {source.helm?.valueFiles && source.helm.valueFiles.length > 0 && (
84
- <Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
84
+ <>
85
+ <Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
86
+ <Property label="If a Value File Is Missing" value={source.helm.ignoreMissingValueFiles === true ? 'Skipped' : 'Render fails'} />
87
+ </>
85
88
  )}
86
89
  {source.helm?.parameters && source.helm.parameters.length > 0 && (
87
90
  <Property
@@ -0,0 +1,286 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { renderToStaticMarkup } from "react-dom/server";
5
+ import { expect, it, vi } from "vitest";
6
+ import { HPARenderer } from "./HPARenderer";
7
+ import { VPARenderer } from "./VPARenderer";
8
+ import { KedaScaledObjectRenderer } from "./KedaScaledObjectRenderer";
9
+ import {
10
+ initNavigationMap,
11
+ resetNavigationMap,
12
+ } from "../../../utils/navigation";
13
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
14
+
15
+ it.each(["HPA", "VPA"])(
16
+ "retains a %s custom scale target API group",
17
+ async (kind) => {
18
+ initNavigationMap([
19
+ {
20
+ group: "custom.example.io",
21
+ version: "v1",
22
+ kind: "Deployment",
23
+ name: "deployments",
24
+ namespaced: true,
25
+ isCrd: true,
26
+ verbs: ["get"],
27
+ },
28
+ ]);
29
+ const element = document.createElement("div");
30
+ const root = createRoot(element);
31
+ const onNavigate = vi.fn();
32
+ const target = {
33
+ apiVersion: "custom.example.io/v1",
34
+ kind: "Deployment",
35
+ name: "same-name",
36
+ };
37
+ const data = {
38
+ metadata: { namespace: "team" },
39
+ spec: kind === "HPA" ? { scaleTargetRef: target } : { targetRef: target },
40
+ };
41
+ try {
42
+ await act(async () =>
43
+ root.render(
44
+ kind === "HPA" ? (
45
+ <HPARenderer data={data} onNavigate={onNavigate} />
46
+ ) : (
47
+ <VPARenderer data={data} onNavigate={onNavigate} />
48
+ ),
49
+ ),
50
+ );
51
+ const button = [...element.querySelectorAll("button")].find(
52
+ (b) => b.textContent === "Deployment/same-name",
53
+ );
54
+ expect(button).toBeDefined();
55
+ expect([...element.querySelectorAll("button")].filter(b => b.textContent?.startsWith("Deployment/same-name"))).toHaveLength(1);
56
+ await act(async () => button!.click());
57
+ expect(onNavigate).toHaveBeenCalledWith({
58
+ kind: "Deployment",
59
+ namespace: "team",
60
+ name: "same-name",
61
+ group: "custom.example.io",
62
+ });
63
+ } finally {
64
+ await act(async () => root.unmount());
65
+ resetNavigationMap();
66
+ }
67
+ },
68
+ );
69
+
70
+ it.each(["v1", "", undefined])("keeps Object metric resources separate from the scale target and measures its own Namespace (apiVersion %s)", async (namespaceAPIVersion) => {
71
+ const element = document.createElement("div");
72
+ const root = createRoot(element);
73
+ const onNavigate = vi.fn();
74
+ const data = {
75
+ metadata: { namespace: "team" },
76
+ spec: {
77
+ scaleTargetRef: {
78
+ apiVersion: "apps/v1",
79
+ kind: "Deployment",
80
+ name: "workload",
81
+ },
82
+ metrics: [
83
+ {
84
+ type: "Object",
85
+ object: {
86
+ metric: { name: "requests" },
87
+ describedObject: {
88
+ apiVersion: "networking.k8s.io/v1",
89
+ kind: "Ingress",
90
+ name: "frontend",
91
+ },
92
+ },
93
+ },
94
+ {
95
+ type: "Object",
96
+ object: {
97
+ metric: { name: "queue" },
98
+ describedObject: {
99
+ apiVersion: "custom.example.io/v1",
100
+ kind: "Widget",
101
+ name: "input",
102
+ },
103
+ },
104
+ },
105
+ {
106
+ type: "Object",
107
+ object: {
108
+ metric: { name: "namespace-metric" },
109
+ describedObject: {
110
+ apiVersion: namespaceAPIVersion,
111
+ kind: "Namespace",
112
+ name: "cannot-escape",
113
+ },
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ };
119
+ try {
120
+ await act(async () =>
121
+ root.render(<HPARenderer data={data} onNavigate={onNavigate} />),
122
+ );
123
+ for (const [label, expected] of [
124
+ [
125
+ "Ingress/frontend",
126
+ { kind: "Ingress", group: "networking.k8s.io", namespace: "team", name: "frontend" },
127
+ ],
128
+ [
129
+ "Widget/input",
130
+ {
131
+ kind: "Widget",
132
+ group: "custom.example.io",
133
+ namespace: "team",
134
+ name: "input",
135
+ },
136
+ ],
137
+ [
138
+ "Namespace/team",
139
+ { kind: "Namespace", group: "", namespace: "", name: "team" },
140
+ ],
141
+ ] as const) {
142
+ const button = [...element.querySelectorAll("button")].find(
143
+ (b) => b.textContent === label,
144
+ );
145
+ expect(button).toBeDefined();
146
+ await act(async () => button!.click());
147
+ expect(onNavigate).toHaveBeenLastCalledWith(expected);
148
+ }
149
+ expect(element.textContent).toContain("Object metric sources");
150
+ expect(element.textContent).toContain("This HPA's namespace");
151
+ expect(element.textContent).not.toContain("Namespace/cannot-escape");
152
+ } finally {
153
+ await act(async () => root.unmount());
154
+ }
155
+ });
156
+
157
+ it.each(["HPA", "VPA"])("does not infer missing %s target identity", (kind) => {
158
+ const target = { name: "ambiguous" };
159
+ const data = {
160
+ metadata: { namespace: "team" },
161
+ spec: kind === "HPA" ? { scaleTargetRef: target } : { targetRef: target },
162
+ };
163
+ const html = renderToStaticMarkup(
164
+ kind === "HPA" ? (
165
+ <HPARenderer data={data} onNavigate={() => {}} />
166
+ ) : (
167
+ <VPARenderer data={data} onNavigate={() => {}} />
168
+ ),
169
+ );
170
+ const doc = new DOMParser().parseFromString(html, "text/html");
171
+ expect(
172
+ [...doc.querySelectorAll("button")].some((b) =>
173
+ b.textContent?.includes("ambiguous"),
174
+ ),
175
+ ).toBe(false);
176
+ expect(html).not.toContain("Deployment/ambiguous");
177
+ });
178
+
179
+ it.each([
180
+ [
181
+ { name: "worker" },
182
+ { kind: "Deployment", group: "apps", namespace: "team", name: "worker" },
183
+ ],
184
+ [
185
+ { name: "worker", apiVersion: "", kind: "" },
186
+ { kind: "Deployment", group: "apps", namespace: "team", name: "worker" },
187
+ ],
188
+ [
189
+ { apiVersion: "custom.example.io/v1", kind: "Deployment", name: "worker" },
190
+ {
191
+ kind: "Deployment",
192
+ group: "custom.example.io",
193
+ namespace: "team",
194
+ name: "worker",
195
+ },
196
+ ],
197
+ ])(
198
+ "preserves KEDA documented defaults and explicit custom API identity",
199
+ async (target, expected) => {
200
+ const element = document.createElement("div");
201
+ const root = createRoot(element);
202
+ const onNavigate = vi.fn();
203
+ try {
204
+ await act(async () =>
205
+ root.render(
206
+ <KedaScaledObjectRenderer
207
+ data={{
208
+ metadata: { namespace: "team" },
209
+ spec: { scaleTargetRef: target },
210
+ }}
211
+ onNavigate={onNavigate}
212
+ />,
213
+ ),
214
+ );
215
+ const button = [...element.querySelectorAll("button")].find(
216
+ (b) => b.textContent === "Deployment/worker",
217
+ );
218
+ expect(button).toBeDefined();
219
+ await act(async () => button!.click());
220
+ expect(onNavigate).toHaveBeenCalledWith(expected);
221
+ } finally {
222
+ await act(async () => root.unmount());
223
+ }
224
+ },
225
+ );
226
+
227
+ it.each([
228
+ ["Deployment", "apps"], ["StatefulSet", "apps"], ["DaemonSet", "apps"], ["ReplicaSet", "apps"],
229
+ ["Job", "batch"], ["CronJob", "batch"], ["ReplicationController", ""],
230
+ ])("keeps versionless VPA %s target navigation", async (kind, group) => {
231
+ const element = document.createElement("div");
232
+ const root = createRoot(element);
233
+ const onNavigate = vi.fn();
234
+ try {
235
+ await act(async () => root.render(<VPARenderer data={{ metadata: { namespace: "team" }, spec: { targetRef: { kind, name: "workload" } } }} onNavigate={onNavigate} />));
236
+ const link = [...element.querySelectorAll("button")].find(button => button.textContent === `${kind}/workload`);
237
+ expect(link).toBeDefined();
238
+ await act(async () => link!.click());
239
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind, group, namespace: "team", name: "workload" });
240
+ } finally {
241
+ await act(async () => root.unmount());
242
+ }
243
+ });
244
+
245
+ it("pairs each diagnosis metric card with its declared Object source once", async () => {
246
+ const element = document.createElement("div");
247
+ const root = createRoot(element);
248
+ const onNavigate = vi.fn();
249
+ const objects = [
250
+ { metric: { name: "requests" }, describedObject: { apiVersion: "v1", kind: "Endpoints", name: "input" } },
251
+ { metric: { name: "requests" }, describedObject: { apiVersion: "custom.example.io/v1", kind: "Endpoints", name: "input" } },
252
+ { metric: { name: "queue_depth" }, describedObject: { kind: "Namespace", name: "ignored" } },
253
+ ];
254
+ try {
255
+ await act(async () => root.render(<HPARenderer
256
+ data={{ metadata: { namespace: "team" }, spec: { metrics: objects.map(object => ({ type: "Object", object })) } }}
257
+ hpaDiagnosis={{ state: "stabilized", summary: "Stabilized", target: {}, bounds: { min: 1, max: 3, current: 2, desired: 2 }, metrics: [
258
+ { type: "Object", name: "Endpoints/input/requests", current: "10", status: "ok" },
259
+ { type: "Object", name: "Endpoints (custom.example.io)/input/requests", current: "20", status: "ok" },
260
+ { type: "Object", name: "Namespace/team/queue_depth", current: "7", status: "ok" },
261
+ { type: "Object", name: "Service/stale/requests", current: "3", status: "status_only" },
262
+ ] }} onNavigate={onNavigate} />));
263
+ expect(element.textContent).not.toContain("Object metric sources");
264
+ expect(element.textContent).not.toContain("ignored");
265
+ const endpoints = [...element.querySelectorAll("button")].filter(button => button.textContent === "Endpoints/input");
266
+ expect(endpoints).toHaveLength(2);
267
+ await act(async () => endpoints[0].click());
268
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Endpoints", group: "", namespace: "team", name: "input" });
269
+ await act(async () => endpoints[1].click());
270
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Endpoints", group: "custom.example.io", namespace: "team", name: "input" });
271
+ const namespace = [...element.querySelectorAll("button")].find(button => button.textContent === "Namespace/team");
272
+ expect(namespace).toBeDefined();
273
+ await act(async () => namespace!.click());
274
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Namespace", group: "", namespace: "", name: "team" });
275
+ expect([...element.querySelectorAll("button")].some(button => button.textContent === "Service/stale")).toBe(false);
276
+ for (const value of ["Current 10", "Current 20", "Current 7"]) expect(element.textContent).toContain(value);
277
+ } finally {
278
+ await act(async () => root.unmount());
279
+ }
280
+ });
281
+
282
+ it("shows the documented default Target API for KEDA", () => {
283
+ const html = renderToStaticMarkup(<KedaScaledObjectRenderer data={{ metadata: { namespace: "team" }, spec: { scaleTargetRef: { name: "worker" } } }} />);
284
+ expect(html).toContain("Target API");
285
+ expect(html).toContain("apps/v1");
286
+ });
@@ -27,6 +27,7 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
27
27
  const className = getClusterClass(data)
28
28
  const version = getClusterVersion(data)
29
29
  const topology = spec.topology || {}
30
+ const classNamespace = (topology.classRef?.name ? topology.classRef.namespace : topology.classNamespace) || data.metadata?.namespace || ''
30
31
 
31
32
  // v1beta2 replica fields
32
33
  const cpReady = status.controlPlane?.readyReplicas ?? status.controlPlane?.replicas
@@ -201,10 +202,10 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
201
202
  </Section>
202
203
 
203
204
  {/* Topology (ClusterClass-based) */}
204
- {topology.class && (
205
+ {className !== '-' && (
205
206
  <Section title="Topology" icon={Layers}>
206
207
  <PropertyList>
207
- <Property label="Class" value={topology.class} />
208
+ <Property label="Class" value={<ResourceLink name={className} kind="clusterclasses" group="cluster.x-k8s.io" namespace={classNamespace} onNavigate={onNavigate} />} />
208
209
  {topology.version && <Property label="Version" value={topology.version} />}
209
210
  {topology.controlPlane?.replicas != null && (
210
211
  <Property label="CP Replicas" value={String(topology.controlPlane.replicas)} />
@@ -0,0 +1,27 @@
1
+ // @vitest-environment jsdom
2
+ import { act, type ReactNode } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { CAPIClusterRenderer } from './CAPIClusterRenderer'
6
+
7
+ async function rendered(element: ReactNode, check: (container: HTMLDivElement) => Promise<void> | void) {
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
9
+ const container = document.createElement('div')
10
+ const root = createRoot(container)
11
+ try { await act(async () => root.render(element)); await check(container) }
12
+ finally { await act(async () => root.unmount()); Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false }) }
13
+ }
14
+ async function click(container: HTMLDivElement, text: string) {
15
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === text)
16
+ expect(button, `missing link ${text}`).toBeDefined()
17
+ await act(async () => button!.click())
18
+ }
19
+
20
+ describe('exact reference navigation', () => {
21
+ it('uses the v1beta2 ClusterClass reference namespace',async()=>{
22
+ const navigate=vi.fn()
23
+ await rendered(<CAPIClusterRenderer data={{metadata:{namespace:'app'},spec:{topology:{classRef:{name:'standard',namespace:'classes'}}}}} onNavigate={navigate}/>,async container=>{
24
+ await click(container,'standard');expect(navigate).toHaveBeenCalledWith({kind:'clusterclasses',group:'cluster.x-k8s.io',namespace:'classes',name:'standard'})
25
+ })
26
+ })
27
+ })
@@ -27,3 +27,26 @@ describe('CNPGBackupRenderer — spec.target is instance selection, not a restor
27
27
  expect(html(backup())).not.toContain('Backup Target')
28
28
  })
29
29
  })
30
+
31
+
32
+ describe('CNPGBackupRenderer plugin destination', () => {
33
+ it.each([undefined, { barmanObjectName: 'ignored-store', serverName: 'ignored-server' }, { serverName: 'ignored-server' }])('explains the Cluster destination and ignored parameters: %j', (parameters) => {
34
+ const out = renderToString(<CNPGBackupRenderer data={backup({ spec: { method: 'plugin', pluginConfiguration: { name: 'barman-cloud.cloudnative-pg.io', parameters } } })} onNavigate={() => {}} />)
35
+ expect(out).toContain('From the Cluster')
36
+ expect(out).toContain('barman-cloud plugin')
37
+ expect(out).not.toContain('Object Store')
38
+ expect(out).not.toContain('ignored-store')
39
+ expect(out).not.toContain('ignored-server')
40
+ expect(out.match(/<button[^>]*>(?:pg|ignored-store|ignored-server)<\/button>/g)).toHaveLength(1)
41
+ expect(out.includes('Ignored by the barman-cloud plugin')).toBe(!!parameters)
42
+ })
43
+
44
+ it('does not assert a third-party parameter is an ObjectStore destination', () => {
45
+ const out = html(backup({ spec: { method: 'plugin', pluginConfiguration: { name: 'other.example.com', parameters: { barmanObjectName: 'custom-store' } } } }))
46
+ expect(out).toContain('other.example.com')
47
+ expect(out).toContain('Unknown: Radar does not model this plugin')
48
+ expect(out).not.toContain('custom-store')
49
+ expect(out).not.toContain('Object Store')
50
+ expect(out).not.toContain('Ignored by')
51
+ })
52
+ })
@@ -12,7 +12,7 @@ import {
12
12
  getCNPGBackupServerName,
13
13
  getCNPGBackupError,
14
14
  getCNPGBackupTarget,
15
- CNPG_BARMAN_OBJECTSTORE_GROUP,
15
+ CNPG_BARMAN_PLUGIN_NAME,
16
16
  } from '../resource-utils-cnpg'
17
17
 
18
18
  interface CNPGBackupRendererProps {
@@ -51,24 +51,16 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
51
51
  <PropertyList>
52
52
  <Property label="Phase" value={phase} />
53
53
  <Property label="Method" value={getCNPGBackupMethod(data)} />
54
- {/* A plugin-taken backup lands in an ObjectStore rather than in the
55
- Cluster's own barmanObjectStore, so naming the plugin is what
56
- tells an operator where to go looking. */}
57
54
  {backupPlugin && <Property label="Plugin" value={backupPlugin.name} />}
58
- {backupPlugin?.parameters?.barmanObjectName && (
55
+ {backupPlugin && (
59
56
  <Property
60
- label="Object Store"
61
- value={
62
- <ResourceLink
63
- name={backupPlugin.parameters.barmanObjectName}
64
- kind="objectstores"
65
- group={CNPG_BARMAN_OBJECTSTORE_GROUP}
66
- namespace={data.metadata?.namespace || ''}
67
- onNavigate={onNavigate}
68
- />
69
- }
57
+ label="Destination"
58
+ value={backupPlugin.name === CNPG_BARMAN_PLUGIN_NAME ? "From the Cluster's barman-cloud plugin" : 'Unknown: Radar does not model this plugin’s destination'}
70
59
  />
71
60
  )}
61
+ {backupPlugin?.name === CNPG_BARMAN_PLUGIN_NAME && backupPlugin.parameters && Object.keys(backupPlugin.parameters).length > 0 && (
62
+ <Property label="Plugin parameters" value="Ignored by the barman-cloud plugin; the destination comes from the Cluster" />
63
+ )}
72
64
  <Property label="Duration" value={getCNPGBackupDuration(data)} />
73
65
  <Property label="Backup Name" value={getCNPGBackupName(data)} />
74
66
  {data.status?.instanceID?.podName && (
@@ -102,10 +94,7 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
102
94
  }
103
95
  return clusterName
104
96
  })()} />
105
- {/* Destination and server name are in-tree barmanObjectStore fields.
106
- Under the plugin method they are never populated because both live
107
- on the ObjectStore, and rendering them as "-" reads as "not
108
- configured" rather than "recorded elsewhere". */}
97
+ {/* Plugin backups do not report the in-tree destination fields. */}
109
98
  {!backupPlugin && (
110
99
  <>
111
100
  <Property label="Destination" value={getCNPGBackupDestinationPath(data)} />