@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.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +29 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- 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-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
:
|
|
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 =
|
|
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
|
|
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) => {
|
|
@@ -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
|
+
}
|