@skyhook-io/k8s-ui 1.16.3 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- 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/checks/types.ts +2 -13
- 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/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- 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 +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- 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 +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- 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.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -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 +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- 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 +7 -8
- 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 +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- 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/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +57 -17
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- 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/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- 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/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- 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 -24
- 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-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +99 -39
- 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/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- 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 +8 -74
- 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/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- 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 +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- 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 +21 -2
- package/src/types/core.ts +60 -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 +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -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 +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -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
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState, useId } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
AlertTriangle,
|
|
4
|
-
ExternalLink,
|
|
5
|
-
Loader2,
|
|
6
|
-
RefreshCw,
|
|
7
|
-
X,
|
|
8
|
-
} from 'lucide-react'
|
|
2
|
+
import { Loader2, RefreshCw, X } from 'lucide-react'
|
|
9
3
|
import { clsx } from 'clsx'
|
|
10
4
|
|
|
11
5
|
import type {
|
|
@@ -16,11 +10,33 @@ import type {
|
|
|
16
10
|
} from '../../types/core'
|
|
17
11
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
18
12
|
import { Badge } from '../ui/Badge'
|
|
13
|
+
import {
|
|
14
|
+
canConfirmGitOpsWrite,
|
|
15
|
+
gitOpsWriteGuardKey,
|
|
16
|
+
type GitOpsWrite,
|
|
17
|
+
type GitOpsWriteGuard,
|
|
18
|
+
} from '../../utils/gitops-write-guard'
|
|
19
|
+
import { GitOpsWriteWarning } from './GitOpsWriteWarning'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/** The GitOps write guard for the object whose Pod template the dialog
|
|
22
|
+
* writes, evaluated by the host: for {@link SET_IMAGE_WRITES} until the
|
|
23
|
+
* containers are known, then for {@link setImageWrites} of the containers
|
|
24
|
+
* {@link onSelectionChange} reports. */
|
|
25
|
+
export interface SetImageOwnership {
|
|
26
|
+
guard: GitOpsWriteGuard
|
|
27
|
+
onOpenOwner?: () => void
|
|
28
|
+
/** The containers the dialog lists and the ones being changed, so the
|
|
29
|
+
* guard can judge exactly those (null until the containers load). */
|
|
30
|
+
onSelectionChange?: (selection: SetImageSelection | null) => void
|
|
31
|
+
/** {@link setImageSelectionKey} of the selection `guard` was evaluated
|
|
32
|
+
* for. When set, Update waits until it matches the dialog's current
|
|
33
|
+
* selection. */
|
|
34
|
+
selectionKey?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SetImageSelection {
|
|
38
|
+
containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]
|
|
39
|
+
changed: Pick<WorkloadContainerImage, 'type' | 'name'>[]
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
export interface SetImageDialogProps {
|
|
@@ -28,7 +44,8 @@ export interface SetImageDialogProps {
|
|
|
28
44
|
workloadLabel: string
|
|
29
45
|
workloadName: string
|
|
30
46
|
workloadResource: string
|
|
31
|
-
|
|
47
|
+
/** Undefined while the host can't verify ownership; the dialog then blocks. */
|
|
48
|
+
ownership?: SetImageOwnership
|
|
32
49
|
pending?: boolean
|
|
33
50
|
onClose: () => void
|
|
34
51
|
onLoad: () => Promise<WorkloadImageInventory>
|
|
@@ -83,31 +100,49 @@ export function reconcileRefreshedImageDrafts(
|
|
|
83
100
|
return { drafts: nextDrafts, changedCurrentKeys }
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
export function setImageSelectionKey(selection: SetImageSelection | null): string {
|
|
104
|
+
if (!selection) return ''
|
|
105
|
+
const rows = (refs: SetImageSelection['containers']) => refs.map((c) => [c.type, c.name])
|
|
106
|
+
return JSON.stringify([rows(selection.containers), rows(selection.changed)])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One write per container, by name: the guard's verdict for exactly the
|
|
110
|
+
* images being changed. */
|
|
111
|
+
export function setImageWrites(containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]): GitOpsWrite[] {
|
|
112
|
+
return containers.map((container) => ({
|
|
113
|
+
scope: 'spec',
|
|
114
|
+
paths: [`spec.template.spec.${container.type === 'initContainer' ? 'initContainers' : 'containers'}[name=${container.name}].image`],
|
|
115
|
+
description: container.name,
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Before the containers load, the guard covers every container image.
|
|
120
|
+
export const SET_IMAGE_WRITES: GitOpsWrite[] = [
|
|
121
|
+
{
|
|
122
|
+
scope: 'spec',
|
|
123
|
+
paths: [
|
|
124
|
+
'spec.template.spec.containers[*].image',
|
|
125
|
+
'spec.template.spec.initContainers[*].image',
|
|
126
|
+
],
|
|
127
|
+
description: 'Container images',
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
86
131
|
export function canSubmitImageUpdates({
|
|
87
132
|
updateCount,
|
|
88
133
|
hasEmptyImage,
|
|
89
|
-
|
|
90
|
-
ownershipResolved,
|
|
91
|
-
acknowledged,
|
|
134
|
+
ownershipConfirmable,
|
|
92
135
|
busy,
|
|
93
136
|
loadFailed = false,
|
|
94
137
|
}: {
|
|
95
138
|
updateCount: number
|
|
96
139
|
hasEmptyImage: boolean
|
|
97
|
-
managed
|
|
98
|
-
|
|
99
|
-
acknowledged: boolean
|
|
140
|
+
/** Ownership resolved and, when it's managed, acknowledged. */
|
|
141
|
+
ownershipConfirmable: boolean
|
|
100
142
|
busy: boolean
|
|
101
143
|
loadFailed?: boolean
|
|
102
144
|
}): boolean {
|
|
103
|
-
return
|
|
104
|
-
updateCount > 0 &&
|
|
105
|
-
!hasEmptyImage &&
|
|
106
|
-
ownershipResolved &&
|
|
107
|
-
(!managed || acknowledged) &&
|
|
108
|
-
!busy &&
|
|
109
|
-
!loadFailed
|
|
110
|
-
)
|
|
145
|
+
return updateCount > 0 && !hasEmptyImage && ownershipConfirmable && !busy && !loadFailed
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
export function describeImageUpdateBehavior(
|
|
@@ -140,7 +175,7 @@ export function SetImageDialog({
|
|
|
140
175
|
workloadLabel,
|
|
141
176
|
workloadName,
|
|
142
177
|
workloadResource,
|
|
143
|
-
|
|
178
|
+
ownership,
|
|
144
179
|
pending = false,
|
|
145
180
|
onClose,
|
|
146
181
|
onLoad,
|
|
@@ -155,7 +190,9 @@ export function SetImageDialog({
|
|
|
155
190
|
const [submitting, setSubmitting] = useState(false)
|
|
156
191
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
157
192
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
158
|
-
|
|
193
|
+
// The guard key the acknowledgment was given for: a different verdict
|
|
194
|
+
// (fresh evidence, other containers, a changed policy) asks again.
|
|
195
|
+
const [ackedKey, setAckedKey] = useState('')
|
|
159
196
|
const [changedCurrentKeys, setChangedCurrentKeys] = useState<string[]>([])
|
|
160
197
|
const onLoadRef = useRef(onLoad)
|
|
161
198
|
const loadRequestRef = useRef(0)
|
|
@@ -216,7 +253,7 @@ export function SetImageDialog({
|
|
|
216
253
|
setDrafts({})
|
|
217
254
|
setChangedCurrentKeys([])
|
|
218
255
|
setSubmitError(null)
|
|
219
|
-
|
|
256
|
+
setAckedKey('')
|
|
220
257
|
void loadInventory()
|
|
221
258
|
}, [open, workloadLabel, workloadName, workloadResource])
|
|
222
259
|
|
|
@@ -229,13 +266,27 @@ export function SetImageDialog({
|
|
|
229
266
|
(container) => !(drafts[containerKey(container)] ?? '').trim(),
|
|
230
267
|
) ?? false
|
|
231
268
|
const busy = pending || submitting
|
|
232
|
-
const ownershipResolved =
|
|
269
|
+
const ownershipResolved = ownership !== undefined
|
|
270
|
+
const onSelectionChange = ownership?.onSelectionChange
|
|
271
|
+
const selectionKey = setImageSelectionKey(
|
|
272
|
+
inventory ? { containers: inventory.containers, changed: updates } : null,
|
|
273
|
+
)
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!onSelectionChange) return
|
|
276
|
+
const [containers, changed] = selectionKey ? (JSON.parse(selectionKey) as [string[][], string[][]]) : [null, null]
|
|
277
|
+
const toRefs = (rows: string[][]) => rows.map(([type, name]) => ({ type: type as WorkloadContainerImage['type'], name }))
|
|
278
|
+
onSelectionChange(containers && changed ? { containers: toRefs(containers), changed: toRefs(changed) } : null)
|
|
279
|
+
}, [onSelectionChange, selectionKey])
|
|
280
|
+
const guard = ownership?.guard
|
|
281
|
+
// The host evaluates the guard after it hears of an edit; until then the
|
|
282
|
+
// verdict on screen is for other containers.
|
|
283
|
+
const guardCurrent = ownership?.selectionKey === undefined || ownership.selectionKey === selectionKey
|
|
284
|
+
const guardKey = gitOpsWriteGuardKey(guard)
|
|
285
|
+
const acknowledged = Boolean(guardKey) && ackedKey === guardKey
|
|
233
286
|
const canSubmit = canSubmitImageUpdates({
|
|
234
287
|
updateCount: updates.length,
|
|
235
288
|
hasEmptyImage,
|
|
236
|
-
|
|
237
|
-
ownershipResolved,
|
|
238
|
-
acknowledged,
|
|
289
|
+
ownershipConfirmable: guardCurrent && canConfirmGitOpsWrite(guard, acknowledged),
|
|
239
290
|
busy,
|
|
240
291
|
loadFailed: Boolean(loadError),
|
|
241
292
|
})
|
|
@@ -378,57 +429,14 @@ export function SetImageDialog({
|
|
|
378
429
|
{describeImageUpdateBehavior(inventory.behavior)}
|
|
379
430
|
</div>
|
|
380
431
|
|
|
381
|
-
{
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<p className="mt-1 text-xs text-theme-text-secondary">
|
|
390
|
-
Managed by{' '}
|
|
391
|
-
{managedSources
|
|
392
|
-
.map((source) => `${source.type} ${source.label}`)
|
|
393
|
-
.join(' and ')}
|
|
394
|
-
. Update the declared source for a durable change; use
|
|
395
|
-
this action only as a break-glass operation.
|
|
396
|
-
</p>
|
|
397
|
-
<div className="mt-2 flex flex-wrap gap-2">
|
|
398
|
-
{managedSources
|
|
399
|
-
.filter((source) => source.onOpen)
|
|
400
|
-
.map((source) => (
|
|
401
|
-
<button
|
|
402
|
-
key={`${source.type}-${source.label}`}
|
|
403
|
-
type="button"
|
|
404
|
-
onClick={source.onOpen}
|
|
405
|
-
className="inline-flex items-center gap-1 text-xs font-medium text-skyhook-600 hover:underline dark:text-skyhook-400"
|
|
406
|
-
>
|
|
407
|
-
{source.type === 'GitOps'
|
|
408
|
-
? 'View GitOps owner'
|
|
409
|
-
: 'View Helm release'}
|
|
410
|
-
<ExternalLink className="h-3 w-3" />
|
|
411
|
-
</button>
|
|
412
|
-
))}
|
|
413
|
-
</div>
|
|
414
|
-
<label className="mt-3 flex cursor-pointer items-start gap-2 text-xs text-theme-text-primary">
|
|
415
|
-
<input
|
|
416
|
-
type="checkbox"
|
|
417
|
-
checked={acknowledged}
|
|
418
|
-
onChange={(event) =>
|
|
419
|
-
setAcknowledged(event.target.checked)
|
|
420
|
-
}
|
|
421
|
-
disabled={busy}
|
|
422
|
-
className="mt-0.5 h-3.5 w-3.5 accent-skyhook-500"
|
|
423
|
-
/>
|
|
424
|
-
<span>
|
|
425
|
-
I understand the declared source may overwrite this live
|
|
426
|
-
change.
|
|
427
|
-
</span>
|
|
428
|
-
</label>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
+
{guard && (
|
|
433
|
+
<GitOpsWriteWarning
|
|
434
|
+
guard={guard}
|
|
435
|
+
acknowledged={acknowledged}
|
|
436
|
+
onAcknowledgedChange={(value) => setAckedKey(value ? guardKey : '')}
|
|
437
|
+
onOpenOwner={ownership?.onOpenOwner}
|
|
438
|
+
disabled={busy}
|
|
439
|
+
/>
|
|
432
440
|
)}
|
|
433
441
|
|
|
434
442
|
<ContainerImageGroup
|
|
@@ -5,8 +5,11 @@ export {
|
|
|
5
5
|
DrainPlanDialog, DrainPlanContent, canConfirmDrain, planMatches, emptyDirPodsAtRisk, DEFAULT_DRAIN_DIALOG_OPTIONS,
|
|
6
6
|
type DrainPlan, type DrainPlanPod, type DrainOutcome, type DrainDialogOptions,
|
|
7
7
|
} from './DrainPlanDialog'
|
|
8
|
-
export { SetImageDialog, type
|
|
8
|
+
export { SetImageDialog, SET_IMAGE_WRITES, setImageWrites, setImageSelectionKey, type SetImageOwnership, type SetImageSelection, type SetImageDialogProps } from './SetImageDialog'
|
|
9
|
+
export { GitOpsWriteWarning, type GitOpsWriteWarningProps } from './GitOpsWriteWarning'
|
|
9
10
|
export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
|
|
10
11
|
export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
|
|
11
12
|
export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
|
|
12
13
|
export { classifyDiffLine, hasDiffBodyChange, DiffLine } from './UnifiedDiff'
|
|
14
|
+
|
|
15
|
+
export { ActionConfirmDialog, type ActionConfirmDialogProps, type ActionConfirmResult, type ActionWrite } from './ActionConfirmDialog'
|
|
@@ -35,20 +35,7 @@ import type { NavigateToResource } from '../../utils/navigation'
|
|
|
35
35
|
import { builtinGroupForKind } from '../../utils/api-resources'
|
|
36
36
|
import { kindToPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '../../utils/navigation'
|
|
37
37
|
import { pluralize } from '../../utils/pluralize'
|
|
38
|
-
|
|
39
|
-
/** Format resource age (e.g., "3d", "5h", "10m") */
|
|
40
|
-
function formatResourceAge(createdAt: string): string {
|
|
41
|
-
const diff = Date.now() - new Date(createdAt).getTime()
|
|
42
|
-
const mins = Math.floor(diff / 60000)
|
|
43
|
-
if (mins < 1) return '<1m'
|
|
44
|
-
if (mins < 60) return `${mins}m`
|
|
45
|
-
const hours = Math.floor(mins / 60)
|
|
46
|
-
if (hours < 24) return `${hours}h`
|
|
47
|
-
const days = Math.floor(hours / 24)
|
|
48
|
-
if (days < 30) return `${days}d`
|
|
49
|
-
const months = Math.floor(days / 30)
|
|
50
|
-
return `${months}mo`
|
|
51
|
-
}
|
|
38
|
+
import { formatCompactAge } from '../../utils/format'
|
|
52
39
|
|
|
53
40
|
export type { ActivityTypeFilter, ActivityFilterKey }
|
|
54
41
|
|
|
@@ -736,7 +723,7 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
|
|
|
736
723
|
{item.createdAt && (
|
|
737
724
|
<Tooltip content={`Created: ${new Date(item.createdAt).toLocaleString()}`}>
|
|
738
725
|
<span className="text-xs text-theme-text-quaternary">
|
|
739
|
-
• {
|
|
726
|
+
• {formatCompactAge(item.createdAt)} old
|
|
740
727
|
</span>
|
|
741
728
|
</Tooltip>
|
|
742
729
|
)}
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
} from './shared'
|
|
63
63
|
import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
64
64
|
import { clampLensToSelection, formatLensDuration, MIN_WINDOW_MS, type ScrubberRange } from './scrubber-math'
|
|
65
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
65
66
|
|
|
66
67
|
// Predefined zoom levels (window widths in hours): 15m, 30m, 1h, 2h, 4h, 8h,
|
|
67
68
|
// 12h, 1d, 2d, 3d, 7d. Hoisted so the controlled-window adapter can size the
|
|
@@ -674,7 +675,7 @@ function calculateInterestingnessWithBreakdown(lane: ResourceLane): ScoreBreakdo
|
|
|
674
675
|
// Core workloads
|
|
675
676
|
Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
|
|
676
677
|
Service: 45, Ingress: 45, Gateway: 45,
|
|
677
|
-
HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
|
|
678
|
+
HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42, UDPRoute: 42,
|
|
678
679
|
Job: 40, CronJob: 40, Workflow: 40, CronWorkflow: 40,
|
|
679
680
|
Pod: 30,
|
|
680
681
|
HorizontalPodAutoscaler: 25,
|
|
@@ -2884,15 +2885,6 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
|
|
|
2884
2885
|
const markerClasses = getMarkerStyle()
|
|
2885
2886
|
|
|
2886
2887
|
// Build tooltip text - focus on what happened, explain the color meaning
|
|
2887
|
-
const getRelativeTime = (timestamp: string) => {
|
|
2888
|
-
const diff = Date.now() - new Date(timestamp).getTime()
|
|
2889
|
-
const mins = Math.floor(diff / 60000)
|
|
2890
|
-
if (mins < 1) return 'just now'
|
|
2891
|
-
if (mins < 60) return `${mins}m ago`
|
|
2892
|
-
const hours = Math.floor(mins / 60)
|
|
2893
|
-
if (hours < 24) return `${hours}h ago`
|
|
2894
|
-
return `${Math.floor(hours / 24)}d ago`
|
|
2895
|
-
}
|
|
2896
2888
|
|
|
2897
2889
|
// Get human-readable operation label with color indicator
|
|
2898
2890
|
const getOperationLabel = () => {
|
|
@@ -2922,7 +2914,7 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
|
|
|
2922
2914
|
const msg = event.message.length > 110 ? event.message.slice(0, 110) + '…' : event.message
|
|
2923
2915
|
tooltipLines.push(msg)
|
|
2924
2916
|
}
|
|
2925
|
-
tooltipLines.push(
|
|
2917
|
+
tooltipLines.push(formatRelativeAgeTime(event.timestamp))
|
|
2926
2918
|
if (isHistoricalEvent(event)) tooltipLines.push('(from metadata)')
|
|
2927
2919
|
|
|
2928
2920
|
const tooltipText = tooltipLines.join(' · ')
|
|
@@ -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'
|
|
@@ -46,7 +47,8 @@ const EDGE_COLORS = {
|
|
|
46
47
|
'exposes': '#3b82f6', // Blue for service exposure
|
|
47
48
|
'manages': '#64748b', // Gray for management relationships
|
|
48
49
|
'configures': '#f59e0b', // Amber for config
|
|
49
|
-
'uses': '#ec4899', // Pink
|
|
50
|
+
'uses': '#ec4899', // Pink: autoscalers, mounted claims, dependencies
|
|
51
|
+
'protects': '#8b5cf6', // Violet for policies and disruption budgets
|
|
50
52
|
} as const
|
|
51
53
|
|
|
52
54
|
function getEdgeColor(type: string, isTrafficView: boolean): string {
|
|
@@ -59,10 +61,11 @@ function getEdgeColor(type: string, isTrafficView: boolean): string {
|
|
|
59
61
|
|
|
60
62
|
// Human-readable edge legend for the resources view (traffic view is all-green).
|
|
61
63
|
const EDGE_LEGEND: { label: string; color: string }[] = [
|
|
62
|
-
{ label: '
|
|
64
|
+
{ label: 'manages', color: EDGE_COLORS['manages'] },
|
|
63
65
|
{ label: 'exposes', color: EDGE_COLORS['exposes'] },
|
|
64
66
|
{ label: 'configures', color: EDGE_COLORS['configures'] },
|
|
65
|
-
{ label: '
|
|
67
|
+
{ label: 'uses', color: EDGE_COLORS['uses'] },
|
|
68
|
+
{ label: 'protects', color: EDGE_COLORS['protects'] },
|
|
66
69
|
{ label: 'routes to', color: EDGE_COLORS['routes-to'] },
|
|
67
70
|
]
|
|
68
71
|
|
|
@@ -546,75 +549,6 @@ export function TopologyGraph({
|
|
|
546
549
|
return { nodes: newNodes, edges: newEdges }
|
|
547
550
|
}, [])
|
|
548
551
|
|
|
549
|
-
// Transform to per-group Internet nodes in traffic view with grouping
|
|
550
|
-
const createPerGroupInternetNodes = useCallback((
|
|
551
|
-
nodes: TopologyNode[],
|
|
552
|
-
edges: TopologyEdge[],
|
|
553
|
-
groupMode: GroupingMode
|
|
554
|
-
): { nodes: TopologyNode[]; edges: TopologyEdge[] } => {
|
|
555
|
-
if (groupMode === 'none') {
|
|
556
|
-
return { nodes, edges }
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Find the single Internet node
|
|
560
|
-
const internetNode = nodes.find(n => n.kind === 'Internet')
|
|
561
|
-
if (!internetNode) {
|
|
562
|
-
return { nodes, edges }
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Find all ingresses/gateways and group them
|
|
566
|
-
const ingresses = nodes.filter(n => n.kind === 'Ingress' || n.kind === 'Gateway')
|
|
567
|
-
const groupsWithIngresses = new Map<string, TopologyNode[]>()
|
|
568
|
-
|
|
569
|
-
for (const ingress of ingresses) {
|
|
570
|
-
const groupKey = getGroupKey(ingress, groupMode)
|
|
571
|
-
if (groupKey) {
|
|
572
|
-
if (!groupsWithIngresses.has(groupKey)) {
|
|
573
|
-
groupsWithIngresses.set(groupKey, [])
|
|
574
|
-
}
|
|
575
|
-
groupsWithIngresses.get(groupKey)!.push(ingress)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// If no groups with ingresses, keep original
|
|
580
|
-
if (groupsWithIngresses.size === 0) {
|
|
581
|
-
return { nodes, edges }
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Remove original Internet node and its edges
|
|
585
|
-
const newNodes = nodes.filter(n => n.id !== internetNode.id)
|
|
586
|
-
const newEdges = edges.filter(e => e.source !== internetNode.id)
|
|
587
|
-
|
|
588
|
-
// Create per-group Internet nodes
|
|
589
|
-
for (const [groupKey, groupIngresses] of groupsWithIngresses) {
|
|
590
|
-
const internetId = `internet-${groupMode}-${groupKey}`
|
|
591
|
-
|
|
592
|
-
// Add Internet node for this group with group metadata
|
|
593
|
-
newNodes.push({
|
|
594
|
-
id: internetId,
|
|
595
|
-
kind: 'Internet',
|
|
596
|
-
name: 'Internet',
|
|
597
|
-
status: 'healthy',
|
|
598
|
-
data: {
|
|
599
|
-
// Add group metadata so it gets grouped with its ingresses
|
|
600
|
-
namespace: groupMode === 'namespace' ? groupKey : groupIngresses[0]?.data?.namespace,
|
|
601
|
-
labels: groupMode === 'app' ? { 'app.kubernetes.io/name': groupKey } : {},
|
|
602
|
-
},
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
// Add edges from this Internet node to its ingresses
|
|
606
|
-
for (const ingress of groupIngresses) {
|
|
607
|
-
newEdges.push({
|
|
608
|
-
id: `${internetId}-to-${ingress.id}`,
|
|
609
|
-
source: internetId,
|
|
610
|
-
target: ingress.id,
|
|
611
|
-
type: 'routes-to',
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return { nodes: newNodes, edges: newEdges }
|
|
617
|
-
}, [])
|
|
618
552
|
|
|
619
553
|
// Prepare topology data with expanded pod groups
|
|
620
554
|
const { workingNodes, workingEdges } = useMemo(() => {
|
|
@@ -634,13 +568,13 @@ export function TopologyGraph({
|
|
|
634
568
|
|
|
635
569
|
// In traffic view with grouping, create per-group Internet nodes
|
|
636
570
|
if (isTrafficView && groupingMode !== 'none') {
|
|
637
|
-
const result =
|
|
571
|
+
const result = splitInternetByGroup(nodes, edges, groupingMode)
|
|
638
572
|
nodes = result.nodes
|
|
639
573
|
edges = result.edges
|
|
640
574
|
}
|
|
641
575
|
|
|
642
576
|
return { workingNodes: nodes, workingEdges: edges }
|
|
643
|
-
}, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode
|
|
577
|
+
}, [topology, expandedPodGroups, expandPodGroup, isTrafficView, groupingMode])
|
|
644
578
|
|
|
645
579
|
// Handle card click in card-grid view — find the topology node and open drawer
|
|
646
580
|
const handleCardClick = useCallback((nodeId: string) => {
|
|
@@ -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
|
+
})
|