@skyhook-io/k8s-ui 1.17.0 → 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 (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. 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
@@ -42,6 +42,7 @@ import { formatRelativeAgeTime } from '../../utils/format'
42
42
 
43
43
  interface ResourceActionsBarProps {
44
44
  resource: SelectedResource
45
+ leadingActions?: React.ReactNode
45
46
  data: any
46
47
  onClose?: () => void
47
48
  hideLogs?: boolean
@@ -80,6 +81,7 @@ interface ResourceActionsBarProps {
80
81
  cascadeDependents?: CascadeDependent[]
81
82
  cascadeLoading?: boolean
82
83
  cascadeRootResolved?: boolean
84
+ cascadePreviewFailed?: boolean
83
85
 
84
86
  // Workload restart
85
87
  onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
@@ -155,13 +157,13 @@ interface ResourceActionsBarProps {
155
157
  }
156
158
 
157
159
  export function ResourceActionsBar({
158
- resource, data, onClose, hideLogs, showYaml, onToggleYaml,
160
+ resource, data, leadingActions, onClose, hideLogs, showYaml, onToggleYaml,
159
161
  onCompareTo,
160
162
  onCompareAcrossClusters,
161
163
  canExec, canViewLogs, canPortForward,
162
164
  onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
163
165
  renderPortForward,
164
- onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved,
166
+ onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved, cascadePreviewFailed,
165
167
  onRestart, isRestarting,
166
168
  onLoadImages, onSetImages, isSettingImages, imageOwnership,
167
169
  revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
@@ -291,6 +293,8 @@ export function ResourceActionsBar({
291
293
 
292
294
  return (
293
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}
294
298
  {/* Kind-specific actions (left) */}
295
299
  {kind === 'pods' && (
296
300
  <>
@@ -586,8 +590,8 @@ export function ResourceActionsBar({
586
590
  </button>
587
591
  )}
588
592
 
589
- {/* Spacer pushes universal actions to the right */}
590
- <div className="flex-1" />
593
+ </div>
594
+ <div className="ml-auto flex flex-wrap items-center gap-1.5" data-action-group="utility">
591
595
 
592
596
  {/* Universal actions (right-aligned). The AI/Diagnose action is NOT here — it
593
597
  lives in the detail header chrome (see WorkloadView), set apart from these
@@ -673,6 +677,8 @@ export function ResourceActionsBar({
673
677
  </Tooltip>
674
678
  )}
675
679
 
680
+ </div>
681
+
676
682
  <ForceDeleteConfirmDialog
677
683
  open={showDeleteConfirm}
678
684
  onClose={() => setShowDeleteConfirm(false)}
@@ -684,6 +690,7 @@ export function ResourceActionsBar({
684
690
  cascadeDependents={cascadeDependents}
685
691
  cascadeLoading={cascadeLoading}
686
692
  cascadeRootResolved={cascadeRootResolved}
693
+ cascadePreviewFailed={cascadePreviewFailed}
687
694
  />
688
695
 
689
696
  {onLoadImages && onSetImages && (
@@ -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,8 +974,8 @@ 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} />}
@@ -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)
@@ -11,3 +11,5 @@ export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult
11
11
  export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
12
12
  export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
13
13
  export { classifyDiffLine, hasDiffBodyChange, DiffLine } from './UnifiedDiff'
14
+
15
+ export { ActionConfirmDialog, type ActionConfirmDialogProps, type ActionConfirmResult, type ActionWrite } from './ActionConfirmDialog'
@@ -675,7 +675,7 @@ function calculateInterestingnessWithBreakdown(lane: ResourceLane): ScoreBreakdo
675
675
  // Core workloads
676
676
  Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
677
677
  Service: 45, Ingress: 45, Gateway: 45,
678
- HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
678
+ HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42, UDPRoute: 42,
679
679
  Job: 40, CronJob: 40, Workflow: 40, CronWorkflow: 40,
680
680
  Pod: 30,
681
681
  HorizontalPodAutoscaler: 25,
@@ -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'
@@ -548,75 +549,6 @@ export function TopologyGraph({
548
549
  return { nodes: newNodes, edges: newEdges }
549
550
  }, [])
550
551
 
551
- // Transform to per-group Internet nodes in traffic view with grouping
552
- const createPerGroupInternetNodes = useCallback((
553
- nodes: TopologyNode[],
554
- edges: TopologyEdge[],
555
- groupMode: GroupingMode
556
- ): { nodes: TopologyNode[]; edges: TopologyEdge[] } => {
557
- if (groupMode === 'none') {
558
- return { nodes, edges }
559
- }
560
-
561
- // Find the single Internet node
562
- const internetNode = nodes.find(n => n.kind === 'Internet')
563
- if (!internetNode) {
564
- return { nodes, edges }
565
- }
566
-
567
- // Find all ingresses/gateways and group them
568
- const ingresses = nodes.filter(n => n.kind === 'Ingress' || n.kind === 'Gateway')
569
- const groupsWithIngresses = new Map<string, TopologyNode[]>()
570
-
571
- for (const ingress of ingresses) {
572
- const groupKey = getGroupKey(ingress, groupMode)
573
- if (groupKey) {
574
- if (!groupsWithIngresses.has(groupKey)) {
575
- groupsWithIngresses.set(groupKey, [])
576
- }
577
- groupsWithIngresses.get(groupKey)!.push(ingress)
578
- }
579
- }
580
-
581
- // If no groups with ingresses, keep original
582
- if (groupsWithIngresses.size === 0) {
583
- return { nodes, edges }
584
- }
585
-
586
- // Remove original Internet node and its edges
587
- const newNodes = nodes.filter(n => n.id !== internetNode.id)
588
- const newEdges = edges.filter(e => e.source !== internetNode.id)
589
-
590
- // Create per-group Internet nodes
591
- for (const [groupKey, groupIngresses] of groupsWithIngresses) {
592
- const internetId = `internet-${groupMode}-${groupKey}`
593
-
594
- // Add Internet node for this group with group metadata
595
- newNodes.push({
596
- id: internetId,
597
- kind: 'Internet',
598
- name: 'Internet',
599
- status: 'healthy',
600
- data: {
601
- // Add group metadata so it gets grouped with its ingresses
602
- namespace: groupMode === 'namespace' ? groupKey : groupIngresses[0]?.data?.namespace,
603
- labels: groupMode === 'app' ? { 'app.kubernetes.io/name': groupKey } : {},
604
- },
605
- })
606
-
607
- // Add edges from this Internet node to its ingresses
608
- for (const ingress of groupIngresses) {
609
- newEdges.push({
610
- id: `${internetId}-to-${ingress.id}`,
611
- source: internetId,
612
- target: ingress.id,
613
- type: 'routes-to',
614
- })
615
- }
616
- }
617
-
618
- return { nodes: newNodes, edges: newEdges }
619
- }, [])
620
552
 
621
553
  // Prepare topology data with expanded pod groups
622
554
  const { workingNodes, workingEdges } = useMemo(() => {
@@ -636,13 +568,13 @@ export function TopologyGraph({
636
568
 
637
569
  // In traffic view with grouping, create per-group Internet nodes
638
570
  if (isTrafficView && groupingMode !== 'none') {
639
- const result = createPerGroupInternetNodes(nodes, edges, groupingMode)
571
+ const result = splitInternetByGroup(nodes, edges, groupingMode)
640
572
  nodes = result.nodes
641
573
  edges = result.edges
642
574
  }
643
575
 
644
576
  return { workingNodes: nodes, workingEdges: edges }
645
- }, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode, createPerGroupInternetNodes])
577
+ }, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode])
646
578
 
647
579
  // Handle card click in card-grid view — find the topology node and open drawer
648
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
+ })
@@ -0,0 +1,55 @@
1
+ import type { GroupingMode, TopologyEdge, TopologyNode } from '../../types'
2
+ import { getGroupKey } from './layout'
3
+
4
+ /**
5
+ * Splits the traffic view's single Internet node into one per group, so each
6
+ * namespace or app shows its own entry from outside. Every original Internet
7
+ * edge is kept, whatever kind its entrypoint is (Ingress, Gateway, Istio
8
+ * Gateway, Knative Service, Traefik route, Contour HTTPProxy); an entrypoint
9
+ * with no group stays connected to the original Internet node.
10
+ */
11
+ export function splitInternetByGroup(
12
+ nodes: TopologyNode[],
13
+ edges: TopologyEdge[],
14
+ groupMode: GroupingMode,
15
+ ): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
16
+ if (groupMode === 'none') return { nodes, edges }
17
+ const internetNode = nodes.find(n => n.kind === 'Internet')
18
+ if (!internetNode) return { nodes, edges }
19
+
20
+ const nodeById = new Map(nodes.map(n => [n.id, n]))
21
+ const groups = new Map<string, { entry: TopologyNode; edges: TopologyEdge[] }>()
22
+ const regrouped = new Set<TopologyEdge>()
23
+ for (const edge of edges) {
24
+ if (edge.source !== internetNode.id) continue
25
+ const target = nodeById.get(edge.target)
26
+ const groupKey = target ? getGroupKey(target, groupMode) : null
27
+ if (!target || !groupKey) continue
28
+ if (!groups.has(groupKey)) groups.set(groupKey, { entry: target, edges: [] })
29
+ groups.get(groupKey)!.edges.push(edge)
30
+ regrouped.add(edge)
31
+ }
32
+ if (groups.size === 0) return { nodes, edges }
33
+
34
+ const keepOriginal = edges.some(e => e.source === internetNode.id && !regrouped.has(e))
35
+ const newNodes = keepOriginal ? [...nodes] : nodes.filter(n => n.id !== internetNode.id)
36
+ const newEdges = edges.filter(e => !regrouped.has(e))
37
+ for (const [groupKey, group] of groups) {
38
+ const internetId = `internet-${groupMode}-${groupKey}`
39
+ newNodes.push({
40
+ id: internetId,
41
+ kind: 'Internet',
42
+ name: 'Internet',
43
+ status: 'healthy',
44
+ data: {
45
+ // Group metadata so the node lands inside its entrypoints' group.
46
+ namespace: groupMode === 'namespace' ? groupKey : group.entry.data?.namespace,
47
+ labels: groupMode === 'app' ? { 'app.kubernetes.io/name': groupKey } : {},
48
+ },
49
+ })
50
+ for (const edge of group.edges) {
51
+ newEdges.push({ ...edge, id: `${internetId}-to-${edge.target}`, source: internetId })
52
+ }
53
+ }
54
+ return { nodes: newNodes, edges: newEdges }
55
+ }
@@ -864,7 +864,7 @@ function computeWorkloadCards(
864
864
  const KIND_PRIORITY: Record<string, number> = {
865
865
  'Deployment': 1, 'Rollout': 1, 'StatefulSet': 2, 'DaemonSet': 3,
866
866
  'CronJob': 4, 'CronWorkflow': 4, 'Job': 5, 'Workflow': 5, 'Service': 6, 'Gateway': 7,
867
- 'HTTPRoute': 6, 'GRPCRoute': 6, 'TCPRoute': 6, 'TLSRoute': 6, 'Ingress': 7,
867
+ 'HTTPRoute': 6, 'GRPCRoute': 6, 'TCPRoute': 6, 'TLSRoute': 6, 'UDPRoute': 6, 'Ingress': 7,
868
868
  'CalicoNetworkPolicy': 10, 'CalicoGlobalNetworkPolicy': 10,
869
869
  'CalicoStagedNetworkPolicy': 10, 'CalicoStagedGlobalNetworkPolicy': 10,
870
870
  'CalicoStagedKubernetesNetworkPolicy': 10,
@@ -85,6 +85,7 @@ const KIND: Record<string, string> = {
85
85
  HTTPRoute: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
86
86
  GRPCRoute: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
87
87
  TCPRoute: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
88
+ UDPRoute: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
88
89
  TLSRoute: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
89
90
 
90
91
  // Config
@@ -0,0 +1,68 @@
1
+ import { useEffect, useState, type ReactNode } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { toneTextClass } from './status-tone'
4
+ import { Collapse, CollapseChevron, useDisclosure } from './Collapse'
5
+
6
+ /** A folded section's one-line summary, and whether it opens on its own. */
7
+ export interface FoldSummary {
8
+ text: string
9
+ /** Something in the section needs a look: it opens itself. */
10
+ attention: boolean
11
+ }
12
+
13
+ export function SectionHeading({ children, hint }: { children: ReactNode; hint?: ReactNode }) {
14
+ return (
15
+ <div className="mb-2 mt-5 flex items-baseline gap-2 first:mt-0">
16
+ <h3 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">{children}</h3>
17
+ {hint && <span className="text-[11px] text-theme-text-tertiary">{hint}</span>}
18
+ </div>
19
+ )
20
+ }
21
+
22
+ /**
23
+ * A section folded to one summary line. It opens itself when `attention`
24
+ * turns true (data arriving after the first render included), and stays as
25
+ * the reader left it otherwise.
26
+ */
27
+ export function FoldSection({
28
+ title,
29
+ hint,
30
+ summary,
31
+ attention,
32
+ children,
33
+ }: {
34
+ title: ReactNode
35
+ hint?: ReactNode
36
+ summary: ReactNode
37
+ attention: boolean
38
+ children: ReactNode
39
+ }) {
40
+ const [open, setOpen] = useState(attention)
41
+ useEffect(() => {
42
+ if (attention) setOpen(true)
43
+ }, [attention])
44
+ const d = useDisclosure(open)
45
+ return (
46
+ <div className="mt-5 first:mt-0">
47
+ <h3>
48
+ <button
49
+ type="button"
50
+ {...d.buttonProps}
51
+ onClick={() => setOpen((v) => !v)}
52
+ className="flex w-full items-start gap-2 rounded text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
53
+ >
54
+ <CollapseChevron open={open} className="mt-1 h-3 w-3 shrink-0 text-theme-text-tertiary" />
55
+ {/* Wrapped parts line up under the title, not under the chevron. */}
56
+ <span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
57
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">{title}</span>
58
+ {hint && <span className="text-[11px] text-theme-text-tertiary">{hint}</span>}
59
+ {!open && <span className={clsx('min-w-0 text-sm', attention ? toneTextClass('degraded') : 'text-theme-text-secondary')}>{summary}</span>}
60
+ </span>
61
+ </button>
62
+ </h3>
63
+ <Collapse open={open} id={d.panelId}>
64
+ <div className="pt-2">{children}</div>
65
+ </Collapse>
66
+ </div>
67
+ )
68
+ }