@skyhook-io/k8s-ui 1.0.0
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 +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
import { NodeProps, Handle, Position } from '@xyflow/react'
|
|
3
|
+
import { ChevronDown, ChevronRight, Box, Tag } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
interface GroupNodeData {
|
|
6
|
+
type: 'namespace' | 'app' | 'label'
|
|
7
|
+
name: string
|
|
8
|
+
label?: string
|
|
9
|
+
nodeCount: number
|
|
10
|
+
collapsed: boolean
|
|
11
|
+
onToggleCollapse: (groupId: string) => void
|
|
12
|
+
hideHeader?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GroupNode = memo(function GroupNode({
|
|
16
|
+
id,
|
|
17
|
+
data,
|
|
18
|
+
width,
|
|
19
|
+
height,
|
|
20
|
+
}: NodeProps & { data: GroupNodeData }) {
|
|
21
|
+
const { type, name, label, nodeCount, collapsed, onToggleCollapse, hideHeader } = data
|
|
22
|
+
|
|
23
|
+
const getIcon = () => {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case 'namespace':
|
|
26
|
+
return Box
|
|
27
|
+
case 'app':
|
|
28
|
+
case 'label':
|
|
29
|
+
return Tag
|
|
30
|
+
default:
|
|
31
|
+
return Box
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const getBorderStyle = (): React.CSSProperties => {
|
|
36
|
+
// Must set full 'border' property to override ReactFlow's --xy-node-border
|
|
37
|
+
switch (type) {
|
|
38
|
+
case 'namespace':
|
|
39
|
+
return { border: '2px solid var(--group-border-namespace)' }
|
|
40
|
+
case 'app':
|
|
41
|
+
return { border: '2px solid var(--group-border-app)' }
|
|
42
|
+
case 'label':
|
|
43
|
+
return { border: '2px solid var(--group-border-label)' }
|
|
44
|
+
default:
|
|
45
|
+
return { border: '2px solid var(--border-default)' }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getHeaderBgStyle = (): React.CSSProperties => {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case 'namespace':
|
|
52
|
+
return { backgroundColor: 'var(--group-header-namespace)' }
|
|
53
|
+
case 'app':
|
|
54
|
+
return { backgroundColor: 'var(--group-header-app)' }
|
|
55
|
+
case 'label':
|
|
56
|
+
return { backgroundColor: 'var(--group-header-label)' }
|
|
57
|
+
default:
|
|
58
|
+
return { backgroundColor: 'var(--bg-hover)' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const getLabelStyle = (): React.CSSProperties => {
|
|
63
|
+
switch (type) {
|
|
64
|
+
case 'namespace':
|
|
65
|
+
return { color: 'var(--group-label-namespace)' }
|
|
66
|
+
case 'app':
|
|
67
|
+
return { color: 'var(--group-label-app)' }
|
|
68
|
+
case 'label':
|
|
69
|
+
return { color: 'var(--group-label-label)' }
|
|
70
|
+
default:
|
|
71
|
+
return { color: 'var(--text-secondary)' }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const getIconStyle = (): React.CSSProperties => {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case 'namespace':
|
|
78
|
+
return { color: 'var(--group-icon-namespace)' }
|
|
79
|
+
case 'app':
|
|
80
|
+
return { color: 'var(--group-icon-app)' }
|
|
81
|
+
case 'label':
|
|
82
|
+
return { color: 'var(--group-icon-label)' }
|
|
83
|
+
default:
|
|
84
|
+
return { color: 'var(--text-secondary)' }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Icon = getIcon()
|
|
89
|
+
|
|
90
|
+
// When collapsed, render as a compact card
|
|
91
|
+
if (collapsed) {
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<Handle
|
|
95
|
+
type="target"
|
|
96
|
+
position={Position.Left}
|
|
97
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<div
|
|
101
|
+
className="rounded-xl p-4 cursor-pointer group-header-scaled"
|
|
102
|
+
onClick={() => onToggleCollapse(id)}
|
|
103
|
+
style={{ ...getBorderStyle(), ...getHeaderBgStyle() }}
|
|
104
|
+
>
|
|
105
|
+
<div className="flex items-center gap-4">
|
|
106
|
+
<ChevronRight className="w-8 h-8" style={getIconStyle()} />
|
|
107
|
+
<Icon className="w-9 h-9" style={getIconStyle()} />
|
|
108
|
+
<span className="text-4xl font-bold" style={getLabelStyle()}>{name}</span>
|
|
109
|
+
{label && (
|
|
110
|
+
<span className="text-sm text-theme-text-secondary">({label})</span>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="mt-3 text-xl text-theme-text-secondary">
|
|
114
|
+
{nodeCount} {nodeCount === 1 ? 'resource' : 'resources'}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<Handle
|
|
119
|
+
type="source"
|
|
120
|
+
position={Position.Right}
|
|
121
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
122
|
+
/>
|
|
123
|
+
</>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// When expanded, render as a container with header
|
|
128
|
+
// Children are rendered automatically by ReactFlow via parentId
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<Handle
|
|
132
|
+
type="target"
|
|
133
|
+
position={Position.Left}
|
|
134
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
{/* Container with border - use explicit dimensions from props */}
|
|
138
|
+
{/* Top position adjusts based on CSS variable set by ViewportController */}
|
|
139
|
+
<div
|
|
140
|
+
className="absolute left-0 rounded-xl box-border isolate overflow-hidden bg-theme-surface/40 group-container-adjusted"
|
|
141
|
+
style={{
|
|
142
|
+
width: width || '100%',
|
|
143
|
+
height: height || '100%',
|
|
144
|
+
...getBorderStyle()
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{/* Header bar - background extends full width, content scales, count fixed right */}
|
|
148
|
+
{/* Hidden when hideHeader is true (single namespace view) */}
|
|
149
|
+
{!hideHeader && (
|
|
150
|
+
<div
|
|
151
|
+
className="w-full cursor-pointer relative flex items-center"
|
|
152
|
+
onClick={() => onToggleCollapse(id)}
|
|
153
|
+
style={getHeaderBgStyle()}
|
|
154
|
+
>
|
|
155
|
+
{/* Scaled content */}
|
|
156
|
+
<div
|
|
157
|
+
className="flex items-center group-header-scaled"
|
|
158
|
+
style={{
|
|
159
|
+
padding: '20px 24px',
|
|
160
|
+
gap: '16px',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<ChevronDown
|
|
164
|
+
className="shrink-0 w-8 h-8"
|
|
165
|
+
style={getIconStyle()}
|
|
166
|
+
/>
|
|
167
|
+
<Icon
|
|
168
|
+
className="shrink-0 w-9 h-9"
|
|
169
|
+
style={getIconStyle()}
|
|
170
|
+
/>
|
|
171
|
+
<span
|
|
172
|
+
className="font-bold truncate text-4xl"
|
|
173
|
+
style={getLabelStyle()}
|
|
174
|
+
>
|
|
175
|
+
{name}
|
|
176
|
+
</span>
|
|
177
|
+
{label && (
|
|
178
|
+
<span className="text-sm text-theme-text-secondary truncate">
|
|
179
|
+
({label})
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
{/* Count badge - fixed size, anchored top-right */}
|
|
184
|
+
<span className="absolute right-4 top-4 shrink-0 font-medium text-sm text-theme-text-secondary bg-theme-surface/70 rounded-lg px-3 py-1">
|
|
185
|
+
{nodeCount}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<Handle
|
|
192
|
+
type="source"
|
|
193
|
+
position={Position.Right}
|
|
194
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
)
|
|
198
|
+
})
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
import { Handle, Position } from '@xyflow/react'
|
|
3
|
+
import {
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronUp,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { clsx } from 'clsx'
|
|
8
|
+
import type { NodeKind, HealthStatus } from '../../types'
|
|
9
|
+
import { displayKind } from '../../types'
|
|
10
|
+
import { healthToSeverity, SEVERITY_DOT } from '../../utils/badge-colors'
|
|
11
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
12
|
+
|
|
13
|
+
// Get actionable tooltip content for health issues
|
|
14
|
+
function getIssueTooltip(issue: string | undefined): React.ReactNode {
|
|
15
|
+
if (!issue) return null
|
|
16
|
+
|
|
17
|
+
const issueDetails: Record<string, { title: string; description: string; action: string }> = {
|
|
18
|
+
OOMKilled: {
|
|
19
|
+
title: 'Out of Memory (OOMKilled)',
|
|
20
|
+
description: 'Container exceeded its memory limit and was killed by the kernel.',
|
|
21
|
+
action: 'Increase memory limits or optimize memory usage.',
|
|
22
|
+
},
|
|
23
|
+
CrashLoopBackOff: {
|
|
24
|
+
title: 'CrashLoopBackOff',
|
|
25
|
+
description: 'Container is repeatedly crashing and Kubernetes is backing off restarts.',
|
|
26
|
+
action: 'Check container logs for crash reason.',
|
|
27
|
+
},
|
|
28
|
+
ImagePullBackOff: {
|
|
29
|
+
title: 'ImagePullBackOff',
|
|
30
|
+
description: 'Kubernetes cannot pull the container image.',
|
|
31
|
+
action: 'Verify image name, tag, and registry credentials.',
|
|
32
|
+
},
|
|
33
|
+
ErrImagePull: {
|
|
34
|
+
title: 'Image Pull Error',
|
|
35
|
+
description: 'Failed to pull the container image.',
|
|
36
|
+
action: 'Check image name and registry access.',
|
|
37
|
+
},
|
|
38
|
+
CreateContainerConfigError: {
|
|
39
|
+
title: 'Container Config Error',
|
|
40
|
+
description: 'Invalid container configuration (e.g., missing ConfigMap/Secret).',
|
|
41
|
+
action: 'Verify referenced ConfigMaps and Secrets exist.',
|
|
42
|
+
},
|
|
43
|
+
Pending: {
|
|
44
|
+
title: 'Pending',
|
|
45
|
+
description: 'Pod is waiting to be scheduled to a node.',
|
|
46
|
+
action: 'Check for resource constraints or node availability.',
|
|
47
|
+
},
|
|
48
|
+
FailedScheduling: {
|
|
49
|
+
title: 'Scheduling Failed',
|
|
50
|
+
description: 'No suitable node found for this pod.',
|
|
51
|
+
action: 'Check node resources, taints, tolerations, and affinity rules.',
|
|
52
|
+
},
|
|
53
|
+
Evicted: {
|
|
54
|
+
title: 'Pod Evicted',
|
|
55
|
+
description: 'Pod was evicted from the node (usually due to resource pressure).',
|
|
56
|
+
action: 'Check node resource usage and set appropriate resource requests.',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const details = issueDetails[issue]
|
|
61
|
+
if (!details) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="max-w-xs">
|
|
64
|
+
<div className="font-medium text-red-400">{issue}</div>
|
|
65
|
+
<div className="text-theme-text-secondary text-[10px] mt-1">Click to view details</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="max-w-xs">
|
|
72
|
+
<div className="font-medium text-red-400">{details.title}</div>
|
|
73
|
+
<div className="text-theme-text-secondary text-[10px] mt-1">{details.description}</div>
|
|
74
|
+
<div className="text-blue-400 text-[10px] mt-1.5 border-t border-theme-border pt-1.5">
|
|
75
|
+
💡 {details.action}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default dimensions for unknown CRD kinds
|
|
82
|
+
export const DEFAULT_NODE_DIMENSIONS = { width: 260, height: 56 }
|
|
83
|
+
|
|
84
|
+
// Node dimensions for ELK layout - sized for typical K8s resource names
|
|
85
|
+
export const NODE_DIMENSIONS: Record<NodeKind, { width: number; height: number }> = {
|
|
86
|
+
Internet: { width: 120, height: 52 },
|
|
87
|
+
Ingress: { width: 300, height: 56 },
|
|
88
|
+
Gateway: { width: 300, height: 56 },
|
|
89
|
+
HTTPRoute: { width: 300, height: 56 },
|
|
90
|
+
GRPCRoute: { width: 300, height: 56 },
|
|
91
|
+
TCPRoute: { width: 300, height: 56 },
|
|
92
|
+
TLSRoute: { width: 300, height: 56 },
|
|
93
|
+
Service: { width: 260, height: 56 },
|
|
94
|
+
Deployment: { width: 280, height: 56 },
|
|
95
|
+
Rollout: { width: 280, height: 56 },
|
|
96
|
+
Application: { width: 300, height: 56 }, // ArgoCD Application
|
|
97
|
+
Kustomization: { width: 300, height: 56 }, // FluxCD Kustomization
|
|
98
|
+
HelmRelease: { width: 280, height: 56 }, // FluxCD HelmRelease
|
|
99
|
+
GitRepository: { width: 280, height: 56 }, // FluxCD GitRepository
|
|
100
|
+
DaemonSet: { width: 280, height: 56 },
|
|
101
|
+
StatefulSet: { width: 280, height: 56 },
|
|
102
|
+
ReplicaSet: { width: 280, height: 56 },
|
|
103
|
+
Pod: { width: 300, height: 56 },
|
|
104
|
+
PodGroup: { width: 200, height: 64 },
|
|
105
|
+
ConfigMap: { width: 180, height: 48 },
|
|
106
|
+
Secret: { width: 180, height: 48 },
|
|
107
|
+
HorizontalPodAutoscaler: { width: 280, height: 56 },
|
|
108
|
+
Job: { width: 180, height: 56 },
|
|
109
|
+
CronJob: { width: 200, height: 56 },
|
|
110
|
+
PersistentVolumeClaim: { width: 200, height: 48 },
|
|
111
|
+
Namespace: { width: 180, height: 48 },
|
|
112
|
+
Node: { width: 280, height: 56 },
|
|
113
|
+
NodePool: { width: 260, height: 56 },
|
|
114
|
+
NodeClaim: { width: 260, height: 56 },
|
|
115
|
+
NodeClass: { width: 260, height: 56 },
|
|
116
|
+
KnativeService: { width: 280, height: 56 },
|
|
117
|
+
KnativeConfiguration: { width: 280, height: 56 },
|
|
118
|
+
KnativeRevision: { width: 280, height: 56 },
|
|
119
|
+
KnativeRoute: { width: 280, height: 56 },
|
|
120
|
+
Broker: { width: 280, height: 56 },
|
|
121
|
+
Channel: { width: 280, height: 56 },
|
|
122
|
+
Trigger: { width: 280, height: 56 },
|
|
123
|
+
PingSource: { width: 280, height: 56 },
|
|
124
|
+
ApiServerSource: { width: 280, height: 56 },
|
|
125
|
+
ContainerSource: { width: 280, height: 56 },
|
|
126
|
+
SinkBinding: { width: 280, height: 56 },
|
|
127
|
+
IngressRoute: { width: 280, height: 56 },
|
|
128
|
+
IngressRouteTCP: { width: 280, height: 56 },
|
|
129
|
+
IngressRouteUDP: { width: 280, height: 56 },
|
|
130
|
+
Middleware: { width: 280, height: 56 },
|
|
131
|
+
MiddlewareTCP: { width: 280, height: 56 },
|
|
132
|
+
TraefikService: { width: 280, height: 56 },
|
|
133
|
+
ServersTransport: { width: 280, height: 56 },
|
|
134
|
+
ServersTransportTCP: { width: 300, height: 56 },
|
|
135
|
+
TLSOption: { width: 280, height: 56 },
|
|
136
|
+
TLSStore: { width: 280, height: 56 },
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
// Status indicator color (for dot and left bar) - uses centralized severity colors
|
|
141
|
+
function getStatusDotColor(status: HealthStatus): string {
|
|
142
|
+
const severity = healthToSeverity(status)
|
|
143
|
+
return SEVERITY_DOT[severity]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Cached style objects for status states (avoid creating new objects each render)
|
|
147
|
+
const STATUS_STYLES: Record<HealthStatus, React.CSSProperties> = {
|
|
148
|
+
degraded: {
|
|
149
|
+
border: '2px solid rgb(234 179 8 / 0.6)',
|
|
150
|
+
backgroundColor: 'rgb(251 146 60 / 0.12)',
|
|
151
|
+
},
|
|
152
|
+
unhealthy: {
|
|
153
|
+
border: '2px solid rgb(239 68 68 / 0.7)',
|
|
154
|
+
backgroundColor: 'rgb(248 113 113 / 0.15)',
|
|
155
|
+
},
|
|
156
|
+
healthy: {},
|
|
157
|
+
unknown: {},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getStatusStyle(status: HealthStatus): React.CSSProperties {
|
|
161
|
+
return STATUS_STYLES[status] || STATUS_STYLES.healthy
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
// Format subtitle based on node kind
|
|
166
|
+
function getSubtitle(kind: NodeKind, nodeData: Record<string, unknown>): string {
|
|
167
|
+
switch (kind) {
|
|
168
|
+
case 'Deployment':
|
|
169
|
+
case 'Rollout':
|
|
170
|
+
case 'DaemonSet':
|
|
171
|
+
case 'StatefulSet':
|
|
172
|
+
case 'ReplicaSet': {
|
|
173
|
+
// Use statusSummary if available (includes issue info like "0/3 OOMKilled")
|
|
174
|
+
const statusSummary = nodeData.statusSummary as string
|
|
175
|
+
if (statusSummary) {
|
|
176
|
+
return statusSummary
|
|
177
|
+
}
|
|
178
|
+
const ready = nodeData.readyReplicas ?? 0
|
|
179
|
+
const total = nodeData.totalReplicas ?? 0
|
|
180
|
+
return `${ready}/${total} ready`
|
|
181
|
+
}
|
|
182
|
+
case 'Application': {
|
|
183
|
+
// ArgoCD Application - show sync and health status
|
|
184
|
+
const syncStatus = (nodeData.syncStatus as string) || 'Unknown'
|
|
185
|
+
const healthStatus = (nodeData.healthStatus as string) || 'Unknown'
|
|
186
|
+
return `${syncStatus} • ${healthStatus}`
|
|
187
|
+
}
|
|
188
|
+
case 'Kustomization': {
|
|
189
|
+
// FluxCD Kustomization - show ready status and resource count
|
|
190
|
+
const ready = (nodeData.ready as string) || 'Unknown'
|
|
191
|
+
const resources = nodeData.resourceCount as number
|
|
192
|
+
return resources ? `${ready} • ${resources} resources` : ready
|
|
193
|
+
}
|
|
194
|
+
case 'HelmRelease': {
|
|
195
|
+
// FluxCD HelmRelease - show ready status and revision
|
|
196
|
+
const ready = (nodeData.ready as string) || 'Unknown'
|
|
197
|
+
const revision = nodeData.revision as number
|
|
198
|
+
return revision ? `${ready} • rev ${revision}` : ready
|
|
199
|
+
}
|
|
200
|
+
case 'GitRepository': {
|
|
201
|
+
// FluxCD GitRepository - show ready status and branch/revision
|
|
202
|
+
const ready = (nodeData.ready as string) || 'Unknown'
|
|
203
|
+
const branch = nodeData.branch as string
|
|
204
|
+
return branch ? `${ready} • ${branch}` : ready
|
|
205
|
+
}
|
|
206
|
+
case 'Pod':
|
|
207
|
+
return (nodeData.phase as string) || 'Unknown'
|
|
208
|
+
case 'Service': {
|
|
209
|
+
const svcType = (nodeData.type as string) || 'ClusterIP'
|
|
210
|
+
const port = nodeData.port
|
|
211
|
+
return port ? `${svcType} :${port}` : svcType
|
|
212
|
+
}
|
|
213
|
+
case 'Ingress':
|
|
214
|
+
return (nodeData.hostname as string) || 'No host'
|
|
215
|
+
case 'Gateway': {
|
|
216
|
+
const listeners = nodeData.listenerCount as number || 0
|
|
217
|
+
const addresses = nodeData.addresses as string[]
|
|
218
|
+
const addr = addresses?.length ? addresses[0] : ''
|
|
219
|
+
return addr ? `${listeners} listeners • ${addr}` : `${listeners} listeners`
|
|
220
|
+
}
|
|
221
|
+
case 'HTTPRoute':
|
|
222
|
+
case 'GRPCRoute':
|
|
223
|
+
case 'TCPRoute':
|
|
224
|
+
case 'TLSRoute': {
|
|
225
|
+
const hostnames = nodeData.hostnames as string[]
|
|
226
|
+
const rulesCount = nodeData.rulesCount as number || 0
|
|
227
|
+
const host = hostnames?.length ? hostnames[0] : ''
|
|
228
|
+
return host ? `${host} • ${rulesCount} rules` : `${rulesCount} rules`
|
|
229
|
+
}
|
|
230
|
+
case 'HorizontalPodAutoscaler': {
|
|
231
|
+
const min = nodeData.minReplicas ?? 1
|
|
232
|
+
const max = nodeData.maxReplicas ?? 10
|
|
233
|
+
const current = nodeData.current ?? 0
|
|
234
|
+
return `${current} (${min}-${max})`
|
|
235
|
+
}
|
|
236
|
+
case 'ConfigMap':
|
|
237
|
+
return `${nodeData.keys ?? 0} keys`
|
|
238
|
+
case 'Secret':
|
|
239
|
+
return `${nodeData.keys ?? 0} keys`
|
|
240
|
+
case 'PersistentVolumeClaim': {
|
|
241
|
+
const storage = (nodeData.storage as string) || ''
|
|
242
|
+
const phase = (nodeData.phase as string) || ''
|
|
243
|
+
return storage ? `${storage} (${phase})` : phase
|
|
244
|
+
}
|
|
245
|
+
case 'PodGroup': {
|
|
246
|
+
const count = (nodeData.podCount as number) || 0
|
|
247
|
+
const healthy = (nodeData.healthy as number) || 0
|
|
248
|
+
const unhealthy = (nodeData.unhealthy as number) || 0
|
|
249
|
+
if (unhealthy > 0) {
|
|
250
|
+
return `${count} pods (${unhealthy} unhealthy)`
|
|
251
|
+
}
|
|
252
|
+
return `${count} pods (${healthy} healthy)`
|
|
253
|
+
}
|
|
254
|
+
case 'KnativeService': {
|
|
255
|
+
const url = nodeData.url as string
|
|
256
|
+
if (url) {
|
|
257
|
+
// Show just the hostname from the URL for compactness
|
|
258
|
+
try {
|
|
259
|
+
return new URL(url).hostname
|
|
260
|
+
} catch {
|
|
261
|
+
return url
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return (nodeData.latestRevision as string) || ''
|
|
265
|
+
}
|
|
266
|
+
case 'Node': {
|
|
267
|
+
const instanceType = nodeData.instanceType as string
|
|
268
|
+
return instanceType || ''
|
|
269
|
+
}
|
|
270
|
+
case 'Internet':
|
|
271
|
+
return ''
|
|
272
|
+
default:
|
|
273
|
+
return ''
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface K8sResourceNodeProps {
|
|
278
|
+
data: {
|
|
279
|
+
kind: NodeKind
|
|
280
|
+
name: string
|
|
281
|
+
status: HealthStatus
|
|
282
|
+
nodeData: Record<string, unknown>
|
|
283
|
+
selected?: boolean
|
|
284
|
+
onExpand?: (nodeId: string) => void
|
|
285
|
+
onCollapse?: (nodeId: string) => void
|
|
286
|
+
isExpanded?: boolean
|
|
287
|
+
}
|
|
288
|
+
id: string
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const K8sResourceNode = memo(function K8sResourceNode({
|
|
292
|
+
data,
|
|
293
|
+
id,
|
|
294
|
+
}: K8sResourceNodeProps) {
|
|
295
|
+
const { kind, name, status, nodeData, selected, onExpand, onCollapse, isExpanded } = data
|
|
296
|
+
const subtitle = getSubtitle(kind, nodeData)
|
|
297
|
+
const isInternet = kind === 'Internet'
|
|
298
|
+
const isPodGroup = kind === 'PodGroup'
|
|
299
|
+
const isSmallNode = kind === 'ConfigMap' || kind === 'Secret' || kind === 'HorizontalPodAutoscaler'
|
|
300
|
+
const canExpand = isPodGroup && onExpand && !isExpanded
|
|
301
|
+
const canCollapse = isPodGroup && onCollapse && isExpanded
|
|
302
|
+
const statusIssue = nodeData.statusIssue as string | undefined
|
|
303
|
+
const issueTooltip = getIssueTooltip(statusIssue)
|
|
304
|
+
|
|
305
|
+
// CSS class for icon (replaces Lucide SVG - saves ~5 DOM elements per node)
|
|
306
|
+
const iconClass = `topology-icon topology-icon-${kind.toLowerCase()}`
|
|
307
|
+
|
|
308
|
+
if (isInternet) {
|
|
309
|
+
return (
|
|
310
|
+
<>
|
|
311
|
+
<Handle
|
|
312
|
+
type="target"
|
|
313
|
+
position={Position.Left}
|
|
314
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
315
|
+
/>
|
|
316
|
+
<div
|
|
317
|
+
className={clsx(
|
|
318
|
+
'flex items-center gap-2 px-4 py-2 rounded-full',
|
|
319
|
+
'bg-blue-500/10 border border-blue-500/30',
|
|
320
|
+
'shadow-lg shadow-blue-500/20',
|
|
321
|
+
selected && 'ring-2 ring-blue-400'
|
|
322
|
+
)}
|
|
323
|
+
>
|
|
324
|
+
<span className="topology-icon topology-icon-internet" style={{ width: 20, height: 20 }} />
|
|
325
|
+
<span className="text-sm font-medium text-blue-300">Internet</span>
|
|
326
|
+
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
327
|
+
</div>
|
|
328
|
+
<Handle
|
|
329
|
+
type="source"
|
|
330
|
+
position={Position.Right}
|
|
331
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
332
|
+
/>
|
|
333
|
+
</>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<>
|
|
339
|
+
<Handle
|
|
340
|
+
type="target"
|
|
341
|
+
position={Position.Left}
|
|
342
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
343
|
+
/>
|
|
344
|
+
|
|
345
|
+
<div
|
|
346
|
+
className={clsx(
|
|
347
|
+
'relative rounded-lg overflow-hidden',
|
|
348
|
+
'bg-theme-surface topology-node-card',
|
|
349
|
+
selected && 'ring-2 ring-blue-400',
|
|
350
|
+
isSmallNode && 'opacity-90',
|
|
351
|
+
// Status bar via CSS pseudo-element (defined in index.css)
|
|
352
|
+
(status === 'healthy' || status === 'unknown') && 'topology-node-status-bar',
|
|
353
|
+
status === 'healthy' && 'topology-node-status-healthy',
|
|
354
|
+
status === 'unknown' && 'topology-node-status-unknown'
|
|
355
|
+
)}
|
|
356
|
+
style={{
|
|
357
|
+
width: NODE_DIMENSIONS[kind]?.width ?? DEFAULT_NODE_DIMENSIONS.width,
|
|
358
|
+
...getStatusStyle(status),
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
|
|
362
|
+
{/* Content */}
|
|
363
|
+
<div className={clsx(
|
|
364
|
+
'pl-3 pr-3',
|
|
365
|
+
isSmallNode ? 'py-2' : 'py-2.5'
|
|
366
|
+
)}>
|
|
367
|
+
{/* Header row: icon + kind label + expand/collapse + status dot */}
|
|
368
|
+
<div className="flex items-center gap-1.5 mb-0.5">
|
|
369
|
+
<span className={iconClass} />
|
|
370
|
+
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary font-medium">
|
|
371
|
+
{isPodGroup ? 'Pod Group' : displayKind(kind)}
|
|
372
|
+
</span>
|
|
373
|
+
{/* Expand/Collapse button for PodGroup */}
|
|
374
|
+
{canExpand && (
|
|
375
|
+
<button
|
|
376
|
+
onClick={(e) => {
|
|
377
|
+
e.stopPropagation()
|
|
378
|
+
onExpand(id)
|
|
379
|
+
}}
|
|
380
|
+
className="ml-auto p-0.5 hover:bg-theme-elevated rounded transition-colors"
|
|
381
|
+
title="Expand to show individual pods"
|
|
382
|
+
>
|
|
383
|
+
<ChevronDown className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
384
|
+
</button>
|
|
385
|
+
)}
|
|
386
|
+
{canCollapse && (
|
|
387
|
+
<button
|
|
388
|
+
onClick={(e) => {
|
|
389
|
+
e.stopPropagation()
|
|
390
|
+
onCollapse(id)
|
|
391
|
+
}}
|
|
392
|
+
className="ml-auto p-0.5 hover:bg-theme-elevated rounded transition-colors"
|
|
393
|
+
title="Collapse back to group"
|
|
394
|
+
>
|
|
395
|
+
<ChevronUp className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
396
|
+
</button>
|
|
397
|
+
)}
|
|
398
|
+
{issueTooltip ? (
|
|
399
|
+
<Tooltip content={issueTooltip} position="right">
|
|
400
|
+
<span
|
|
401
|
+
className={clsx(
|
|
402
|
+
canExpand || canCollapse ? '' : 'ml-auto',
|
|
403
|
+
'w-1.5 h-1.5 rounded-full cursor-help',
|
|
404
|
+
getStatusDotColor(status)
|
|
405
|
+
)}
|
|
406
|
+
/>
|
|
407
|
+
</Tooltip>
|
|
408
|
+
) : (
|
|
409
|
+
<span
|
|
410
|
+
className={clsx(
|
|
411
|
+
canExpand || canCollapse ? '' : 'ml-auto',
|
|
412
|
+
'w-1.5 h-1.5 rounded-full',
|
|
413
|
+
getStatusDotColor(status)
|
|
414
|
+
)}
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Name */}
|
|
420
|
+
<div className="text-sm font-medium text-theme-text-primary truncate pr-1">
|
|
421
|
+
{name}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{/* Subtitle */}
|
|
425
|
+
{subtitle && (
|
|
426
|
+
<div className="text-xs text-theme-text-secondary truncate mt-0.5">
|
|
427
|
+
{subtitle}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<Handle
|
|
434
|
+
type="source"
|
|
435
|
+
position={Position.Right}
|
|
436
|
+
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
437
|
+
/>
|
|
438
|
+
</>
|
|
439
|
+
)
|
|
440
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FolderTree } from 'lucide-react'
|
|
2
|
+
import type { TopologyMode, GroupingMode } from '../../types/core'
|
|
3
|
+
|
|
4
|
+
interface TopologyControlsProps {
|
|
5
|
+
viewMode: TopologyMode
|
|
6
|
+
onViewModeChange: (mode: TopologyMode) => void
|
|
7
|
+
groupingMode: GroupingMode
|
|
8
|
+
onGroupingModeChange: (mode: GroupingMode) => void
|
|
9
|
+
showNoGrouping?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TopologyControls({
|
|
13
|
+
viewMode,
|
|
14
|
+
onViewModeChange,
|
|
15
|
+
groupingMode,
|
|
16
|
+
onGroupingModeChange,
|
|
17
|
+
showNoGrouping = true,
|
|
18
|
+
}: TopologyControlsProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
21
|
+
{/* Grouping selector */}
|
|
22
|
+
<div className="flex items-center gap-1.5 px-2 py-1.5 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg">
|
|
23
|
+
<FolderTree className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
24
|
+
<select
|
|
25
|
+
value={groupingMode}
|
|
26
|
+
onChange={(e) => onGroupingModeChange(e.target.value as GroupingMode)}
|
|
27
|
+
className="appearance-none bg-transparent text-theme-text-primary text-xs focus:outline-none"
|
|
28
|
+
>
|
|
29
|
+
{showNoGrouping && (
|
|
30
|
+
<option value="none" className="bg-theme-surface">No Grouping</option>
|
|
31
|
+
)}
|
|
32
|
+
<option value="namespace" className="bg-theme-surface">By Namespace</option>
|
|
33
|
+
<option value="app" className="bg-theme-surface">By App Label</option>
|
|
34
|
+
</select>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* View mode toggle */}
|
|
38
|
+
<div className="flex items-center gap-0.5 p-1 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg">
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => onViewModeChange('resources')}
|
|
41
|
+
className={`px-2.5 py-1 text-xs rounded-md transition-colors ${
|
|
42
|
+
viewMode === 'resources'
|
|
43
|
+
? 'bg-blue-500 text-theme-text-primary'
|
|
44
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
Resources
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => onViewModeChange('traffic')}
|
|
51
|
+
className={`px-2.5 py-1 text-xs rounded-md transition-colors ${
|
|
52
|
+
viewMode === 'traffic'
|
|
53
|
+
? 'bg-blue-500 text-theme-text-primary'
|
|
54
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
55
|
+
}`}
|
|
56
|
+
>
|
|
57
|
+
Traffic
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|