@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
@@ -23,6 +23,7 @@ import {
23
23
  getCNPGWALArchivingFailure,
24
24
  getCNPGLastBackupFailure,
25
25
  classifyCNPGClusterPhase,
26
+ cnpgBlockedPhaseExplanation,
26
27
  getCNPGClusterAvailability,
27
28
  CNPG_BARMAN_OBJECTSTORE_GROUP,
28
29
  } from '../resource-utils-cnpg'
@@ -100,6 +101,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
100
101
  const isFailover = phaseBucket === 'failing'
101
102
  const isSwitchover = phase === 'Switchover in progress'
102
103
  const isTerminal = phaseBucket === 'terminal'
104
+ const blocked = cnpgBlockedPhaseExplanation(phase, data.status?.phaseReason)
103
105
  // "The cluster is otherwise serving normally, so nothing else here will look
104
106
  // wrong" is a claim about the REST OF THIS DRAWER, so any other banner
105
107
  // falsifies it. It can no longer lean on isDegraded now that a
@@ -113,11 +115,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
113
115
  <>
114
116
  {/* Problem alerts */}
115
117
  {isTerminal && (
116
- <AlertBanner
117
- variant="error"
118
- title="Reconciliation has stopped"
119
- message={`${phase}. This state does not resolve on its own — the operator has stopped reconciling this cluster and it needs manual intervention.`}
120
- />
118
+ <AlertBanner variant="error" title={blocked.title} message={blocked.message} />
121
119
  )}
122
120
  {hasSplitBrain && (
123
121
  <AlertBanner
@@ -343,7 +341,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
343
341
  {/* Storage */}
344
342
  <Section title="Storage" icon={HardDrive} defaultExpanded>
345
343
  <PropertyList>
346
- <Property label="Data Size" value={getCNPGClusterStorage(data)} />
344
+ <Property label="Requested size" value={getCNPGClusterStorage(data)} />
347
345
  <Property label="Storage Class" value={getCNPGClusterStorageClass(data)} />
348
346
  {/* Requested size and class are spec-side intentions. These are what the
349
347
  operator reports about the volumes that actually exist. */}
@@ -364,7 +362,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
364
362
  <div className="mt-2 pt-2 border-t border-theme-border">
365
363
  <div className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-1">WAL Storage</div>
366
364
  <PropertyList>
367
- {walStorage.size && <Property label="Size" value={walStorage.size} />}
365
+ {walStorage.size && <Property label="Requested size" value={walStorage.size} />}
368
366
  {walStorage.storageClass && <Property label="Storage Class" value={walStorage.storageClass} />}
369
367
  </PropertyList>
370
368
  </div>
@@ -195,6 +195,31 @@ export function CNPGSubscriptionRenderer({
195
195
  )
196
196
  }
197
197
 
198
+ /**
199
+ * DatabaseRole (CNPG 1.30+) — one PostgreSQL role as its own object. The
200
+ * Cluster's spec.managed.roles wins for the same name; the operator then
201
+ * reports this object not applied, and that message renders verbatim above.
202
+ */
203
+ export function CNPGDatabaseRoleRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
204
+ const spec = data?.spec ?? {}
205
+ const details: Array<{ label: string; value: React.ReactNode }> = [
206
+ { label: 'Role', value: spec.name ?? '-' },
207
+ { label: 'Login', value: spec.login === true ? 'Allowed' : 'Not allowed' },
208
+ ]
209
+ if (spec.superuser === true) details.push({ label: 'Superuser', value: 'Yes' })
210
+ if (spec.disablePassword === true) details.push({ label: 'Password', value: 'Disabled' })
211
+ else if (spec.passwordSecret?.name) details.push({ label: 'Password Secret', value: spec.passwordSecret.name })
212
+ if (spec.validUntil) details.push({ label: 'Valid Until', value: spec.validUntil })
213
+ if (Array.isArray(spec.inRoles) && spec.inRoles.length > 0) details.push({ label: 'Member Of', value: spec.inRoles.join(', ') })
214
+ if (spec.connectionLimit != null && spec.connectionLimit !== -1) details.push({ label: 'Connection Limit', value: String(spec.connectionLimit) })
215
+ const cc = spec.clientCertificate
216
+ if (cc && cc.enabled !== false) {
217
+ const exp = data?.status?.clientCertificate?.expiration
218
+ details.push({ label: 'Client Certificate', value: `${data?.metadata?.name}-client-cert · ${exp ? `expires ${exp}` : 'expiry not reported'}` })
219
+ }
220
+ return <CNPGDeclarativeRenderer data={data} onNavigate={onNavigate} details={details} />
221
+ }
222
+
198
223
  /**
199
224
  * ImageCatalog and ClusterImageCatalog — the PostgreSQL images a Cluster may
200
225
  * use, pinned per major version.
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { CNPGScheduledBackupRenderer } from './CNPGScheduledBackupRenderer'
4
+
5
+ const schedule = (name: string, parameters?: { barmanObjectName?: string; serverName?: string }) => ({
6
+ apiVersion: 'postgresql.cnpg.io/v1',
7
+ kind: 'ScheduledBackup',
8
+ metadata: { name: 'nightly', namespace: 'db' },
9
+ spec: { cluster: { name: 'pg' }, schedule: '0 0 0 * * *', method: 'plugin', pluginConfiguration: { name, parameters } },
10
+ })
11
+
12
+ describe('CNPGScheduledBackupRenderer plugin destination', () => {
13
+ it.each([undefined, { barmanObjectName: 'ignored-store', serverName: 'ignored-server' }, { serverName: 'ignored-server' }])('explains the Cluster destination and ignored parameters: %j', (parameters) => {
14
+ const out = renderToString(<CNPGScheduledBackupRenderer data={schedule('barman-cloud.cloudnative-pg.io', parameters)} onNavigate={() => {}} />)
15
+ expect(out).toContain('From the Cluster')
16
+ expect(out).toContain('barman-cloud plugin')
17
+ expect(out).not.toContain('Object Store')
18
+ expect(out).not.toContain('ignored-store')
19
+ expect(out).not.toContain('ignored-server')
20
+ expect(out.match(/<button[^>]*>(?:pg|ignored-store|ignored-server)<\/button>/g)).toHaveLength(1)
21
+ expect(out.includes('Ignored by the barman-cloud plugin')).toBe(!!parameters)
22
+ })
23
+
24
+ it('does not assert a third-party parameter is an ObjectStore destination', () => {
25
+ const out = renderToString(<CNPGScheduledBackupRenderer data={schedule('other.example.com', { barmanObjectName: 'custom-store' })} />)
26
+ expect(out).toContain('other.example.com')
27
+ expect(out).toContain('Unknown: Radar does not model this plugin')
28
+ expect(out).not.toContain('custom-store')
29
+ expect(out).not.toContain('Object Store')
30
+ expect(out).not.toContain('Ignored by')
31
+ })
32
+ })
@@ -11,7 +11,7 @@ import {
11
11
  getCNPGScheduledBackupIsImmediate,
12
12
  getCNPGBackupPlugin,
13
13
  getCNPGScheduledBackupOwnerRef,
14
- CNPG_BARMAN_OBJECTSTORE_GROUP,
14
+ CNPG_BARMAN_PLUGIN_NAME,
15
15
  } from '../resource-utils-cnpg'
16
16
 
17
17
  interface CNPGScheduledBackupRendererProps {
@@ -64,24 +64,16 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
64
64
  return clusterName
65
65
  })()} />
66
66
  <Property label="Method" value={getCNPGScheduledBackupMethod(data)} />
67
- {/* Under the plugin method the destination lives on an ObjectStore in
68
- another API group, so naming it is the only route from here to
69
- where these backups will actually land. */}
70
67
  {schedulePlugin && <Property label="Plugin" value={schedulePlugin.name} />}
71
- {schedulePlugin?.parameters?.barmanObjectName && (
68
+ {schedulePlugin && (
72
69
  <Property
73
- label="Object Store"
74
- value={
75
- <ResourceLink
76
- name={schedulePlugin.parameters.barmanObjectName}
77
- kind="objectstores"
78
- group={CNPG_BARMAN_OBJECTSTORE_GROUP}
79
- namespace={data.metadata?.namespace || ''}
80
- onNavigate={onNavigate}
81
- />
82
- }
70
+ label="Destination"
71
+ value={schedulePlugin.name === CNPG_BARMAN_PLUGIN_NAME ? "From the Cluster's barman-cloud plugin" : 'Unknown: Radar does not model this plugin’s destination'}
83
72
  />
84
73
  )}
74
+ {schedulePlugin?.name === CNPG_BARMAN_PLUGIN_NAME && schedulePlugin.parameters && Object.keys(schedulePlugin.parameters).length > 0 && (
75
+ <Property label="Plugin parameters" value="Ignored by the barman-cloud plugin; the destination comes from the Cluster" />
76
+ )}
85
77
  <Property label="Owner Reference" value={getCNPGScheduledBackupOwnerRef(data)} />
86
78
  </PropertyList>
87
79
  </Section>
@@ -0,0 +1,35 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { CompositeRenderer } from './CompositeRenderer'
6
+ import { initNavigationMap, resetNavigationMap } from '../../../utils/navigation'
7
+
8
+ describe('CompositeRenderer composed-resource links', () => {
9
+ it('resolves a composed Kind within its own API group, not the core Kind it reuses', async () => {
10
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
11
+ initNavigationMap([
12
+ { group: '', version: 'v1', kind: 'Node', name: 'nodes', namespaced: false, isCrd: false, verbs: ['get'] },
13
+ { group: 'relationships.radar.test', version: 'v1', kind: 'Node', name: 'workerhosts', namespaced: false, isCrd: true, verbs: ['get'] },
14
+ ])
15
+ const container = document.createElement('div')
16
+ const root = createRoot(container)
17
+ const onNavigate = vi.fn()
18
+ try {
19
+ await act(async () => root.render(<CompositeRenderer onNavigate={onNavigate} data={{
20
+ apiVersion: 'platform.example.org/v1alpha1',
21
+ kind: 'Database',
22
+ metadata: { name: 'example' },
23
+ spec: { resourceRefs: [{ apiVersion: 'relationships.radar.test/v1', kind: 'Node', name: 'worker' }] },
24
+ }} />))
25
+ const link = [...container.querySelectorAll('button')].find(button => button.textContent === 'worker')
26
+ expect(link).toBeDefined()
27
+ await act(async () => link!.click())
28
+ expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'workerhosts', group: 'relationships.radar.test', name: 'worker' }))
29
+ } finally {
30
+ await act(async () => root.unmount())
31
+ resetNavigationMap()
32
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
33
+ }
34
+ })
35
+ })
@@ -14,7 +14,7 @@ import {
14
14
  isCrossplanePaused,
15
15
  type CrossplaneResourceRef,
16
16
  } from '../resource-utils-crossplane'
17
- import { kindToPlural } from '../../../utils/navigation'
17
+ import { kindToPluralWithGroup } from '../../../utils/navigation'
18
18
  import type { StatusBadge } from '../resource-utils'
19
19
 
20
20
  /**
@@ -158,7 +158,7 @@ export function CompositeRenderer({ data, onNavigate, composedRefStatuses, compo
158
158
  value={
159
159
  <ResourceLink
160
160
  name={boundXR.name}
161
- kind={kindToPlural(boundXR.kind)}
161
+ kind={kindToPluralWithGroup(boundXR.kind, groupFromApiVersion(boundXR.apiVersion))}
162
162
  namespace={boundXR.namespace || ''}
163
163
  group={groupFromApiVersion(boundXR.apiVersion) || undefined}
164
164
  onNavigate={onNavigate}
@@ -242,8 +242,8 @@ function ComposedRefRow({
242
242
  statusEntry?: ComposedRefStatus
243
243
  onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
244
244
  }) {
245
- const kindPlural = kindToPlural(ref_.kind)
246
245
  const group = groupFromApiVersion(ref_.apiVersion)
246
+ const kindPlural = kindToPluralWithGroup(ref_.kind, group)
247
247
  return (
248
248
  <div className="flex items-center gap-2 py-1 px-2 rounded hover:bg-theme-hover text-sm">
249
249
  <Box className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
@@ -4,7 +4,7 @@ import { Layers, GitBranch, FileText, Boxes, ScrollText } from 'lucide-react'
4
4
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
6
6
  import { CodeViewer } from '../../ui/CodeViewer'
7
- import { kindToPlural } from '../../../utils/navigation'
7
+ import { kindToPluralWithGroup } from '../../../utils/navigation'
8
8
 
9
9
  interface CompositionRendererProps {
10
10
  data: any
@@ -74,11 +74,11 @@ function CompositionBody({ data, onNavigate, revision }: CompositionRendererProp
74
74
  <div className="mt-2 text-xs text-theme-text-secondary">
75
75
  Backed by XRD:{' '}
76
76
  <ResourceLink
77
- name={`${kindToPlural(xrdKind)}.${xrdGroup}`}
77
+ name={`${kindToPluralWithGroup(xrdKind, xrdGroup)}.${xrdGroup}`}
78
78
  kind="compositeresourcedefinitions"
79
79
  namespace=""
80
80
  onNavigate={onNavigate}
81
- label={<span className="font-mono">{kindToPlural(xrdKind)}.{xrdGroup}</span>}
81
+ label={<span className="font-mono">{kindToPluralWithGroup(xrdKind, xrdGroup)}.{xrdGroup}</span>}
82
82
  />
83
83
  </div>
84
84
  )}
@@ -0,0 +1,34 @@
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 { ContourHTTPProxyRenderer } from './ContourHTTPProxyRenderer'
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.each([['tls/', 'tls'], ['/', 'app']])('resolves Contour TLS secret prefix %s to namespace %s',async(prefix, namespace)=>{
22
+ const navigate=vi.fn()
23
+ await rendered(<ContourHTTPProxyRenderer data={{metadata:{namespace:'app'},spec:{virtualhost:{fqdn:'example.test',tls:{secretName:`${prefix}server`,clientValidation:{caSecret:`${prefix}ca`,crlSecret:`${prefix}crl`}}}}}} onNavigate={navigate}/>,async container=>{
24
+ for(const name of ['server','ca','crl']) {await click(container,`${prefix}${name}`);expect(navigate).toHaveBeenLastCalledWith({kind:'secrets',group:'',namespace,name})}
25
+ })
26
+ })
27
+ it('links valid long Contour Secret names',async()=>{
28
+ const navigate=vi.fn()
29
+ const name='a'.repeat(70)
30
+ await rendered(<ContourHTTPProxyRenderer data={{metadata:{namespace:'app'},spec:{virtualhost:{tls:{secretName:name}}}}} onNavigate={navigate}/>,async container=>{
31
+ await click(container,name);expect(navigate).toHaveBeenCalledWith({kind:'secrets',group:'',namespace:'app',name})
32
+ })
33
+ })
34
+ })
@@ -1,6 +1,7 @@
1
1
  import { Globe, Route, Layers, Lock } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
+ getContourSecretReference,
4
5
  getHTTPProxyFQDN,
5
6
  getHTTPProxyRouteCount,
6
7
  getHTTPProxyServiceCount,
@@ -174,16 +175,16 @@ export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyR
174
175
  {tls && (
175
176
  <Section title="TLS" icon={Lock} defaultExpanded>
176
177
  <PropertyList>
177
- {tls.secretName && (
178
- <Property label="Secret" value={
179
- <ResourceLink
180
- name={tls.secretName}
181
- kind="secrets"
182
- namespace={ns}
183
- onNavigate={onNavigate}
184
- />
185
- } />
186
- )}
178
+ {[
179
+ { label: 'Secret', name: tls.secretName },
180
+ { label: 'Client CA Secret', name: tls.clientValidation?.caSecret },
181
+ { label: 'Client CRL Secret', name: tls.clientValidation?.crlSecret },
182
+ ].filter(item => item.name).map(item => {
183
+ const ref = getContourSecretReference(item.name, ns)
184
+ return <Property key={item.label} label={item.label} value={ref
185
+ ? <ResourceLink {...ref} kind="secrets" group="" label={item.name} onNavigate={onNavigate} />
186
+ : item.name} />
187
+ })}
187
188
  {tls.minimumProtocolVersion && (
188
189
  <Property label="Min Protocol" value={tls.minimumProtocolVersion} />
189
190
  )}
@@ -0,0 +1,31 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { CronJobRenderer } from './CronJobRenderer'
4
+ import { getCronJobStatus } from '../resource-utils'
5
+
6
+ describe('CronJob scheduling and execution evidence', () => {
7
+ it('does not turn a missing recorded completion into an execution failure', () => {
8
+ const html = renderToStaticMarkup(<CronJobRenderer data={{
9
+ spec: { schedule: '0 * * * *', timeZone: 'UTC', startingDeadlineSeconds: 0 },
10
+ status: { lastScheduleTime: '2026-01-02T12:00:00Z', lastSuccessfulTime: '2026-01-02T11:00:00Z' },
11
+ }} />)
12
+ expect(html).toContain('Latest completion not verified')
13
+ expect(html).not.toContain('Recent Jobs Failing')
14
+ expect(html).toContain('>UTC<')
15
+ expect(html).toContain('>0s<')
16
+ })
17
+
18
+ it('shows suspension without claiming an old Job recovered', () => {
19
+ const html = renderToStaticMarkup(<CronJobRenderer data={{
20
+ spec: { schedule: '* * * * *', suspend: true },
21
+ status: { lastScheduleTime: '2026-01-02T12:00:00Z' },
22
+ }} />)
23
+ expect(html).toContain('CronJob Suspended')
24
+ expect(html).not.toContain('Latest completion not verified')
25
+ expect(html).toContain('Controller local time (zone not reported)')
26
+ })
27
+
28
+ it('does not grade enabled configuration as successful scheduling', () => {
29
+ expect(getCronJobStatus({ spec: { schedule: '* * * * *' }, status: {} })).toMatchObject({ text: 'Enabled', level: 'neutral' })
30
+ })
31
+ })
@@ -5,7 +5,23 @@ import { TimeValue } from '../../ui/ScheduleValue'
5
5
  import { formatRelativeAgeTime } from '../../../utils/format'
6
6
 
7
7
  interface CronJobRendererProps {
8
- data: any
8
+ data: {
9
+ metadata?: { namespace?: string }
10
+ spec?: {
11
+ schedule?: string
12
+ timeZone?: string
13
+ suspend?: boolean
14
+ concurrencyPolicy?: string
15
+ startingDeadlineSeconds?: number
16
+ successfulJobsHistoryLimit?: number
17
+ failedJobsHistoryLimit?: number
18
+ }
19
+ status?: {
20
+ lastScheduleTime?: string
21
+ lastSuccessfulTime?: string
22
+ active?: { name: string; namespace?: string }[]
23
+ }
24
+ }
9
25
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
10
26
  }
11
27
 
@@ -13,21 +29,13 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
13
29
  const status = data.status || {}
14
30
  const spec = data.spec || {}
15
31
 
16
- // Check for issues or notable states
17
32
  const isSuspended = spec.suspend === true
18
33
  const hasNeverRun = !status.lastScheduleTime
19
34
 
20
- // "Recent failures" means the latest scheduled run didn't reach success.
21
- // Only meaningful once nothing is running — while a job is active the most
22
- // recent run is simply still in flight, not failing. With concurrencyPolicy
23
- // Allow an older failed run can overlap a new active one; CronJob status only
24
- // carries aggregate timestamps, so we accept missing that rare case here (the
25
- // failed Job still surfaces in the job list / topology) rather than reviving
26
- // the false positive that fired on every normal in-flight run.
27
35
  const activeJobs = status.active?.length || 0
28
36
  const lastSchedule = status.lastScheduleTime ? new Date(status.lastScheduleTime).getTime() : 0
29
37
  const lastSuccess = status.lastSuccessfulTime ? new Date(status.lastSuccessfulTime).getTime() : 0
30
- const recentFailures = activeJobs === 0 && lastSchedule > 0 && lastSchedule > lastSuccess
38
+ const completionUnverified = activeJobs === 0 && lastSchedule > 0 && lastSchedule > lastSuccess
31
39
 
32
40
  return (
33
41
  <>
@@ -46,23 +54,23 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
46
54
  <AlertBanner
47
55
  variant="info"
48
56
  title="Never Scheduled"
49
- message="This CronJob has never run. Check the schedule and starting deadline settings."
57
+ message="Kubernetes has not recorded a scheduled run. Check the schedule and controller Events."
50
58
  />
51
59
  )}
52
60
 
53
- {/* Recent failures warning */}
54
- {recentFailures && !isSuspended && (
61
+ {completionUnverified && !isSuspended && (
55
62
  <AlertBanner
56
- variant="error"
57
- title="Recent Jobs Failing"
58
- message={<>Jobs have been scheduled but haven't succeeded recently. Last success: {formatRelativeAgeTime(status.lastSuccessfulTime, 'never')}. Check job history and pod logs.</>}
63
+ variant="info"
64
+ title="Latest completion not verified"
65
+ message={<>No successful completion is recorded after the latest scheduled run. Last success: {formatRelativeAgeTime(status.lastSuccessfulTime, 'never')}. Inspect the Job history to check the execution outcome.</>}
59
66
  />
60
67
  )}
61
68
 
62
69
  <Section title="Schedule" icon={Clock}>
63
70
  <PropertyList>
64
71
  <Property label="Schedule" value={spec.schedule} />
65
- <Property label="Human" value={cronToHuman(spec.schedule)} />
72
+ <Property label="Human" value={cronToHuman(spec.schedule ?? '')} />
73
+ <Property label="Time Zone" value={spec.timeZone ?? 'Controller local time (zone not reported)'} />
66
74
  <Property label="Suspend" value={spec.suspend ? 'Yes' : 'No'} />
67
75
  <Property label="Last Schedule" value={<TimeValue timestamp={status.lastScheduleTime} fallback="Never" />} />
68
76
  <Property label="Last Success" value={<TimeValue timestamp={status.lastSuccessfulTime} fallback="Never" />} />
@@ -73,16 +81,16 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
73
81
  <Section title="Configuration">
74
82
  <PropertyList>
75
83
  <Property label="Concurrency" value={spec.concurrencyPolicy || 'Allow'} />
76
- <Property label="Starting Deadline" value={spec.startingDeadlineSeconds ? `${spec.startingDeadlineSeconds}s` : 'None'} />
84
+ <Property label="Starting Deadline" value={spec.startingDeadlineSeconds != null ? `${spec.startingDeadlineSeconds}s` : 'None'} />
77
85
  <Property label="Success History" value={spec.successfulJobsHistoryLimit ?? 3} />
78
86
  <Property label="Failed History" value={spec.failedJobsHistoryLimit ?? 1} />
79
87
  </PropertyList>
80
88
  </Section>
81
89
 
82
- {status.active?.length > 0 && (
90
+ {status.active && status.active.length > 0 && (
83
91
  <Section title="Active Jobs">
84
92
  <div className="space-y-1">
85
- {status.active.map((job: any) => (
93
+ {status.active.map((job) => (
86
94
  <div key={job.name} className="text-sm">
87
95
  <ResourceLink name={job.name} kind="jobs" namespace={job.namespace || data.metadata?.namespace || ''} onNavigate={onNavigate} />
88
96
  </div>
@@ -1,8 +1,9 @@
1
1
  // @vitest-environment jsdom
2
2
  import { act } from 'react'
3
3
  import { createRoot } from 'react-dom/client'
4
- import { expect, it, vi } from 'vitest'
4
+ import { describe, expect, it, vi } from 'vitest'
5
5
  import { EndpointSliceRenderer } from './EndpointSliceRenderer'
6
+ import { initNavigationMap, resetNavigationMap } from '../../../utils/navigation'
6
7
 
7
8
  vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
8
9
 
@@ -22,3 +23,40 @@ it('navigates to the labeled Service as a core-group resource', async () => {
22
23
  expect(onNavigate).toHaveBeenCalledWith({ kind: 'Service', group: '', namespace: 'team', name: 'web' })
23
24
  await act(async () => root.unmount())
24
25
  })
26
+
27
+ describe('EndpointSliceRenderer target navigation', () => {
28
+ it('navigates exact target identity, controller-written Pod targets and Node placement', async () => {
29
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
30
+ initNavigationMap([{ group: 'custom.example.io', version: 'v1', kind: 'Node', name: 'nodes', namespaced: false, isCrd: true, verbs: ['get'] }])
31
+ const container = document.createElement('div')
32
+ const root = createRoot(container)
33
+ const onNavigate = vi.fn()
34
+ const click = async (label: string) => {
35
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === label)
36
+ expect(button).toBeDefined()
37
+ await act(async () => button!.click())
38
+ }
39
+ try {
40
+ await act(async () => root.render(<EndpointSliceRenderer onNavigate={onNavigate} data={{ metadata: { namespace: 'slice-team' }, endpoints: [
41
+ { addresses: ['10.0.0.1'], nodeName: 'worker', targetRef: { apiVersion: 'custom.example.io/v1', kind: 'Node', namespace: 'irrelevant', name: 'custom-target' } },
42
+ { addresses: ['10.0.0.2'], targetRef: { apiVersion: 'v1', kind: 'Pod', namespace: 'target-team', name: 'backend' } },
43
+ { addresses: ['10.0.0.3'], targetRef: { kind: 'Pod', namespace: 'slice-team', name: 'controller-written', uid: 'pod-uid' } },
44
+ { addresses: ['10.0.0.4'], targetRef: { kind: 'Widget', namespace: 'slice-team', name: 'unknown-api' } },
45
+ ] }} />))
46
+ await click('Node custom-target')
47
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Node', group: 'custom.example.io', namespace: '', name: 'custom-target' })
48
+ await click('Pod target-team/backend')
49
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Pod', group: '', namespace: 'target-team', name: 'backend' })
50
+ await click('Pod slice-team/controller-written')
51
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Pod', group: '', namespace: 'slice-team', name: 'controller-written' })
52
+ await click('worker')
53
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Node', group: '', namespace: '', name: 'worker' })
54
+ expect([...container.querySelectorAll('button')].some(button => button.textContent?.includes('unknown-api'))).toBe(false)
55
+ expect(container.textContent).toContain('unknown-api')
56
+ } finally {
57
+ await act(async () => root.unmount())
58
+ resetNavigationMap()
59
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
60
+ }
61
+ })
62
+ })
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
4
4
  import { Badge } from '../../ui/Badge'
5
5
  import type { ResourceRef } from '../../../types'
6
+ import { objectReferenceToResourceRef } from '../../../utils/navigation'
6
7
 
7
8
  interface EndpointSliceRendererProps {
8
9
  data: any
@@ -17,8 +18,7 @@ function endpointAddressCount(endpoints: any[]): number {
17
18
  return endpoints.reduce((total, endpoint) => total + (endpoint.addresses?.length || 0), 0)
18
19
  }
19
20
 
20
- function endpointTargetLabel(endpoint: any): string | null {
21
- const target = endpoint.targetRef
21
+ function endpointTargetLabel(target: any): string | null {
22
22
  if (!target?.kind || !target?.name) return null
23
23
  return target.namespace ? `${target.kind} ${target.namespace}/${target.name}` : `${target.kind} ${target.name}`
24
24
  }
@@ -85,8 +85,8 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
85
85
  <div className="space-y-2">
86
86
  {endpoints.map((endpoint: any, index: number) => {
87
87
  const ready = isEndpointReady(endpoint)
88
- const targetLabel = endpointTargetLabel(endpoint)
89
- const target = endpoint.targetRef
88
+ const targetRef = objectReferenceToResourceRef(endpoint.targetRef)
89
+ const targetLabel = endpointTargetLabel(targetRef || endpoint.targetRef)
90
90
  return (
91
91
  <div key={`${endpoint.addresses?.join(',') || 'endpoint'}-${index}`} className="card-inner text-sm space-y-3">
92
92
  <div className="flex items-start justify-between gap-3">
@@ -98,20 +98,15 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
98
98
  </Badge>
99
99
  ))}
100
100
  </div>
101
- {targetLabel && (
101
+ {targetLabel && (onNavigate && targetRef ? (
102
102
  <button
103
103
  type="button"
104
104
  className="text-xs text-theme-text-secondary hover:text-accent-text"
105
- disabled={!onNavigate}
106
- onClick={() => onNavigate?.({
107
- kind: target.kind,
108
- namespace: target.namespace || metadata.namespace,
109
- name: target.name,
110
- })}
105
+ onClick={() => onNavigate(targetRef)}
111
106
  >
112
107
  {targetLabel}
113
108
  </button>
114
- )}
109
+ ) : <span className="text-xs text-theme-text-secondary">{targetLabel}</span>)}
115
110
  </div>
116
111
  <div className="flex flex-wrap justify-end gap-1.5 shrink-0">
117
112
  <span className={clsx('badge-sm', ready ? 'status-healthy' : 'status-unhealthy')}>
@@ -127,7 +122,13 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
127
122
  </div>
128
123
  {(endpoint.nodeName || endpoint.zone) && (
129
124
  <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-theme-text-tertiary">
130
- {endpoint.nodeName && <span>Node: {endpoint.nodeName}</span>}
125
+ {endpoint.nodeName && <span>Node: {onNavigate ? (
126
+ <button
127
+ type="button"
128
+ className="hover:text-accent-text hover:underline"
129
+ onClick={() => onNavigate({ kind: 'Node', group: '', namespace: '', name: endpoint.nodeName })}
130
+ >{endpoint.nodeName}</button>
131
+ ) : endpoint.nodeName}</span>}
131
132
  {endpoint.zone && <span>Zone: {endpoint.zone}</span>}
132
133
  </div>
133
134
  )}
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react'
1
2
  import { Package, Settings, CheckCircle2, History, FileCode2, GitBranch } from 'lucide-react'
2
3
  import { clsx } from 'clsx'
3
4
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
@@ -5,12 +6,21 @@ import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
5
6
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
7
  import { TimeValue } from '../../ui/ScheduleValue'
7
8
 
9
+ export interface FluxValuesReference {
10
+ kind?: 'ConfigMap' | 'Secret'
11
+ name: string
12
+ optional?: boolean
13
+ valuesKey?: string
14
+ targetPath?: string
15
+ }
16
+
8
17
  interface FluxHelmReleaseRendererProps {
9
18
  data: any
10
19
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
20
+ renderValuesReference?: (reference: FluxValuesReference) => ReactNode
11
21
  }
12
22
 
13
- export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRendererProps) {
23
+ export function FluxHelmReleaseRenderer({ data, onNavigate, renderValuesReference }: FluxHelmReleaseRendererProps) {
14
24
  const status = data.status || {}
15
25
  const spec = data.spec || {}
16
26
  const conditions = (status.conditions || []) as FluxCondition[]
@@ -59,7 +69,7 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
59
69
  }
60
70
 
61
71
  // Values info
62
- const valuesFrom = spec.valuesFrom || []
72
+ const valuesFrom: FluxValuesReference[] = spec.valuesFrom || []
63
73
  const values = spec.values || {}
64
74
  const valueKeys = Object.keys(values)
65
75
 
@@ -149,15 +159,18 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
149
159
  Values From
150
160
  </div>
151
161
  <div className="space-y-1">
152
- {valuesFrom.map((ref: any, idx: number) => (
162
+ {valuesFrom.map((ref, idx) => (
153
163
  <div key={idx} className="card-inner text-sm flex items-center gap-2 flex-wrap">
154
164
  <span className="badge-sm status-neutral">{ref.kind || 'ConfigMap'}</span>
155
- <ResourceLink
156
- name={ref.name}
157
- kind={ref.kind === 'Secret' ? 'secrets' : 'configmaps'}
158
- namespace={data.metadata?.namespace || ''}
159
- onNavigate={onNavigate}
160
- />
165
+ {ref.optional && <span className="badge-sm status-neutral">Optional</span>}
166
+ {renderValuesReference ? renderValuesReference(ref) : (
167
+ <ResourceLink
168
+ name={ref.name}
169
+ kind={ref.kind === 'Secret' ? 'secrets' : 'configmaps'}
170
+ namespace={data.metadata?.namespace || ''}
171
+ onNavigate={onNavigate}
172
+ />
173
+ )}
161
174
  {ref.valuesKey && (
162
175
  <span className="text-theme-text-tertiary text-xs">
163
176
  key: {ref.valuesKey}