@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
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@skyhook-io/k8s-ui",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/skyhook-io/radar",
7
+ "directory": "packages/k8s-ui"
8
+ },
9
+ "type": "module",
10
+ "main": "./src/index.ts",
11
+ "types": "./src/index.ts",
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./types": "./src/types/index.ts",
15
+ "./utils": "./src/utils/index.ts",
16
+ "./components/resources/*": "./src/components/resources/*.ts",
17
+ "./components/ui/*": "./src/components/ui/*.tsx",
18
+ "./hooks/*": "./src/hooks/*.ts",
19
+ "./resources": "./src/components/resources/index.ts",
20
+ "./logs": "./src/components/logs/index.ts",
21
+ "./timeline": "./src/components/timeline/index.ts",
22
+ "./topology": "./src/components/topology/index.ts"
23
+ },
24
+ "scripts": {
25
+ "tsc": "tsc --noEmit",
26
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
27
+ },
28
+ "dependencies": {
29
+ "@monaco-editor/react": "^4.7.0",
30
+ "react-virtuoso": "^4.18.1",
31
+ "shiki": "^4.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "@xterm/addon-fit": ">=0.10.0",
35
+ "@xterm/addon-web-links": ">=0.11.0",
36
+ "@xterm/xterm": ">=5.0.0",
37
+ "@xyflow/react": ">=12.0.0",
38
+ "clsx": ">=2.0.0",
39
+ "diff": ">=5.0.0",
40
+ "elkjs": ">=0.9.0",
41
+ "lucide-react": ">=0.400.0",
42
+ "react": ">=18.0.0",
43
+ "react-dom": ">=18.0.0",
44
+ "yaml": ">=2.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/diff": "^7.0.2",
48
+ "@types/react": "^19.2.14",
49
+ "@types/react-dom": "^19.2.3",
50
+ "@xterm/addon-fit": "^0.11.0",
51
+ "@xterm/addon-web-links": "^0.12.0",
52
+ "@xterm/xterm": "^6.0.0",
53
+ "@xyflow/react": "^12.10.1",
54
+ "clsx": "^2.1.1",
55
+ "diff": "^8.0.3",
56
+ "elkjs": "^0.11.1",
57
+ "lucide-react": "^0.575.0",
58
+ "react": "^19.2.4",
59
+ "react-dom": "^19.2.4",
60
+ "typescript": "^5.9.3",
61
+ "yaml": "^2.7.1"
62
+ }
63
+ }
@@ -0,0 +1,204 @@
1
+ import { useState, useCallback, useRef, useEffect, ReactNode } from 'react'
2
+ import { DURATION_DOCK } from '../../utils/animation'
3
+ import { X, ChevronDown, ChevronUp, Terminal, FileText, Trash2, Layers, Maximize2, Minimize2 } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+ import { useDock, DockTab } from './DockContext'
6
+ import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
7
+
8
+ const MIN_HEIGHT = 200
9
+ const DEFAULT_HEIGHT = 400
10
+ const MAX_HEIGHT_RATIO = 0.7
11
+ const MAXIMIZED_TOP_OFFSET = 48
12
+
13
+ interface BottomDockProps {
14
+ renderTabContent: (tab: DockTab, isActive: boolean) => ReactNode
15
+ /** Offset from the left edge in px — use to avoid overlapping a fixed sidebar */
16
+ leftOffset?: number
17
+ }
18
+
19
+ export function BottomDock({ renderTabContent, leftOffset = 0 }: BottomDockProps) {
20
+ const { tabs, activeTabId, isExpanded, removeTab, setActiveTab, toggleExpanded, closeAll } = useDock()
21
+ const [height, setHeight] = useState(DEFAULT_HEIGHT)
22
+ const [isMaximized, setIsMaximized] = useState(false)
23
+ const isDragging = useRef(false)
24
+ const startY = useRef(0)
25
+ const startHeight = useRef(0)
26
+
27
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
28
+ isDragging.current = true
29
+ startY.current = e.clientY
30
+ startHeight.current = height
31
+ document.body.style.cursor = 'ns-resize'
32
+ document.body.style.userSelect = 'none'
33
+ }, [height])
34
+
35
+ useEffect(() => {
36
+ const handleMouseMove = (e: MouseEvent) => {
37
+ if (!isDragging.current) return
38
+ const maxHeight = window.innerHeight * MAX_HEIGHT_RATIO
39
+ const newHeight = Math.min(maxHeight, Math.max(MIN_HEIGHT, startHeight.current - (e.clientY - startY.current)))
40
+ setHeight(newHeight)
41
+ }
42
+
43
+ const handleMouseUp = () => {
44
+ isDragging.current = false
45
+ document.body.style.cursor = ''
46
+ document.body.style.userSelect = ''
47
+ }
48
+
49
+ document.addEventListener('mousemove', handleMouseMove)
50
+ document.addEventListener('mouseup', handleMouseUp)
51
+
52
+ return () => {
53
+ document.removeEventListener('mousemove', handleMouseMove)
54
+ document.removeEventListener('mouseup', handleMouseUp)
55
+ }
56
+ }, [])
57
+
58
+ const toggleMaximized = useCallback(() => {
59
+ setIsMaximized(prev => !prev)
60
+ }, [])
61
+
62
+ useEffect(() => {
63
+ if (!isMaximized) return
64
+ const handleKeyDown = (e: KeyboardEvent) => {
65
+ if (e.key === 'Escape') setIsMaximized(false)
66
+ }
67
+ window.addEventListener('keydown', handleKeyDown)
68
+ return () => window.removeEventListener('keydown', handleKeyDown)
69
+ }, [isMaximized])
70
+
71
+ useRegisterShortcut({
72
+ id: 'dock-toggle',
73
+ keys: 'Ctrl+`',
74
+ description: 'Toggle dock',
75
+ category: 'Dock',
76
+ scope: 'global',
77
+ handler: () => toggleExpanded(),
78
+ enabled: tabs.length > 0,
79
+ })
80
+
81
+ if (tabs.length === 0) {
82
+ return null
83
+ }
84
+
85
+ const effectiveHeight = !isExpanded ? 36 : isMaximized ? `calc(100vh - ${MAXIMIZED_TOP_OFFSET}px)` : height
86
+
87
+ return (
88
+ <div
89
+ className="fixed bottom-0 right-0 bg-theme-base border-t border-theme-border flex flex-col z-40"
90
+ style={{ height: effectiveHeight, left: leftOffset, transition: `height ${DURATION_DOCK}ms ease-out, left ${DURATION_DOCK}ms ease-out` }}
91
+ >
92
+ {isExpanded && !isMaximized && (
93
+ <div
94
+ className="absolute top-0 left-0 right-0 h-1 cursor-ns-resize hover:bg-blue-500/50 transition-colors"
95
+ onMouseDown={handleMouseDown}
96
+ />
97
+ )}
98
+
99
+ <div className="flex items-center h-9 px-2 bg-theme-surface border-b border-theme-border">
100
+ <div className="flex items-center gap-1 flex-1 overflow-x-auto">
101
+ {tabs.map(tab => (
102
+ <TabButton
103
+ key={tab.id}
104
+ tab={tab}
105
+ isActive={tab.id === activeTabId}
106
+ onSelect={() => {
107
+ setActiveTab(tab.id)
108
+ if (!isExpanded) toggleExpanded()
109
+ }}
110
+ onClose={() => removeTab(tab.id)}
111
+ />
112
+ ))}
113
+ </div>
114
+
115
+ <div className="flex items-center gap-1 ml-2">
116
+ {tabs.length > 1 && (
117
+ <button
118
+ onClick={closeAll}
119
+ className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
120
+ title="Close all"
121
+ >
122
+ <Trash2 className="w-3.5 h-3.5" />
123
+ </button>
124
+ )}
125
+ {isExpanded && (
126
+ <button
127
+ onClick={toggleMaximized}
128
+ className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
129
+ title={isMaximized ? 'Restore (Esc)' : 'Maximize'}
130
+ >
131
+ {isMaximized ? (
132
+ <Minimize2 className="w-3.5 h-3.5" />
133
+ ) : (
134
+ <Maximize2 className="w-3.5 h-3.5" />
135
+ )}
136
+ </button>
137
+ )}
138
+ <button
139
+ onClick={() => { if (isMaximized) setIsMaximized(false); toggleExpanded() }}
140
+ className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
141
+ title={isExpanded ? 'Collapse' : 'Expand'}
142
+ >
143
+ {isExpanded ? (
144
+ <ChevronDown className="w-4 h-4" />
145
+ ) : (
146
+ <ChevronUp className="w-4 h-4" />
147
+ )}
148
+ </button>
149
+ </div>
150
+ </div>
151
+
152
+ {isExpanded && (
153
+ <div className="flex-1 overflow-hidden w-full relative">
154
+ {tabs.map(tab => (
155
+ <div
156
+ key={tab.id}
157
+ className={tab.id === activeTabId ? 'absolute inset-0' : 'absolute inset-0 invisible'}
158
+ >
159
+ {renderTabContent(tab, tab.id === activeTabId)}
160
+ </div>
161
+ ))}
162
+ </div>
163
+ )}
164
+ </div>
165
+ )
166
+ }
167
+
168
+ function TabButton({
169
+ tab,
170
+ isActive,
171
+ onSelect,
172
+ onClose,
173
+ }: {
174
+ tab: DockTab
175
+ isActive: boolean
176
+ onSelect: () => void
177
+ onClose: () => void
178
+ }) {
179
+ const Icon = tab.type === 'terminal' ? Terminal : tab.type === 'workload-logs' ? Layers : FileText
180
+
181
+ return (
182
+ <div
183
+ className={clsx(
184
+ 'flex items-center gap-1.5 px-2 py-1 rounded text-xs cursor-pointer group',
185
+ isActive
186
+ ? 'bg-theme-elevated text-theme-text-primary'
187
+ : 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated/50'
188
+ )}
189
+ onClick={onSelect}
190
+ >
191
+ <Icon className="w-3.5 h-3.5" />
192
+ <span className="truncate max-w-[120px]">{tab.title}</span>
193
+ <button
194
+ onClick={(e) => {
195
+ e.stopPropagation()
196
+ onClose()
197
+ }}
198
+ className="p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-theme-hover"
199
+ >
200
+ <X className="w-3 h-3" />
201
+ </button>
202
+ </div>
203
+ )
204
+ }
@@ -0,0 +1,212 @@
1
+ import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'
2
+
3
+ export type DockTabType = 'terminal' | 'logs' | 'workload-logs'
4
+
5
+ export interface DockTab {
6
+ id: string
7
+ type: DockTabType
8
+ title: string
9
+ // Common app context (optional — app-specific, used by tab content components)
10
+ orgId?: string
11
+ clusterId?: string
12
+ clusterName?: string
13
+ // Terminal / logs props
14
+ namespace?: string
15
+ podName?: string
16
+ containerName?: string
17
+ containers?: string[]
18
+ // Workload logs props
19
+ workloadKind?: string
20
+ workloadName?: string
21
+ }
22
+
23
+ export interface DockContextValue {
24
+ tabs: DockTab[]
25
+ activeTabId: string | null
26
+ isExpanded: boolean
27
+ addTab: (tab: Omit<DockTab, 'id'>) => string
28
+ removeTab: (id: string) => void
29
+ setActiveTab: (id: string) => void
30
+ toggleExpanded: () => void
31
+ setExpanded: (expanded: boolean) => void
32
+ closeAll: () => void
33
+ }
34
+
35
+ const DockContext = createContext<DockContextValue | null>(null)
36
+
37
+ let tabIdCounter = 0
38
+
39
+ export function DockProvider({ children }: { children: ReactNode }) {
40
+ const [tabs, setTabs] = useState<DockTab[]>([])
41
+ const [activeTabId, setActiveTabId] = useState<string | null>(null)
42
+ const [isExpanded, setIsExpanded] = useState(false)
43
+ // Keep a ref to the latest tabs for deduplication without stale-closure issues
44
+ const tabsRef = useRef<DockTab[]>(tabs)
45
+ useEffect(() => { tabsRef.current = tabs }, [tabs])
46
+
47
+ const addTab = useCallback((tabData: Omit<DockTab, 'id'>) => {
48
+ const existingTab = tabsRef.current.find(t => {
49
+ if (t.type !== tabData.type) return false
50
+ // Include orgId + clusterId so same-named pods/workloads on different clusters don't deduplicate
51
+ if (t.orgId !== tabData.orgId || t.clusterId !== tabData.clusterId) return false
52
+ if (t.type === 'workload-logs') {
53
+ return t.namespace === tabData.namespace &&
54
+ t.workloadKind === tabData.workloadKind &&
55
+ t.workloadName === tabData.workloadName
56
+ }
57
+ return t.namespace === tabData.namespace &&
58
+ t.podName === tabData.podName &&
59
+ t.containerName === tabData.containerName
60
+ })
61
+
62
+ if (existingTab) {
63
+ setActiveTabId(existingTab.id)
64
+ setIsExpanded(true)
65
+ return existingTab.id
66
+ }
67
+
68
+ const id = `dock-tab-${++tabIdCounter}`
69
+ const newTab: DockTab = { ...tabData, id }
70
+
71
+ setTabs(prev => [...prev, newTab])
72
+ setActiveTabId(id)
73
+ setIsExpanded(true)
74
+
75
+ return id
76
+ }, [])
77
+
78
+ const removeTab = useCallback((id: string) => {
79
+ setTabs(prev => {
80
+ const newTabs = prev.filter(t => t.id !== id)
81
+
82
+ if (activeTabId === id && newTabs.length > 0) {
83
+ setActiveTabId(newTabs[newTabs.length - 1].id)
84
+ } else if (newTabs.length === 0) {
85
+ setActiveTabId(null)
86
+ setIsExpanded(false)
87
+ }
88
+
89
+ return newTabs
90
+ })
91
+ }, [activeTabId])
92
+
93
+ const setActiveTab = useCallback((id: string) => {
94
+ setActiveTabId(id)
95
+ }, [])
96
+
97
+ const toggleExpanded = useCallback(() => {
98
+ setIsExpanded(prev => !prev)
99
+ }, [])
100
+
101
+ const closeAll = useCallback(() => {
102
+ setTabs([])
103
+ setActiveTabId(null)
104
+ setIsExpanded(false)
105
+ }, [])
106
+
107
+ return (
108
+ <DockContext.Provider value={{
109
+ tabs,
110
+ activeTabId,
111
+ isExpanded,
112
+ addTab,
113
+ removeTab,
114
+ setActiveTab,
115
+ toggleExpanded,
116
+ setExpanded: setIsExpanded,
117
+ closeAll,
118
+ }}>
119
+ {children}
120
+ </DockContext.Provider>
121
+ )
122
+ }
123
+
124
+ export function useDock() {
125
+ const context = useContext(DockContext)
126
+ if (!context) {
127
+ throw new Error('useDock must be used within a DockProvider')
128
+ }
129
+ return context
130
+ }
131
+
132
+ // Convenience hooks for adding specific tab types
133
+
134
+ export function useOpenTerminal() {
135
+ const { addTab } = useDock()
136
+
137
+ return (opts: {
138
+ namespace: string
139
+ podName: string
140
+ containerName: string
141
+ containers: string[]
142
+ orgId?: string
143
+ clusterId?: string
144
+ clusterName?: string
145
+ }) => {
146
+ addTab({
147
+ type: 'terminal',
148
+ title: `${opts.podName}/${opts.containerName}`,
149
+ namespace: opts.namespace,
150
+ podName: opts.podName,
151
+ containerName: opts.containerName,
152
+ containers: opts.containers,
153
+ orgId: opts.orgId,
154
+ clusterId: opts.clusterId,
155
+ clusterName: opts.clusterName,
156
+ })
157
+ }
158
+ }
159
+
160
+ export function useOpenLogs() {
161
+ const { addTab } = useDock()
162
+
163
+ return (opts: {
164
+ namespace: string
165
+ podName: string
166
+ containers: string[]
167
+ containerName?: string
168
+ orgId?: string
169
+ clusterId?: string
170
+ clusterName?: string
171
+ }) => {
172
+ const title = opts.containerName
173
+ ? `${opts.podName}/${opts.containerName}`
174
+ : opts.podName
175
+
176
+ addTab({
177
+ type: 'logs',
178
+ title,
179
+ namespace: opts.namespace,
180
+ podName: opts.podName,
181
+ containerName: opts.containerName,
182
+ containers: opts.containers,
183
+ orgId: opts.orgId,
184
+ clusterId: opts.clusterId,
185
+ clusterName: opts.clusterName,
186
+ })
187
+ }
188
+ }
189
+
190
+ export function useOpenWorkloadLogs() {
191
+ const { addTab } = useDock()
192
+
193
+ return (opts: {
194
+ namespace: string
195
+ workloadKind: string
196
+ workloadName: string
197
+ orgId?: string
198
+ clusterId?: string
199
+ clusterName?: string
200
+ }) => {
201
+ addTab({
202
+ type: 'workload-logs',
203
+ title: `${opts.workloadName} logs`,
204
+ namespace: opts.namespace,
205
+ workloadKind: opts.workloadKind,
206
+ workloadName: opts.workloadName,
207
+ orgId: opts.orgId,
208
+ clusterId: opts.clusterId,
209
+ clusterName: opts.clusterName,
210
+ })
211
+ }
212
+ }