@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
@@ -1,8 +1,8 @@
1
1
  import { Activity, Boxes, Server } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, AlertBanner, ConditionsSection } from '../../ui/drawer-components'
4
- import { formatAge } from '../resource-utils'
5
4
  import { canaryStepTemplateRefs } from './RolloutRenderer'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface ExperimentRendererProps {
8
8
  data: any
@@ -72,7 +72,7 @@ export function ExperimentRenderer({ data, onNavigate }: ExperimentRendererProps
72
72
  <Property label="Phase" value={<span className={clsx('badge', phaseClass(phase))}>{phase}</span>} />
73
73
  <Property label="Duration" value={spec.duration} />
74
74
  <Property label="Message" value={status.message} />
75
- <Property label="Available At" value={status.availableAt ? formatAge(status.availableAt) : undefined} />
75
+ <Property label="Available At" value={status.availableAt ? <TimeValue timestamp={status.availableAt} /> : undefined} />
76
76
  {ownerRollout && (
77
77
  <Property
78
78
  label="Rollout"
@@ -3,10 +3,10 @@ import { KeyRound } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
6
- import { formatAge } from '../resource-utils'
7
6
  import { SEVERITY_BADGE_COLORS, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
8
7
  import { pluralize } from '../../../utils/pluralize'
9
8
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
9
+ import { TimeValue } from '../../ui/ScheduleValue'
10
10
 
11
11
  interface ExposedSecretReportRendererProps {
12
12
  data: any
@@ -45,7 +45,7 @@ export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendere
45
45
  <Property label="Container" value={containerName} />
46
46
  <Property label="Image" value={image} />
47
47
  <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
48
- <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
48
+ <Property label="Scanned" value={<TimeValue timestamp={report.updateTimestamp} />} />
49
49
  </PropertyList>
50
50
  </Section>
51
51
 
@@ -1,10 +1,10 @@
1
1
  import { KeyRound, RefreshCw, CloudCog } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
3
+ import { TimeValue } from '../../ui/ScheduleValue'
3
4
  import {
4
5
  getExternalSecretStatus,
5
6
  getExternalSecretStore,
6
7
  getExternalSecretRefreshInterval,
7
- getExternalSecretLastSync,
8
8
  getExternalSecretTargetName,
9
9
  getExternalSecretTargetCreationPolicy,
10
10
  getExternalSecretTargetDeletionPolicy,
@@ -47,7 +47,7 @@ export function ExternalSecretRenderer({ data, onNavigate }: ExternalSecretRende
47
47
  <Section title="Sync Status" icon={RefreshCw} defaultExpanded>
48
48
  <PropertyList>
49
49
  <Property label="Status" value={esStatus.text} />
50
- <Property label="Last Sync" value={getExternalSecretLastSync(data)} />
50
+ <Property label="Last Sync" value={<TimeValue timestamp={data.status?.refreshTime} />} />
51
51
  <Property label="Refresh Interval" value={getExternalSecretRefreshInterval(data)} />
52
52
  <Property label="Target Secret" value={(() => {
53
53
  const targetName = getExternalSecretTargetName(data)
@@ -1,9 +1,9 @@
1
1
  import { Package, Settings, CheckCircle2, History, FileCode2, GitBranch } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
4
- import { formatAge } from '../resource-utils'
5
4
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
6
5
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
+ import { TimeValue } from '../../ui/ScheduleValue'
7
7
 
8
8
  interface FluxHelmReleaseRendererProps {
9
9
  data: any
@@ -249,13 +249,13 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
249
249
  {lastRelease.firstDeployed && (
250
250
  <Property
251
251
  label="First Deployed"
252
- value={formatAge(lastRelease.firstDeployed)}
252
+ value={<TimeValue timestamp={lastRelease.firstDeployed} />}
253
253
  />
254
254
  )}
255
255
  {lastRelease.lastDeployed && (
256
256
  <Property
257
257
  label="Last Deployed"
258
- value={formatAge(lastRelease.lastDeployed)}
258
+ value={<TimeValue timestamp={lastRelease.lastDeployed} />}
259
259
  />
260
260
  )}
261
261
  </PropertyList>
@@ -319,7 +319,7 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
319
319
  {status.lastHandledReconcileAt && (
320
320
  <Property
321
321
  label="Last Reconciled"
322
- value={formatAge(status.lastHandledReconcileAt)}
322
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
323
323
  />
324
324
  )}
325
325
  </PropertyList>
@@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network, Filter } 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 GRPCRouteRendererProps {
7
8
  data: any
@@ -32,22 +33,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
32
33
  ? parentStatuses[0].conditions
33
34
  : undefined
34
35
 
35
- function toGatewayRef(ref: any): ResourceRef {
36
- return {
37
- kind: 'Gateway',
38
- namespace: ref.namespace || routeNs,
39
- name: ref.name,
40
- group: 'gateway.networking.k8s.io',
41
- }
42
- }
43
-
44
- function toServiceRef(backend: any): ResourceRef {
45
- return {
46
- kind: 'Service',
47
- namespace: backend.namespace || routeNs,
48
- name: backend.name,
49
- }
50
- }
36
+ const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
37
+ const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)
51
38
 
52
39
  return (
53
40
  <>
@@ -59,8 +46,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
59
46
  const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
60
47
  const gwName = p.parentRef?.name || 'unknown'
61
48
  return cond?.reason
62
- ? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
63
- : `Gateway "${gwName}" has not accepted this route.`
49
+ ? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
50
+ : `Parent "${gwName}" has not accepted this route.`
64
51
  }).join('; ')}
65
52
  />
66
53
  )}
@@ -90,12 +77,12 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
90
77
  }
91
78
  />
92
79
  <Property
93
- label="Parent Gateways"
80
+ label="Parents"
94
81
  value={
95
82
  parentRefs.length > 0 ? (
96
83
  <div className="flex flex-wrap gap-1">
97
84
  {parentRefs.map((ref: any, i: number) => (
98
- <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
85
+ <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
99
86
  ))}
100
87
  </div>
101
88
  ) : 'None'
@@ -129,7 +116,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
129
116
  <div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
130
117
  <span className="italic text-theme-text-tertiary">all</span>
131
118
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
132
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
119
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
133
120
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
134
121
  </div>
135
122
  )}
@@ -141,7 +128,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
141
128
  return (
142
129
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
143
130
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
144
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
131
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
145
132
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
146
133
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
147
134
  </div>
@@ -176,7 +163,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
176
163
  {matchIdx === 0 && backendRefs.length === 1 && (
177
164
  <>
178
165
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
179
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
166
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
180
167
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
181
168
  </>
182
169
  )}
@@ -189,7 +176,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
189
176
  return (
190
177
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
191
178
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
192
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
179
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
193
180
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
194
181
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
195
182
  </div>
@@ -0,0 +1,63 @@
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 { HTTPRouteRenderer } from './HTTPRouteRenderer'
6
+ import { GRPCRouteRenderer } from './GRPCRouteRenderer'
7
+ import { SimpleRouteRenderer } from './SimpleRouteRenderer'
8
+
9
+ const backend = { name: 'custom-backend', kind: 'Widget', group: 'relationships.radar.test', namespace: 'backends' }
10
+ const parent = { name: 'mesh-parent', kind: 'Service', group: '', namespace: 'mesh' }
11
+
12
+ function routeData(kind: string) {
13
+ return {
14
+ metadata: { name: 'route', namespace: 'app' },
15
+ status: { parents: [{ parentRef: parent, conditions: [{ type: 'Accepted', status: 'False', reason: 'Rejected' }] }] },
16
+ spec: {
17
+ parentRefs: [parent],
18
+ rules: [{
19
+ backendRefs: [backend, { name: 'default-service', port: 8080 }],
20
+ ...(kind === 'HTTPRoute' ? { filters: [{ type: 'RequestMirror', requestMirror: { backendRef: { ...backend, name: 'mirror' } } }] } : {}),
21
+ }],
22
+ },
23
+ }
24
+ }
25
+
26
+ describe('Gateway route reference navigation', () => {
27
+ it.each(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute'] as const)('preserves backend and parent identities in %s', async kind => {
28
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
29
+ const container = document.createElement('div')
30
+ const root = createRoot(container)
31
+ const onNavigate = vi.fn()
32
+ const data = routeData(kind)
33
+ const click = async (name: string) => {
34
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent?.includes(name))
35
+ expect(button, `button ${name}`).toBeDefined()
36
+ await act(async () => button!.click())
37
+ }
38
+ try {
39
+ await act(async () => root.render(
40
+ kind === 'HTTPRoute' ? <HTTPRouteRenderer data={data} onNavigate={onNavigate} />
41
+ : kind === 'GRPCRoute' ? <GRPCRouteRenderer data={data} onNavigate={onNavigate} />
42
+ : <SimpleRouteRenderer kind={kind} data={data} onNavigate={onNavigate} />,
43
+ ))
44
+ expect(container.textContent).toContain('Parents')
45
+ expect(container.textContent).toContain('Parent "mesh-parent"')
46
+ expect(container.textContent).not.toContain('Gateway "mesh-parent"')
47
+
48
+ await click('custom-backend')
49
+ expect(onNavigate).toHaveBeenLastCalledWith(backend)
50
+ await click('default-service')
51
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Service', group: '', namespace: 'app', name: 'default-service' })
52
+ await click('mesh-parent')
53
+ expect(onNavigate).toHaveBeenLastCalledWith(parent)
54
+ if (kind === 'HTTPRoute') {
55
+ await click('mirror')
56
+ expect(onNavigate).toHaveBeenLastCalledWith({ ...backend, name: 'mirror' })
57
+ }
58
+ } finally {
59
+ await act(async () => root.unmount())
60
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
61
+ }
62
+ })
63
+ })
@@ -1,9 +1,9 @@
1
1
  import { GitBranch, FolderGit, CheckCircle2 } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
3
- import { formatAge } from '../resource-utils'
4
3
  import { formatBytes } from '../../../utils/format'
5
4
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
6
5
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
+ import { TimeValue } from '../../ui/ScheduleValue'
7
7
 
8
8
  interface GitRepositoryRendererProps {
9
9
  data: any
@@ -87,7 +87,7 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
87
87
  <Property label="Digest" value={artifact.digest} />
88
88
  <Property
89
89
  label="Last Updated"
90
- value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
90
+ value={<TimeValue timestamp={artifact.lastUpdateTime} />}
91
91
  />
92
92
  {artifact.size && (
93
93
  <Property label="Size" value={formatBytes(artifact.size)} />
@@ -104,7 +104,7 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
104
104
  {status.lastHandledReconcileAt && (
105
105
  <Property
106
106
  label="Last Reconciled"
107
- value={formatAge(status.lastHandledReconcileAt)}
107
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
108
108
  />
109
109
  )}
110
110
  </PropertyList>
@@ -1,16 +1,17 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { Activity, Cpu } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
4
+ import { Section, PropertyList, Property, ConditionsSection, ResourceLink, LabelSelectorDisplay, type ConditionTone } from '../../ui/drawer-components'
5
5
  import { Badge } from '../../ui/Badge'
6
- import { kindToPlural } from '../../../utils/navigation'
7
- import { formatAge } from '../resource-utils'
6
+ import { objectReferenceToResourceRef, apiVersionToGroup } from '../../../utils/navigation'
7
+ import type { ResourceRef } from '../../../types'
8
+ import { TimeValue } from '../../ui/ScheduleValue'
8
9
  import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
9
10
  import type { HPADiagnosis } from '../../../types'
10
11
 
11
12
  interface HPARendererProps {
12
13
  data: any
13
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
14
+ onNavigate?: (ref: ResourceRef) => void
14
15
  hpaDiagnosis?: HPADiagnosis
15
16
  /** Optional host-provided section rendered after Conditions — used to inject Prometheus-backed charts. */
16
17
  extraSections?: ReactNode
@@ -37,6 +38,11 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
37
38
  const status = data.status || {}
38
39
  const spec = data.spec || {}
39
40
  const metrics = status.currentMetrics || []
41
+ const namespace = data.metadata?.namespace
42
+ const targetRef = spec.scaleTargetRef || {}
43
+ const target = objectReferenceToResourceRef({ ...targetRef, namespace })
44
+ const hasDiagnosisMetrics = !!hpaDiagnosis?.metrics?.length
45
+ const objectMetrics = (spec.metrics || []).filter((metric: any) => metric.type === 'Object' && metric.object)
40
46
 
41
47
  return (
42
48
  <>
@@ -49,32 +55,26 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
49
55
  <Section title="Scaling" icon={Cpu}>
50
56
  <PropertyList>
51
57
  <Property label="Target" value={
52
- spec.scaleTargetRef?.name ? (
53
- <ResourceLink
54
- name={spec.scaleTargetRef.name}
55
- kind={kindToPlural(spec.scaleTargetRef.kind || 'Deployment')}
56
- namespace={data.metadata?.namespace || ''}
57
- label={`${spec.scaleTargetRef.kind}/${spec.scaleTargetRef.name}`}
58
- onNavigate={onNavigate}
59
- />
60
- ) : undefined
58
+ targetRef.name ? (target ? (
59
+ <ResourceLink {...target} label={`${targetRef.kind}/${targetRef.name}`} onNavigate={onNavigate} />
60
+ ) : `${targetRef.kind || 'Unknown kind'}/${targetRef.name}`) : undefined
61
61
  } />
62
62
  <Property label="Current" value={status.currentReplicas} />
63
63
  <Property label="Desired" value={status.desiredReplicas} />
64
64
  <Property label="Min" value={spec.minReplicas ?? 1} />
65
65
  <Property label="Max" value={spec.maxReplicas} />
66
- {status.lastScaleTime && <Property label="Last Scale" value={formatAge(status.lastScaleTime)} />}
66
+ {status.lastScaleTime && <Property label="Last Scale" value={<TimeValue timestamp={status.lastScaleTime} />} />}
67
67
  </PropertyList>
68
68
  </Section>
69
69
 
70
70
  {hpaDiagnosis?.metrics && hpaDiagnosis.metrics.length > 0 && (
71
71
  <Section title="Metrics" defaultExpanded>
72
72
  <div className="space-y-2">
73
- {hpaDiagnosis.metrics.map((metric) => (
74
- <div key={`${metric.type}-${metric.name}`} className="card-inner">
73
+ {hpaDiagnosis.metrics.map((metric, index) => (
74
+ <div key={`${metric.type}-${metric.name}-${index}`} className="card-inner">
75
75
  <div className="flex flex-wrap items-center justify-between gap-2 text-sm">
76
76
  <div>
77
- <div className="font-medium text-theme-text-primary">{metric.name}</div>
77
+ <div className="font-medium text-theme-text-primary break-words">{spec.metrics?.[index]?.type === 'Object' ? spec.metrics[index].object.metric.name : metric.name}</div>
78
78
  <div className="text-xs text-theme-text-tertiary">{metric.type}</div>
79
79
  </div>
80
80
  <div className="flex flex-wrap items-center gap-2 text-xs text-theme-text-secondary">
@@ -85,6 +85,11 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
85
85
  </Badge>
86
86
  </div>
87
87
  </div>
88
+ {spec.metrics?.[index]?.type === 'Object' && (
89
+ <div className="mt-2">
90
+ <HPAObjectMetricSource object={spec.metrics[index].object} namespace={namespace} onNavigate={onNavigate} />
91
+ </div>
92
+ )}
88
93
  </div>
89
94
  ))}
90
95
  </div>
@@ -121,9 +126,44 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
121
126
  </Section>
122
127
  )}
123
128
 
129
+ {!hasDiagnosisMetrics && objectMetrics.length > 0 && (
130
+ <Section title="Object metric sources" defaultExpanded>
131
+ <div className="space-y-3">
132
+ {objectMetrics.map((metric: any, i: number) => (
133
+ <div key={`${metric.object.metric.name}-${i}`} className="card-inner">
134
+ <HPAObjectMetricSource object={metric.object} namespace={namespace} onNavigate={onNavigate} showMetric />
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </Section>
139
+ )}
140
+
124
141
  <ConditionsSection conditions={status.conditions} getConditionTone={hpaConditionTone} />
125
142
 
126
143
  {extraSections}
127
144
  </>
128
145
  )
129
146
  }
147
+
148
+ function HPAObjectMetricSource({ object, namespace, onNavigate, showMetric = false }: {
149
+ object: any
150
+ namespace?: string
151
+ onNavigate?: (ref: ResourceRef) => void
152
+ showMetric?: boolean
153
+ }) {
154
+ const described = object.describedObject || {}
155
+ const namespaceMetric = described.kind === 'Namespace' && apiVersionToGroup(described.apiVersion) === ''
156
+ const name = namespaceMetric ? namespace : described.name
157
+ const source = objectReferenceToResourceRef({ ...described, name, namespace })
158
+ return (
159
+ <PropertyList>
160
+ {showMetric && <Property label="Metric" value={object.metric.name} />}
161
+ <Property label="Object" value={source ? (
162
+ <ResourceLink {...source} label={`${described.kind}/${name}`} onNavigate={onNavigate} />
163
+ ) : described.name ? `${described.kind || 'Unknown kind'}/${described.name}` : undefined} />
164
+ {source?.group && <Property label="API Group" value={source.group} />}
165
+ {object.metric.selector && <Property label="Selector" value={<LabelSelectorDisplay selector={object.metric.selector} />} />}
166
+ {namespaceMetric && <Property label="Scope" value="This HPA's namespace" />}
167
+ </PropertyList>
168
+ )
169
+ }
@@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network, Filter } 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 HTTPRouteRendererProps {
7
8
  data: any
@@ -32,22 +33,8 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
32
33
  ? parentStatuses[0].conditions
33
34
  : undefined
34
35
 
35
- function toGatewayRef(ref: any): ResourceRef {
36
- return {
37
- kind: 'Gateway',
38
- namespace: ref.namespace || routeNs,
39
- name: ref.name,
40
- group: 'gateway.networking.k8s.io',
41
- }
42
- }
43
-
44
- function toServiceRef(backend: any): ResourceRef {
45
- return {
46
- kind: 'Service',
47
- namespace: backend.namespace || routeNs,
48
- name: backend.name,
49
- }
50
- }
36
+ const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
37
+ const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)
51
38
 
52
39
  return (
53
40
  <>
@@ -59,8 +46,8 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
59
46
  const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
60
47
  const gwName = p.parentRef?.name || 'unknown'
61
48
  return cond?.reason
62
- ? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
63
- : `Gateway "${gwName}" has not accepted this route.`
49
+ ? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
50
+ : `Parent "${gwName}" has not accepted this route.`
64
51
  }).join('; ')}
65
52
  />
66
53
  )}
@@ -90,12 +77,12 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
90
77
  }
91
78
  />
92
79
  <Property
93
- label="Parent Gateways"
80
+ label="Parents"
94
81
  value={
95
82
  parentRefs.length > 0 ? (
96
83
  <div className="flex flex-wrap gap-1">
97
84
  {parentRefs.map((ref: any, i: number) => (
98
- <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
85
+ <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
99
86
  ))}
100
87
  </div>
101
88
  ) : 'None'
@@ -129,7 +116,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
129
116
  <div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
130
117
  <span className="italic text-theme-text-tertiary">all</span>
131
118
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
132
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
119
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
133
120
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
134
121
  </div>
135
122
  )}
@@ -141,7 +128,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
141
128
  return (
142
129
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
143
130
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
144
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
131
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
145
132
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
146
133
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
147
134
  </div>
@@ -176,7 +163,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
176
163
  {matchIdx === 0 && backendRefs.length === 1 && (
177
164
  <>
178
165
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
179
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
166
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
180
167
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
181
168
  </>
182
169
  )}
@@ -189,7 +176,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
189
176
  return (
190
177
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
191
178
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
192
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
179
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
193
180
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
194
181
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
195
182
  </div>
@@ -234,7 +221,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
234
221
  )}
235
222
  {filter.type === 'RequestMirror' && filter.requestMirror && (
236
223
  <span className="text-theme-text-tertiary">
237
- {filter.requestMirror.backendRef?.name || 'unknown'}
224
+ {filter.requestMirror.backendRef && <ResourceRefBadge resourceRef={toBackendRef(filter.requestMirror.backendRef)} onClick={onNavigate} />}
238
225
  {filter.requestMirror.backendRef?.port ? `:${filter.requestMirror.backendRef.port}` : ''}
239
226
  </span>
240
227
  )}
@@ -1,10 +1,10 @@
1
1
  import { Database, CheckCircle2, Shield } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
3
3
  import { Badge } from '../../ui/Badge'
4
- import { formatAge } from '../resource-utils'
5
4
  import { formatBytes } from '../../../utils/format'
6
5
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
7
6
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
7
+ import { TimeValue } from '../../ui/ScheduleValue'
8
8
 
9
9
  interface HelmRepositoryRendererProps {
10
10
  data: any
@@ -98,7 +98,7 @@ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
98
98
  <Property label="Digest" value={artifact.digest} />
99
99
  <Property
100
100
  label="Last Updated"
101
- value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
101
+ value={<TimeValue timestamp={artifact.lastUpdateTime} />}
102
102
  />
103
103
  {artifact.size && (
104
104
  <Property label="Size" value={formatBytes(artifact.size)} />
@@ -115,7 +115,7 @@ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
115
115
  {status.lastHandledReconcileAt && (
116
116
  <Property
117
117
  label="Last Reconciled"
118
- value={formatAge(status.lastHandledReconcileAt)}
118
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
119
119
  />
120
120
  )}
121
121
  </PropertyList>
@@ -0,0 +1,52 @@
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 { IngressRenderer } from './IngressRenderer'
6
+ import { IngressClassRenderer } from './IngressClassRenderer'
7
+ import { ingressClassParametersResourceRef } from '../../../utils/ingress-class-references'
8
+
9
+ describe('ingressClassParametersResourceRef', () => {
10
+ it('defaults to the core group and cluster scope, and requires a namespace for namespaced parameters', () => {
11
+ expect(ingressClassParametersResourceRef({ kind: 'IngressClassParams', apiGroup: 'elbv2.k8s.aws', name: 'shared' }))
12
+ .toEqual({ kind: 'IngressClassParams', group: 'elbv2.k8s.aws', namespace: '', name: 'shared' })
13
+ expect(ingressClassParametersResourceRef({ kind: 'ConfigMap', scope: 'Namespace', namespace: 'controller', name: 'settings' }))
14
+ .toEqual({ kind: 'ConfigMap', group: '', namespace: 'controller', name: 'settings' })
15
+ expect(ingressClassParametersResourceRef({ kind: 'ConfigMap', scope: 'Namespace', name: 'settings' })).toBeNull()
16
+ })
17
+ })
18
+
19
+ describe('Ingress class navigation', () => {
20
+ it('links the Ingress class and both parameter scopes, but leaves the legacy annotation as text', async () => {
21
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
22
+ const container = document.createElement('div')
23
+ const root = createRoot(container)
24
+ const onNavigate = vi.fn()
25
+ const click = async (name: string) => {
26
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === name)
27
+ expect(button).toBeDefined()
28
+ await act(async () => button!.click())
29
+ }
30
+ try {
31
+ await act(async () => root.render(<IngressRenderer onNavigate={onNavigate} data={{ metadata: { namespace: 'app', annotations: { 'kubernetes.io/ingress.class': 'old-controller' } }, spec: { ingressClassName: 'public' } }} />))
32
+ await click('public')
33
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'ingressclasses', group: 'networking.k8s.io', namespace: '', name: 'public' })
34
+
35
+ await act(async () => root.render(<IngressRenderer onNavigate={onNavigate} data={{ metadata: { annotations: { 'kubernetes.io/ingress.class': 'old-controller' } }, spec: {} }} />))
36
+ expect(container.textContent).toContain('old-controller')
37
+ expect([...container.querySelectorAll('button')].some(button => button.textContent === 'old-controller')).toBe(false)
38
+
39
+ for (const parameters of [
40
+ { name: 'shared', kind: 'IngressClassParams', apiGroup: 'elbv2.k8s.aws' },
41
+ { name: 'settings', kind: 'ConfigMap', scope: 'Namespace' as const, namespace: 'controller' },
42
+ ]) {
43
+ await act(async () => root.render(<IngressClassRenderer onNavigate={onNavigate} data={{ spec: { controller: 'example.test/ingress', parameters } }} />))
44
+ await click(parameters.name)
45
+ expect(onNavigate).toHaveBeenLastCalledWith(ingressClassParametersResourceRef(parameters))
46
+ }
47
+ } finally {
48
+ await act(async () => root.unmount())
49
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
50
+ }
51
+ })
52
+ })
@@ -1,14 +1,18 @@
1
1
  import { Globe } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ResourceLink } from '../../ui/drawer-components'
4
+ import type { ResourceRef } from '../../../types'
5
+ import { ingressClassParametersResourceRef } from '../../../utils/ingress-class-references'
4
6
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
5
7
 
6
8
  interface IngressClassRendererProps {
7
9
  data: any
10
+ onNavigate?: (ref: ResourceRef) => void
8
11
  }
9
12
 
10
- export function IngressClassRenderer({ data }: IngressClassRendererProps) {
13
+ export function IngressClassRenderer({ data, onNavigate }: IngressClassRendererProps) {
11
14
  const spec = data.spec || {}
15
+ const parametersRef = spec.parameters ? ingressClassParametersResourceRef(spec.parameters) : null
12
16
  const annotations = data.metadata?.annotations || {}
13
17
 
14
18
  const isDefault = annotations['ingressclass.kubernetes.io/is-default-class'] === 'true'
@@ -39,7 +43,10 @@ export function IngressClassRenderer({ data }: IngressClassRendererProps) {
39
43
  <PropertyList>
40
44
  {spec.parameters.apiGroup && <Property label="API Group" value={spec.parameters.apiGroup} />}
41
45
  <Property label="Kind" value={spec.parameters.kind} />
42
- <Property label="Name" value={spec.parameters.name} />
46
+ <Property label="Name" value={parametersRef
47
+ ? <ResourceLink {...parametersRef} onNavigate={onNavigate} />
48
+ : spec.parameters.name
49
+ } />
43
50
  {spec.parameters.namespace && <Property label="Namespace" value={spec.parameters.namespace} />}
44
51
  {spec.parameters.scope && <Property label="Scope" value={spec.parameters.scope} />}
45
52
  </PropertyList>