@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,11 +1,5 @@
1
1
  import { useEffect, useMemo, useRef, useState, useId } from 'react'
2
- import {
3
- AlertTriangle,
4
- ExternalLink,
5
- Loader2,
6
- RefreshCw,
7
- X,
8
- } from 'lucide-react'
2
+ import { Loader2, RefreshCw, X } from 'lucide-react'
9
3
  import { clsx } from 'clsx'
10
4
 
11
5
  import type {
@@ -16,11 +10,33 @@ import type {
16
10
  } from '../../types/core'
17
11
  import { DialogPortal } from '../ui/DialogPortal'
18
12
  import { Badge } from '../ui/Badge'
13
+ import {
14
+ canConfirmGitOpsWrite,
15
+ gitOpsWriteGuardKey,
16
+ type GitOpsWrite,
17
+ type GitOpsWriteGuard,
18
+ } from '../../utils/gitops-write-guard'
19
+ import { GitOpsWriteWarning } from './GitOpsWriteWarning'
19
20
 
20
- export interface ManagedImageSource {
21
- type: 'GitOps' | 'Helm'
22
- label: string
23
- onOpen?: () => void
21
+ /** The GitOps write guard for the object whose Pod template the dialog
22
+ * writes, evaluated by the host: for {@link SET_IMAGE_WRITES} until the
23
+ * containers are known, then for {@link setImageWrites} of the containers
24
+ * {@link onSelectionChange} reports. */
25
+ export interface SetImageOwnership {
26
+ guard: GitOpsWriteGuard
27
+ onOpenOwner?: () => void
28
+ /** The containers the dialog lists and the ones being changed, so the
29
+ * guard can judge exactly those (null until the containers load). */
30
+ onSelectionChange?: (selection: SetImageSelection | null) => void
31
+ /** {@link setImageSelectionKey} of the selection `guard` was evaluated
32
+ * for. When set, Update waits until it matches the dialog's current
33
+ * selection. */
34
+ selectionKey?: string
35
+ }
36
+
37
+ export interface SetImageSelection {
38
+ containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]
39
+ changed: Pick<WorkloadContainerImage, 'type' | 'name'>[]
24
40
  }
25
41
 
26
42
  export interface SetImageDialogProps {
@@ -28,7 +44,8 @@ export interface SetImageDialogProps {
28
44
  workloadLabel: string
29
45
  workloadName: string
30
46
  workloadResource: string
31
- managedSources?: ManagedImageSource[]
47
+ /** Undefined while the host can't verify ownership; the dialog then blocks. */
48
+ ownership?: SetImageOwnership
32
49
  pending?: boolean
33
50
  onClose: () => void
34
51
  onLoad: () => Promise<WorkloadImageInventory>
@@ -83,31 +100,49 @@ export function reconcileRefreshedImageDrafts(
83
100
  return { drafts: nextDrafts, changedCurrentKeys }
84
101
  }
85
102
 
103
+ export function setImageSelectionKey(selection: SetImageSelection | null): string {
104
+ if (!selection) return ''
105
+ const rows = (refs: SetImageSelection['containers']) => refs.map((c) => [c.type, c.name])
106
+ return JSON.stringify([rows(selection.containers), rows(selection.changed)])
107
+ }
108
+
109
+ /** One write per container, by name: the guard's verdict for exactly the
110
+ * images being changed. */
111
+ export function setImageWrites(containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]): GitOpsWrite[] {
112
+ return containers.map((container) => ({
113
+ scope: 'spec',
114
+ paths: [`spec.template.spec.${container.type === 'initContainer' ? 'initContainers' : 'containers'}[name=${container.name}].image`],
115
+ description: container.name,
116
+ }))
117
+ }
118
+
119
+ // Before the containers load, the guard covers every container image.
120
+ export const SET_IMAGE_WRITES: GitOpsWrite[] = [
121
+ {
122
+ scope: 'spec',
123
+ paths: [
124
+ 'spec.template.spec.containers[*].image',
125
+ 'spec.template.spec.initContainers[*].image',
126
+ ],
127
+ description: 'Container images',
128
+ },
129
+ ]
130
+
86
131
  export function canSubmitImageUpdates({
87
132
  updateCount,
88
133
  hasEmptyImage,
89
- managed,
90
- ownershipResolved,
91
- acknowledged,
134
+ ownershipConfirmable,
92
135
  busy,
93
136
  loadFailed = false,
94
137
  }: {
95
138
  updateCount: number
96
139
  hasEmptyImage: boolean
97
- managed: boolean
98
- ownershipResolved: boolean
99
- acknowledged: boolean
140
+ /** Ownership resolved and, when it's managed, acknowledged. */
141
+ ownershipConfirmable: boolean
100
142
  busy: boolean
101
143
  loadFailed?: boolean
102
144
  }): boolean {
103
- return (
104
- updateCount > 0 &&
105
- !hasEmptyImage &&
106
- ownershipResolved &&
107
- (!managed || acknowledged) &&
108
- !busy &&
109
- !loadFailed
110
- )
145
+ return updateCount > 0 && !hasEmptyImage && ownershipConfirmable && !busy && !loadFailed
111
146
  }
112
147
 
113
148
  export function describeImageUpdateBehavior(
@@ -140,7 +175,7 @@ export function SetImageDialog({
140
175
  workloadLabel,
141
176
  workloadName,
142
177
  workloadResource,
143
- managedSources,
178
+ ownership,
144
179
  pending = false,
145
180
  onClose,
146
181
  onLoad,
@@ -155,7 +190,9 @@ export function SetImageDialog({
155
190
  const [submitting, setSubmitting] = useState(false)
156
191
  const [loadError, setLoadError] = useState<string | null>(null)
157
192
  const [submitError, setSubmitError] = useState<string | null>(null)
158
- const [acknowledged, setAcknowledged] = useState(false)
193
+ // The guard key the acknowledgment was given for: a different verdict
194
+ // (fresh evidence, other containers, a changed policy) asks again.
195
+ const [ackedKey, setAckedKey] = useState('')
159
196
  const [changedCurrentKeys, setChangedCurrentKeys] = useState<string[]>([])
160
197
  const onLoadRef = useRef(onLoad)
161
198
  const loadRequestRef = useRef(0)
@@ -216,7 +253,7 @@ export function SetImageDialog({
216
253
  setDrafts({})
217
254
  setChangedCurrentKeys([])
218
255
  setSubmitError(null)
219
- setAcknowledged(false)
256
+ setAckedKey('')
220
257
  void loadInventory()
221
258
  }, [open, workloadLabel, workloadName, workloadResource])
222
259
 
@@ -229,13 +266,27 @@ export function SetImageDialog({
229
266
  (container) => !(drafts[containerKey(container)] ?? '').trim(),
230
267
  ) ?? false
231
268
  const busy = pending || submitting
232
- const ownershipResolved = managedSources !== undefined
269
+ const ownershipResolved = ownership !== undefined
270
+ const onSelectionChange = ownership?.onSelectionChange
271
+ const selectionKey = setImageSelectionKey(
272
+ inventory ? { containers: inventory.containers, changed: updates } : null,
273
+ )
274
+ useEffect(() => {
275
+ if (!onSelectionChange) return
276
+ const [containers, changed] = selectionKey ? (JSON.parse(selectionKey) as [string[][], string[][]]) : [null, null]
277
+ const toRefs = (rows: string[][]) => rows.map(([type, name]) => ({ type: type as WorkloadContainerImage['type'], name }))
278
+ onSelectionChange(containers && changed ? { containers: toRefs(containers), changed: toRefs(changed) } : null)
279
+ }, [onSelectionChange, selectionKey])
280
+ const guard = ownership?.guard
281
+ // The host evaluates the guard after it hears of an edit; until then the
282
+ // verdict on screen is for other containers.
283
+ const guardCurrent = ownership?.selectionKey === undefined || ownership.selectionKey === selectionKey
284
+ const guardKey = gitOpsWriteGuardKey(guard)
285
+ const acknowledged = Boolean(guardKey) && ackedKey === guardKey
233
286
  const canSubmit = canSubmitImageUpdates({
234
287
  updateCount: updates.length,
235
288
  hasEmptyImage,
236
- managed: (managedSources?.length ?? 0) > 0,
237
- ownershipResolved,
238
- acknowledged,
289
+ ownershipConfirmable: guardCurrent && canConfirmGitOpsWrite(guard, acknowledged),
239
290
  busy,
240
291
  loadFailed: Boolean(loadError),
241
292
  })
@@ -378,57 +429,14 @@ export function SetImageDialog({
378
429
  {describeImageUpdateBehavior(inventory.behavior)}
379
430
  </div>
380
431
 
381
- {managedSources && managedSources.length > 0 && (
382
- <div className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-3">
383
- <div className="flex items-start gap-2">
384
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
385
- <div className="min-w-0 flex-1">
386
- <p className="text-xs font-medium text-amber-800 dark:text-amber-300">
387
- The declared source may overwrite this live change
388
- </p>
389
- <p className="mt-1 text-xs text-theme-text-secondary">
390
- Managed by{' '}
391
- {managedSources
392
- .map((source) => `${source.type} ${source.label}`)
393
- .join(' and ')}
394
- . Update the declared source for a durable change; use
395
- this action only as a break-glass operation.
396
- </p>
397
- <div className="mt-2 flex flex-wrap gap-2">
398
- {managedSources
399
- .filter((source) => source.onOpen)
400
- .map((source) => (
401
- <button
402
- key={`${source.type}-${source.label}`}
403
- type="button"
404
- onClick={source.onOpen}
405
- className="inline-flex items-center gap-1 text-xs font-medium text-skyhook-600 hover:underline dark:text-skyhook-400"
406
- >
407
- {source.type === 'GitOps'
408
- ? 'View GitOps owner'
409
- : 'View Helm release'}
410
- <ExternalLink className="h-3 w-3" />
411
- </button>
412
- ))}
413
- </div>
414
- <label className="mt-3 flex cursor-pointer items-start gap-2 text-xs text-theme-text-primary">
415
- <input
416
- type="checkbox"
417
- checked={acknowledged}
418
- onChange={(event) =>
419
- setAcknowledged(event.target.checked)
420
- }
421
- disabled={busy}
422
- className="mt-0.5 h-3.5 w-3.5 accent-skyhook-500"
423
- />
424
- <span>
425
- I understand the declared source may overwrite this live
426
- change.
427
- </span>
428
- </label>
429
- </div>
430
- </div>
431
- </div>
432
+ {guard && (
433
+ <GitOpsWriteWarning
434
+ guard={guard}
435
+ acknowledged={acknowledged}
436
+ onAcknowledgedChange={(value) => setAckedKey(value ? guardKey : '')}
437
+ onOpenOwner={ownership?.onOpenOwner}
438
+ disabled={busy}
439
+ />
432
440
  )}
433
441
 
434
442
  <ContainerImageGroup
@@ -5,8 +5,11 @@ export {
5
5
  DrainPlanDialog, DrainPlanContent, canConfirmDrain, planMatches, emptyDirPodsAtRisk, DEFAULT_DRAIN_DIALOG_OPTIONS,
6
6
  type DrainPlan, type DrainPlanPod, type DrainOutcome, type DrainDialogOptions,
7
7
  } from './DrainPlanDialog'
8
- export { SetImageDialog, type ManagedImageSource, type SetImageDialogProps } from './SetImageDialog'
8
+ export { SetImageDialog, SET_IMAGE_WRITES, setImageWrites, setImageSelectionKey, type SetImageOwnership, type SetImageSelection, type SetImageDialogProps } from './SetImageDialog'
9
+ export { GitOpsWriteWarning, type GitOpsWriteWarningProps } from './GitOpsWriteWarning'
9
10
  export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
10
11
  export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
11
12
  export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
12
13
  export { classifyDiffLine, hasDiffBodyChange, DiffLine } from './UnifiedDiff'
14
+
15
+ export { ActionConfirmDialog, type ActionConfirmDialogProps, type ActionConfirmResult, type ActionWrite } from './ActionConfirmDialog'
@@ -35,20 +35,7 @@ import type { NavigateToResource } from '../../utils/navigation'
35
35
  import { builtinGroupForKind } from '../../utils/api-resources'
36
36
  import { kindToPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '../../utils/navigation'
37
37
  import { pluralize } from '../../utils/pluralize'
38
-
39
- /** Format resource age (e.g., "3d", "5h", "10m") */
40
- function formatResourceAge(createdAt: string): string {
41
- const diff = Date.now() - new Date(createdAt).getTime()
42
- const mins = Math.floor(diff / 60000)
43
- if (mins < 1) return '<1m'
44
- if (mins < 60) return `${mins}m`
45
- const hours = Math.floor(mins / 60)
46
- if (hours < 24) return `${hours}h`
47
- const days = Math.floor(hours / 24)
48
- if (days < 30) return `${days}d`
49
- const months = Math.floor(days / 30)
50
- return `${months}mo`
51
- }
38
+ import { formatCompactAge } from '../../utils/format'
52
39
 
53
40
  export type { ActivityTypeFilter, ActivityFilterKey }
54
41
 
@@ -736,7 +723,7 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
736
723
  {item.createdAt && (
737
724
  <Tooltip content={`Created: ${new Date(item.createdAt).toLocaleString()}`}>
738
725
  <span className="text-xs text-theme-text-quaternary">
739
- • {formatResourceAge(item.createdAt)} old
726
+ • {formatCompactAge(item.createdAt)} old
740
727
  </span>
741
728
  </Tooltip>
742
729
  )}
@@ -62,6 +62,7 @@ import {
62
62
  } from './shared'
63
63
  import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
64
64
  import { clampLensToSelection, formatLensDuration, MIN_WINDOW_MS, type ScrubberRange } from './scrubber-math'
65
+ import { formatRelativeAgeTime } from '../../utils/format'
65
66
 
66
67
  // Predefined zoom levels (window widths in hours): 15m, 30m, 1h, 2h, 4h, 8h,
67
68
  // 12h, 1d, 2d, 3d, 7d. Hoisted so the controlled-window adapter can size the
@@ -674,7 +675,7 @@ function calculateInterestingnessWithBreakdown(lane: ResourceLane): ScoreBreakdo
674
675
  // Core workloads
675
676
  Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
676
677
  Service: 45, Ingress: 45, Gateway: 45,
677
- HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
678
+ HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42, UDPRoute: 42,
678
679
  Job: 40, CronJob: 40, Workflow: 40, CronWorkflow: 40,
679
680
  Pod: 30,
680
681
  HorizontalPodAutoscaler: 25,
@@ -2884,15 +2885,6 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
2884
2885
  const markerClasses = getMarkerStyle()
2885
2886
 
2886
2887
  // Build tooltip text - focus on what happened, explain the color meaning
2887
- const getRelativeTime = (timestamp: string) => {
2888
- const diff = Date.now() - new Date(timestamp).getTime()
2889
- const mins = Math.floor(diff / 60000)
2890
- if (mins < 1) return 'just now'
2891
- if (mins < 60) return `${mins}m ago`
2892
- const hours = Math.floor(mins / 60)
2893
- if (hours < 24) return `${hours}h ago`
2894
- return `${Math.floor(hours / 24)}d ago`
2895
- }
2896
2888
 
2897
2889
  // Get human-readable operation label with color indicator
2898
2890
  const getOperationLabel = () => {
@@ -2922,7 +2914,7 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
2922
2914
  const msg = event.message.length > 110 ? event.message.slice(0, 110) + '…' : event.message
2923
2915
  tooltipLines.push(msg)
2924
2916
  }
2925
- tooltipLines.push(getRelativeTime(event.timestamp))
2917
+ tooltipLines.push(formatRelativeAgeTime(event.timestamp))
2926
2918
  if (isHistoricalEvent(event)) tooltipLines.push('(from metadata)')
2927
2919
 
2928
2920
  const tooltipText = tooltipLines.join(' · ')
@@ -172,6 +172,7 @@ export const TIMELINE_RESOURCE_KINDS = [
172
172
  'HTTPRoute',
173
173
  'GRPCRoute',
174
174
  'TCPRoute',
175
+ 'UDPRoute',
175
176
  'TLSRoute',
176
177
  'ReplicaSet',
177
178
  'DaemonSet',
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { baseSubtitle } from './K8sResourceNode'
2
+ import { baseSubtitle, knativeDomainsTooltipLines } from './K8sResourceNode'
3
3
 
4
4
  describe('baseSubtitle', () => {
5
5
  it('describes workflow templates without dropping newer topology kinds', () => {
@@ -18,6 +18,23 @@ describe('baseSubtitle', () => {
18
18
  })).toBe('ClusterIP :80 +2 more')
19
19
  })
20
20
 
21
+ it('describes a domain-mapped Knative Service by its custom domain', () => {
22
+ expect(baseSubtitle('KnativeService', { url: 'http://api.team.svc.cluster.local' })).toBe('api.team.svc.cluster.local')
23
+ expect(baseSubtitle('KnativeService', { url: 'http://api.team.svc.cluster.local', mappedDomains: ['api.example.com'] })).toBe('api.example.com')
24
+ expect(baseSubtitle('KnativeService', { mappedDomains: ['a.example.com', 'b.example.com'] })).toBe('a.example.com +1')
25
+ })
26
+
27
+ it('labels a mapped Knative Service URL by what it is', () => {
28
+ const text = (data: Record<string, unknown>) => knativeDomainsTooltipLines(data).map((l) => l.text)
29
+ // Private: the Service URL is the internal address, shown once.
30
+ expect(text({ mappedDomains: ['api.example.com'], url: 'http://api.team.svc.cluster.local', internalUrl: 'http://api.team.svc.cluster.local' }))
31
+ .toEqual(['api.example.com', 'Internal: http://api.team.svc.cluster.local'])
32
+ // Public: its own URL isn't internal.
33
+ expect(text({ mappedDomains: ['shop.example.com'], url: 'https://shop.team.example.org', internalUrl: 'http://shop.team.svc.cluster.local' }))
34
+ .toEqual(['shop.example.com', 'Service URL: https://shop.team.example.org', 'Internal: http://shop.team.svc.cluster.local'])
35
+ expect(text({ url: 'https://shop.team.example.org' })).toEqual([])
36
+ })
37
+
21
38
  it('describes a Calico staged deletion as a removal, not as selecting everything', () => {
22
39
  // A staged deletion carries no selector, because the Calico API forbids one.
23
40
  // Falling through to the empty-selector default would label the broadest
@@ -200,6 +200,32 @@ export function servicePortsTooltip(ports: ServicePortEntry[]): React.ReactNode
200
200
  }
201
201
 
202
202
  // Default dimensions for unknown CRD kinds
203
+ // A domain-mapped Knative Service shows its custom domain as the subtitle; the
204
+ // tooltip lists every mapped domain, then the Service's own URL when it is
205
+ // public, and its cluster-internal address.
206
+ export function knativeDomainsTooltipLines(nodeData: Record<string, unknown>): { text: string; secondary?: boolean }[] {
207
+ const mapped = (nodeData.mappedDomains as string[] | undefined) || [];
208
+ if (mapped.length === 0) return [];
209
+ const url = nodeData.url as string | undefined;
210
+ const internalUrl = nodeData.internalUrl as string | undefined;
211
+ const lines: { text: string; secondary?: boolean }[] = mapped.map((domain) => ({ text: domain }));
212
+ if (url && url !== internalUrl) lines.push({ text: `Service URL: ${url}`, secondary: true });
213
+ if (internalUrl) lines.push({ text: `Internal: ${internalUrl}`, secondary: true });
214
+ return lines;
215
+ }
216
+
217
+ function knativeDomainsTooltip(nodeData: Record<string, unknown>): React.ReactNode | null {
218
+ const lines = knativeDomainsTooltipLines(nodeData);
219
+ if (lines.length === 0) return null;
220
+ return (
221
+ <div className="space-y-0.5">
222
+ {lines.map((line) => (
223
+ <div key={line.text} className={line.secondary ? 'text-theme-text-tertiary' : undefined}>{line.text}</div>
224
+ ))}
225
+ </div>
226
+ );
227
+ }
228
+
203
229
  export const DEFAULT_NODE_DIMENSIONS = { width: 260, height: 84 };
204
230
 
205
231
  const NODE_NAME_LENGTH_LIMITS: Partial<Record<NodeKind, number>> = {
@@ -218,6 +244,7 @@ export const NODE_DIMENSIONS: Record<
218
244
  HTTPRoute: { width: 300, height: 84 },
219
245
  GRPCRoute: { width: 300, height: 84 },
220
246
  TCPRoute: { width: 300, height: 84 },
247
+ UDPRoute: { width: 300, height: 84 },
221
248
  TLSRoute: { width: 300, height: 84 },
222
249
  Service: { width: 260, height: 84 },
223
250
  Deployment: { width: 280, height: 84 },
@@ -440,7 +467,8 @@ export function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>):
440
467
  case "HTTPRoute":
441
468
  case "GRPCRoute":
442
469
  case "TCPRoute":
443
- case "TLSRoute": {
470
+ case "TLSRoute":
471
+ case "UDPRoute": {
444
472
  const hostnames = nodeData.hostnames as string[];
445
473
  const rulesCount = (nodeData.rulesCount as number) || 0;
446
474
  const host = hostnames?.length ? hostnames[0] : "";
@@ -488,6 +516,10 @@ export function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>):
488
516
  return `${count} pods (${healthy} healthy)`;
489
517
  }
490
518
  case "KnativeService": {
519
+ const mapped = (nodeData.mappedDomains as string[] | undefined) || [];
520
+ if (mapped.length > 0) {
521
+ return mapped.length > 1 ? `${mapped[0]} +${mapped.length - 1}` : mapped[0];
522
+ }
491
523
  const url = nodeData.url as string;
492
524
  if (url) {
493
525
  // Show just the hostname from the URL for compactness
@@ -558,7 +590,10 @@ export const K8sResourceNode = memo(function K8sResourceNode({
558
590
  const subtitle = getSubtitle(kind, nodeData)
559
591
  const portsTooltip = kind === 'Service'
560
592
  ? servicePortsTooltip((nodeData.ports as ServicePortEntry[] | undefined) || [])
561
- : null
593
+ : kind === 'KnativeService'
594
+ ? knativeDomainsTooltip(nodeData)
595
+ : null
596
+ const clusterLocal = kind === 'KnativeService' && nodeData.visibility === 'cluster-local'
562
597
  const isInternet = kind === 'Internet'
563
598
  const isPodGroup = kind === 'PodGroup'
564
599
  const isSmallNode = kind === 'ConfigMap' || kind === 'Secret' || kind === 'ServiceAccount' || kind === 'SealedSecret' || kind === 'ServiceMonitor' || kind === 'PodMonitor' || kind === 'HorizontalPodAutoscaler'
@@ -664,6 +699,13 @@ export const K8sResourceNode = memo(function K8sResourceNode({
664
699
  </span>
665
700
  </Tooltip>
666
701
  )}
702
+ {clusterLocal && (
703
+ <Tooltip content="Reachable only inside the cluster, so it has no Internet edge" position="right">
704
+ <Badge tone="note" size="sm" className="!text-[9px] !px-1 !py-0.5 normal-case tracking-normal cursor-help">
705
+ Cluster-local
706
+ </Badge>
707
+ </Tooltip>
708
+ )}
667
709
  {trafficRole && (
668
710
  <Badge
669
711
  tone={trafficRole === 'canary' || trafficRole === 'preview' ? 'accent1' : 'accent2'}
@@ -33,6 +33,7 @@ const RESOURCE_KINDS: {
33
33
  { kind: 'GRPCRoute', label: 'GRPCRoute', icon: getTopologyIcon('GRPCRoute'), color: 'text-purple-300', category: 'networking' },
34
34
  { kind: 'TCPRoute', label: 'TCPRoute', icon: getTopologyIcon('TCPRoute'), color: 'text-purple-300', category: 'networking' },
35
35
  { kind: 'TLSRoute', label: 'TLSRoute', icon: getTopologyIcon('TLSRoute'), color: 'text-purple-300', category: 'networking' },
36
+ { kind: 'UDPRoute', label: 'UDPRoute', icon: getTopologyIcon('UDPRoute'), color: 'text-purple-300', category: 'networking' },
36
37
  { kind: 'Service', label: 'Service', icon: getTopologyIcon('Service'), color: 'text-blue-400', category: 'networking' },
37
38
  { kind: 'CalicoNetworkPolicy', label: 'Calico NetworkPolicy', icon: getTopologyIcon('CalicoNetworkPolicy'), color: 'text-teal-400', category: 'custom' },
38
39
  { kind: 'CalicoGlobalNetworkPolicy', label: 'Calico GlobalNetworkPolicy', icon: getTopologyIcon('CalicoGlobalNetworkPolicy'), color: 'text-teal-400', category: 'custom' },
@@ -32,6 +32,7 @@ import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
32
32
 
33
33
  import { K8sResourceNode } from './K8sResourceNode'
34
34
  import { GroupNode } from './GroupNode'
35
+ import { splitInternetByGroup } from './internet-groups'
35
36
  import { NEUTRAL_OWNER, type WorkloadFocus } from '../../utils/workload-colors'
36
37
  import { ownershipOf } from '../../utils/topology-neighborhood'
37
38
  import { buildHierarchicalElkGraph, applyHierarchicalLayout, getGroupKey, isGroupEffectivelyCollapsed, type GroupDisplayLevel } from './layout'
@@ -46,7 +47,8 @@ const EDGE_COLORS = {
46
47
  'exposes': '#3b82f6', // Blue for service exposure
47
48
  'manages': '#64748b', // Gray for management relationships
48
49
  'configures': '#f59e0b', // Amber for config
49
- 'uses': '#ec4899', // Pink for HPA
50
+ 'uses': '#ec4899', // Pink: autoscalers, mounted claims, dependencies
51
+ 'protects': '#8b5cf6', // Violet for policies and disruption budgets
50
52
  } as const
51
53
 
52
54
  function getEdgeColor(type: string, isTrafficView: boolean): string {
@@ -59,10 +61,11 @@ function getEdgeColor(type: string, isTrafficView: boolean): string {
59
61
 
60
62
  // Human-readable edge legend for the resources view (traffic view is all-green).
61
63
  const EDGE_LEGEND: { label: string; color: string }[] = [
62
- { label: 'owns', color: EDGE_COLORS['manages'] },
64
+ { label: 'manages', color: EDGE_COLORS['manages'] },
63
65
  { label: 'exposes', color: EDGE_COLORS['exposes'] },
64
66
  { label: 'configures', color: EDGE_COLORS['configures'] },
65
- { label: 'scales', color: EDGE_COLORS['uses'] },
67
+ { label: 'uses', color: EDGE_COLORS['uses'] },
68
+ { label: 'protects', color: EDGE_COLORS['protects'] },
66
69
  { label: 'routes to', color: EDGE_COLORS['routes-to'] },
67
70
  ]
68
71
 
@@ -546,75 +549,6 @@ export function TopologyGraph({
546
549
  return { nodes: newNodes, edges: newEdges }
547
550
  }, [])
548
551
 
549
- // Transform to per-group Internet nodes in traffic view with grouping
550
- const createPerGroupInternetNodes = useCallback((
551
- nodes: TopologyNode[],
552
- edges: TopologyEdge[],
553
- groupMode: GroupingMode
554
- ): { nodes: TopologyNode[]; edges: TopologyEdge[] } => {
555
- if (groupMode === 'none') {
556
- return { nodes, edges }
557
- }
558
-
559
- // Find the single Internet node
560
- const internetNode = nodes.find(n => n.kind === 'Internet')
561
- if (!internetNode) {
562
- return { nodes, edges }
563
- }
564
-
565
- // Find all ingresses/gateways and group them
566
- const ingresses = nodes.filter(n => n.kind === 'Ingress' || n.kind === 'Gateway')
567
- const groupsWithIngresses = new Map<string, TopologyNode[]>()
568
-
569
- for (const ingress of ingresses) {
570
- const groupKey = getGroupKey(ingress, groupMode)
571
- if (groupKey) {
572
- if (!groupsWithIngresses.has(groupKey)) {
573
- groupsWithIngresses.set(groupKey, [])
574
- }
575
- groupsWithIngresses.get(groupKey)!.push(ingress)
576
- }
577
- }
578
-
579
- // If no groups with ingresses, keep original
580
- if (groupsWithIngresses.size === 0) {
581
- return { nodes, edges }
582
- }
583
-
584
- // Remove original Internet node and its edges
585
- const newNodes = nodes.filter(n => n.id !== internetNode.id)
586
- const newEdges = edges.filter(e => e.source !== internetNode.id)
587
-
588
- // Create per-group Internet nodes
589
- for (const [groupKey, groupIngresses] of groupsWithIngresses) {
590
- const internetId = `internet-${groupMode}-${groupKey}`
591
-
592
- // Add Internet node for this group with group metadata
593
- newNodes.push({
594
- id: internetId,
595
- kind: 'Internet',
596
- name: 'Internet',
597
- status: 'healthy',
598
- data: {
599
- // Add group metadata so it gets grouped with its ingresses
600
- namespace: groupMode === 'namespace' ? groupKey : groupIngresses[0]?.data?.namespace,
601
- labels: groupMode === 'app' ? { 'app.kubernetes.io/name': groupKey } : {},
602
- },
603
- })
604
-
605
- // Add edges from this Internet node to its ingresses
606
- for (const ingress of groupIngresses) {
607
- newEdges.push({
608
- id: `${internetId}-to-${ingress.id}`,
609
- source: internetId,
610
- target: ingress.id,
611
- type: 'routes-to',
612
- })
613
- }
614
- }
615
-
616
- return { nodes: newNodes, edges: newEdges }
617
- }, [])
618
552
 
619
553
  // Prepare topology data with expanded pod groups
620
554
  const { workingNodes, workingEdges } = useMemo(() => {
@@ -634,13 +568,13 @@ export function TopologyGraph({
634
568
 
635
569
  // In traffic view with grouping, create per-group Internet nodes
636
570
  if (isTrafficView && groupingMode !== 'none') {
637
- const result = createPerGroupInternetNodes(nodes, edges, groupingMode)
571
+ const result = splitInternetByGroup(nodes, edges, groupingMode)
638
572
  nodes = result.nodes
639
573
  edges = result.edges
640
574
  }
641
575
 
642
576
  return { workingNodes: nodes, workingEdges: edges }
643
- }, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode, createPerGroupInternetNodes])
577
+ }, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode])
644
578
 
645
579
  // Handle card click in card-grid view — find the topology node and open drawer
646
580
  const handleCardClick = useCallback((nodeId: string) => {
@@ -48,6 +48,7 @@ function getKindColor(kind: string): string {
48
48
  case 'GRPCRoute':
49
49
  case 'TCPRoute':
50
50
  case 'TLSRoute':
51
+ case 'UDPRoute':
51
52
  return 'text-purple-300'
52
53
  case 'Service':
53
54
  return 'text-blue-400'
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { TopologyEdge, TopologyNode } from '../../types'
3
+ import { splitInternetByGroup } from './internet-groups'
4
+
5
+ const node = (id: string, kind: string, namespace?: string): TopologyNode =>
6
+ ({ id, kind, name: id, status: 'healthy', data: namespace ? { namespace } : {} }) as TopologyNode
7
+ const fromInternet = (target: string): TopologyEdge => ({ id: `internet-to-${target}`, source: 'internet', target, type: 'routes-to' })
8
+
9
+ describe('splitInternetByGroup', () => {
10
+ it('keeps every entrypoint connected, not only Ingresses and Gateways', () => {
11
+ const nodes = [
12
+ node('internet', 'Internet'),
13
+ node('ingress/shop/web', 'Ingress', 'shop'),
14
+ node('knativeservice/shop/api', 'KnativeService', 'shop'),
15
+ node('ingressroute/edge/dash', 'IngressRoute', 'edge'),
16
+ ]
17
+ const edges = nodes.slice(1).map(n => fromInternet(n.id))
18
+ const out = splitInternetByGroup(nodes, edges, 'namespace')
19
+
20
+ const reached = out.edges.filter(e => out.nodes.find(n => n.id === e.source)?.kind === 'Internet').map(e => `${e.source} -> ${e.target}`)
21
+ expect(reached.sort()).toEqual([
22
+ 'internet-namespace-edge -> ingressroute/edge/dash',
23
+ 'internet-namespace-shop -> ingress/shop/web',
24
+ 'internet-namespace-shop -> knativeservice/shop/api',
25
+ ])
26
+ expect(out.nodes.some(n => n.id === 'internet')).toBe(false)
27
+ })
28
+
29
+ it('leaves an ungrouped entrypoint on the original Internet node', () => {
30
+ const nodes = [node('internet', 'Internet'), node('ingress/shop/web', 'Ingress', 'shop'), node('gateway//shared', 'Gateway')]
31
+ const out = splitInternetByGroup(nodes, nodes.slice(1).map(n => fromInternet(n.id)), 'namespace')
32
+ expect(out.edges.some(e => e.source === 'internet' && e.target === 'gateway//shared')).toBe(true)
33
+ expect(out.nodes.some(n => n.id === 'internet')).toBe(true)
34
+ })
35
+ })