@skyhook-io/k8s-ui 1.16.3 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -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/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,88 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { SecretRenderer } from './SecretRenderer'
7
+
8
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
9
+
10
+ const secret = {
11
+ apiVersion: 'v1',
12
+ kind: 'Secret',
13
+ metadata: { name: 'db', namespace: 'shop', resourceVersion: '7' },
14
+ type: 'Opaque',
15
+ data: { password: btoa('old') },
16
+ }
17
+
18
+ let element: HTMLDivElement
19
+ let root: Root
20
+
21
+ beforeEach(() => {
22
+ element = document.createElement('div')
23
+ document.body.append(element)
24
+ root = createRoot(element)
25
+ })
26
+
27
+ afterEach(async () => {
28
+ await act(async () => root.unmount())
29
+ element.remove()
30
+ })
31
+
32
+ async function render(onSaveSecretValue?: (yaml: string) => Promise<void>) {
33
+ await act(async () => root.render(<SecretRenderer data={secret} resourceData={secret} onSaveSecretValue={onSaveSecretValue} />))
34
+ }
35
+
36
+ function button(label: string): HTMLButtonElement {
37
+ const match = [...document.querySelectorAll('button')].find(b => b.textContent?.trim() === label || b.title === label)
38
+ if (!match) throw new Error(`no button "${label}"`)
39
+ return match
40
+ }
41
+
42
+ async function editAndSave(value: string) {
43
+ await act(async () => button('Reveal').click())
44
+ await act(async () => button('Edit value').click())
45
+ const textarea = element.querySelector('textarea')!
46
+ await act(async () => {
47
+ Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')!.set!.call(textarea, value)
48
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
49
+ })
50
+ await act(async () => button('Save').click())
51
+ await act(async () => button('Update').click())
52
+ }
53
+
54
+ const httpError = (status: number, message: string) => Object.assign(new Error(message), { status })
55
+
56
+ describe('SecretRenderer value edit', () => {
57
+ it('keeps the edit open and says why when the Secret changed since it loaded', async () => {
58
+ await render(vi.fn().mockRejectedValue(httpError(409, 'resource changed after review')))
59
+ await editAndSave('new')
60
+ expect(element.textContent).toContain('Not saved')
61
+ expect(element.textContent).toContain('This Secret changed after it loaded. Once the view reloads it, save again')
62
+ expect(element.querySelector('textarea')?.value).toBe('new')
63
+ })
64
+
65
+ it('says the Secret is gone when the save finds it deleted', async () => {
66
+ await render(vi.fn().mockRejectedValue(httpError(404, 'secrets "db" not found')))
67
+ await editAndSave('new')
68
+ expect(element.textContent).toContain('Not saved')
69
+ expect(element.textContent).toContain('This Secret no longer exists in the cluster.')
70
+ })
71
+
72
+ it('shows the server message for any other failure', async () => {
73
+ await render(vi.fn().mockRejectedValue(httpError(403, 'secrets "db" is forbidden')))
74
+ await editAndSave('new')
75
+ expect(element.textContent).toContain('Not saved')
76
+ expect(element.textContent).toContain('secrets "db" is forbidden')
77
+ })
78
+
79
+ it('closes the editor once the save handler is taken away', async () => {
80
+ await render(vi.fn().mockResolvedValue(undefined))
81
+ await act(async () => button('Reveal').click())
82
+ await act(async () => button('Edit value').click())
83
+ expect(element.querySelector('textarea')).not.toBeNull()
84
+ await render(undefined)
85
+ expect(element.querySelector('textarea')).toBeNull()
86
+ expect(element.textContent).not.toContain('Save')
87
+ })
88
+ })
@@ -8,6 +8,7 @@ import { ReflectorSummary, ReflectorSection, isReflectorMirror, reflectorEditNot
8
8
  import type { SecretCertificateInfo, CertificateInfo, Relationships, ResourceRef } from '../../../types'
9
9
  import { pluralize } from '../../../utils/pluralize'
10
10
  import { cleanResourceForYaml } from '../../../utils/yaml'
11
+ import { isFetchError } from '../../../types/fetch-error'
11
12
 
12
13
  interface SecretRendererProps {
13
14
  data: any
@@ -31,6 +32,7 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
31
32
  const [editingKey, setEditingKey] = useState<string | null>(null)
32
33
  const [editValue, setEditValue] = useState('')
33
34
  const [showSaveConfirm, setShowSaveConfirm] = useState(false)
35
+ const [saveError, setSaveError] = useState<{ reason?: string } | null>(null)
34
36
  const textareaRef = useRef<HTMLTextAreaElement>(null)
35
37
  const dataKeys = Object.keys(data.data || {})
36
38
  const isImmutable = data.immutable === true
@@ -68,12 +70,14 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
68
70
  function startEdit(key: string, decoded: string) {
69
71
  setEditingKey(key)
70
72
  setEditValue(decoded)
73
+ setSaveError(null)
71
74
  }
72
75
 
73
76
  function cancelEdit() {
74
77
  setEditingKey(null)
75
78
  setEditValue('')
76
79
  setShowSaveConfirm(false)
80
+ setSaveError(null)
77
81
  }
78
82
 
79
83
  const handleSave = useCallback(async (key: string, newValue: string) => {
@@ -83,14 +87,16 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
83
87
  // btoa is byte-only; round-trip through encodeURIComponent/unescape so non-ASCII secret values survive base64 encoding.
84
88
  cleaned.data[key] = btoa(unescape(encodeURIComponent(newValue)))
85
89
  const yaml = yamlStringify(cleaned, { lineWidth: 0, indent: 2 })
90
+ setSaveError(null)
86
91
  try {
87
92
  await onSaveSecretValue(yaml)
88
93
  setEditingKey(null)
89
94
  setEditValue('')
90
95
  setShowSaveConfirm(false)
91
- } catch {
92
- // Error is handled by the mutation (toast)
96
+ } catch (error) {
97
+ // The edit stays open with the value as typed, so it can be saved again.
93
98
  setShowSaveConfirm(false)
99
+ setSaveError(secretSaveErrorMessage(error))
94
100
  }
95
101
  }, [onSaveSecretValue, resourceData])
96
102
 
@@ -171,8 +177,10 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
171
177
  {dataKeys.map((key) => {
172
178
  const decoded = decodeBase64(data.data[key])
173
179
  const isBinary = decoded === '[binary data]'
174
- const isEditing = editingKey === key
175
180
  const canEdit = onSaveSecretValue && !isImmutable && !isBinary
181
+ // Without a save handler (the Secret was deleted, say) an open
182
+ // editor would have a Save button that does nothing.
183
+ const isEditing = editingKey === key && Boolean(canEdit)
176
184
 
177
185
  return (
178
186
  <div key={key} className="card-inner">
@@ -221,13 +229,21 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
221
229
  <textarea
222
230
  ref={textareaRef}
223
231
  value={editValue}
224
- onChange={(e) => setEditValue(e.target.value)}
232
+ onChange={(e) => {
233
+ setEditValue(e.target.value)
234
+ setSaveError(null)
235
+ }}
225
236
  autoCorrect="off"
226
237
  autoCapitalize="off"
227
238
  className="w-full bg-theme-base rounded p-2 text-xs text-theme-text-secondary font-mono border border-blue-500/50 focus:border-blue-500 focus:outline-none resize-none overflow-hidden whitespace-pre-wrap"
228
239
  style={{ minHeight: '60px' }}
229
240
  disabled={isSaving}
230
241
  />
242
+ {saveError && (
243
+ <div role="alert" className="mt-2">
244
+ <AlertBanner variant="error" title="Not saved" message={saveError.reason} />
245
+ </div>
246
+ )}
231
247
  <div className="flex items-center gap-2 mt-2">
232
248
  <button
233
249
  onClick={() => setShowSaveConfirm(true)}
@@ -267,7 +283,7 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
267
283
 
268
284
  <ReflectorSection data={data} reflection={relationships?.reflection} onNavigate={onNavigate} />
269
285
 
270
- {editingKey && (
286
+ {editingKey && onSaveSecretValue && (
271
287
  <ConfirmDialog
272
288
  open={showSaveConfirm}
273
289
  onClose={() => setShowSaveConfirm(false)}
@@ -284,6 +300,19 @@ export function SecretRenderer({ data, relationships, onNavigate, certificateInf
284
300
  )
285
301
  }
286
302
 
303
+ function secretSaveErrorMessage(error: unknown): { reason?: string } {
304
+ if (isFetchError(error) && error.status === 404) {
305
+ return { reason: 'This Secret no longer exists in the cluster.' }
306
+ }
307
+ // The view reloads the Secret after a failed save, but that read can trail
308
+ // the cluster by a moment, so the message can't promise it already shows
309
+ // the change.
310
+ if (isFetchError(error) && error.status === 409) {
311
+ return { reason: 'This Secret changed after it loaded. Once the view reloads it, save again to apply your value to the latest version.' }
312
+ }
313
+ return { reason: error instanceof Error && error.message ? error.message : undefined }
314
+ }
315
+
287
316
  function CertificateInfoSection({ cert, index, total }: { cert: CertificateInfo; index: number; total: number }) {
288
317
  const expiryTextColor = cert.expired || cert.daysLeft <= 7
289
318
  ? 'text-red-400'
@@ -0,0 +1,43 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { expect, it } from 'vitest'
3
+ import { ServiceRenderer } from './ServiceRenderer'
4
+
5
+ const data = { metadata: { name: 'web', namespace: 'team' }, spec: { selector: { app: 'web' } } }
6
+ const props = { data, onCopy: () => {}, copied: null }
7
+ const slice = (name: string, ready = true) => ({
8
+ metadata: { name },
9
+ addressType: 'IPv4',
10
+ endpoints: [{ addresses: ['10.0.0.1'], conditions: { ready } }],
11
+ })
12
+
13
+ it('renders supplied slices for a selector Service when the host enables the inventory', () => {
14
+ const html = renderToStaticMarkup(<ServiceRenderer {...props} endpointSlicesEnabled endpointSlices={[slice('published', false)]} />)
15
+ expect(html).toContain('EndpointSlices')
16
+ expect(html).toContain('published')
17
+ expect(html).toContain('0/1')
18
+ expect(html).not.toContain('Showing the first')
19
+ })
20
+
21
+ it('keeps the section hidden for selector Services unless the host enables it', () => {
22
+ expect(renderToStaticMarkup(<ServiceRenderer {...props} endpointSlices={[slice('published')]} />)).not.toContain('EndpointSlices')
23
+ const selectorless = { ...data, spec: {} }
24
+ expect(renderToStaticMarkup(<ServiceRenderer {...props} data={selectorless} endpointSlices={[slice('manual')]} />)).toContain('manual')
25
+ })
26
+
27
+ it('separates unavailable, loading and ready-empty inventory', () => {
28
+ const error = renderToStaticMarkup(<ServiceRenderer {...props} endpointSlicesEnabled endpointSlices={[]} endpointSlicesError="Read denied" />)
29
+ expect(error).toContain('EndpointSlice inventory unavailable')
30
+ expect(error).toContain('Read denied')
31
+ expect(error).not.toContain('No EndpointSlices found')
32
+ expect(renderToStaticMarkup(<ServiceRenderer {...props} endpointSlicesEnabled endpointSlicesLoading />)).toContain('Loading EndpointSlices')
33
+ expect(renderToStaticMarkup(<ServiceRenderer {...props} endpointSlicesEnabled endpointSlices={[]} />)).toContain('No EndpointSlices found')
34
+ })
35
+
36
+ it('says when the inventory was cut off', () => {
37
+ const html = renderToStaticMarkup(<ServiceRenderer {...props} endpointSlicesEnabled endpointSlicesTruncated endpointSlices={[slice('a'), slice('b')]} />)
38
+ expect(html).toContain('Showing the first 2 EndpointSlices.')
39
+ })
40
+
41
+ it('does not show slice inventory for ExternalName even when supplied', () => {
42
+ expect(renderToStaticMarkup(<ServiceRenderer {...props} data={{ ...data, spec: { type: 'ExternalName', externalName: 'example.org' } }} endpointSlicesEnabled endpointSlices={[]} />)).not.toContain('EndpointSlices')
43
+ })
@@ -18,6 +18,9 @@ interface ServiceRendererProps {
18
18
  copied: string | null
19
19
  endpointSlices?: any[]
20
20
  endpointSlicesLoading?: boolean
21
+ endpointSlicesEnabled?: boolean
22
+ endpointSlicesError?: string
23
+ endpointSlicesTruncated?: boolean
21
24
  onNavigate?: (ref: ResourceRef) => void
22
25
  renderPortAction?: (props: ServicePortRenderProps) => ReactNode
23
26
  renderPortPanel?: (props: ServicePortRenderProps) => ReactNode
@@ -38,7 +41,7 @@ function endpointSliceReadyClass(ready: number, total: number): string {
38
41
  return 'status-unhealthy'
39
42
  }
40
43
 
41
- export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpointSlicesLoading, onNavigate, renderPortAction, renderPortPanel }: ServiceRendererProps) {
44
+ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpointSlicesLoading, endpointSlicesEnabled, endpointSlicesError, endpointSlicesTruncated, onNavigate, renderPortAction, renderPortPanel }: ServiceRendererProps) {
42
45
  const spec = data.spec || {}
43
46
  const ports = spec.ports || []
44
47
  const lbIngress = data.status?.loadBalancer?.ingress || []
@@ -111,10 +114,12 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
111
114
  </Section>
112
115
  )}
113
116
 
114
- {hasNoSelector && !isExternalName && (
115
- <Section title="EndpointSlices" icon={Radio}>
117
+ {!isExternalName && (hasNoSelector || endpointSlicesEnabled) && (
118
+ <Section title="EndpointSlices" icon={Radio} defaultExpanded>
116
119
  {endpointSlicesLoading ? (
117
120
  <div className="text-sm text-theme-text-tertiary">Loading EndpointSlices…</div>
121
+ ) : endpointSlicesError ? (
122
+ <AlertBanner variant="warning" title="EndpointSlice inventory unavailable" message={endpointSlicesError} />
118
123
  ) : endpointSlices && endpointSlices.length > 0 ? (
119
124
  <div className="space-y-2">
120
125
  {endpointSlices.map((slice: any) => {
@@ -147,6 +152,9 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
147
152
  </button>
148
153
  )
149
154
  })}
155
+ {endpointSlicesTruncated && (
156
+ <div className="text-xs text-theme-text-tertiary">Showing the first {endpointSlices.length} EndpointSlices.</div>
157
+ )}
150
158
  </div>
151
159
  ) : (
152
160
  <div className="text-sm text-theme-text-tertiary">No EndpointSlices found for this Service.</div>
@@ -2,10 +2,11 @@ import { Globe, ArrowRight, Network } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import { Badge } from '../../ui/Badge'
4
4
  import type { ResourceRef } from '../../../types'
5
+ import { gatewayBackendResourceRef, gatewayParentResourceRef } from '../../../utils/gateway-references'
5
6
 
6
7
  interface SimpleRouteRendererProps {
7
8
  data: any
8
- kind: 'TCPRoute' | 'TLSRoute'
9
+ kind: 'TCPRoute' | 'TLSRoute' | 'UDPRoute'
9
10
  onNavigate?: (ref: ResourceRef) => void
10
11
  }
11
12
 
@@ -35,22 +36,8 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
35
36
  ? parentStatuses[0].conditions
36
37
  : undefined
37
38
 
38
- function toGatewayRef(ref: any): ResourceRef {
39
- return {
40
- kind: 'Gateway',
41
- namespace: ref.namespace || routeNs,
42
- name: ref.name,
43
- group: 'gateway.networking.k8s.io',
44
- }
45
- }
46
-
47
- function toServiceRef(backend: any): ResourceRef {
48
- return {
49
- kind: 'Service',
50
- namespace: backend.namespace || routeNs,
51
- name: backend.name,
52
- }
53
- }
39
+ const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
40
+ const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)
54
41
 
55
42
  return (
56
43
  <>
@@ -62,8 +49,8 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
62
49
  const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
63
50
  const gwName = p.parentRef?.name || 'unknown'
64
51
  return cond?.reason
65
- ? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
66
- : `Gateway "${gwName}" has not accepted this route.`
52
+ ? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
53
+ : `Parent "${gwName}" has not accepted this route.`
67
54
  }).join('; ')}
68
55
  />
69
56
  )}
@@ -95,12 +82,12 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
95
82
  />
96
83
  )}
97
84
  <Property
98
- label="Parent Gateways"
85
+ label="Parents"
99
86
  value={
100
87
  parentRefs.length > 0 ? (
101
88
  <div className="flex flex-wrap gap-1">
102
89
  {parentRefs.map((ref: any, i: number) => (
103
- <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
90
+ <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
104
91
  ))}
105
92
  </div>
106
93
  ) : 'None'
@@ -131,7 +118,7 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
131
118
  const pct = hasWeights ? Math.round(((b.weight ?? 1) / totalWeight) * 100) : null
132
119
  return (
133
120
  <span key={bi} className="flex items-center gap-1">
134
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
121
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
135
122
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
136
123
  {pct !== null && (
137
124
  <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>
@@ -1,11 +1,13 @@
1
1
  import { Cpu } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
- import { kindToPlural } from '../../../utils/navigation'
4
+ import { objectReferenceToResourceRef } from '../../../utils/navigation'
5
+ import type { ResourceRef } from '../../../types'
6
+ import { CORE_RESOURCES } from '../../../utils/api-resources'
5
7
 
6
8
  interface VPARendererProps {
7
9
  data: any
8
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
10
+ onNavigate?: (ref: ResourceRef) => void
9
11
  }
10
12
 
11
13
  function formatResource(value: string | undefined): string {
@@ -17,6 +19,16 @@ export function VPARenderer({ data, onNavigate }: VPARendererProps) {
17
19
  const spec = data.spec || {}
18
20
  const status = data.status || {}
19
21
  const targetRef = spec.targetRef || {}
22
+ const namespace = data.metadata?.namespace
23
+ const builtin = !targetRef.apiVersion ? CORE_RESOURCES.find(resource => resource.kind === targetRef.kind) : undefined
24
+ const builtinAPIVersion = builtin
25
+ ? (builtin.group ? `${builtin.group}/${builtin.version}` : builtin.version)
26
+ : targetRef.kind === 'ReplicationController' ? 'v1' : undefined
27
+ const target = objectReferenceToResourceRef({
28
+ ...targetRef,
29
+ apiVersion: targetRef.apiVersion || builtinAPIVersion,
30
+ namespace,
31
+ })
20
32
  const updatePolicy = spec.updatePolicy || {}
21
33
  const resourcePolicy = spec.resourcePolicy || {}
22
34
  const containerPolicies = resourcePolicy.containerPolicies || []
@@ -63,15 +75,9 @@ export function VPARenderer({ data, onNavigate }: VPARendererProps) {
63
75
  <Section title="Configuration" icon={Cpu}>
64
76
  <PropertyList>
65
77
  <Property label="Target" value={
66
- targetRef.name ? (
67
- <ResourceLink
68
- name={targetRef.name}
69
- kind={kindToPlural(targetRef.kind || 'Deployment')}
70
- namespace={data.metadata?.namespace || ''}
71
- label={`${targetRef.kind}/${targetRef.name}`}
72
- onNavigate={onNavigate}
73
- />
74
- ) : undefined
78
+ targetRef.name ? (target ? (
79
+ <ResourceLink {...target} label={`${targetRef.kind}/${targetRef.name}`} onNavigate={onNavigate} />
80
+ ) : `${targetRef.kind || 'Unknown kind'}/${targetRef.name}`) : undefined
75
81
  } />
76
82
  <Property label="Update Mode" value={
77
83
  <span className={clsx(
@@ -9,8 +9,6 @@ import {
9
9
  getBSLRegion,
10
10
  getBSLDefault,
11
11
  getBSLAccessMode,
12
- getBSLLastValidation,
13
- getBSLLastSynced,
14
12
  getBSLMessage,
15
13
  } from '../resource-utils-velero'
16
14
  import { VeleroPhaseValue } from './velero-cells'
@@ -111,8 +109,8 @@ export function VeleroBSLRenderer({ data, storedBackups, storedTotal, restorable
111
109
  <Property label="Phase" value={
112
110
  <VeleroPhaseValue status={bslStatus} phase={status.phase || ''} />
113
111
  } />
114
- <Property label="Last Validation" value={getBSLLastValidation(data)} />
115
- <Property label="Last Synced" value={getBSLLastSynced(data)} />
112
+ <Property label="Last Validation" value={<TimeValue timestamp={data.status?.lastValidationTime} />} />
113
+ <Property label="Last Synced" value={<TimeValue timestamp={data.status?.lastSyncedTime} />} />
116
114
  {status.lastSyncedRevision && (
117
115
  <Property label="Last Synced Revision" value={
118
116
  <span className="text-sm font-mono text-theme-text-secondary break-all">{status.lastSyncedRevision}</span>
@@ -235,18 +233,18 @@ export function VeleroBSLRenderer({ data, storedBackups, storedTotal, restorable
235
233
  recovers. */}
236
234
  {bslStatus.level === 'healthy' ? (
237
235
  <>
238
- Most recent restorable point: <TimeValue timestamp={newest} /> ago
236
+ Most recent restorable point: <TimeValue timestamp={newest} />
239
237
  </>
240
238
  ) : bslStatus.level === 'unhealthy' ? (
241
239
  <>
242
240
  Most recent point held here, once this location is reachable again:{' '}
243
- <TimeValue timestamp={newest} /> ago
241
+ <TimeValue timestamp={newest} />
244
242
  </>
245
243
  ) : (
246
244
  // Unknown: do not imply it is unreachable, and do not call it
247
245
  // restorable either. Say what the timestamp is.
248
246
  <>
249
- Most recent point held here: <TimeValue timestamp={newest} /> ago
247
+ Most recent point held here: <TimeValue timestamp={newest} />
250
248
  </>
251
249
  )}
252
250
  </div>
@@ -26,7 +26,7 @@ import {
26
26
  } from '../resource-utils-velero'
27
27
  import { VeleroPhaseValue } from './velero-cells'
28
28
  import { veleroPhaseLabel } from '../resource-utils-velero'
29
- import { formatAge } from '../resource-utils'
29
+ import { TimeValue } from '../../ui/ScheduleValue'
30
30
 
31
31
  interface VeleroBackupRendererProps {
32
32
  data: any
@@ -140,10 +140,10 @@ export function VeleroBackupRenderer({ data, storageLocationPhase, storageLocati
140
140
  <Property label="Queue Position" value={String(queuePosition)} />
141
141
  )}
142
142
  {status.startTimestamp && (
143
- <Property label="Started" value={formatAge(status.startTimestamp) + ' ago'} />
143
+ <Property label="Started" value={<TimeValue timestamp={status.startTimestamp} />} />
144
144
  )}
145
145
  {status.completionTimestamp && (
146
- <Property label="Completed" value={formatAge(status.completionTimestamp) + ' ago'} />
146
+ <Property label="Completed" value={<TimeValue timestamp={status.completionTimestamp} />} />
147
147
  )}
148
148
  <Property label="Duration" value={getBackupDuration(data)} />
149
149
  <Property label="Expiration" value={getBackupExpiry(data)} />
@@ -6,10 +6,10 @@ import {
6
6
  getBackupRepositoryVolumeNamespace,
7
7
  getBackupRepositoryLocation,
8
8
  getBackupRepositoryMaintenanceFrequency,
9
- getBackupRepositoryLastMaintenance,
10
9
  getBackupRepositoryMessage,
11
10
  } from '../resource-utils-velero'
12
11
  import { VeleroPhaseValue } from './velero-cells'
12
+ import { TimeValue } from '../../ui/ScheduleValue'
13
13
 
14
14
  interface VeleroBackupRepositoryRendererProps {
15
15
  data: any
@@ -52,7 +52,7 @@ export function VeleroBackupRepositoryRenderer({ data, onNavigate }: VeleroBacku
52
52
  last run is far older than its frequency is the shape of a
53
53
  controller that has stopped. The two are shown together so that
54
54
  comparison does not need a second screen. */}
55
- <Property label="Last Maintenance" value={getBackupRepositoryLastMaintenance(data)} />
55
+ <Property label="Last Maintenance" value={<TimeValue timestamp={data.status?.lastMaintenanceTime} />} />
56
56
  <Property label="Maintenance Frequency" value={getBackupRepositoryMaintenanceFrequency(data)} />
57
57
  </PropertyList>
58
58
  </Section>
@@ -22,8 +22,8 @@ import {
22
22
  } from '../resource-utils-velero'
23
23
  import { VeleroPhaseValue } from './velero-cells'
24
24
  import { veleroPhaseLabel } from '../resource-utils-velero'
25
- import { formatAge } from '../resource-utils'
26
25
  import { VeleroRunMessages, type VeleroRunMessagesFetch } from './VeleroRunMessages'
26
+ import { TimeValue } from '../../ui/ScheduleValue'
27
27
 
28
28
  interface VeleroRestoreRendererProps {
29
29
  data: any
@@ -125,10 +125,10 @@ export function VeleroRestoreRenderer({ data, messages, onNavigate }: VeleroRest
125
125
  />
126
126
  } />
127
127
  {status.startTimestamp && (
128
- <Property label="Started" value={formatAge(status.startTimestamp) + ' ago'} />
128
+ <Property label="Started" value={<TimeValue timestamp={status.startTimestamp} />} />
129
129
  )}
130
130
  {status.completionTimestamp && (
131
- <Property label="Completed" value={formatAge(status.completionTimestamp) + ' ago'} />
131
+ <Property label="Completed" value={<TimeValue timestamp={status.completionTimestamp} />} />
132
132
  )}
133
133
  <Property label="Duration" value={getRestoreDuration(data)} />
134
134
  <Property label="Errors" value={
@@ -3,13 +3,13 @@ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, Resour
3
3
  import {
4
4
  getScheduleStatus,
5
5
  getScheduleCron,
6
- getScheduleLastBackup,
7
6
  getSchedulePaused,
8
7
  getScheduleTemplate,
9
8
  getScheduleUseOwnerReferences,
10
9
  getScheduleValidationErrors,
11
10
  } from '../resource-utils-velero'
12
11
  import { VeleroPhaseValue } from './velero-cells'
12
+ import { TimeValue } from '../../ui/ScheduleValue'
13
13
 
14
14
  interface VeleroScheduleRendererProps {
15
15
  data: any
@@ -64,7 +64,7 @@ export function VeleroScheduleRenderer({ data, onNavigate }: VeleroScheduleRende
64
64
  <Property label="Cron Schedule" value={
65
65
  <span className="font-mono text-sm">{getScheduleCron(data)}</span>
66
66
  } />
67
- <Property label="Last Backup" value={getScheduleLastBackup(data)} />
67
+ <Property label="Last Backup" value={<TimeValue timestamp={data.status?.lastBackup} fallback="Never" />} />
68
68
  <Property label="Paused" value={isPaused ? 'Yes' : 'No'} />
69
69
  <Property label="Use Owner References" value={getScheduleUseOwnerReferences(data) ? 'Yes' : 'No'} />
70
70
  </PropertyList>
@@ -4,8 +4,8 @@ import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
6
6
  import { Input } from '../../ui/Input'
7
- import { formatAge } from '../resource-utils'
8
7
  import { SEVERITY_BADGE_COLORS, SEVERITY_BAR_COLORS, SEVERITY_TEXT_COLORS, SEVERITY_ORDER, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
8
+ import { TimeValue } from '../../ui/ScheduleValue'
9
9
 
10
10
  interface VulnerabilityReportRendererProps {
11
11
  data: any
@@ -161,7 +161,7 @@ export function VulnerabilityReportRenderer({ data }: VulnerabilityReportRendere
161
161
  } />
162
162
  )}
163
163
  <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
164
- <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
164
+ <Property label="Scanned" value={<TimeValue timestamp={report.updateTimestamp} />} />
165
165
  </PropertyList>
166
166
  </Section>
167
167
 
@@ -1,9 +1,10 @@
1
1
  import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
- import { formatAge, formatDuration } from '../resource-utils'
4
+ import { formatDuration } from '../resource-utils'
5
5
  import { buildWorkflowExecutionModel, flattenWorkflowExecution, WorkflowExecutionModel, WorkflowExecutionNode, WorkflowTemplateReference } from '../../../utils/workflow-execution'
6
6
  import { SEVERITY_TEXT } from '../../../utils/badge-colors'
7
+ import { TimeValue } from '../../ui/ScheduleValue'
7
8
 
8
9
  interface WorkflowRendererProps {
9
10
  data: any
@@ -145,8 +146,8 @@ export function WorkflowRenderer({ data, onNavigate }: WorkflowRendererProps) {
145
146
  </span>
146
147
  } />
147
148
  {duration && <Property label="Duration" value={duration} />}
148
- {status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
149
- <Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
149
+ {status.startedAt && <Property label="Started" value={<TimeValue timestamp={status.startedAt} />} />}
150
+ <Property label="Finished" value={<TimeValue timestamp={status.finishedAt} fallback="Running..." />} />
150
151
  {workflowTemplateRef && <Property label="Definition" value={<TemplateRefLink refInfo={workflowTemplateRef} onNavigate={onNavigate} />} />}
151
152
  {status.progress && (
152
153
  <Property label="Progress" value={status.progress} />
@@ -71,7 +71,7 @@ export function KyvernoModernPolicyCell({ resource, column }: { resource: any; c
71
71
  </span>
72
72
  )
73
73
  }
74
- return <span className="text-sm text-theme-text-secondary">{formatAge(last)} ago</span>
74
+ return <span className="text-sm text-theme-text-secondary">{formatAge(last)}</span>
75
75
  }
76
76
  case 'schedule': {
77
77
  const schedule = getKyvernoSchedule(resource)
@@ -1,10 +1,10 @@
1
1
  import { useState, useId } from 'react'
2
2
  import { Box } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { formatAge } from '../../resource-utils'
5
4
  import { Collapse, CollapseChevron, disclosurePanelId } from '../../../ui/Collapse'
6
5
  import { revisionRoleBadges } from '../../../shared/ResourceActionsBar'
7
6
  import type { WorkloadRevision, WorkloadPodInfo } from '../../../../types'
7
+ import { formatRelativeAgeTime } from '../../../../utils/format'
8
8
 
9
9
  interface ReplicaSetProgressionProps {
10
10
  revisions: WorkloadRevision[]
@@ -90,7 +90,7 @@ export function ReplicaSetProgression({ revisions, pods, namespace, onNavigate }
90
90
  ))}
91
91
  </div>
92
92
  <span className="shrink-0 text-xs tabular-nums text-theme-text-tertiary">{rev.replicas ?? 0} replicas</span>
93
- <span className="shrink-0 text-xs text-theme-text-tertiary">{formatAge(rev.createdAt)}</span>
93
+ <span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(rev.createdAt)}</span>
94
94
  </button>
95
95
  <Collapse open={isOpen} mountLazily id={disclosurePanelId(panelBase, rev.number)}>
96
96
  <div className="space-y-0.5 border-t border-theme-border/50 px-2 py-1.5 pl-8">
@@ -73,7 +73,7 @@ export function getClusterStatus(resource: any): StatusBadge {
73
73
  }
74
74
 
75
75
  export function getClusterClass(resource: any): string {
76
- return resource.spec?.topology?.class || '-'
76
+ return resource.spec?.topology?.classRef?.name || resource.spec?.topology?.class || '-'
77
77
  }
78
78
 
79
79
  export function getClusterVersion(resource: any): string {