@skyhook-io/k8s-ui 1.8.10 → 1.8.12
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/package.json +7 -6
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +697 -0
- package/src/components/applications/ApplicationDetail.tsx +3269 -474
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +13 -6
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
- package/src/components/gitops/insights/insights-helpers.ts +74 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
- package/src/components/issues/IssuesView.tsx +91 -52
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
- package/src/components/issues/ResourceIssuesSection.tsx +46 -144
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +3 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +142 -111
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +375 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +149 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +25 -17
- package/src/components/topology/index.ts +2 -0
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +48 -32
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2706 -475
- package/src/components/workload/index.ts +1 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +2 -1
- package/src/types/core.ts +57 -4
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +201 -1
- package/src/utils/applications.ts +1182 -393
- package/src/utils/index.ts +3 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +1024 -1
- package/src/utils/resource-hierarchy.ts +925 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface TopologyBreadcrumbProps {
|
|
2
|
+
/** The single active namespace to show. */
|
|
3
|
+
namespace: string
|
|
4
|
+
/** When set, renders a clickable "All Namespaces" crumb that clears the filter. */
|
|
5
|
+
onClear?: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Namespace-context crumb for the topology overlay bar, shown when the graph is
|
|
10
|
+
* scoped to a single namespace. Place it in the host's overlay-bar composition
|
|
11
|
+
* (e.g. above the search) so it reads as page context.
|
|
12
|
+
*/
|
|
13
|
+
export function TopologyBreadcrumb({ namespace, onClear }: TopologyBreadcrumbProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center gap-1.5 pointer-events-auto">
|
|
16
|
+
{onClear && (
|
|
17
|
+
<button
|
|
18
|
+
onClick={onClear}
|
|
19
|
+
className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
20
|
+
>
|
|
21
|
+
All Namespaces
|
|
22
|
+
</button>
|
|
23
|
+
)}
|
|
24
|
+
{onClear && <span className="text-xs text-theme-text-tertiary">/</span>}
|
|
25
|
+
<span className="text-xs font-medium text-theme-text-secondary bg-theme-surface/80 backdrop-blur-sm border border-theme-border/50 rounded-md px-2 py-0.5">
|
|
26
|
+
{namespace}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -80,7 +80,9 @@ export function TopologyControls({
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
|
-
|
|
83
|
+
// Layout-neutral (positioned by the host's overlay bar): `relative` anchors
|
|
84
|
+
// its own dropdowns; `pointer-events-auto` inside the pointer-events-none bar.
|
|
85
|
+
<div className="relative flex items-center gap-2 pointer-events-auto">
|
|
84
86
|
{/* Freshness/liveness status — backed for legibility over the canvas but
|
|
85
87
|
borderless + divided off, so it reads as a status, not another control. */}
|
|
86
88
|
{leadingSlot && (
|
|
@@ -45,13 +45,22 @@ const RESOURCE_KINDS: {
|
|
|
45
45
|
{ kind: 'PodGroup', label: 'Pod Group', icon: getTopologyIcon('PodGroup'), color: 'text-lime-400', category: 'workloads' },
|
|
46
46
|
{ kind: 'Job', label: 'Job', icon: getTopologyIcon('Job'), color: 'text-orange-400', category: 'workloads' },
|
|
47
47
|
{ kind: 'CronJob', label: 'CronJob', icon: getTopologyIcon('CronJob'), color: 'text-orange-300', category: 'workloads' },
|
|
48
|
+
{ kind: 'Workflow', label: 'Workflow', icon: getTopologyIcon('Workflow'), color: 'text-purple-400', category: 'workloads' },
|
|
49
|
+
{ kind: 'CronWorkflow', label: 'CronWorkflow', icon: getTopologyIcon('CronWorkflow'), color: 'text-purple-300', category: 'workloads' },
|
|
48
50
|
|
|
49
51
|
// Config
|
|
50
52
|
{ kind: 'ConfigMap', label: 'ConfigMap', icon: getTopologyIcon('ConfigMap'), color: 'text-amber-400', category: 'config' },
|
|
51
53
|
{ kind: 'Secret', label: 'Secret', icon: getTopologyIcon('Secret'), color: 'text-red-400', category: 'config' },
|
|
54
|
+
{ kind: 'ServiceAccount', label: 'Service Account', icon: getTopologyIcon('ServiceAccount'), color: 'text-fuchsia-400', category: 'config' },
|
|
55
|
+
{ kind: 'SealedSecret', label: 'Sealed Secret', icon: getTopologyIcon('SealedSecret'), color: 'text-violet-400', category: 'config' },
|
|
56
|
+
{ kind: 'ServiceMonitor', label: 'Service Monitor', icon: getTopologyIcon('ServiceMonitor'), color: 'text-sky-400', category: 'config' },
|
|
57
|
+
{ kind: 'PodMonitor', label: 'Pod Monitor', icon: getTopologyIcon('PodMonitor'), color: 'text-sky-400', category: 'config' },
|
|
58
|
+
{ kind: 'WorkflowTemplate', label: 'WorkflowTemplate', icon: getTopologyIcon('WorkflowTemplate'), color: 'text-purple-300', category: 'config' },
|
|
59
|
+
{ kind: 'ClusterWorkflowTemplate', label: 'ClusterWorkflowTemplate', icon: getTopologyIcon('ClusterWorkflowTemplate'), color: 'text-purple-300', category: 'config' },
|
|
52
60
|
|
|
53
61
|
// Scaling
|
|
54
62
|
{ kind: 'HorizontalPodAutoscaler', label: 'HPA', icon: getTopologyIcon('HorizontalPodAutoscaler'), color: 'text-pink-400', category: 'scaling' },
|
|
63
|
+
{ kind: 'ScaledJob', label: 'ScaledJob', icon: getTopologyIcon('ScaledJob'), color: 'text-pink-300', category: 'scaling' },
|
|
55
64
|
|
|
56
65
|
// Knative
|
|
57
66
|
{ kind: 'KnativeService', label: 'Knative Service', icon: getTopologyIcon('KnativeService'), color: 'text-fuchsia-400', category: 'custom' },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ReactFlow,
|
|
4
4
|
ReactFlowProvider,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type NodeTypes,
|
|
15
15
|
type NodeChange,
|
|
16
16
|
type Viewport,
|
|
17
|
+
type FitViewOptions,
|
|
17
18
|
BackgroundVariant,
|
|
18
19
|
MarkerType,
|
|
19
20
|
} from '@xyflow/react'
|
|
@@ -22,6 +23,8 @@ import { toCanvas } from 'html-to-image'
|
|
|
22
23
|
|
|
23
24
|
import { AlertTriangle, ChevronsDownUp, ChevronsUpDown, Download, Info, Layers, LayoutGrid, Loader2, Maximize, Minus, Pause, Play, Plus, RotateCw, Shield } from 'lucide-react'
|
|
24
25
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
26
|
+
import { TopologyOverlayBar } from './TopologyOverlayBar'
|
|
27
|
+
import { Input } from '../ui/Input'
|
|
25
28
|
import { Tooltip } from '../ui/Tooltip'
|
|
26
29
|
import { useToast } from '../ui/Toast'
|
|
27
30
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
@@ -184,10 +187,6 @@ interface TopologyGraphProps {
|
|
|
184
187
|
onTogglePause?: () => void
|
|
185
188
|
/** Called when user clicks "maximize" on a namespace group — sets namespace filter to just that namespace */
|
|
186
189
|
onMaximizeNamespace?: (namespace: string) => void
|
|
187
|
-
/** Shown as a breadcrumb label when viewing a single namespace */
|
|
188
|
-
namespaceBreadcrumb?: string
|
|
189
|
-
/** Called when breadcrumb "back" is clicked to return to all-namespace view */
|
|
190
|
-
onClearNamespace?: () => void
|
|
191
190
|
/** Serialized namespace filter — when this changes, reset groupLevels for fresh smart default */
|
|
192
191
|
namespacesKey?: string
|
|
193
192
|
/** Node to pan/zoom the canvas to. Bump focusNonce to re-trigger for the same id. */
|
|
@@ -201,6 +200,13 @@ interface TopologyGraphProps {
|
|
|
201
200
|
/** Hover a node → reports its TopologyNode (null on leave). Drives the rail's
|
|
202
201
|
* reciprocal highlight. */
|
|
203
202
|
onNodeHover?: (node: TopologyNode | null) => void
|
|
203
|
+
/** Toggle a stable Deployment's normally-collapsed ReplicaSet tier. */
|
|
204
|
+
onToggleReplicaSets?: (ownerID: string) => void
|
|
205
|
+
/** Padding reserved around fit-to-view operations for overlaid UI. */
|
|
206
|
+
fitViewPadding?: FitViewOptions['padding']
|
|
207
|
+
/** Host overlay content (e.g. a search + controls row), stacked above the
|
|
208
|
+
* topology's own status banners in the overlay bar. */
|
|
209
|
+
children?: ReactNode
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
export function TopologyGraph({
|
|
@@ -214,13 +220,14 @@ export function TopologyGraph({
|
|
|
214
220
|
paused = false,
|
|
215
221
|
onTogglePause,
|
|
216
222
|
onMaximizeNamespace,
|
|
217
|
-
namespaceBreadcrumb,
|
|
218
|
-
onClearNamespace,
|
|
219
223
|
namespacesKey = '',
|
|
220
224
|
focusNodeId,
|
|
221
225
|
focusNonce,
|
|
222
226
|
focusedOwnerId,
|
|
223
227
|
onNodeHover,
|
|
228
|
+
onToggleReplicaSets,
|
|
229
|
+
fitViewPadding = 0.15,
|
|
230
|
+
children,
|
|
224
231
|
}: TopologyGraphProps) {
|
|
225
232
|
const isTrafficView = viewMode === 'traffic'
|
|
226
233
|
const [nodes, setNodes, onNodesChangeBase] = useNodesState([] as Node[])
|
|
@@ -701,6 +708,7 @@ export function TopologyGraph({
|
|
|
701
708
|
onExpand: isExpandablePodGroup ? handleExpandPodGroup : undefined,
|
|
702
709
|
onCollapse: expandedFromGroup ? handleCollapsePodGroup : undefined,
|
|
703
710
|
isExpanded: isExpandablePodGroup ? expandedPodGroups.has(node.id) : undefined,
|
|
711
|
+
onToggleReplicaSets: nodeData?.replicaSetsExpandable ? onToggleReplicaSets : undefined,
|
|
704
712
|
},
|
|
705
713
|
}
|
|
706
714
|
})
|
|
@@ -736,7 +744,7 @@ export function TopologyGraph({
|
|
|
736
744
|
// No cleanup function - we use version-based invalidation instead
|
|
737
745
|
// This prevents React's effect re-runs from canceling in-flight layouts
|
|
738
746
|
// when the actual structure hasn't changed
|
|
739
|
-
}, [workingNodes, workingEdges, structureKey, groupingMode, hideGroupHeader, collapsedGroups, groupLevels, handleSetLevel, handleCardClick, onMaximizeNamespace, isTrafficView, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, setNodes, setEdges, layoutRetryCount])
|
|
747
|
+
}, [workingNodes, workingEdges, structureKey, groupingMode, hideGroupHeader, collapsedGroups, groupLevels, handleSetLevel, handleCardClick, onMaximizeNamespace, isTrafficView, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, onToggleReplicaSets, setNodes, setEdges, layoutRetryCount])
|
|
740
748
|
|
|
741
749
|
// Handle node click
|
|
742
750
|
const handleNodeClick = useCallback(
|
|
@@ -829,6 +837,55 @@ export function TopologyGraph({
|
|
|
829
837
|
})
|
|
830
838
|
}, [focusedOwnerId, setNodes, nodes])
|
|
831
839
|
|
|
840
|
+
// Data-only topology updates must repaint existing nodes without forcing an
|
|
841
|
+
// ELK relayout. Structure hashing deliberately ignores status and metadata;
|
|
842
|
+
// keep the React Flow node payload in sync on that fast path.
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
const topologyById = new Map(workingNodes.map(node => [node.id, node]))
|
|
845
|
+
setNodes(nds => {
|
|
846
|
+
let changed = false
|
|
847
|
+
const updated = nds.map(node => {
|
|
848
|
+
if (node.type === 'group') return node
|
|
849
|
+
const topologyNode = topologyById.get(node.id)
|
|
850
|
+
if (!topologyNode) return node
|
|
851
|
+
const nodeData = topologyNode.data as Record<string, unknown>
|
|
852
|
+
const pods = nodeData.pods
|
|
853
|
+
const isExpandablePodGroup = topologyNode.kind === 'PodGroup' && Array.isArray(pods) && pods.length > 0
|
|
854
|
+
const expandedFromGroup = nodeData.expandedFromGroup as string | undefined
|
|
855
|
+
const onExpand = isExpandablePodGroup ? handleExpandPodGroup : undefined
|
|
856
|
+
const onCollapse = expandedFromGroup ? handleCollapsePodGroup : undefined
|
|
857
|
+
const isExpanded = isExpandablePodGroup ? expandedPodGroups.has(node.id) : undefined
|
|
858
|
+
const toggleReplicaSets = nodeData.replicaSetsExpandable ? onToggleReplicaSets : undefined
|
|
859
|
+
if (
|
|
860
|
+
node.data?.kind === topologyNode.kind &&
|
|
861
|
+
node.data?.name === topologyNode.name &&
|
|
862
|
+
node.data?.status === topologyNode.status &&
|
|
863
|
+
node.data?.nodeData === topologyNode.data &&
|
|
864
|
+
node.data?.onExpand === onExpand &&
|
|
865
|
+
node.data?.onCollapse === onCollapse &&
|
|
866
|
+
node.data?.isExpanded === isExpanded &&
|
|
867
|
+
node.data?.onToggleReplicaSets === toggleReplicaSets
|
|
868
|
+
) return node
|
|
869
|
+
changed = true
|
|
870
|
+
return {
|
|
871
|
+
...node,
|
|
872
|
+
data: {
|
|
873
|
+
...node.data,
|
|
874
|
+
kind: topologyNode.kind,
|
|
875
|
+
name: topologyNode.name,
|
|
876
|
+
status: topologyNode.status,
|
|
877
|
+
nodeData: topologyNode.data,
|
|
878
|
+
onExpand,
|
|
879
|
+
onCollapse,
|
|
880
|
+
isExpanded,
|
|
881
|
+
onToggleReplicaSets: toggleReplicaSets,
|
|
882
|
+
},
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
return changed ? updated : nds
|
|
886
|
+
})
|
|
887
|
+
}, [workingNodes, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, onToggleReplicaSets, setNodes])
|
|
888
|
+
|
|
832
889
|
if (!topology) {
|
|
833
890
|
return <PaneLoader label="Loading topology…" className="absolute inset-0" />
|
|
834
891
|
}
|
|
@@ -875,86 +932,83 @@ export function TopologyGraph({
|
|
|
875
932
|
|
|
876
933
|
return (
|
|
877
934
|
<ReactFlowProvider>
|
|
878
|
-
{/*
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
<
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
935
|
+
{/* Overlay bar: status banners lead (a partial-data/RBAC warning must read
|
|
936
|
+
before the user trusts the graph), host children stack below. Flex
|
|
937
|
+
column so nothing overlaps. Banners de-absoluted — the bar owns position/z. */}
|
|
938
|
+
<TopologyOverlayBar>
|
|
939
|
+
{/* Warning banner for partial topology data */}
|
|
940
|
+
{topology?.warnings && topology.warnings.length > 0 && (() => {
|
|
941
|
+
const rbacWarnings = topology.warnings.filter(w => w.includes('RBAC not granted'))
|
|
942
|
+
const viewWarnings = topology.warnings.filter(w => w.startsWith('Topology view:'))
|
|
943
|
+
const otherWarnings = topology.warnings.filter(w => !w.includes('RBAC not granted') && !w.startsWith('Topology view:'))
|
|
944
|
+
const isAllRbac = otherWarnings.length === 0 && viewWarnings.length === 0
|
|
945
|
+
const isViewLimited = otherWarnings.length === 0 && rbacWarnings.length === 0 && viewWarnings.length > 0
|
|
946
|
+
return (
|
|
947
|
+
<div className={`pointer-events-auto ${isViewLimited ? 'max-w-xl' : 'w-full'} ${isAllRbac ? 'bg-amber-500/10 border-amber-500/20' : 'bg-amber-500/10 border-amber-500/30'} border rounded-lg p-2 backdrop-blur-sm`}>
|
|
948
|
+
<div className="flex items-start gap-2">
|
|
949
|
+
{isAllRbac ? (
|
|
950
|
+
<Shield className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
951
|
+
) : (
|
|
952
|
+
<AlertTriangle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
|
953
|
+
)}
|
|
954
|
+
<div className="text-sm">
|
|
955
|
+
<span className="font-medium text-amber-400">
|
|
956
|
+
{isAllRbac ? 'Limited Access:' : isViewLimited ? 'View limited:' : 'Warning:'}
|
|
957
|
+
</span>
|
|
958
|
+
<span className="text-theme-text-secondary ml-1">
|
|
959
|
+
{isAllRbac
|
|
960
|
+
? `${pluralize(rbacWarnings.length, 'resource type')} not accessible due to RBAC restrictions.`
|
|
961
|
+
: isViewLimited
|
|
962
|
+
? 'Large retained run history is summarized to keep the graph readable.'
|
|
963
|
+
: 'Some resources failed to load. Data may be incomplete.'}
|
|
964
|
+
</span>
|
|
965
|
+
<details className="mt-1">
|
|
966
|
+
<summary className="text-xs text-amber-400/80 hover:text-amber-400">
|
|
967
|
+
Show details ({topology.warnings.length})
|
|
968
|
+
</summary>
|
|
969
|
+
<ul className="mt-1 text-xs text-theme-text-tertiary space-y-0.5">
|
|
970
|
+
{rbacWarnings.length > 0 && otherWarnings.length > 0 && (
|
|
971
|
+
<li className="text-amber-400/60 font-medium mt-1">RBAC restrictions:</li>
|
|
972
|
+
)}
|
|
973
|
+
{rbacWarnings.map((w, i) => (
|
|
974
|
+
<li key={`rbac-${i}`} className="font-mono">{w}</li>
|
|
975
|
+
))}
|
|
976
|
+
{viewWarnings.length > 0 && (rbacWarnings.length > 0 || otherWarnings.length > 0) && (
|
|
977
|
+
<li className="text-amber-400/60 font-medium mt-1">View limits:</li>
|
|
978
|
+
)}
|
|
979
|
+
{viewWarnings.map((w, i) => (
|
|
980
|
+
<li key={`view-${i}`} className="font-mono">{w}</li>
|
|
981
|
+
))}
|
|
982
|
+
{otherWarnings.length > 0 && (rbacWarnings.length > 0 || viewWarnings.length > 0) && (
|
|
983
|
+
<li className="text-amber-400/60 font-medium mt-1">Other warnings:</li>
|
|
984
|
+
)}
|
|
985
|
+
{otherWarnings.map((w, i) => (
|
|
986
|
+
<li key={`other-${i}`} className="font-mono">{w}</li>
|
|
987
|
+
))}
|
|
988
|
+
</ul>
|
|
989
|
+
</details>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
)
|
|
994
|
+
})()}
|
|
995
|
+
{/* Layout error banner - shown even when stale nodes exist */}
|
|
996
|
+
{layoutError && nodes.length > 0 && (
|
|
997
|
+
<div className="w-full pointer-events-auto bg-red-500/10 border border-red-500/30 rounded-lg p-2 backdrop-blur-sm">
|
|
904
998
|
<div className="flex items-start gap-2">
|
|
905
|
-
|
|
906
|
-
<Shield className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
907
|
-
) : (
|
|
908
|
-
<AlertTriangle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
|
909
|
-
)}
|
|
999
|
+
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
|
910
1000
|
<div className="text-sm">
|
|
911
|
-
<span className="font-medium text-
|
|
912
|
-
{isAllRbac ? 'Limited Access:' : 'Warning:'}
|
|
913
|
-
</span>
|
|
1001
|
+
<span className="font-medium text-red-400">Layout Error:</span>
|
|
914
1002
|
<span className="text-theme-text-secondary ml-1">
|
|
915
|
-
|
|
916
|
-
? `${pluralize(rbacWarnings.length, 'resource type')} not accessible due to RBAC restrictions.`
|
|
917
|
-
: 'Some resources failed to load. Data may be incomplete.'}
|
|
1003
|
+
Failed to update layout. Showing previous view.
|
|
918
1004
|
</span>
|
|
919
|
-
<
|
|
920
|
-
<summary className="text-xs text-amber-400/80 hover:text-amber-400">
|
|
921
|
-
Show details ({topology.warnings.length})
|
|
922
|
-
</summary>
|
|
923
|
-
<ul className="mt-1 text-xs text-theme-text-tertiary space-y-0.5">
|
|
924
|
-
{rbacWarnings.length > 0 && otherWarnings.length > 0 && (
|
|
925
|
-
<li className="text-amber-400/60 font-medium mt-1">RBAC restrictions:</li>
|
|
926
|
-
)}
|
|
927
|
-
{rbacWarnings.map((w, i) => (
|
|
928
|
-
<li key={`rbac-${i}`} className="font-mono">{w}</li>
|
|
929
|
-
))}
|
|
930
|
-
{otherWarnings.length > 0 && rbacWarnings.length > 0 && (
|
|
931
|
-
<li className="text-amber-400/60 font-medium mt-1">Other warnings:</li>
|
|
932
|
-
)}
|
|
933
|
-
{otherWarnings.map((w, i) => (
|
|
934
|
-
<li key={`other-${i}`} className="font-mono">{w}</li>
|
|
935
|
-
))}
|
|
936
|
-
</ul>
|
|
937
|
-
</details>
|
|
1005
|
+
<p className="mt-1 text-xs text-theme-text-tertiary font-mono">{layoutError}</p>
|
|
938
1006
|
</div>
|
|
939
1007
|
</div>
|
|
940
1008
|
</div>
|
|
941
|
-
)
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
{layoutError && nodes.length > 0 && (
|
|
945
|
-
<div className="absolute top-2 left-2 right-2 z-10 bg-red-500/10 border border-red-500/30 rounded-lg p-2 backdrop-blur-sm">
|
|
946
|
-
<div className="flex items-start gap-2">
|
|
947
|
-
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
|
948
|
-
<div className="text-sm">
|
|
949
|
-
<span className="font-medium text-red-400">Layout Error:</span>
|
|
950
|
-
<span className="text-theme-text-secondary ml-1">
|
|
951
|
-
Failed to update layout. Showing previous view.
|
|
952
|
-
</span>
|
|
953
|
-
<p className="mt-1 text-xs text-theme-text-tertiary font-mono">{layoutError}</p>
|
|
954
|
-
</div>
|
|
955
|
-
</div>
|
|
956
|
-
</div>
|
|
957
|
-
)}
|
|
1009
|
+
)}
|
|
1010
|
+
{children}
|
|
1011
|
+
</TopologyOverlayBar>
|
|
958
1012
|
{/* Summary-mode pill — pod tier collapsed to per-workload/service counts */}
|
|
959
1013
|
{topology?.summaryMode && (
|
|
960
1014
|
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/30 rounded-full px-3 py-1 backdrop-blur-sm">
|
|
@@ -974,7 +1028,7 @@ export function TopologyGraph({
|
|
|
974
1028
|
onNodeMouseLeave={handleNodeMouseLeave}
|
|
975
1029
|
nodeTypes={nodeTypes}
|
|
976
1030
|
fitView
|
|
977
|
-
fitViewOptions={{ padding:
|
|
1031
|
+
fitViewOptions={{ padding: fitViewPadding }}
|
|
978
1032
|
minZoom={0.1}
|
|
979
1033
|
maxZoom={2}
|
|
980
1034
|
proOptions={{ hideAttribution: true }}
|
|
@@ -1021,6 +1075,7 @@ export function TopologyGraph({
|
|
|
1021
1075
|
paused={paused}
|
|
1022
1076
|
onTogglePause={onTogglePause}
|
|
1023
1077
|
onExportingChange={setIsExporting}
|
|
1078
|
+
fitViewPadding={fitViewPadding}
|
|
1024
1079
|
/>
|
|
1025
1080
|
</div>
|
|
1026
1081
|
{!isTrafficView && (
|
|
@@ -1057,6 +1112,7 @@ export function TopologyGraph({
|
|
|
1057
1112
|
focusNodeId={focusNodeId}
|
|
1058
1113
|
focusNonce={focusNonce}
|
|
1059
1114
|
onRequestExpandForNode={expandGroupForNode}
|
|
1115
|
+
fitViewPadding={fitViewPadding}
|
|
1060
1116
|
/>
|
|
1061
1117
|
</ReactFlow>
|
|
1062
1118
|
</ReactFlowProvider>
|
|
@@ -1240,9 +1296,8 @@ function ExportImageButton({ onExportingChange }: { onExportingChange: (v: boole
|
|
|
1240
1296
|
>
|
|
1241
1297
|
<div className="text-sm font-medium text-theme-text-primary mb-3">Export topology</div>
|
|
1242
1298
|
<label className="block text-xs text-theme-text-secondary mb-1">Filename</label>
|
|
1243
|
-
<
|
|
1299
|
+
<Input
|
|
1244
1300
|
ref={inputRef}
|
|
1245
|
-
type="text"
|
|
1246
1301
|
value={filename}
|
|
1247
1302
|
onChange={(e) => setFilename(e.target.value)}
|
|
1248
1303
|
className="w-full px-2 py-1.5 text-sm bg-theme-base border border-theme-border rounded text-theme-text-primary outline-none focus:border-blue-500 mb-3"
|
|
@@ -1340,11 +1395,13 @@ function CustomControlButtons({
|
|
|
1340
1395
|
paused,
|
|
1341
1396
|
onTogglePause,
|
|
1342
1397
|
onExportingChange,
|
|
1398
|
+
fitViewPadding,
|
|
1343
1399
|
}: {
|
|
1344
1400
|
showExportButton: boolean
|
|
1345
1401
|
paused: boolean
|
|
1346
1402
|
onTogglePause?: () => void
|
|
1347
1403
|
onExportingChange: (v: boolean) => void
|
|
1404
|
+
fitViewPadding: FitViewOptions['padding']
|
|
1348
1405
|
}) {
|
|
1349
1406
|
const { zoomIn, zoomOut, fitView } = useReactFlow()
|
|
1350
1407
|
const TIP = 100
|
|
@@ -1361,7 +1418,7 @@ function CustomControlButtons({
|
|
|
1361
1418
|
</button>
|
|
1362
1419
|
</Tooltip>
|
|
1363
1420
|
<Tooltip content="Fit view" delay={TIP} position="right">
|
|
1364
|
-
<button className="react-flow__controls-button" onClick={() => fitView({ padding:
|
|
1421
|
+
<button className="react-flow__controls-button" onClick={() => fitView({ padding: fitViewPadding, duration: 400 })}>
|
|
1365
1422
|
<Maximize className="w-3 h-3" />
|
|
1366
1423
|
</button>
|
|
1367
1424
|
</Tooltip>
|
|
@@ -1394,6 +1451,7 @@ function ViewportController({
|
|
|
1394
1451
|
focusNodeId,
|
|
1395
1452
|
focusNonce = 0,
|
|
1396
1453
|
onRequestExpandForNode,
|
|
1454
|
+
fitViewPadding,
|
|
1397
1455
|
}: {
|
|
1398
1456
|
viewMode: string
|
|
1399
1457
|
layoutRetryCount: number
|
|
@@ -1403,6 +1461,7 @@ function ViewportController({
|
|
|
1403
1461
|
focusNodeId?: string
|
|
1404
1462
|
focusNonce?: number
|
|
1405
1463
|
onRequestExpandForNode?: (nodeId: string) => void
|
|
1464
|
+
fitViewPadding: FitViewOptions['padding']
|
|
1406
1465
|
}) {
|
|
1407
1466
|
const { fitView, zoomIn, zoomOut, setViewport, getViewport, getInternalNode, setCenter } = useReactFlow()
|
|
1408
1467
|
const nodes = useNodes() // Reactive hook to watch node changes
|
|
@@ -1431,7 +1490,7 @@ function ViewportController({
|
|
|
1431
1490
|
description: 'Fit graph to screen',
|
|
1432
1491
|
category: 'Topology',
|
|
1433
1492
|
scope: 'topology',
|
|
1434
|
-
handler: () => fitView({ padding:
|
|
1493
|
+
handler: () => fitView({ padding: fitViewPadding, duration: VIEWPORT_ANIMATION_DURATION }),
|
|
1435
1494
|
},
|
|
1436
1495
|
{
|
|
1437
1496
|
id: 'topology-zoom-in',
|
|
@@ -1508,14 +1567,14 @@ function ViewportController({
|
|
|
1508
1567
|
if (nodesJustPopulated || viewModeChanged || retryRequested || fitViewRequested) {
|
|
1509
1568
|
const timeoutId = setTimeout(() => {
|
|
1510
1569
|
fitView({
|
|
1511
|
-
padding:
|
|
1570
|
+
padding: fitViewPadding,
|
|
1512
1571
|
duration: nodesJustPopulated ? 0 : VIEWPORT_ANIMATION_DURATION,
|
|
1513
1572
|
})
|
|
1514
1573
|
}, 10)
|
|
1515
1574
|
|
|
1516
1575
|
return () => clearTimeout(timeoutId)
|
|
1517
1576
|
}
|
|
1518
|
-
}, [viewMode, layoutRetryCount, fitViewCounter, nodes.length, fitView])
|
|
1577
|
+
}, [viewMode, layoutRetryCount, fitViewCounter, nodes.length, fitView, fitViewPadding])
|
|
1519
1578
|
|
|
1520
1579
|
// Pan/zoom to a single searched node. Gated on focusNonce so the same
|
|
1521
1580
|
// node can be re-focused, and so this never fires on background updates.
|
|
@@ -1563,10 +1622,10 @@ function ViewportController({
|
|
|
1563
1622
|
if (!fitAllAfterLayoutRef?.current) return
|
|
1564
1623
|
const id = setTimeout(() => {
|
|
1565
1624
|
fitAllAfterLayoutRef.current = false
|
|
1566
|
-
fitView({ padding:
|
|
1625
|
+
fitView({ padding: fitViewPadding, duration: VIEWPORT_ANIMATION_DURATION })
|
|
1567
1626
|
}, 250)
|
|
1568
1627
|
return () => clearTimeout(id)
|
|
1569
|
-
}, [nodes, fitView, fitAllAfterLayoutRef])
|
|
1628
|
+
}, [nodes, fitView, fitAllAfterLayoutRef, fitViewPadding])
|
|
1570
1629
|
|
|
1571
1630
|
return null
|
|
1572
1631
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
|
|
4
|
+
interface TopologyOverlayBarProps {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
/** Extra classes merged onto the bar container. */
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Top-anchored overlay bar for the topology canvas. Stacks children top-to-
|
|
12
|
+
* bottom (flex column, source order); wrap items in your own `flex` div to put
|
|
13
|
+
* them on one line. Everything stacks, so overlays never collide.
|
|
14
|
+
*
|
|
15
|
+
* `pointer-events-none` so the canvas stays pannable in the gaps — interactive
|
|
16
|
+
* children opt back in with `pointer-events-auto`. Children are `items-start`
|
|
17
|
+
* (content width); use `w-full` for a full-width row.
|
|
18
|
+
*/
|
|
19
|
+
export function TopologyOverlayBar({ children, className }: TopologyOverlayBarProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={clsx(
|
|
23
|
+
'pointer-events-none absolute inset-x-2 top-2 z-10 flex flex-col items-start gap-2',
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { Search, X, ChevronRight } from 'lucide-react'
|
|
4
|
+
import { Input } from '../ui/Input'
|
|
3
5
|
import { getTopologyIcon } from '../../utils/resource-icons'
|
|
4
6
|
import { clsx } from 'clsx'
|
|
5
7
|
import type { TopologyNode } from '../../types'
|
|
@@ -23,12 +25,12 @@ interface TopologySearchProps {
|
|
|
23
25
|
* empty-state hint above. Defaults to "current view".
|
|
24
26
|
*/
|
|
25
27
|
viewModeLabel?: string
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* (
|
|
30
|
-
*/
|
|
31
|
-
|
|
28
|
+
/** Element the search overlay portals into — typically the topology pane. Set
|
|
29
|
+
* it when the trigger lives inside a `pointer-events-none` overlay bar: the
|
|
30
|
+
* overlay then renders OUTSIDE that bar (so its backdrop is clickable) and is
|
|
31
|
+
* `absolute inset-0` scoped to this container (dims only the pane). Omit to
|
|
32
|
+
* fall back to a viewport-level `fixed` overlay on document.body. */
|
|
33
|
+
overlayContainer?: HTMLElement | null
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
// Icon mapping for different resource kinds
|
|
@@ -70,7 +72,7 @@ function getKindColor(kind: string): string {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, viewModeLabel,
|
|
75
|
+
export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, viewModeLabel, overlayContainer }: TopologySearchProps) {
|
|
74
76
|
const [isOpen, setIsOpen] = useState(false)
|
|
75
77
|
const [query, setQuery] = useState('')
|
|
76
78
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
@@ -194,15 +196,20 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
194
196
|
setTimeout(() => inputRef.current?.focus(), 0)
|
|
195
197
|
}, [])
|
|
196
198
|
|
|
199
|
+
// Portal the overlay OUT of the (pointer-events-none) overlay bar the trigger
|
|
200
|
+
// sits in. With a container → `absolute inset-0` scoped to it (the pane);
|
|
201
|
+
// without → a viewport `fixed` overlay on document.body.
|
|
202
|
+
const portalTarget = overlayContainer ?? (typeof document !== 'undefined' ? document.body : null)
|
|
203
|
+
const overlayPositionClass = overlayContainer ? 'absolute' : 'fixed'
|
|
204
|
+
|
|
197
205
|
return (
|
|
198
206
|
<>
|
|
199
207
|
{/* Search trigger button */}
|
|
200
208
|
<button
|
|
201
209
|
onClick={handleOpen}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)}
|
|
210
|
+
// Layout-neutral (positioned by the host's overlay bar); pointer-events-
|
|
211
|
+
// auto inside the pointer-events-none bar.
|
|
212
|
+
className="pointer-events-auto flex items-center gap-2 px-3 py-2 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg text-theme-text-secondary hover:text-theme-text-primary hover:border-theme-border-light transition-colors"
|
|
206
213
|
>
|
|
207
214
|
<Search className="w-4 h-4" />
|
|
208
215
|
<span className="text-sm">Search</span>
|
|
@@ -211,9 +218,10 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
211
218
|
</kbd>
|
|
212
219
|
</button>
|
|
213
220
|
|
|
214
|
-
{/*
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
{/* Portaled out of the overlay bar (see portalTarget above) so the
|
|
222
|
+
backdrop is clickable and the dim covers only the pane, not the app. */}
|
|
223
|
+
{isOpen && portalTarget && createPortal(
|
|
224
|
+
<div className={`${overlayPositionClass} inset-0 z-50 flex items-start justify-center pt-[10vh]`}>
|
|
217
225
|
{/* Backdrop */}
|
|
218
226
|
<div
|
|
219
227
|
className="absolute inset-0 bg-theme-base/60 backdrop-blur-sm"
|
|
@@ -225,9 +233,8 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
225
233
|
{/* Search input */}
|
|
226
234
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
|
|
227
235
|
<Search className="w-5 h-5 text-theme-text-secondary" />
|
|
228
|
-
<
|
|
236
|
+
<Input
|
|
229
237
|
ref={inputRef}
|
|
230
|
-
type="text"
|
|
231
238
|
value={query}
|
|
232
239
|
onChange={(e) => setQuery(e.target.value)}
|
|
233
240
|
onKeyDown={handleInputKeyDown}
|
|
@@ -339,7 +346,8 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
339
346
|
</div>
|
|
340
347
|
)}
|
|
341
348
|
</div>
|
|
342
|
-
</div
|
|
349
|
+
</div>,
|
|
350
|
+
portalTarget,
|
|
343
351
|
)}
|
|
344
352
|
</>
|
|
345
353
|
)
|
|
@@ -4,4 +4,6 @@ export { GroupNode } from './GroupNode'
|
|
|
4
4
|
export { TopologySearch } from './TopologySearch'
|
|
5
5
|
export { TopologyFilterSidebar } from './TopologyFilterSidebar'
|
|
6
6
|
export { TopologyControls } from './TopologyControls'
|
|
7
|
+
export { TopologyOverlayBar } from './TopologyOverlayBar'
|
|
8
|
+
export { TopologyBreadcrumb } from './TopologyBreadcrumb'
|
|
7
9
|
export { buildHierarchicalElkGraph, applyHierarchicalLayout, getGroupKey, setLayoutEngine } from './layout'
|
|
@@ -827,8 +827,16 @@ function computeWorkloadCards(
|
|
|
827
827
|
subtitle = (d.type as string) || 'ClusterIP'
|
|
828
828
|
} else if (primary.kind === 'CronJob') {
|
|
829
829
|
subtitle = (d.schedule as string) || ''
|
|
830
|
+
} else if (primary.kind === 'CronWorkflow') {
|
|
831
|
+
subtitle = (d.schedule as string) || ''
|
|
830
832
|
} else if (primary.kind === 'Job') {
|
|
831
833
|
subtitle = (d.phase as string) || ''
|
|
834
|
+
} else if (primary.kind === 'Workflow') {
|
|
835
|
+
subtitle = (d.progress as string) || (d.phase as string) || ''
|
|
836
|
+
} else if (primary.kind === 'WorkflowTemplate' || primary.kind === 'ClusterWorkflowTemplate') {
|
|
837
|
+
const entrypoint = d.entrypoint as string
|
|
838
|
+
const templateCount = d.templateCount as number
|
|
839
|
+
subtitle = entrypoint || (templateCount ? `${templateCount} templates` : '')
|
|
832
840
|
} else if (primary.kind === 'Application') {
|
|
833
841
|
const sync = d.syncStatus as string
|
|
834
842
|
const health = d.healthStatus as string
|
|
@@ -855,10 +863,10 @@ function computeWorkloadCards(
|
|
|
855
863
|
// Shared kind priority map — used by both pickGroupName and computeWorkloadCards
|
|
856
864
|
const KIND_PRIORITY: Record<string, number> = {
|
|
857
865
|
'Deployment': 1, 'Rollout': 1, 'StatefulSet': 2, 'DaemonSet': 3,
|
|
858
|
-
'CronJob': 4, 'Job': 5, 'Service': 6, 'Gateway': 7,
|
|
866
|
+
'CronJob': 4, 'CronWorkflow': 4, 'Job': 5, 'Workflow': 5, 'Service': 6, 'Gateway': 7,
|
|
859
867
|
'HTTPRoute': 6, 'GRPCRoute': 6, 'TCPRoute': 6, 'TLSRoute': 6, 'Ingress': 7,
|
|
860
868
|
'ReplicaSet': 8, 'Pod': 9, 'PodGroup': 9,
|
|
861
|
-
'ConfigMap': 10, 'Secret': 10, 'PersistentVolumeClaim': 10, 'HorizontalPodAutoscaler': 10,
|
|
869
|
+
'ConfigMap': 10, 'Secret': 10, 'ServiceAccount': 10, 'SealedSecret': 10, 'ServiceMonitor': 10, 'PodMonitor': 10, 'WorkflowTemplate': 10, 'ClusterWorkflowTemplate': 10, 'PersistentVolumeClaim': 10, 'HorizontalPodAutoscaler': 10,
|
|
862
870
|
'KnativeService': 1, 'KnativeConfiguration': 3, 'KnativeRevision': 4, 'KnativeRoute': 2,
|
|
863
871
|
'Broker': 2, 'Channel': 2, 'Trigger': 3, 'PingSource': 3, 'ApiServerSource': 3,
|
|
864
872
|
'ContainerSource': 3, 'SinkBinding': 3,
|