@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
@@ -552,7 +552,7 @@ export function EditableYamlView({
552
552
  {!readOnly && (
553
553
  <button
554
554
  onClick={handleStartEdit}
555
- className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-theme-elevated rounded"
555
+ className="flex items-center gap-1 px-2 py-1 text-xs text-accent-text hover:text-accent-light hover:bg-theme-elevated rounded"
556
556
  >
557
557
  <Pencil className="w-3.5 h-3.5" />
558
558
  Edit
@@ -0,0 +1,86 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { evaluateGitOpsWriteGuard, type GitOpsWriteGuardInput } from '../../utils/gitops-write-guard'
5
+ import { GitOpsWriteWarning } from './GitOpsWriteWarning'
6
+
7
+ const base: GitOpsWriteGuardInput = {
8
+ target: { kind: 'Deployment', group: 'apps', namespace: 'prod', name: 'api' },
9
+ owner: { tool: 'argocd', kind: 'applications', namespace: 'argocd', name: 'api' },
10
+ writes: [{ scope: 'spec', paths: ['spec.replicas'] }],
11
+ evidence: {
12
+ uid: 'u',
13
+ resourceVersion: '1',
14
+ owner: null,
15
+ policy: { tool: 'argocd', auto: true, selfHeal: true, prune: false, suspended: null },
16
+ paths: [{ path: 'spec.replicas', lastApplied: 'present', ownedBy: [], ownedByGitOps: false }],
17
+ },
18
+ }
19
+
20
+ function render(input: GitOpsWriteGuardInput, onOpenOwner?: () => void) {
21
+ return renderToStaticMarkup(
22
+ <GitOpsWriteWarning
23
+ guard={evaluateGitOpsWriteGuard(input)}
24
+ acknowledged={false}
25
+ onAcknowledgedChange={() => {}}
26
+ onOpenOwner={onOpenOwner}
27
+ />,
28
+ )
29
+ }
30
+
31
+ describe('GitOpsWriteWarning', () => {
32
+ it('renders the revert warning, owner link and required acknowledgment', () => {
33
+ const html = render(base, () => {})
34
+ expect(html).toContain('Argo CD Application argocd/api will revert this change.')
35
+ expect(html).toContain('overwritten at the next sync')
36
+ expect(html).toContain('change it in the GitOps source')
37
+ expect(html).toContain('View Argo CD Application')
38
+ expect(html).toContain('type="checkbox"')
39
+ expect(html).toContain('I understand Argo CD will revert this.')
40
+ })
41
+
42
+ it('asks the acknowledgment in the verdict’s own terms', () => {
43
+ const html = render({
44
+ ...base,
45
+ evidence: { ...base.evidence!, policy: { tool: 'argocd', auto: false, selfHeal: false, prune: false, suspended: null } },
46
+ })
47
+ expect(html).toContain('Argo CD Application argocd/api may revert this change.')
48
+ expect(html).toContain('I understand Argo CD may revert this.')
49
+ })
50
+
51
+ it('shows a checking state while ownership resolves', () => {
52
+ const html = render({ ...base, ownerPending: true })
53
+ expect(html).toContain('Checking GitOps ownership…')
54
+ expect(html).not.toContain('type="checkbox"')
55
+ })
56
+
57
+ it('renders a quiet note without acknowledgment at info level', () => {
58
+ const html = render({ ...base, writes: [{ scope: 'create-child' }] })
59
+ expect(html).toContain('Managed by Argo CD Application argocd/api.')
60
+ expect(html).not.toContain('type="checkbox"')
61
+ })
62
+
63
+ it('leaves vertical spacing to the parent', () => {
64
+ for (const input of [base, { ...base, ownerPending: true }, { ...base, writes: [{ scope: 'create-child' as const }] }]) {
65
+ const root = render(input).match(/^<div class="([^"]*)"/)?.[1] ?? ''
66
+ expect(root).not.toMatch(/(^|\s)m[bt]-/)
67
+ }
68
+ })
69
+
70
+ it('renders nothing when unmanaged', () => {
71
+ expect(render({ ...base, owner: null })).toBe('')
72
+ })
73
+
74
+ it('lists per-write reasons when they differ', () => {
75
+ const html = render({
76
+ ...base,
77
+ writes: [
78
+ { scope: 'spec', paths: ['spec.replicas'], description: 'Replicas' },
79
+ { scope: 'delete', description: 'Delete' },
80
+ ],
81
+ evidence: { ...base.evidence!, policy: { tool: 'argocd', auto: false, selfHeal: false, prune: false, suspended: null } },
82
+ })
83
+ expect(html).toContain('Replicas:')
84
+ expect(html).toContain('Delete:')
85
+ })
86
+ })
@@ -0,0 +1,135 @@
1
+ import { ExternalLink, GitBranch, Info, Loader2 } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+
4
+ import { AlertBanner } from '../ui/drawer-components'
5
+ import {
6
+ canConfirmGitOpsWrite,
7
+ gitOpsWriteHeadline,
8
+ guardToolLabel,
9
+ type GitOpsWriteAssessment,
10
+ type GitOpsWriteGuard,
11
+ } from '../../utils/gitops-write-guard'
12
+
13
+ export { canConfirmGitOpsWrite }
14
+
15
+ export interface GitOpsWriteWarningProps {
16
+ guard: GitOpsWriteGuard
17
+ acknowledged: boolean
18
+ onAcknowledgedChange: (acknowledged: boolean) => void
19
+ /** Opens the GitOps owner (or Helm release) the guard names. */
20
+ onOpenOwner?: () => void
21
+ disabled?: boolean
22
+ className?: string
23
+ }
24
+
25
+ const OWNER_LINK_LABEL = {
26
+ applications: 'View Argo CD Application',
27
+ kustomizations: 'View Flux Kustomization',
28
+ helmreleases: 'View Flux HelmRelease',
29
+ } as const
30
+
31
+ function reasonLines(perWrite: GitOpsWriteAssessment[], levels: GitOpsWriteGuard['level'][]): string[] {
32
+ const relevant = perWrite.filter((entry) => levels.includes(entry.level) && entry.reason)
33
+ const distinct = new Set(relevant.map((entry) => entry.reason))
34
+ if (distinct.size <= 1) return [...distinct]
35
+ return relevant.map((entry) =>
36
+ entry.write.description ? `${entry.write.description}: ${entry.reason}` : entry.reason,
37
+ )
38
+ }
39
+
40
+ export function GitOpsWriteWarning({
41
+ guard,
42
+ acknowledged,
43
+ onAcknowledgedChange,
44
+ onOpenOwner,
45
+ disabled = false,
46
+ // Not undefined: AlertBanner would add its own mb-4, and a margin utility
47
+ // overrides the parent's space-y gap.
48
+ className = '',
49
+ }: GitOpsWriteWarningProps) {
50
+ if (guard.pending) {
51
+ return (
52
+ <div
53
+ className={clsx(
54
+ 'flex items-center gap-2 rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-xs text-theme-text-secondary',
55
+ className,
56
+ )}
57
+ >
58
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
59
+ Checking GitOps ownership…
60
+ </div>
61
+ )
62
+ }
63
+ if (guard.level === 'none') return null
64
+
65
+ const headline = gitOpsWriteHeadline(guard)
66
+ const ownerLink = onOpenOwner ? (
67
+ <button
68
+ type="button"
69
+ onClick={onOpenOwner}
70
+ className="inline-flex items-center gap-1 text-xs font-medium text-skyhook-600 hover:underline dark:text-skyhook-400"
71
+ >
72
+ {guard.owner ? OWNER_LINK_LABEL[guard.owner.kind] : 'View Helm release'}
73
+ <ExternalLink className="h-3 w-3" />
74
+ </button>
75
+ ) : null
76
+
77
+ if (guard.level === 'info') {
78
+ const lines = reasonLines(guard.perWrite, ['info'])
79
+ return (
80
+ <div
81
+ className={clsx(
82
+ 'flex items-start gap-2 rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-xs text-theme-text-secondary',
83
+ className,
84
+ )}
85
+ >
86
+ <Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
87
+ <div className="min-w-0 space-y-1">
88
+ <p>
89
+ <span className="font-medium text-theme-text-primary">{headline}</span> {lines.join(' ')}
90
+ </p>
91
+ {ownerLink}
92
+ </div>
93
+ </div>
94
+ )
95
+ }
96
+
97
+ const tool = guardToolLabel(guard)
98
+ const lines = reasonLines(guard.perWrite, ['will-revert', 'may-revert'])
99
+ const durableFix = guard.owner
100
+ ? 'For a lasting change, change it in the GitOps source.'
101
+ : guard.helmRelease
102
+ ? 'For a lasting change, change the release’s chart values.'
103
+ : null
104
+ return (
105
+ <AlertBanner
106
+ variant="warning"
107
+ icon={GitBranch}
108
+ title={headline}
109
+ className={className}
110
+ message={
111
+ lines.length === 1 ? (
112
+ <>
113
+ {lines[0]}
114
+ {durableFix ? ` ${durableFix}` : ''}
115
+ </>
116
+ ) : durableFix ?? undefined
117
+ }
118
+ items={lines.length > 1 ? lines : undefined}
119
+ >
120
+ {ownerLink && <div className="mt-2">{ownerLink}</div>}
121
+ <label className="mt-2 flex cursor-pointer items-start gap-2 text-xs text-theme-text-primary">
122
+ <input
123
+ type="checkbox"
124
+ checked={acknowledged}
125
+ onChange={(event) => onAcknowledgedChange(event.target.checked)}
126
+ disabled={disabled}
127
+ className="mt-0.5 h-3.5 w-3.5 accent-skyhook-500"
128
+ />
129
+ <span>
130
+ I understand {tool} {guard.level === 'will-revert' ? 'will' : 'may'} revert this.
131
+ </span>
132
+ </label>
133
+ </AlertBanner>
134
+ )
135
+ }
@@ -1,3 +1,4 @@
1
+ import { getNodeLifecycle } from '../../utils/node-lifecycle'
1
2
  import { useEffect, useState, useRef, useId } from 'react'
2
3
  import {
3
4
  RefreshCw,
@@ -28,11 +29,12 @@ import type { SelectedResource, WorkloadRevision } from '../../types'
28
29
  import type { RadarUpgradeRequirement } from '../../types/fetch-error'
29
30
  import { displayKindName } from '../ui/drawer-components'
30
31
  import { getDefaultContainerName } from '../resources/resource-utils'
31
- import { SetImageDialog, type ManagedImageSource } from './SetImageDialog'
32
+ import { SetImageDialog, type SetImageOwnership } from './SetImageDialog'
32
33
  import { DrainPlanDialog, DEFAULT_DRAIN_DIALOG_OPTIONS, type DrainDialogOptions, type DrainPlan } from './DrainPlanDialog'
33
34
  import type { WorkloadImageInventory, WorkloadImageUpdate } from '../../types/core'
34
35
  import { isArgoRolloutResource } from '../../utils/workload-rollout'
35
36
  import { isCoreBatchJob } from '../../utils/api-resources'
37
+ import { formatRelativeAgeTime } from '../../utils/format'
36
38
 
37
39
  // ============================================================================
38
40
  // ACTIONS BAR - Interactive buttons that change based on resource kind
@@ -40,6 +42,7 @@ import { isCoreBatchJob } from '../../utils/api-resources'
40
42
 
41
43
  interface ResourceActionsBarProps {
42
44
  resource: SelectedResource
45
+ leadingActions?: React.ReactNode
43
46
  data: any
44
47
  onClose?: () => void
45
48
  hideLogs?: boolean
@@ -78,6 +81,7 @@ interface ResourceActionsBarProps {
78
81
  cascadeDependents?: CascadeDependent[]
79
82
  cascadeLoading?: boolean
80
83
  cascadeRootResolved?: boolean
84
+ cascadePreviewFailed?: boolean
81
85
 
82
86
  // Workload restart
83
87
  onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
@@ -86,7 +90,7 @@ interface ResourceActionsBarProps {
86
90
  onLoadImages?: (params: { kind: string; namespace: string; name: string }) => Promise<WorkloadImageInventory>
87
91
  onSetImages?: (params: { kind: string; namespace: string; name: string; updates: WorkloadImageUpdate[] }) => Promise<unknown>
88
92
  isSettingImages?: boolean
89
- managedImageSources?: ManagedImageSource[]
93
+ imageOwnership?: SetImageOwnership
90
94
 
91
95
  // Rollback
92
96
  revisions?: WorkloadRevision[]
@@ -153,15 +157,15 @@ interface ResourceActionsBarProps {
153
157
  }
154
158
 
155
159
  export function ResourceActionsBar({
156
- resource, data, onClose, hideLogs, showYaml, onToggleYaml,
160
+ resource, data, leadingActions, onClose, hideLogs, showYaml, onToggleYaml,
157
161
  onCompareTo,
158
162
  onCompareAcrossClusters,
159
163
  canExec, canViewLogs, canPortForward,
160
164
  onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
161
165
  renderPortForward,
162
- onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved,
166
+ onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved, cascadePreviewFailed,
163
167
  onRestart, isRestarting,
164
- onLoadImages, onSetImages, isSettingImages, managedImageSources,
168
+ onLoadImages, onSetImages, isSettingImages, imageOwnership,
165
169
  revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
166
170
  onRolloutPromoteFull,
167
171
  onTriggerCronJob, isTriggeringCronJob,
@@ -289,6 +293,8 @@ export function ResourceActionsBar({
289
293
 
290
294
  return (
291
295
  <div className="flex items-center gap-1.5 px-4 py-2 flex-wrap">
296
+ <div className="flex min-w-0 flex-wrap items-center gap-1.5 empty:hidden" data-action-group="object">
297
+ {leadingActions}
292
298
  {/* Kind-specific actions (left) */}
293
299
  {kind === 'pods' && (
294
300
  <>
@@ -358,7 +364,7 @@ export function ResourceActionsBar({
358
364
  </button>
359
365
  )}
360
366
 
361
- {canNodeWrite && (
367
+ {canNodeWrite && !getNodeLifecycle(data).removing && (
362
368
  <>
363
369
  {data?.spec?.unschedulable ? (
364
370
  onUncordonNode && (
@@ -584,8 +590,8 @@ export function ResourceActionsBar({
584
590
  </button>
585
591
  )}
586
592
 
587
- {/* Spacer pushes universal actions to the right */}
588
- <div className="flex-1" />
593
+ </div>
594
+ <div className="ml-auto flex flex-wrap items-center gap-1.5" data-action-group="utility">
589
595
 
590
596
  {/* Universal actions (right-aligned). The AI/Diagnose action is NOT here — it
591
597
  lives in the detail header chrome (see WorkloadView), set apart from these
@@ -660,7 +666,7 @@ export function ResourceActionsBar({
660
666
  </div>
661
667
  )}
662
668
 
663
- {onDelete && (
669
+ {onDelete && !((kind === 'node' || kind === 'nodes') && data?.metadata?.deletionTimestamp) && (
664
670
  <Tooltip content="Delete resource">
665
671
  <button
666
672
  onClick={() => setShowDeleteConfirm(true)}
@@ -671,6 +677,8 @@ export function ResourceActionsBar({
671
677
  </Tooltip>
672
678
  )}
673
679
 
680
+ </div>
681
+
674
682
  <ForceDeleteConfirmDialog
675
683
  open={showDeleteConfirm}
676
684
  onClose={() => setShowDeleteConfirm(false)}
@@ -682,6 +690,7 @@ export function ResourceActionsBar({
682
690
  cascadeDependents={cascadeDependents}
683
691
  cascadeLoading={cascadeLoading}
684
692
  cascadeRootResolved={cascadeRootResolved}
693
+ cascadePreviewFailed={cascadePreviewFailed}
685
694
  />
686
695
 
687
696
  {onLoadImages && onSetImages && (
@@ -690,7 +699,7 @@ export function ResourceActionsBar({
690
699
  workloadLabel={`${displayKindName(resource.kind, data?.kind)} ${resource.namespace}/${resource.name}`}
691
700
  workloadName={resource.name}
692
701
  workloadResource={resource.kind}
693
- managedSources={managedImageSources}
702
+ ownership={imageOwnership}
694
703
  pending={isSettingImages}
695
704
  onClose={() => setShowSetImage(false)}
696
705
  onLoad={() => onLoadImages({ kind: resource.kind, namespace: resource.namespace, name: resource.name })}
@@ -1127,19 +1136,6 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
1127
1136
  setPromoteError(outcome.error ?? { message: 'The promotion did not go through.', controllerLagging: false })
1128
1137
  }
1129
1138
 
1130
- function formatTimeAgo(dateStr: string): string {
1131
- const date = new Date(dateStr)
1132
- const now = new Date()
1133
- const seconds = Math.floor((now.getTime() - date.getTime()) / 1000)
1134
- if (seconds < 60) return `${seconds}s ago`
1135
- const minutes = Math.floor(seconds / 60)
1136
- if (minutes < 60) return `${minutes}m ago`
1137
- const hours = Math.floor(minutes / 60)
1138
- if (hours < 24) return `${hours}h ago`
1139
- const days = Math.floor(hours / 24)
1140
- return `${days}d ago`
1141
- }
1142
-
1143
1139
  function getImageTag(image: string): string {
1144
1140
  if (!image) return '-'
1145
1141
  const parts = image.split(':')
@@ -1231,7 +1227,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
1231
1227
  <RevisionImage image={rev.image} displayImage={getImageTag(rev.image)} />
1232
1228
  </td>
1233
1229
  <td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
1234
- {formatTimeAgo(rev.createdAt)}
1230
+ {formatRelativeAgeTime(rev.createdAt)}
1235
1231
  </td>
1236
1232
  <td className="py-2 text-right">
1237
1233
  <div className="flex items-center gap-1 justify-end">
@@ -193,6 +193,7 @@ import {
193
193
  CNPGDatabaseRenderer,
194
194
  CNPGPublicationRenderer,
195
195
  CNPGSubscriptionRenderer,
196
+ CNPGDatabaseRoleRenderer,
196
197
  CNPGImageCatalogRenderer,
197
198
  KyvernoGlobalContextRenderer,
198
199
  KyvernoUpdateRequestRenderer,
@@ -454,7 +455,7 @@ const KNOWN_KINDS = new Set([
454
455
  'rollouts', 'analysisruns', 'analysistemplates', 'clusteranalysistemplates', 'experiments', 'certificates', 'workflows', 'persistentvolumes',
455
456
  'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
456
457
  'orders', 'challenges',
457
- 'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
458
+ 'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'udproutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
458
459
  'networkpolicies', 'networkpolicy',
459
460
  'globalnetworkpolicies', 'globalnetworkpolicy',
460
461
  'stagednetworkpolicies', 'stagednetworkpolicy',
@@ -494,7 +495,7 @@ const KNOWN_KINDS = new Set([
494
495
  'externalsecrets', 'clusterexternalsecrets', 'secretstores', 'clustersecretstores',
495
496
  'clusters', 'scheduledbackups', 'poolers', 'objectstores',
496
497
  'globalcontextentries', 'updaterequests', 'ephemeralreports', 'clusterephemeralreports',
497
- 'databases', 'publications', 'imagecatalogs', 'clusterimagecatalogs',
498
+ 'databases', 'publications', 'imagecatalogs', 'clusterimagecatalogs', 'databaseroles',
498
499
  'virtualservices', 'destinationrules', 'serviceentries',
499
500
  'peerauthentications', 'authorizationpolicies',
500
501
  'mutatingwebhookconfigurations', 'validatingwebhookconfigurations',
@@ -702,7 +703,7 @@ export function ResourceRendererDispatch({
702
703
  const isGroupGatedKind =
703
704
  kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
704
705
  || kind === 'objectstores' || kind === 'databases' || kind === 'publications'
705
- || kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs' || kind === 'jobsets'
706
+ || kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs' || kind === 'databaseroles' || kind === 'jobsets'
706
707
  || kind === 'policies' || kind === 'rollouts' || kind === 'experiments'
707
708
  || kind === 'rayjobs' || kind === 'rayclusters' || kind === 'rayservices' || kind === 'admissionchecks' || kind === 'provisioningrequests'
708
709
  || kind === 'machines' || kind === 'machinesets' || kind === 'workloads' || kind === 'localqueues' || kind === 'clusterqueues'
@@ -713,7 +714,7 @@ export function ResourceRendererDispatch({
713
714
  || ((kind === 'scheduledbackups' || kind === 'poolers') && isCNPGApiVersion)
714
715
  || (kind === 'objectstores' && isApiGroup(data?.apiVersion, 'barmancloud.cnpg.io'))
715
716
  || ((kind === 'databases' || kind === 'publications' || kind === 'imagecatalogs'
716
- || kind === 'clusterimagecatalogs') && isCNPGApiVersion)
717
+ || kind === 'clusterimagecatalogs' || kind === 'databaseroles') && isCNPGApiVersion)
717
718
  || (kind === 'subscriptions'
718
719
  && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'messaging.knative.dev')))
719
720
  || (kind === 'policies' && isApiGroup(data?.apiVersion, 'kyverno.io'))
@@ -800,7 +801,7 @@ export function ResourceRendererDispatch({
800
801
  {kind !== 'events' && <EventsSection fullscreen={!!renderSidebar} events={events || []} updates={updates || []} isLoading={eventsLoading ?? false} eventsError={eventsError ?? null} updatesError={updatesError ?? null} hint={eventsHint} />}
801
802
  <LabelsSection data={data} />
802
803
  <AnnotationsSection data={data} />
803
- <MetadataSection data={data} />
804
+ <MetadataSection data={data} onNavigate={onNavigate} />
804
805
  </>
805
806
  )
806
807
 
@@ -860,6 +861,7 @@ export function ResourceRendererDispatch({
860
861
  {kind === 'grpcroutes' && <GRPCRouteRenderer data={data} onNavigate={onNavigate} />}
861
862
  {kind === 'tcproutes' && <SimpleRouteRenderer data={data} kind="TCPRoute" onNavigate={onNavigate} />}
862
863
  {kind === 'tlsroutes' && <SimpleRouteRenderer data={data} kind="TLSRoute" onNavigate={onNavigate} />}
864
+ {kind === 'udproutes' && <SimpleRouteRenderer data={data} kind="UDPRoute" onNavigate={onNavigate} />}
863
865
  {kind === 'sealedsecrets' && <SealedSecretRenderer data={data} onNavigate={onNavigate} />}
864
866
  {(kind === 'workflowtemplates' || kind === 'clusterworkflowtemplates') && <WorkflowTemplateRenderer data={data} />}
865
867
  {isCoreNetworkPolicy && <NetworkPolicyRenderer data={data} />}
@@ -926,6 +928,7 @@ export function ResourceRendererDispatch({
926
928
  {kind === 'databases' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGDatabaseComp data={data} onNavigate={onNavigate} />}
927
929
  {kind === 'publications' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGPublicationComp data={data} onNavigate={onNavigate} />}
928
930
  {kind === 'subscriptions' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGSubscriptionComp data={data} onNavigate={onNavigate} />}
931
+ {kind === 'databaseroles' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGDatabaseRoleRenderer data={data} onNavigate={onNavigate} />}
929
932
  {(kind === 'imagecatalogs' || kind === 'clusterimagecatalogs') && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGImageCatalogComp data={data} onNavigate={onNavigate} />}
930
933
  {kind === 'backups' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGBackupRenderer data={data} onNavigate={onNavigate} />}
931
934
  {kind === 'backups' && isApiGroup(data.apiVersion, 'velero.io') && <VeleroBackupComp data={data} onNavigate={onNavigate} />}
@@ -971,14 +974,14 @@ export function ResourceRendererDispatch({
971
974
  {kind === 'azuremanagedcontrolplanes' && <AzureManagedControlPlaneRenderer data={data} onNavigate={onNavigate} />}
972
975
  {kind === 'azuremanagedmachinepools' && <AzureManagedMachinePoolRenderer data={data} onNavigate={onNavigate} />}
973
976
  {kind === 'azuremachines' && <AzureMachineRenderer data={data} onNavigate={onNavigate} />}
974
- {kind === 'virtualservices' && <IstioVirtualServiceRenderer data={data} onNavigate={onNavigate} />}
975
- {kind === 'destinationrules' && <IstioDestinationRuleRenderer data={data} onNavigate={onNavigate} />}
977
+ {kind === 'virtualservices' && <IstioVirtualServiceRenderer data={data} onNavigate={onNavigate} serviceDomain={relationships?.istioServiceDomain} />}
978
+ {kind === 'destinationrules' && <IstioDestinationRuleRenderer data={data} onNavigate={onNavigate} serviceDomain={relationships?.istioServiceDomain} />}
976
979
  {kind === 'serviceentries' && <IstioServiceEntryRenderer data={data} />}
977
980
  {kind === 'peerauthentications' && <IstioPeerAuthenticationRenderer data={data} />}
978
981
  {kind === 'authorizationpolicies' && <IstioAuthorizationPolicyRenderer data={data} />}
979
982
  {kind === 'mutatingwebhookconfigurations' && <WebhookConfigRenderer data={data} isMutating />}
980
983
  {kind === 'validatingwebhookconfigurations' && <WebhookConfigRenderer data={data} />}
981
- {kind === 'ingressclasses' && <IngressClassRenderer data={data} />}
984
+ {kind === 'ingressclasses' && <IngressClassRenderer data={data} onNavigate={onNavigate} />}
982
985
  {kind === 'priorityclasses' && <PriorityClassRenderer data={data} />}
983
986
  {kind === 'runtimeclasses' && <RuntimeClassRenderer data={data} />}
984
987
  {kind === 'leases' && <LeaseRenderer data={data} />}
@@ -1190,7 +1193,7 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
1190
1193
  return getGatewayStatus(data)
1191
1194
  }
1192
1195
  if (k === 'gatewayclasses') return getGatewayClassStatus(data)
1193
- if (k === 'httproutes' || k === 'grpcroutes' || k === 'tcproutes' || k === 'tlsroutes') return getRouteStatus(data)
1196
+ if (k === 'httproutes' || k === 'grpcroutes' || k === 'tcproutes' || k === 'tlsroutes' || k === 'udproutes') return getRouteStatus(data)
1194
1197
  if (k === 'sealedsecrets') return getSealedSecretStatus(data)
1195
1198
  if (k === 'poddisruptionbudgets') return getPDBStatus(data)
1196
1199
  if (k === 'gitrepositories') return getGitRepositoryStatus(data)
@@ -1247,7 +1250,7 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
1247
1250
  }
1248
1251
  // Group-guarded for the same reason as the renderers: these plurals are
1249
1252
  // generic enough that another operator can serve them.
1250
- if ((k === 'databases' || k === 'publications' || k === 'subscriptions') && isApiGroup(data.apiVersion, CNPG_GROUP)) {
1253
+ if ((k === 'databases' || k === 'publications' || k === 'subscriptions' || k === 'databaseroles') && isApiGroup(data.apiVersion, CNPG_GROUP)) {
1251
1254
  return getCNPGDeclarativeStatus(data)
1252
1255
  }
1253
1256
  if (k === 'globalcontextentries') return getKyvernoGlobalContextStatus(data)
@@ -1,11 +1,14 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import type { WorkloadContainerImage } from '../../types/core'
4
+ import { canConfirmGitOpsWrite, evaluateGitOpsWriteGuard, gitOpsWriteGuardKey } from '../../utils/gitops-write-guard'
4
5
  import {
6
+ SET_IMAGE_WRITES,
5
7
  canSubmitImageUpdates,
6
8
  changedImageUpdates,
7
9
  describeImageUpdateBehavior,
8
10
  reconcileRefreshedImageDrafts,
11
+ setImageWrites,
9
12
  } from './SetImageDialog'
10
13
 
11
14
  const containers: WorkloadContainerImage[] = [
@@ -104,26 +107,92 @@ describe('reconcileRefreshedImageDrafts', () => {
104
107
  })
105
108
 
106
109
  describe('canSubmitImageUpdates', () => {
110
+ const target = { kind: 'Deployment', group: 'apps', namespace: 'prod', name: 'api' }
111
+ const managedGuard = evaluateGitOpsWriteGuard({
112
+ target,
113
+ owner: { tool: 'argocd', kind: 'applications', namespace: 'argocd', name: 'api' },
114
+ writes: SET_IMAGE_WRITES,
115
+ })
116
+ const unmanagedGuard = evaluateGitOpsWriteGuard({ target, owner: null, writes: SET_IMAGE_WRITES })
117
+ const pendingGuard = evaluateGitOpsWriteGuard({ target, owner: null, ownerPending: true, writes: SET_IMAGE_WRITES })
107
118
  const ready = {
108
119
  updateCount: 1,
109
120
  hasEmptyImage: false,
110
- managed: false,
111
- ownershipResolved: true,
112
- acknowledged: false,
121
+ ownershipConfirmable: canConfirmGitOpsWrite(unmanagedGuard, false),
113
122
  busy: false,
114
123
  }
115
124
 
116
125
  it('requires break-glass acknowledgement for managed resources', () => {
117
- expect(canSubmitImageUpdates({ ...ready, managed: true })).toBe(false)
126
+ expect(managedGuard.requiresAck).toBe(true)
127
+ expect(
128
+ canSubmitImageUpdates({ ...ready, ownershipConfirmable: canConfirmGitOpsWrite(managedGuard, false) }),
129
+ ).toBe(false)
118
130
  expect(
119
- canSubmitImageUpdates({ ...ready, managed: true, acknowledged: true }),
131
+ canSubmitImageUpdates({ ...ready, ownershipConfirmable: canConfirmGitOpsWrite(managedGuard, true) }),
120
132
  ).toBe(true)
121
133
  })
122
134
 
135
+ it('gates until ownership resolves', () => {
136
+ expect(canConfirmGitOpsWrite(undefined, true)).toBe(false)
137
+ expect(canConfirmGitOpsWrite(pendingGuard, true)).toBe(false)
138
+ expect(canSubmitImageUpdates(ready)).toBe(true)
139
+ })
140
+
123
141
  it('blocks empty images and concurrent submissions', () => {
124
142
  expect(canSubmitImageUpdates({ ...ready, hasEmptyImage: true })).toBe(false)
125
143
  expect(canSubmitImageUpdates({ ...ready, busy: true })).toBe(false)
126
144
  expect(canSubmitImageUpdates({ ...ready, loadFailed: true })).toBe(false)
127
- expect(canSubmitImageUpdates({ ...ready, ownershipResolved: false })).toBe(false)
145
+ expect(canSubmitImageUpdates({ ...ready, ownershipConfirmable: false })).toBe(false)
146
+ })
147
+ })
148
+
149
+ describe('setImageWrites', () => {
150
+ it('names each container, so the verdict is for exactly the images being changed', () => {
151
+ expect(setImageWrites(containers).map((w) => w.paths)).toEqual([
152
+ ['spec.template.spec.containers[name=app].image'],
153
+ ['spec.template.spec.containers[name=sidecar].image'],
154
+ ['spec.template.spec.initContainers[name=migrate].image'],
155
+ ])
156
+ })
157
+
158
+ it("asks again when the evidence doesn't cover a changed container", () => {
159
+ // Evidence read before the dialog listed the init container covers only
160
+ // the app image, which the owner ignores.
161
+ const guard = evaluateGitOpsWriteGuard({
162
+ target: { kind: 'Deployment', group: 'apps', namespace: 'prod', name: 'api' },
163
+ owner: { tool: 'argocd', kind: 'applications', namespace: 'argocd', name: 'api' },
164
+ writes: setImageWrites([{ type: 'initContainer', name: 'migrate' }]),
165
+ evidence: {
166
+ uid: 'u',
167
+ resourceVersion: '1',
168
+ owner: null,
169
+ policy: { tool: 'argocd', auto: true, selfHeal: true, prune: false, suspended: null, respectIgnoreDifferences: true },
170
+ paths: [{ path: 'spec.template.spec.containers[name=app].image', lastApplied: 'present', ownedBy: [], ownedByGitOps: false, ignored: 'effective', ignoredBy: 'spec.ignoreDifferences' }],
171
+ },
172
+ })
173
+ expect(guard.level).toBe('may-revert')
174
+ expect(canConfirmGitOpsWrite(guard, false)).toBe(false)
175
+ })
176
+ })
177
+
178
+ describe('acknowledgment scope', () => {
179
+ it("an acknowledgment for one container's image doesn't cover another's with the same verdict", () => {
180
+ const guardFor = (name: string) =>
181
+ evaluateGitOpsWriteGuard({
182
+ target: { kind: 'Deployment', group: 'apps', namespace: 'prod', name: 'api' },
183
+ owner: { tool: 'argocd', kind: 'applications', namespace: 'argocd', name: 'api' },
184
+ writes: setImageWrites([{ type: 'container', name }]),
185
+ evidence: {
186
+ uid: 'u',
187
+ resourceVersion: '1',
188
+ owner: null,
189
+ policy: { tool: 'argocd', auto: true, selfHeal: true, prune: false, suspended: null },
190
+ paths: ['app', 'sidecar'].map((n) => ({ path: `spec.template.spec.containers[name=${n}].image`, lastApplied: 'present' as const, ownedBy: [], ownedByGitOps: false })),
191
+ },
192
+ })
193
+ const sidecar = guardFor('sidecar')
194
+ const app = guardFor('app')
195
+ expect(app.level).toBe(sidecar.level)
196
+ expect(gitOpsWriteGuardKey(app)).not.toBe(gitOpsWriteGuardKey(sidecar))
128
197
  })
129
198
  })