@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,3 +1,4 @@
1
+ import { dedupeResourceRefs } from '../../utils/resource-refs'
1
2
  import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
2
3
  export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
3
4
  import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
@@ -37,6 +38,7 @@ import {
37
38
  } from 'lucide-react'
38
39
  import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
39
40
  import type { GitOpsStatus } from '../../types/gitops'
41
+ import { isFetchError } from '../../types/fetch-error'
40
42
  import type { NavigateToResource } from '../../utils/navigation'
41
43
  import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
42
44
  import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
@@ -76,6 +78,7 @@ import { ServicePortCards, type ServicePortRenderProps } from '../resources/rend
76
78
  import { rolloutMayAdvanceAutomatically, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
77
79
  import { WorkloadRolloutNotice } from './WorkloadRolloutNotice'
78
80
  import { isCoreBatchJob } from '../../utils/api-resources'
81
+ import { formatRelativeAgeTime } from '../../utils/format'
79
82
 
80
83
  export type WorkloadTabType = 'overview' | 'spec' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
81
84
  type TabType = WorkloadTabType
@@ -87,6 +90,8 @@ export interface WorkloadExtraTab {
87
90
  icon?: ReactNode
88
91
  after?: WorkloadTabType
89
92
  replaces?: WorkloadTabType
93
+ /** Trailing adornment after the label, e.g. a status mark. */
94
+ badge?: ReactNode
90
95
  render: () => ReactNode
91
96
  }
92
97
 
@@ -142,6 +147,23 @@ interface WorkloadViewProps {
142
147
  * the Escape shortcut.
143
148
  */
144
149
  breadcrumb?: ReactNode
150
+ /** Where the object lives, shown on the title line before its name (e.g. "Workspace / View /"). */
151
+ titlePrefix?: ReactNode
152
+ /**
153
+ * Kind, status and namespace on the title line, wrapping below the name
154
+ * only when there is no room; other chips (image, owners) keep a second
155
+ * row. The header actions then wrap below the title as a group rather
156
+ * than squeezing it.
157
+ */
158
+ inlineBadges?: boolean
159
+ /** A note right after the namespace, e.g. that it is outside the namespace filter. */
160
+ namespaceNote?: ReactNode
161
+ /** Replaces the derived status badge on the title line, e.g. with the controller's own reported phase. */
162
+ renderStatusBadge?: (resource: any) => ReactNode
163
+ /** A short status right after the controller status on the title line, e.g. a derived health verdict. */
164
+ statusNote?: ReactNode
165
+ /** Leave out the kind badge, for a host whose title prefix already names the kind. */
166
+ hideKindBadge?: boolean
145
167
  /** Suppress the standalone back arrow — for embeddings where "back" has no
146
168
  * meaningful target (a single-workload app has no app graph to return to). */
147
169
  hideBackButton?: boolean
@@ -281,6 +303,8 @@ interface WorkloadViewProps {
281
303
  * resource rather than from owner labels on a managed object.
282
304
  */
283
305
  onNavigateGitOpsPath?: (path: string) => void
306
+ /** Opens the live traffic view focused on this workload. */
307
+ onOpenLiveTraffic?: () => void
284
308
 
285
309
  // ── Render props for platform-specific content ───────────────────────────
286
310
  /** Render the logs tab content */
@@ -314,6 +338,14 @@ interface WorkloadViewProps {
314
338
  }) => ReactNode
315
339
  /** Extra tabs for the expanded view (e.g. a domain's own sections). */
316
340
  extraTabs?: WorkloadExtraTab[]
341
+ /** Expanded view: a line under the title, above the tabs, shown on every tab. */
342
+ subheader?: ReactNode
343
+ /** Expanded view: tab ids in display order; tabs not listed follow in their usual order. */
344
+ tabOrder?: string[]
345
+ /** Expanded view: relabel, lead, or replace the whole "Spec & status" body. */
346
+ specTab?: { label?: string; icon?: ReactNode; lead?: ReactNode; render?: () => ReactNode }
347
+ /** Domain actions rendered in the header (drawer and expanded). */
348
+ renderHeaderActions?: (props: { resource: any; context: 'drawer' | 'expanded'; onNavigate?: NavigateToResource }) => ReactNode
317
349
  /** Render a full replacement for the expanded Overview tab. */
318
350
  renderExpandedOverview?: (props: {
319
351
  kind: string
@@ -404,6 +436,12 @@ export function WorkloadView({
404
436
  initialTab,
405
437
  group,
406
438
  breadcrumb,
439
+ titlePrefix,
440
+ inlineBadges = false,
441
+ namespaceNote,
442
+ renderStatusBadge,
443
+ statusNote,
444
+ hideKindBadge = false,
407
445
  hideBackButton,
408
446
  scopeControls,
409
447
  compactHeader,
@@ -464,6 +502,10 @@ export function WorkloadView({
464
502
  renderExpandedOverview,
465
503
  renderSummary,
466
504
  extraTabs,
505
+ tabOrder,
506
+ specTab,
507
+ subheader,
508
+ renderHeaderActions,
467
509
  renderRelatedYaml,
468
510
  renderMetricsTab,
469
511
  renderCostTab,
@@ -494,6 +536,7 @@ export function WorkloadView({
494
536
  helmOwnerSource,
495
537
  onOpenHelmRelease,
496
538
  onNavigateGitOpsPath,
539
+ onOpenLiveTraffic,
497
540
  }: WorkloadViewProps) {
498
541
  // The live object preserves exact CRD capitalization (RayJob, TFJob); the URL
499
542
  // plural remains the API resource name used by fetches and mutations.
@@ -688,6 +731,11 @@ export function WorkloadView({
688
731
  setTimeout(() => setCopied(null), 2000)
689
732
  }, [])
690
733
 
734
+ // The edit is applied on top of the copy on screen, so the server is asked to
735
+ // reject it if the Secret was deleted or changed since that copy loaded.
736
+ // Otherwise the save would recreate a deleted Secret, or put back the old
737
+ // value of a key someone else changed.
738
+ const resourceVersion: string | undefined = resource?.metadata?.resourceVersion
691
739
  const handleSaveSecretValue = useCallback(async (yaml: string) => {
692
740
  if (!onUpdateResource) return
693
741
  try {
@@ -697,12 +745,14 @@ export function WorkloadView({
697
745
  name,
698
746
  yaml,
699
747
  force: true,
748
+ reviewedResourceVersion: resourceVersion,
700
749
  })
701
750
  setTimeout(() => refetch(), 1000)
702
- } catch {
703
- // Error handled by mutation (toast)
751
+ } catch (error) {
752
+ refetch()
753
+ throw error
704
754
  }
705
- }, [onUpdateResource, apiKind, namespace, name, refetch])
755
+ }, [onUpdateResource, apiKind, namespace, name, resourceVersion, refetch])
706
756
 
707
757
  const handleSaved = useCallback(() => {
708
758
  setSaveSuccess(true)
@@ -765,7 +815,35 @@ export function WorkloadView({
765
815
  ? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
766
816
  : null
767
817
  const rolloutActivity = rolloutDisplay?.activity ?? null
768
- const status = rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
818
+ // A 404 with a cached copy: the object was deleted while this view was
819
+ // open. Everything below that copy describes the past.
820
+ const resourceGone = Boolean(resource) && isFetchError(resourceError) && resourceError.status === 404
821
+ const status = resourceGone
822
+ ? { text: 'Deleted', color: 'status-unknown' }
823
+ : rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
824
+ // The host's actions, and the host's renderers with their per-container
825
+ // terminal, logs, scale and env controls, would act on an object that no
826
+ // longer exists. The plain renderers show the last copy without them.
827
+ const actionsBarPropsForState = resourceGone ? undefined : effectiveActionsBarProps
828
+ const rendererOverridesForState = resourceGone ? undefined : rendererOverrides
829
+ // The host's overview panels and Reachability tab diagnose the live object,
830
+ // and the reachability panel polls for it every few seconds.
831
+ const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
832
+ const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
833
+ const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
834
+ // A host's summary, tabs and subheader can hold actions on the live object.
835
+ const renderSummaryForState = resourceGone ? undefined : renderSummary
836
+ const extraTabsForState = resourceGone ? undefined : extraTabs
837
+ // A remount closes any dialog the bar had open: once its actions are gone,
838
+ // a confirm button left on screen would do nothing.
839
+ const actionsBarKey = resourceGone ? 'gone' : 'live'
840
+ const deletedNotice = resourceGone ? (
841
+ <DeletedResourceNotice
842
+ kindLabel={displayKindName(apiKind, resource?.kind)}
843
+ owner={relationships?.deployment ?? relationships?.owner}
844
+ onNavigateToResource={onNavigateToResource}
845
+ />
846
+ ) : null
769
847
  const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
770
848
  useEffect(() => {
771
849
  if (!recentImageSave) return
@@ -778,7 +856,9 @@ export function WorkloadView({
778
856
  const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
779
857
  return () => window.clearTimeout(timeout)
780
858
  }, [recentImageSave, rolloutActivity?.phase])
781
- useProgressiveRefresh(rolloutMayAutoAdvance || recentImageSave, refetchProp)
859
+ // The rollout state above is the last copy fetched before a 404; a workload
860
+ // the server says is gone has nothing left to roll out.
861
+ useProgressiveRefresh((rolloutMayAutoAdvance || recentImageSave) && !resourceGone, refetchProp)
782
862
  const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
783
863
  const headerImage = metadata.find(m => m.label === 'Image')?.value
784
864
 
@@ -790,7 +870,8 @@ export function WorkloadView({
790
870
  const renderDiagnose = actionsBarProps?.renderDiagnose as
791
871
  | ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
792
872
  | undefined
793
- const diagnoseAction = renderDiagnose?.({
873
+ const headerActions = resource && !resourceGone && renderHeaderActions?.({ resource, context: expanded ? 'expanded' : 'drawer', onNavigate: onNavigateToResource })
874
+ const diagnoseAction = resourceGone ? null : renderDiagnose?.({
794
875
  kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
795
876
  group,
796
877
  namespace,
@@ -809,11 +890,11 @@ export function WorkloadView({
809
890
  const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
810
891
  const requestedTab: TabType = activeTab
811
892
  const summaryContext = { kind, apiKind, namespace, name, group, resource, onNavigate: onNavigateToResource }
812
- const expandedSummary = expanded && resource ? renderSummary?.({ ...summaryContext, context: 'expanded' }) ?? null : null
813
- const drawerSummary = !expanded && resource ? renderSummary?.({ ...summaryContext, context: 'drawer' }) ?? null : null
893
+ const expandedSummary = expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'expanded' }) ?? null : null
894
+ const drawerSummary = !expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'drawer' }) ?? null : null
814
895
  const tabs: DetailShellTab<TabType>[] = [
815
896
  { id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
816
- { id: 'spec', label: 'Spec & status', icon: <FileText className="w-4 h-4" />, hidden: !expandedSummary },
897
+ { id: 'spec', label: specTab?.label ?? 'Spec & status', icon: specTab?.icon ?? <FileText className="w-4 h-4" />, hidden: !expandedSummary },
817
898
  { id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
818
899
  {
819
900
  id: 'timeline',
@@ -827,15 +908,15 @@ export function WorkloadView({
827
908
  id: 'reachability',
828
909
  label: 'Reachability',
829
910
  icon: <Stethoscope className="w-4 h-4" />,
830
- hidden: !(renderDiagnoseTab && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
911
+ hidden: !(renderDiagnoseTabForState && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
831
912
  },
832
913
  { id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
833
914
  { id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
834
915
  ]
835
- const allTabs = mergeExtraTabs(tabs, expanded ? extraTabs : undefined)
916
+ const allTabs = orderTabs(mergeExtraTabs(tabs, expanded ? extraTabsForState : undefined), expanded ? tabOrder : undefined)
836
917
  const requestedTabAvailable = allTabs.some((tab) => tab.id === requestedTab && !tab.hidden)
837
918
  const effectiveTab: TabType = requestedTabAvailable ? requestedTab : 'overview'
838
- const activeExtraTab = expanded ? extraTabs?.find((x) => x.id === effectiveTab) : undefined
919
+ const activeExtraTab = expanded ? extraTabsForState?.find((x) => x.id === effectiveTab) : undefined
839
920
  const shouldCommitFallback =
840
921
  requestedTab !== 'overview' &&
841
922
  !requestedTabAvailable &&
@@ -864,13 +945,13 @@ export function WorkloadView({
864
945
  <span className={clsx('badge', getKindColorOutline(apiKind))}>
865
946
  {displayKindName(apiKind, resource?.kind)}
866
947
  </span>
867
- {status && (
948
+ {renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
868
949
  <span className={clsx('badge', status.color)}>
869
950
  {status.text}
870
951
  </span>
871
952
  )}
872
953
  </div>
873
- <div className="flex items-center gap-1.5">
954
+ <div className="flex shrink-0 flex-nowrap items-center justify-end gap-1.5">
874
955
  {diagnoseAction}
875
956
  {onExpand && (
876
957
  <Tooltip content="Open full view" delay={150} position="bottom">
@@ -929,19 +1010,20 @@ export function WorkloadView({
929
1010
  </Tooltip>
930
1011
  </div>
931
1012
  <p className="text-sm text-theme-text-tertiary">{namespace}</p>
932
- {(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
1013
+ {(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
933
1014
  <div className="mt-1 flex flex-wrap items-center gap-1.5">
934
1015
  {gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
935
1016
  {helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
936
1017
  {gitOpsResourcePath && onNavigateGitOpsPath && (
937
1018
  <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
938
1019
  )}
1020
+ {onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
939
1021
  </div>
940
1022
  )}
941
1023
  </div>
942
1024
 
943
1025
  {/* Actions bar */}
944
- <ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...effectiveActionsBarProps} />
1026
+ <ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} leadingActions={headerActions} />
945
1027
  </div>
946
1028
 
947
1029
  {/* Success animation overlay */}
@@ -956,6 +1038,7 @@ export function WorkloadView({
956
1038
  damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
957
1039
  place. */}
958
1040
  <div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
1041
+ {deletedNotice && <div className="px-4 pt-4">{deletedNotice}</div>}
959
1042
  {!resource ? (
960
1043
  // Fill the drawer body so the loading logo centers in it, not in a
961
1044
  // 128px box pinned to the top (matches the splash/PaneLoader centering).
@@ -1007,9 +1090,9 @@ export function WorkloadView({
1007
1090
  drawerSummary
1008
1091
  ) : (
1009
1092
  <>
1010
- {renderOverviewLead && hasOperationalIssues && (
1093
+ {renderOverviewLeadForState && hasOperationalIssues && (
1011
1094
  <div className="px-4 pt-4">
1012
- {renderOverviewLead({ kind, namespace, name })}
1095
+ {renderOverviewLeadForState({ kind, namespace, name })}
1013
1096
  </div>
1014
1097
  )}
1015
1098
  <ResourceRendererDispatch
@@ -1023,9 +1106,9 @@ export function WorkloadView({
1023
1106
  onCopy={copyToClipboard}
1024
1107
  copied={copied}
1025
1108
  onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
1026
- onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
1109
+ onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
1027
1110
  isSavingSecret={isUpdatingResource}
1028
- rendererOverrides={rendererOverrides}
1111
+ rendererOverrides={rendererOverridesForState}
1029
1112
  resolvedEnvFrom={resolvedEnvFrom}
1030
1113
  renderMetrics={renderMetricsTab}
1031
1114
  events={resourceFocusedK8sEvents}
@@ -1033,7 +1116,7 @@ export function WorkloadView({
1033
1116
  updates={resourceFocusedUpdates}
1034
1117
  eventsError={resourceFocusedK8sError}
1035
1118
  updatesError={resourceFocusedUpdatesError}
1036
- mainFooter={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name, group, context: 'drawer' })}
1119
+ mainFooter={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'drawer' })}
1037
1120
  />
1038
1121
  </>
1039
1122
  )}
@@ -1049,6 +1132,8 @@ export function WorkloadView({
1049
1132
  <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
1050
1133
  <DetailShell
1051
1134
  breadcrumb={breadcrumb}
1135
+ wrapHeader={inlineBadges}
1136
+ subheader={resourceGone ? undefined : subheader}
1052
1137
  nav={
1053
1138
  breadcrumb || hideBackButton ? undefined : (
1054
1139
  <Tooltip content="Go back (Esc)" delay={150} position="bottom">
@@ -1062,67 +1147,111 @@ export function WorkloadView({
1062
1147
  </Tooltip>
1063
1148
  )
1064
1149
  }
1065
- identity={
1066
- <>
1067
- {showOwnershipHeading && ownershipContext ? (
1068
- <OwnershipHeading
1069
- podName={name}
1070
- context={ownershipContext}
1071
- copied={copied === 'name'}
1072
- onCopy={() => copyToClipboard(name, 'name')}
1073
- onNavigateToResource={onNavigateToResource}
1074
- onOpenApplication={onOpenApplication}
1075
- />
1076
- ) : (
1077
- <div className="flex items-center gap-3 mb-1">
1078
- <h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
1079
- <Tooltip content="Copy name" delay={150}>
1080
- <button
1081
- onClick={() => copyToClipboard(name, 'name')}
1082
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
1083
- aria-label="Copy name"
1084
- >
1085
- {copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
1086
- </button>
1087
- </Tooltip>
1088
- </div>
1089
- )}
1090
- <div className="flex items-center gap-3 text-sm text-theme-text-secondary">
1091
- <span className={clsx('badge', getKindColorOutline(apiKind))}>
1092
- {displayKindName(apiKind, resource?.kind)}
1093
- </span>
1094
- {status && (
1095
- <span className={clsx('badge', status.color)}>
1150
+ identity={(() => {
1151
+ const nameAndCopy = (
1152
+ <>
1153
+ <h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
1154
+ <Tooltip content="Copy name" delay={150}>
1155
+ <button
1156
+ onClick={() => copyToClipboard(name, 'name')}
1157
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
1158
+ aria-label="Copy name"
1159
+ >
1160
+ {copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
1161
+ </button>
1162
+ </Tooltip>
1163
+ </>
1164
+ )
1165
+ const kindAndStatus = (
1166
+ <>
1167
+ {!hideKindBadge && (
1168
+ <span className={clsx('badge whitespace-nowrap', getKindColorOutline(apiKind))}>
1169
+ {displayKindName(apiKind, resource?.kind)}
1170
+ </span>
1171
+ )}
1172
+ {renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
1173
+ <span className={clsx('badge whitespace-nowrap', status.color)}>
1096
1174
  {status.text}
1097
1175
  </span>
1098
1176
  )}
1177
+ {!resourceGone && statusNote}
1178
+ </>
1179
+ )
1180
+ const coreBadges = (
1181
+ <>
1182
+ {/* On the title line the kind and status wrap together, never apart. */}
1183
+ {inlineBadges ? <span className="inline-flex items-center gap-x-3">{kindAndStatus}</span> : kindAndStatus}
1099
1184
  {namespace && namespace !== '_' && (
1100
- <span>Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
1185
+ <span className="whitespace-nowrap">Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
1101
1186
  )}
1102
- {headerImage && (
1103
- <Tooltip content={headerImage} delay={300} wrapperClassName="min-w-0 max-w-md">
1104
- <span className="truncate font-mono text-xs">
1105
- {midTruncate(headerImage, 72)}
1106
- </span>
1107
- </Tooltip>
1108
- )}
1109
- {gitopsOwner && (
1110
- <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
1111
- )}
1112
- {helmOwner && (
1113
- <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
1114
- )}
1115
- {gitOpsResourcePath && onNavigateGitOpsPath && (
1116
- <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
1117
- )}
1118
- {relationships?.owner && !showOwnershipHeading && (
1119
- <span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
1187
+ {namespace && namespace !== '_' && namespaceNote}
1188
+ </>
1189
+ )
1190
+ const extraChips = [
1191
+ headerImage && (
1192
+ <Tooltip key="image" content={headerImage} delay={300} wrapperClassName="min-w-0 max-w-md">
1193
+ <span className="truncate font-mono text-xs">
1194
+ {midTruncate(headerImage, 72)}
1195
+ </span>
1196
+ </Tooltip>
1197
+ ),
1198
+ gitopsOwner && (
1199
+ <ManagedByChip key="gitops" owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
1200
+ ),
1201
+ helmOwner && (
1202
+ <HelmManagedByChip key="helm" owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
1203
+ ),
1204
+ gitOpsResourcePath && onNavigateGitOpsPath && (
1205
+ <OpenInGitOpsChip key="open-gitops" onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
1206
+ ),
1207
+ onOpenLiveTraffic && <LiveTrafficChip key="live-traffic" onClick={onOpenLiveTraffic} />,
1208
+ relationships?.owner && !showOwnershipHeading && (
1209
+ <span key="owner">Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
1210
+ ),
1211
+ ].filter(Boolean)
1212
+ if (inlineBadges && !(showOwnershipHeading && ownershipContext)) {
1213
+ // The name and its copy button stay together and truncate only when
1214
+ // the line holds nothing else; the badges wrap below before that.
1215
+ return (
1216
+ <>
1217
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
1218
+ {titlePrefix}
1219
+ <div className="flex min-w-0 max-w-full items-center gap-2">{nameAndCopy}</div>
1220
+ {coreBadges}
1221
+ </div>
1222
+ {extraChips.length > 0 && (
1223
+ <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">{extraChips}</div>
1224
+ )}
1225
+ </>
1226
+ )
1227
+ }
1228
+ return (
1229
+ <>
1230
+ {showOwnershipHeading && ownershipContext ? (
1231
+ <OwnershipHeading
1232
+ podName={name}
1233
+ context={ownershipContext}
1234
+ copied={copied === 'name'}
1235
+ onCopy={() => copyToClipboard(name, 'name')}
1236
+ onNavigateToResource={onNavigateToResource}
1237
+ onOpenApplication={onOpenApplication}
1238
+ />
1239
+ ) : (
1240
+ <div className="flex items-center gap-3 mb-1">
1241
+ {titlePrefix}
1242
+ {nameAndCopy}
1243
+ </div>
1120
1244
  )}
1121
- </div>
1122
- </>
1123
- }
1245
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
1246
+ {coreBadges}
1247
+ {extraChips}
1248
+ </div>
1249
+ </>
1250
+ )
1251
+ })()}
1124
1252
  headerActions={
1125
1253
  <>
1254
+ {headerActions}
1126
1255
  {diagnoseAction}
1127
1256
  <Tooltip content="Refresh" delay={150} position="bottom">
1128
1257
  <button
@@ -1157,12 +1286,13 @@ export function WorkloadView({
1157
1286
  activeTab={effectiveTab}
1158
1287
  onTabChange={handleSetTab}
1159
1288
  scopeControls={scopeControls}
1160
- tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...effectiveActionsBarProps} />}
1289
+ tabStripEnd={<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} hideLogs {...actionsBarPropsForState} />}
1161
1290
  overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
1162
1291
  compactHeader={compactHeader}
1163
1292
  >
1164
1293
  <div className="flex h-full min-h-0 flex-col">
1165
- {effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
1294
+ {deletedNotice && <div className="shrink-0 px-4 pt-4">{deletedNotice}</div>}
1295
+ {!resourceGone && effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
1166
1296
  <div className="shrink-0 px-4 pt-4">
1167
1297
  <WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
1168
1298
  </div>
@@ -1173,13 +1303,15 @@ export function WorkloadView({
1173
1303
  <div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
1174
1304
  ) : effectiveTab === 'overview' && expandedOverview ? (
1175
1305
  <div className="h-full min-h-0">
1176
- {hasOperationalIssues && renderOverviewLead && (
1306
+ {hasOperationalIssues && renderOverviewLeadForState && (
1177
1307
  <div className="px-4 pt-4">
1178
- {renderOverviewLead({ kind, namespace, name })}
1308
+ {renderOverviewLeadForState({ kind, namespace, name })}
1179
1309
  </div>
1180
1310
  )}
1181
1311
  {expandedOverview}
1182
1312
  </div>
1313
+ ) : effectiveTab === 'spec' && specTab?.render && !resourceGone ? (
1314
+ <div className="h-full min-h-0 overflow-y-auto">{specTab.render()}</div>
1183
1315
  ) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
1184
1316
  <InfoTab
1185
1317
  resource={resource}
@@ -1199,22 +1331,29 @@ export function WorkloadView({
1199
1331
  onNavigate={onNavigateToResource}
1200
1332
  onCopy={copyToClipboard}
1201
1333
  copied={copied}
1202
- onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
1334
+ onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
1203
1335
  isSavingSecret={isUpdatingResource}
1204
1336
  onOpenLogs={handleOpenLogs}
1205
1337
  onSwitchToTimeline={() => handleSetTab('timeline')}
1206
1338
  onSwitchToLogs={logsTabVisible ? () => handleSetTab('logs') : undefined}
1207
1339
  onSwitchToTopology={!topologyTabHidden ? () => handleSetTab('topology') : undefined}
1208
- rendererOverrides={rendererOverrides}
1340
+ rendererOverrides={rendererOverridesForState}
1209
1341
  resolvedEnvFrom={resolvedEnvFrom}
1210
1342
  events={overviewEvents}
1211
1343
  eventsLoading={overviewEventsLoading}
1212
1344
  updates={resourceFocusedUpdates}
1213
1345
  eventsError={overviewEventsError}
1214
1346
  updatesError={resourceFocusedUpdatesError}
1215
- extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name, group, context: 'expanded' })}
1347
+ extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
1216
1348
  introContent={overviewIntro}
1217
- leadContent={hasOperationalIssues && renderOverviewLead ? renderOverviewLead({ kind, namespace, name }) : undefined}
1349
+ leadContent={
1350
+ // A host's spec lead replaces the operational issues there: its Overview already leads with them.
1351
+ effectiveTab === 'spec' && specTab?.lead
1352
+ ? specTab.lead
1353
+ : hasOperationalIssues && renderOverviewLeadForState
1354
+ ? renderOverviewLeadForState({ kind, namespace, name })
1355
+ : undefined
1356
+ }
1218
1357
  onEvaluateCapacity={onEvaluateCapacity}
1219
1358
  recentImageSave={recentImageSave}
1220
1359
  />
@@ -1273,9 +1412,9 @@ export function WorkloadView({
1273
1412
  {renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
1274
1413
  </div>
1275
1414
  )}
1276
- {effectiveTab === 'reachability' && renderDiagnoseTab && (
1415
+ {effectiveTab === 'reachability' && renderDiagnoseTabForState && (
1277
1416
  <div className="flex h-full min-h-0 flex-col p-3">
1278
- {renderDiagnoseTab({ kind: resource?.kind || kind, namespace, name })}
1417
+ {renderDiagnoseTabForState({ kind: resource?.kind || kind, namespace, name })}
1279
1418
  </div>
1280
1419
  )}
1281
1420
  {effectiveTab === 'cost' && renderCostTab && (
@@ -1406,6 +1545,34 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
1406
1545
  // SUB-COMPONENTS
1407
1546
  // ============================================================================
1408
1547
 
1548
+ function DeletedResourceNotice({
1549
+ kindLabel,
1550
+ owner,
1551
+ onNavigateToResource,
1552
+ }: {
1553
+ kindLabel: string
1554
+ owner?: ResourceRef
1555
+ onNavigateToResource?: NavigateToResource
1556
+ }) {
1557
+ return (
1558
+ <AlertBanner
1559
+ variant="warning"
1560
+ title={`This ${kindLabel} no longer exists in the cluster`}
1561
+ message="You're looking at the last version Radar loaded before it was deleted."
1562
+ >
1563
+ {owner && onNavigateToResource && (
1564
+ <button
1565
+ type="button"
1566
+ onClick={() => onNavigateToResource(refToSelectedResource(owner))}
1567
+ className="mt-2 text-xs text-accent-text hover:underline"
1568
+ >
1569
+ Go to its {displayKindName(owner.kind, owner.kind)} {owner.name}
1570
+ </button>
1571
+ )}
1572
+ </AlertBanner>
1573
+ )
1574
+ }
1575
+
1409
1576
  function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
1410
1577
  return (
1411
1578
  <Tooltip content="Open this resource in the GitOps tab (tree + insights + ops)" delay={150}>
@@ -1421,6 +1588,21 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
1421
1588
  )
1422
1589
  }
1423
1590
 
1591
+ function LiveTrafficChip({ onClick }: { onClick: () => void }) {
1592
+ return (
1593
+ <Tooltip content="What this workload talks to, from the cluster's traffic source" delay={150}>
1594
+ <button
1595
+ type="button"
1596
+ onClick={onClick}
1597
+ className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-elevated px-1.5 py-0.5 text-[11px] font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
1598
+ >
1599
+ Live traffic
1600
+ <ArrowRight className="h-3 w-3 shrink-0" />
1601
+ </button>
1602
+ </Tooltip>
1603
+ )
1604
+ }
1605
+
1424
1606
  function OwnershipHeading({
1425
1607
  podName,
1426
1608
  context,
@@ -2023,6 +2205,7 @@ function WorkloadOverviewTab({
2023
2205
  const relationshipGroups = buildRelationshipGroups(relationships)
2024
2206
  const servingRelationshipGroups = buildServingRelationshipGroups(relationships)
2025
2207
  const dependencyRelationshipGroups = buildDependencyRelationshipGroups(relationships)
2208
+ const observationGroups = dependencyRelationshipGroups.filter((group) => group.label === 'Monitored By' || group.label === 'Staged Policies')
2026
2209
  const showServingPath = servingRelationshipGroups.length > 0 && overviewShape !== 'job' && overviewShape !== 'cronjob'
2027
2210
  const secondaryRelationshipGroups = showServingPath ? dependencyRelationshipGroups : relationshipGroups
2028
2211
  const relationshipCardTitle =
@@ -2101,6 +2284,14 @@ function WorkloadOverviewTab({
2101
2284
  onNavigate={onNavigate}
2102
2285
  onSwitchToTopology={onSwitchToTopology}
2103
2286
  />
2287
+ {observationGroups.length > 0 && (
2288
+ <ServingDependenciesCard
2289
+ title="Related resources"
2290
+ groups={observationGroups}
2291
+ onNavigate={onNavigate}
2292
+ onSwitchToTopology={onSwitchToTopology}
2293
+ />
2294
+ )}
2104
2295
  {extraContent}
2105
2296
  </div>
2106
2297
  </div>
@@ -2439,8 +2630,8 @@ function buildSummaryMetrics(
2439
2630
  },
2440
2631
  {
2441
2632
  label: 'Latest run',
2442
- value: status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never',
2443
- detail: status.lastSuccessfulTime ? `last success ${formatAge(status.lastSuccessfulTime)}` : 'no successful run recorded',
2633
+ value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never',
2634
+ detail: status.lastSuccessfulTime ? `last success ${formatRelativeAgeTime(status.lastSuccessfulTime)}` : 'no successful run recorded',
2444
2635
  icon: Activity,
2445
2636
  },
2446
2637
  {
@@ -2556,8 +2747,8 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
2556
2747
  { label: 'Schedule', value: spec.schedule || '-' },
2557
2748
  { label: 'Active jobs', value: active, tone: active > 0 ? 'warn' : 'default' },
2558
2749
  { label: 'Suspend', value: suspended ? 'Yes' : 'No', tone: suspended ? 'warn' : 'default' },
2559
- { label: 'Last schedule', value: status.lastScheduleTime ? formatAge(status.lastScheduleTime) : '-' },
2560
- { label: 'Last success', value: status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : '-' },
2750
+ { label: 'Last schedule', value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : '-' },
2751
+ { label: 'Last success', value: status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : '-' },
2561
2752
  ],
2562
2753
  }
2563
2754
  }
@@ -2656,8 +2847,8 @@ function getScalingSummary(
2656
2847
  label: phase,
2657
2848
  detail: '',
2658
2849
  stats: [
2659
- { label: 'Started', value: status.startTime ? formatAge(status.startTime) : '-' },
2660
- { label: 'Finished', value: status.completionTime ? formatAge(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
2850
+ { label: 'Started', value: status.startTime ? formatRelativeAgeTime(status.startTime) : '-' },
2851
+ { label: 'Finished', value: status.completionTime ? formatRelativeAgeTime(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
2661
2852
  { label: 'Parallelism', value: spec.parallelism ?? 1 },
2662
2853
  { label: 'Completions', value: spec.completions ?? 1 },
2663
2854
  { label: 'Completion mode', value: spec.completionMode ?? 'NonIndexed' },
@@ -3297,8 +3488,8 @@ function CronJobRunsCard({
3297
3488
  >
3298
3489
  <div className="mb-3 grid gap-x-8 gap-y-3 sm:grid-cols-2">
3299
3490
  <OverviewStat label="Active jobs" value={activeJobs.length} tone={activeJobs.length > 0 ? 'warn' : 'default'} />
3300
- <OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never'} />
3301
- <OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : 'Never'} />
3491
+ <OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never'} />
3492
+ <OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : 'Never'} />
3302
3493
  <OverviewStat label="Retained history" value={`${spec.successfulJobsHistoryLimit ?? 3} ok / ${spec.failedJobsHistoryLimit ?? 1} failed`} />
3303
3494
  </div>
3304
3495
 
@@ -3755,7 +3946,7 @@ function ActivityCard({
3755
3946
  <div className="min-w-0 flex-1">
3756
3947
  <div className="flex items-center justify-between gap-2">
3757
3948
  <span className="truncate text-sm font-medium text-theme-text-primary">{event.reason || event.eventType || 'update'}</span>
3758
- <span className="shrink-0 text-xs text-theme-text-tertiary">{formatAge(event.timestamp)}</span>
3949
+ <span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(event.timestamp)}</span>
3759
3950
  </div>
3760
3951
  <div className="mt-0.5 line-clamp-2 text-xs text-theme-text-secondary">
3761
3952
  {event.message || event.diff?.summary || `${event.kind} ${event.name}`}
@@ -3790,6 +3981,7 @@ function buildServingRelationshipGroups(relationships: Relationships | undefined
3790
3981
  ...(relationships.ingresses ?? []),
3791
3982
  ...(relationships.gateways ?? []),
3792
3983
  ...(relationships.routes ?? []),
3984
+ ...(relationships.routedFrom ?? []),
3793
3985
  ])
3794
3986
  return [
3795
3987
  { label: 'Services', refs: dedupeResourceRefs(relationships.services ?? []) },
@@ -3804,6 +3996,8 @@ function buildDependencyRelationshipGroups(relationships: Relationships | undefi
3804
3996
  { label: 'Autoscalers', refs: dedupeResourceRefs(relationships.scalers ?? []) },
3805
3997
  { label: 'Disruption budgets', refs: dedupeResourceRefs(relationships.pdbs ?? []) },
3806
3998
  { label: 'Network policies', refs: dedupeResourceRefs(relationships.networkPolicies ?? []) },
3999
+ { label: 'Staged Policies', refs: dedupeResourceRefs(relationships.stagedPolicies ?? []) },
4000
+ { label: 'Monitored By', refs: dedupeResourceRefs(relationships.monitors ?? []) },
3807
4001
  { label: 'Storage claims', refs: dedupeResourceRefs(relationships.resourceClaims ?? []) },
3808
4002
  ].filter((group) => group.refs.length > 0)
3809
4003
  }
@@ -3878,16 +4072,6 @@ function summarizeServingExposure(
3878
4072
  return { label: 'No serving path detected', badge: 'None', severity: 'neutral' }
3879
4073
  }
3880
4074
 
3881
- function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
3882
- const seen = new Set<string>()
3883
- return refs.filter((ref) => {
3884
- const key = resourceRefId(ref)
3885
- if (seen.has(key)) return false
3886
- seen.add(key)
3887
- return true
3888
- })
3889
- }
3890
-
3891
4075
  function resourceRefId(ref: ResourceRef): string {
3892
4076
  return `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
3893
4077
  }
@@ -3913,12 +4097,21 @@ function mergeAndRankEvents(events: TimelineEvent[], updates: TimelineEvent[]):
3913
4097
  })
3914
4098
  }
3915
4099
 
4100
+ function orderTabs(tabs: DetailShellTab<TabType>[], order: string[] | undefined): DetailShellTab<TabType>[] {
4101
+ if (!order || order.length === 0) return tabs
4102
+ const rank = (id: string) => {
4103
+ const i = order.indexOf(id)
4104
+ return i < 0 ? order.length : i
4105
+ }
4106
+ return tabs.map((t, i) => ({ t, i })).sort((a, b) => rank(a.t.id) - rank(b.t.id) || a.i - b.i).map((x) => x.t)
4107
+ }
4108
+
3916
4109
  function mergeExtraTabs(tabs: DetailShellTab<TabType>[], extra: WorkloadExtraTab[] | undefined): DetailShellTab<TabType>[] {
3917
4110
  if (!extra || extra.length === 0) return tabs
3918
4111
  const replaced = new Set(extra.map((x) => x.replaces).filter(Boolean))
3919
4112
  const out = tabs.map((t) => (replaced.has(t.id) ? { ...t, hidden: true } : t))
3920
4113
  for (const x of extra) {
3921
- const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon }
4114
+ const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon, badge: x.badge }
3922
4115
  const anchor = x.after ?? x.replaces
3923
4116
  const idx = anchor ? out.findIndex((t) => t.id === anchor) : -1
3924
4117
  if (idx >= 0) out.splice(idx + 1, 0, tab)