@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.
Files changed (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. 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
+ }