@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,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'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,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
|
+
}
|