@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,333 @@
1
+ import { useEffect, useLayoutEffect, useRef, useCallback, useState } from 'react'
2
+ import { Terminal as XTerm } from '@xterm/xterm'
3
+ import { FitAddon } from '@xterm/addon-fit'
4
+ import { WebLinksAddon } from '@xterm/addon-web-links'
5
+ import '@xterm/xterm/css/xterm.css'
6
+ import { RefreshCw, ChevronDown, Bug } from 'lucide-react'
7
+ import { clsx } from 'clsx'
8
+
9
+ export interface TerminalTabProps {
10
+ namespace: string
11
+ podName: string
12
+ containerName: string
13
+ containers: string[]
14
+ isActive?: boolean
15
+ /** Returns the WebSocket URL to connect to for a given container. */
16
+ createSession: (containerName: string) => Promise<{ wsUrl: string }>
17
+ /** Optional: creates a debug (ephemeral) container. If omitted, the debug button is hidden. */
18
+ createDebugContainer?: (targetContainer: string) => Promise<{ containerName: string }>
19
+ }
20
+
21
+ export function TerminalTab({
22
+ namespace,
23
+ podName,
24
+ containerName,
25
+ containers,
26
+ isActive = true,
27
+ createSession,
28
+ createDebugContainer,
29
+ }: TerminalTabProps) {
30
+ const terminalRef = useRef<HTMLDivElement>(null)
31
+ const xtermRef = useRef<XTerm | null>(null)
32
+ const fitAddonRef = useRef<FitAddon | null>(null)
33
+ const wsRef = useRef<WebSocket | null>(null)
34
+ // Cleanup ref holds the ResizeObserver disconnect from an async setup
35
+ const cleanupRef = useRef<(() => void) | undefined>(undefined)
36
+ // Prevents stale .then() callbacks from running after unmount or reconnect
37
+ const cancelledRef = useRef(false)
38
+ // Stable refs for callbacks — avoids effect re-runs when consumers pass unstable functions
39
+ const createSessionRef = useRef(createSession)
40
+ const createDebugContainerRef = useRef(createDebugContainer)
41
+ useLayoutEffect(() => { createSessionRef.current = createSession }, [createSession])
42
+ useLayoutEffect(() => { createDebugContainerRef.current = createDebugContainer }, [createDebugContainer])
43
+ const [isConnected, setIsConnected] = useState(false)
44
+ const [isConnecting, setIsConnecting] = useState(true)
45
+ const [error, setError] = useState<string | null>(null)
46
+ const [errorType, setErrorType] = useState<string | null>(null)
47
+ const [isCreatingDebug, setIsCreatingDebug] = useState(false)
48
+ const [selectedContainer, setSelectedContainer] = useState(containerName)
49
+
50
+ const connect = useCallback(() => {
51
+ if (!terminalRef.current) return
52
+
53
+ cancelledRef.current = false
54
+ setIsConnecting(true)
55
+ setError(null)
56
+ setErrorType(null)
57
+
58
+ // Clean up existing terminal and connection
59
+ cleanupRef.current?.()
60
+ xtermRef.current?.dispose()
61
+ wsRef.current?.close()
62
+
63
+ const xterm = new XTerm({
64
+ cursorBlink: true,
65
+ fontFamily: 'JetBrains Mono, Menlo, Monaco, monospace',
66
+ fontSize: 13,
67
+ lineHeight: 1.2,
68
+ theme: {
69
+ background: '#0f172a',
70
+ foreground: '#e2e8f0',
71
+ cursor: '#60a5fa',
72
+ cursorAccent: '#0f172a',
73
+ selectionBackground: '#3b82f680',
74
+ black: '#1e293b',
75
+ red: '#f87171',
76
+ green: '#4ade80',
77
+ yellow: '#facc15',
78
+ blue: '#60a5fa',
79
+ magenta: '#c084fc',
80
+ cyan: '#22d3ee',
81
+ white: '#f1f5f9',
82
+ brightBlack: '#475569',
83
+ brightRed: '#fca5a5',
84
+ brightGreen: '#86efac',
85
+ brightYellow: '#fde047',
86
+ brightBlue: '#93c5fd',
87
+ brightMagenta: '#d8b4fe',
88
+ brightCyan: '#67e8f9',
89
+ brightWhite: '#f8fafc',
90
+ },
91
+ })
92
+
93
+ const fitAddon = new FitAddon()
94
+ const webLinksAddon = new WebLinksAddon()
95
+ xterm.loadAddon(fitAddon)
96
+ xterm.loadAddon(webLinksAddon)
97
+ xterm.open(terminalRef.current)
98
+
99
+ xtermRef.current = xterm
100
+ fitAddonRef.current = fitAddon
101
+
102
+ const doFit = (ws?: WebSocket) => {
103
+ const dims = fitAddon.proposeDimensions()
104
+ if (dims) xterm.resize(dims.cols, dims.rows)
105
+ const conn = ws ?? wsRef.current
106
+ if (conn?.readyState === WebSocket.OPEN) {
107
+ conn.send(JSON.stringify({ type: 'resize', rows: xterm.rows, cols: xterm.cols }))
108
+ }
109
+ }
110
+
111
+ requestAnimationFrame(() => {
112
+ doFit()
113
+ setTimeout(doFit, 100)
114
+ })
115
+
116
+ createSessionRef.current(selectedContainer)
117
+ .then(({ wsUrl }) => {
118
+ if (cancelledRef.current) return
119
+ const ws = new WebSocket(wsUrl)
120
+ wsRef.current = ws
121
+
122
+ ws.onopen = () => {
123
+ setIsConnected(true)
124
+ setIsConnecting(false)
125
+ doFit(ws)
126
+ xterm.focus()
127
+ }
128
+
129
+ ws.onmessage = (event) => {
130
+ let msg: Record<string, string> | null = null
131
+ try {
132
+ msg = JSON.parse(event.data as string) as Record<string, string>
133
+ } catch {
134
+ xterm.write(event.data as string)
135
+ return
136
+ }
137
+ if (msg.type === 'output' && msg.data) {
138
+ xterm.write(msg.data)
139
+ } else if (msg.type === 'exit') {
140
+ xterm.write('\r\n\x1b[2m[Process exited]\x1b[0m\r\n')
141
+ } else if (msg.type === 'error' && msg.data) {
142
+ setError(msg.data)
143
+ // Support both camelCase (radar) and snake_case (conduit) error type field
144
+ setErrorType(msg.errorType ?? msg.error_type ?? 'exec_error')
145
+ setIsConnected(false)
146
+ }
147
+ }
148
+
149
+ ws.onerror = () => {
150
+ setError((prev) => prev || 'Connection error')
151
+ setIsConnected(false)
152
+ setIsConnecting(false)
153
+ }
154
+
155
+ ws.onclose = () => {
156
+ setIsConnected(false)
157
+ setIsConnecting(false)
158
+ xterm.write('\r\n\x1b[31mConnection closed\x1b[0m\r\n')
159
+ }
160
+
161
+ xterm.onData((data: string) => {
162
+ if (ws.readyState === WebSocket.OPEN) {
163
+ ws.send(JSON.stringify({ type: 'input', data }))
164
+ }
165
+ })
166
+
167
+ // Debounced resize to avoid infinite loops
168
+ let resizeTimeout: ReturnType<typeof setTimeout> | null = null
169
+ let lastWidth = 0
170
+ let lastHeight = 0
171
+ const resizeObserver = new ResizeObserver((entries) => {
172
+ const entry = entries[0]
173
+ if (!entry) return
174
+ const { width, height } = entry.contentRect
175
+ if (Math.abs(width - lastWidth) < 5 && Math.abs(height - lastHeight) < 5) return
176
+ lastWidth = width
177
+ lastHeight = height
178
+ if (resizeTimeout) clearTimeout(resizeTimeout)
179
+ resizeTimeout = setTimeout(() => {
180
+ if (fitAddonRef.current && xtermRef.current) {
181
+ const dims = fitAddonRef.current.proposeDimensions()
182
+ if (dims) xtermRef.current.resize(dims.cols, dims.rows)
183
+ if (ws.readyState === WebSocket.OPEN) {
184
+ ws.send(JSON.stringify({ type: 'resize', rows: xtermRef.current.rows, cols: xtermRef.current.cols }))
185
+ }
186
+ }
187
+ }, 100)
188
+ })
189
+ if (terminalRef.current) {
190
+ resizeObserver.observe(terminalRef.current)
191
+ cleanupRef.current = () => resizeObserver.disconnect()
192
+ } else {
193
+ resizeObserver.disconnect()
194
+ }
195
+ })
196
+ .catch((err) => {
197
+ setError(err instanceof Error ? err.message : 'Failed to connect')
198
+ setErrorType('exec_error')
199
+ setIsConnecting(false)
200
+ })
201
+ }, [namespace, podName, selectedContainer])
202
+
203
+ useEffect(() => {
204
+ connect()
205
+ return () => {
206
+ cancelledRef.current = true
207
+ cleanupRef.current?.()
208
+ wsRef.current?.close()
209
+ xtermRef.current?.dispose()
210
+ }
211
+ }, [connect])
212
+
213
+ const handleContainerChange = useCallback((container: string) => {
214
+ setSelectedContainer(container)
215
+ }, [])
216
+
217
+ const handleCreateDebugContainer = useCallback(async () => {
218
+ if (!createDebugContainerRef.current) return
219
+ setIsCreatingDebug(true)
220
+ try {
221
+ const { containerName: newContainer } = await createDebugContainerRef.current(selectedContainer)
222
+ setError(null)
223
+ setErrorType(null)
224
+ setSelectedContainer(newContainer)
225
+ } catch (err) {
226
+ setError(err instanceof Error ? err.message : 'Failed to create debug container')
227
+ setErrorType('exec_error')
228
+ } finally {
229
+ setIsCreatingDebug(false)
230
+ }
231
+ }, [selectedContainer])
232
+
233
+ // Refit when tab becomes active (may have been resized while hidden)
234
+ useEffect(() => {
235
+ if (isActive && fitAddonRef.current && xtermRef.current) {
236
+ const dims = fitAddonRef.current.proposeDimensions()
237
+ if (dims) xtermRef.current.resize(dims.cols, dims.rows)
238
+ xtermRef.current.focus()
239
+ }
240
+ }, [isActive])
241
+
242
+ return (
243
+ <div className="relative h-full w-full bg-theme-base overflow-hidden">
244
+ {/* Mini toolbar */}
245
+ <div className="h-8 flex items-center gap-2 px-2 bg-theme-surface/50 border-b border-theme-border/50">
246
+ <span
247
+ title={isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected'}
248
+ className={clsx(
249
+ 'w-2 h-2 rounded-full',
250
+ isConnected ? 'bg-green-500' : isConnecting ? 'bg-yellow-500 animate-pulse' : 'bg-red-500'
251
+ )}
252
+ />
253
+ <span className="text-xs text-theme-text-tertiary">{podName}</span>
254
+
255
+ {containers.length > 1 && (
256
+ <div className="relative">
257
+ <select
258
+ value={selectedContainer}
259
+ onChange={(e) => handleContainerChange(e.target.value)}
260
+ className="appearance-none bg-theme-elevated text-xs text-theme-text-primary px-2 py-0.5 pr-5 rounded border border-theme-border-light focus:outline-none focus:ring-1 focus:ring-blue-500"
261
+ >
262
+ {containers.map((c) => (
263
+ <option key={c} value={c}>{c}</option>
264
+ ))}
265
+ </select>
266
+ <ChevronDown className="absolute right-1 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary pointer-events-none" />
267
+ </div>
268
+ )}
269
+
270
+ {!isConnected && !isConnecting && (
271
+ <button
272
+ onClick={connect}
273
+ className="flex items-center gap-1 px-2 py-0.5 text-xs text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
274
+ >
275
+ <RefreshCw className="w-3 h-3" />
276
+ Reconnect
277
+ </button>
278
+ )}
279
+ </div>
280
+
281
+ {/* Terminal or error — key forces xterm canvas unmount/remount on toggle */}
282
+ {error ? (
283
+ <div key="error" className="absolute top-8 left-0 right-0 bottom-0 flex flex-col items-center justify-center p-4 text-center bg-slate-900">
284
+ {errorType === 'shell_not_found' ? (
285
+ <>
286
+ <div className="text-amber-400 mb-2 text-sm">Shell not available</div>
287
+ <div className="text-xs text-theme-text-tertiary mb-4 max-w-md">
288
+ This container doesn&apos;t have a shell (/bin/sh). This is common with distroless
289
+ or minimal container images. You can create a debug container to troubleshoot.
290
+ </div>
291
+ <div className="flex gap-2">
292
+ {createDebugContainerRef.current && (
293
+ <button
294
+ onClick={handleCreateDebugContainer}
295
+ disabled={isCreatingDebug}
296
+ className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 disabled:opacity-50"
297
+ >
298
+ {isCreatingDebug ? (
299
+ <><RefreshCw className="w-3 h-3 animate-spin" />Creating debug container...</>
300
+ ) : (
301
+ <><Bug className="w-3 h-3" />Start debug container</>
302
+ )}
303
+ </button>
304
+ )}
305
+ <button
306
+ onClick={connect}
307
+ className="flex items-center gap-2 px-3 py-1.5 bg-theme-elevated text-theme-text-primary text-xs rounded hover:bg-theme-hover"
308
+ >
309
+ <RefreshCw className="w-3 h-3" />
310
+ Retry
311
+ </button>
312
+ </div>
313
+ </>
314
+ ) : (
315
+ <>
316
+ <div className="text-red-400 mb-2 text-sm">Failed to connect</div>
317
+ <div className="text-xs text-theme-text-disabled mb-3">{error}</div>
318
+ <button
319
+ onClick={connect}
320
+ className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
321
+ >
322
+ <RefreshCw className="w-3 h-3" />
323
+ Retry
324
+ </button>
325
+ </>
326
+ )}
327
+ </div>
328
+ ) : (
329
+ <div key="terminal" ref={terminalRef} className="absolute top-8 left-0 right-0 bottom-0" />
330
+ )}
331
+ </div>
332
+ )
333
+ }
@@ -0,0 +1,3 @@
1
+ export * from './DockContext'
2
+ export * from './BottomDock'
3
+ export * from './TerminalTab'
@@ -0,0 +1,227 @@
1
+ import { RefreshCw, Pause, Play, Loader2 } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+
4
+ interface GitOpsActionsProps {
5
+ /** The GitOps tool type */
6
+ tool: 'flux' | 'argo'
7
+ /** Whether the resource is currently suspended */
8
+ suspended: boolean
9
+ /** Sync/Reconcile handler */
10
+ onSync?: () => void
11
+ /** Suspend handler */
12
+ onSuspend?: () => void
13
+ /** Resume handler */
14
+ onResume?: () => void
15
+ /** Whether a sync operation is in progress */
16
+ isSyncing?: boolean
17
+ /** Whether a suspend/resume operation is in progress */
18
+ isSuspending?: boolean
19
+ /** Layout direction */
20
+ direction?: 'row' | 'column'
21
+ /** Button size */
22
+ size?: 'sm' | 'md'
23
+ }
24
+
25
+ /**
26
+ * Action buttons for GitOps resources (Sync, Suspend, Resume)
27
+ */
28
+ export function GitOpsActions({
29
+ tool,
30
+ suspended,
31
+ onSync,
32
+ onSuspend,
33
+ onResume,
34
+ isSyncing,
35
+ isSuspending,
36
+ direction = 'row',
37
+ size = 'md',
38
+ }: GitOpsActionsProps) {
39
+ const syncLabel = tool === 'flux' ? 'Reconcile' : 'Sync'
40
+ const buttonClass = size === 'sm'
41
+ ? 'px-2 py-1 text-xs gap-1'
42
+ : 'px-3 py-1.5 text-sm gap-1.5'
43
+ const iconClass = size === 'sm' ? 'w-3 h-3' : 'w-3.5 h-3.5'
44
+
45
+ return (
46
+ <div className={clsx('flex gap-2', direction === 'column' ? 'flex-col' : 'flex-row')}>
47
+ {/* Sync/Reconcile button */}
48
+ {onSync && (
49
+ <button
50
+ onClick={onSync}
51
+ disabled={isSyncing || suspended}
52
+ className={clsx(
53
+ 'flex items-center rounded font-medium transition-colors',
54
+ buttonClass,
55
+ isSyncing
56
+ ? 'bg-blue-500/20 text-blue-400 cursor-wait'
57
+ : suspended
58
+ ? 'bg-theme-elevated text-theme-text-tertiary cursor-not-allowed'
59
+ : 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
60
+ )}
61
+ title={suspended ? 'Cannot sync while suspended' : syncLabel}
62
+ >
63
+ {isSyncing ? (
64
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
65
+ ) : (
66
+ <RefreshCw className={iconClass} />
67
+ )}
68
+ {syncLabel}
69
+ </button>
70
+ )}
71
+
72
+ {/* Suspend/Resume button */}
73
+ {suspended ? (
74
+ onResume && (
75
+ <button
76
+ onClick={onResume}
77
+ disabled={isSuspending}
78
+ className={clsx(
79
+ 'flex items-center rounded font-medium transition-colors',
80
+ buttonClass,
81
+ isSuspending
82
+ ? 'bg-green-500/20 text-green-400 cursor-wait'
83
+ : 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
84
+ )}
85
+ >
86
+ {isSuspending ? (
87
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
88
+ ) : (
89
+ <Play className={iconClass} />
90
+ )}
91
+ Resume
92
+ </button>
93
+ )
94
+ ) : (
95
+ onSuspend && (
96
+ <button
97
+ onClick={onSuspend}
98
+ disabled={isSuspending}
99
+ className={clsx(
100
+ 'flex items-center rounded font-medium transition-colors',
101
+ buttonClass,
102
+ isSuspending
103
+ ? 'bg-yellow-500/20 text-yellow-400 cursor-wait'
104
+ : 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
105
+ )}
106
+ >
107
+ {isSuspending ? (
108
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
109
+ ) : (
110
+ <Pause className={iconClass} />
111
+ )}
112
+ Suspend
113
+ </button>
114
+ )
115
+ )}
116
+ </div>
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Minimal sync-only button for compact UIs
122
+ */
123
+ export function SyncButton({
124
+ onClick,
125
+ loading,
126
+ disabled,
127
+ label = 'Sync',
128
+ size = 'md',
129
+ }: {
130
+ onClick: () => void
131
+ loading?: boolean
132
+ disabled?: boolean
133
+ label?: string
134
+ size?: 'sm' | 'md'
135
+ }) {
136
+ const buttonClass = size === 'sm'
137
+ ? 'px-2 py-1 text-xs gap-1'
138
+ : 'px-3 py-1.5 text-sm gap-1.5'
139
+ const iconClass = size === 'sm' ? 'w-3 h-3' : 'w-3.5 h-3.5'
140
+
141
+ return (
142
+ <button
143
+ onClick={onClick}
144
+ disabled={loading || disabled}
145
+ className={clsx(
146
+ 'flex items-center rounded font-medium transition-colors',
147
+ buttonClass,
148
+ loading || disabled
149
+ ? 'bg-theme-elevated text-theme-text-tertiary cursor-not-allowed'
150
+ : 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
151
+ )}
152
+ >
153
+ {loading ? (
154
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
155
+ ) : (
156
+ <RefreshCw className={iconClass} />
157
+ )}
158
+ {label}
159
+ </button>
160
+ )
161
+ }
162
+
163
+ /**
164
+ * Suspend/Resume toggle button
165
+ */
166
+ export function SuspendToggle({
167
+ suspended,
168
+ onSuspend,
169
+ onResume,
170
+ loading,
171
+ size = 'md',
172
+ }: {
173
+ suspended: boolean
174
+ onSuspend: () => void
175
+ onResume: () => void
176
+ loading?: boolean
177
+ size?: 'sm' | 'md'
178
+ }) {
179
+ const buttonClass = size === 'sm'
180
+ ? 'px-2 py-1 text-xs gap-1'
181
+ : 'px-3 py-1.5 text-sm gap-1.5'
182
+ const iconClass = size === 'sm' ? 'w-3 h-3' : 'w-3.5 h-3.5'
183
+
184
+ if (suspended) {
185
+ return (
186
+ <button
187
+ onClick={onResume}
188
+ disabled={loading}
189
+ className={clsx(
190
+ 'flex items-center rounded font-medium transition-colors',
191
+ buttonClass,
192
+ loading
193
+ ? 'bg-green-500/20 text-green-400 cursor-wait'
194
+ : 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
195
+ )}
196
+ >
197
+ {loading ? (
198
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
199
+ ) : (
200
+ <Play className={iconClass} />
201
+ )}
202
+ Resume
203
+ </button>
204
+ )
205
+ }
206
+
207
+ return (
208
+ <button
209
+ onClick={onSuspend}
210
+ disabled={loading}
211
+ className={clsx(
212
+ 'flex items-center rounded font-medium transition-colors',
213
+ buttonClass,
214
+ loading
215
+ ? 'bg-yellow-500/20 text-yellow-400 cursor-wait'
216
+ : 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
217
+ )}
218
+ >
219
+ {loading ? (
220
+ <Loader2 className={clsx(iconClass, 'animate-spin')} />
221
+ ) : (
222
+ <Pause className={iconClass} />
223
+ )}
224
+ Suspend
225
+ </button>
226
+ )
227
+ }