@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,451 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
2
+ import { Copy, Check, ChevronUp, ChevronDown, X, Search } from 'lucide-react'
3
+ import { codeToHtml } from 'shiki'
4
+
5
+ interface CodeViewerProps {
6
+ code: string
7
+ language?: 'yaml' | 'json' | 'bash' | 'text'
8
+ /** Theme for syntax highlighting. Defaults to 'dark'. */
9
+ theme?: 'dark' | 'light'
10
+ showLineNumbers?: boolean
11
+ maxHeight?: string
12
+ showCopyButton?: boolean
13
+ onCopy?: (text: string) => void
14
+ copied?: boolean
15
+ }
16
+
17
+ export function CodeViewer({
18
+ code,
19
+ language = 'yaml',
20
+ theme = 'dark',
21
+ showLineNumbers = true,
22
+ maxHeight = 'calc(100vh - 300px)',
23
+ showCopyButton = false,
24
+ onCopy,
25
+ copied = false,
26
+ }: CodeViewerProps) {
27
+ const [html, setHtml] = useState<string>('')
28
+ const [highlighting, setHighlighting] = useState(true)
29
+
30
+ // Search state
31
+ const [searchOpen, setSearchOpen] = useState(false)
32
+ const [searchQuery, setSearchQuery] = useState('')
33
+ const [searchDisplay, setSearchDisplay] = useState({ matchCount: 0, currentIndex: 0 })
34
+
35
+ // Use refs for match tracking to avoid re-renders that destroy DOM highlights
36
+ const matchCountRef = useRef(0)
37
+ const currentIndexRef = useRef(0)
38
+
39
+ const wrapperRef = useRef<HTMLDivElement>(null)
40
+ const contentRef = useRef<HTMLDivElement>(null)
41
+ const searchInputRef = useRef<HTMLInputElement>(null)
42
+ const isMouseInsideRef = useRef(false)
43
+ const htmlRef = useRef<string>('')
44
+
45
+ useEffect(() => {
46
+ if (!code) {
47
+ setHtml('')
48
+ setHighlighting(false)
49
+ return
50
+ }
51
+
52
+ setHighlighting(true)
53
+
54
+ const shikiLang = language === 'text' ? 'text' : language
55
+
56
+ codeToHtml(code, {
57
+ lang: shikiLang,
58
+ theme: theme === 'light' ? 'github-light' : 'github-dark',
59
+ })
60
+ .then((highlighted) => {
61
+ setHtml(highlighted)
62
+ htmlRef.current = highlighted
63
+ setHighlighting(false)
64
+ })
65
+ .catch((error) => {
66
+ console.error('Shiki highlighting failed:', error)
67
+ const escaped = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
68
+ const fallback = `<pre><code>${escaped}</code></pre>`
69
+ setHtml(fallback)
70
+ htmlRef.current = fallback
71
+ setHighlighting(false)
72
+ })
73
+ }, [code, language, theme])
74
+
75
+ // Set innerHTML via ref so React doesn't manage it (preserves our <mark> elements across re-renders)
76
+ useEffect(() => {
77
+ if (contentRef.current && !highlighting && html) {
78
+ contentRef.current.innerHTML = html
79
+ }
80
+ }, [html, highlighting])
81
+
82
+ // Update the search display state (triggers re-render only for the search bar UI)
83
+ const updateSearchDisplay = useCallback((count: number, index: number) => {
84
+ setSearchDisplay({ matchCount: count, currentIndex: index })
85
+ }, [])
86
+
87
+ // Clear all <mark> highlights from the content, restore original HTML
88
+ const clearHighlights = useCallback(() => {
89
+ if (contentRef.current && htmlRef.current) {
90
+ contentRef.current.innerHTML = htmlRef.current
91
+ }
92
+ matchCountRef.current = 0
93
+ currentIndexRef.current = 0
94
+ updateSearchDisplay(0, 0)
95
+ }, [updateSearchDisplay])
96
+
97
+ // Walk text nodes and wrap matches in <mark> elements
98
+ const applyHighlights = useCallback((query: string, activeIndex: number) => {
99
+ const container = contentRef.current
100
+ if (!container || !query) {
101
+ clearHighlights()
102
+ return
103
+ }
104
+
105
+ // Restore original HTML first (clean slate)
106
+ if (htmlRef.current) {
107
+ container.innerHTML = htmlRef.current
108
+ }
109
+
110
+ const lowerQuery = query.toLowerCase()
111
+ let totalMatches = 0
112
+
113
+ // Collect all text nodes
114
+ const textNodes: Text[] = []
115
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)
116
+ let node: Text | null
117
+ while ((node = walker.nextNode() as Text | null)) {
118
+ if (node.textContent && node.textContent.length > 0) {
119
+ textNodes.push(node)
120
+ }
121
+ }
122
+
123
+ // Process each text node — find matches and wrap them
124
+ for (const textNode of textNodes) {
125
+ const text = textNode.textContent || ''
126
+ const lowerText = text.toLowerCase()
127
+
128
+ // Find all match positions in this text node
129
+ const positions: number[] = []
130
+ let searchFrom = 0
131
+ while (searchFrom < lowerText.length) {
132
+ const idx = lowerText.indexOf(lowerQuery, searchFrom)
133
+ if (idx === -1) break
134
+ positions.push(idx)
135
+ searchFrom = idx + 1
136
+ }
137
+
138
+ if (positions.length === 0) continue
139
+
140
+ const parent = textNode.parentNode
141
+ if (!parent) continue
142
+
143
+ const frag = document.createDocumentFragment()
144
+ let lastEnd = 0
145
+
146
+ for (const pos of positions) {
147
+ // Text before match
148
+ if (pos > lastEnd) {
149
+ frag.appendChild(document.createTextNode(text.slice(lastEnd, pos)))
150
+ }
151
+
152
+ // The match
153
+ const mark = document.createElement('mark')
154
+ mark.className = totalMatches === activeIndex
155
+ ? 'search-highlight active'
156
+ : 'search-highlight'
157
+ mark.textContent = text.slice(pos, pos + query.length)
158
+ mark.dataset.matchIndex = String(totalMatches)
159
+ frag.appendChild(mark)
160
+
161
+ totalMatches++
162
+ lastEnd = pos + query.length
163
+ }
164
+
165
+ // Remaining text
166
+ if (lastEnd < text.length) {
167
+ frag.appendChild(document.createTextNode(text.slice(lastEnd)))
168
+ }
169
+
170
+ parent.replaceChild(frag, textNode)
171
+ }
172
+
173
+ matchCountRef.current = totalMatches
174
+ const clampedIndex = totalMatches > 0 ? Math.min(activeIndex, totalMatches - 1) : 0
175
+ currentIndexRef.current = clampedIndex
176
+ updateSearchDisplay(totalMatches, clampedIndex)
177
+
178
+ // Scroll to active match
179
+ if (totalMatches > 0) {
180
+ requestAnimationFrame(() => {
181
+ const activeMark = container.querySelector(`mark.search-highlight[data-match-index="${clampedIndex}"]`)
182
+ if (activeMark) {
183
+ activeMark.scrollIntoView({ block: 'center', behavior: 'smooth' })
184
+ }
185
+ })
186
+ }
187
+ }, [clearHighlights, updateSearchDisplay])
188
+
189
+ // Update active match by swapping CSS classes (no DOM rebuild)
190
+ const setActiveMatch = useCallback((newIndex: number) => {
191
+ const container = contentRef.current
192
+ if (!container || matchCountRef.current === 0) return
193
+
194
+ // Remove active from current
195
+ const current = container.querySelector('mark.search-highlight.active')
196
+ if (current) {
197
+ current.className = 'search-highlight'
198
+ }
199
+
200
+ // Add active to new
201
+ const next = container.querySelector(`mark.search-highlight[data-match-index="${newIndex}"]`)
202
+ if (next) {
203
+ next.className = 'search-highlight active'
204
+ next.scrollIntoView({ block: 'center', behavior: 'smooth' })
205
+ }
206
+
207
+ currentIndexRef.current = newIndex
208
+ updateSearchDisplay(matchCountRef.current, newIndex)
209
+ }, [updateSearchDisplay])
210
+
211
+ // Apply highlights when query changes
212
+ useEffect(() => {
213
+ if (!searchOpen) return
214
+ if (!searchQuery) {
215
+ clearHighlights()
216
+ return
217
+ }
218
+
219
+ const timer = setTimeout(() => {
220
+ applyHighlights(searchQuery, 0)
221
+ }, 150)
222
+
223
+ return () => clearTimeout(timer)
224
+ // eslint-disable-next-line react-hooks/exhaustive-deps
225
+ }, [searchQuery, searchOpen])
226
+
227
+ // Re-apply highlights when Shiki finishes while search is open
228
+ useEffect(() => {
229
+ if (searchOpen && searchQuery && !highlighting) {
230
+ const timer = setTimeout(() => {
231
+ applyHighlights(searchQuery, currentIndexRef.current)
232
+ }, 50)
233
+ return () => clearTimeout(timer)
234
+ }
235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
236
+ }, [html, highlighting])
237
+
238
+ // Close search when code changes
239
+ useEffect(() => {
240
+ if (searchOpen) {
241
+ clearHighlights()
242
+ setSearchOpen(false)
243
+ setSearchQuery('')
244
+ }
245
+ // eslint-disable-next-line react-hooks/exhaustive-deps
246
+ }, [code])
247
+
248
+ // Navigation
249
+ const goToNextMatch = useCallback(() => {
250
+ if (matchCountRef.current === 0) return
251
+ const newIndex = (currentIndexRef.current + 1) % matchCountRef.current
252
+ setActiveMatch(newIndex)
253
+ }, [setActiveMatch])
254
+
255
+ const goToPrevMatch = useCallback(() => {
256
+ if (matchCountRef.current === 0) return
257
+ const newIndex = (currentIndexRef.current - 1 + matchCountRef.current) % matchCountRef.current
258
+ setActiveMatch(newIndex)
259
+ }, [setActiveMatch])
260
+
261
+ // Open search
262
+ const openSearch = useCallback(() => {
263
+ setSearchOpen(true)
264
+ requestAnimationFrame(() => {
265
+ searchInputRef.current?.focus()
266
+ searchInputRef.current?.select()
267
+ })
268
+ }, [])
269
+
270
+ // Close search
271
+ const closeSearch = useCallback(() => {
272
+ setSearchOpen(false)
273
+ setSearchQuery('')
274
+ clearHighlights()
275
+ }, [clearHighlights])
276
+
277
+ // Keyboard: Ctrl+F on wrapper
278
+ const handleWrapperKeyDown = useCallback((e: React.KeyboardEvent) => {
279
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
280
+ e.preventDefault()
281
+ e.stopPropagation()
282
+ openSearch()
283
+ }
284
+ }, [openSearch])
285
+
286
+ // Global Ctrl+F when mouse is inside
287
+ useEffect(() => {
288
+ const handleGlobalKeyDown = (e: KeyboardEvent) => {
289
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f' && isMouseInsideRef.current) {
290
+ e.preventDefault()
291
+ e.stopPropagation()
292
+ openSearch()
293
+ }
294
+ }
295
+ document.addEventListener('keydown', handleGlobalKeyDown, true)
296
+ return () => document.removeEventListener('keydown', handleGlobalKeyDown, true)
297
+ }, [openSearch])
298
+
299
+ // Search input keyboard
300
+ const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => {
301
+ if (e.key === 'Escape') {
302
+ e.preventDefault()
303
+ closeSearch()
304
+ } else if (e.key === 'Enter') {
305
+ e.preventDefault()
306
+ if (e.shiftKey) {
307
+ goToPrevMatch()
308
+ } else {
309
+ goToNextMatch()
310
+ }
311
+ }
312
+ }, [closeSearch, goToNextMatch, goToPrevMatch])
313
+
314
+ const handleCopy = () => {
315
+ if (onCopy) {
316
+ onCopy(code)
317
+ } else {
318
+ navigator.clipboard.writeText(code)
319
+ }
320
+ }
321
+
322
+ return (
323
+ <div
324
+ ref={wrapperRef}
325
+ className={`relative rounded-lg overflow-hidden ${theme === 'light' ? 'bg-[#ffffff]' : 'bg-[#0d1117]'}`}
326
+ tabIndex={0}
327
+ onKeyDown={handleWrapperKeyDown}
328
+ onMouseEnter={() => { isMouseInsideRef.current = true }}
329
+ onMouseLeave={() => { isMouseInsideRef.current = false }}
330
+ style={{ outline: 'none' }}
331
+ >
332
+ {showCopyButton && !searchOpen && (
333
+ <button
334
+ onClick={handleCopy}
335
+ className="absolute top-2 right-2 z-10 flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary bg-theme-surface/80 hover:bg-theme-elevated rounded backdrop-blur-sm"
336
+ >
337
+ {copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
338
+ Copy
339
+ </button>
340
+ )}
341
+
342
+ {/* Search bar */}
343
+ {searchOpen && (
344
+ <div className="absolute top-2 right-2 z-20 flex items-center gap-1 px-2 py-1.5 bg-theme-surface border border-theme-border rounded-lg shadow-lg backdrop-blur-sm">
345
+ <Search className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
346
+ <input
347
+ ref={searchInputRef}
348
+ type="text"
349
+ value={searchQuery}
350
+ onChange={(e) => setSearchQuery(e.target.value)}
351
+ onKeyDown={handleSearchKeyDown}
352
+ placeholder="Search..."
353
+ className="w-40 bg-transparent text-xs text-theme-text-primary placeholder:text-theme-text-disabled outline-none"
354
+ autoFocus
355
+ />
356
+ {searchQuery && (
357
+ <span className="text-xs text-theme-text-tertiary whitespace-nowrap shrink-0">
358
+ {searchDisplay.matchCount > 0
359
+ ? `${searchDisplay.currentIndex + 1} of ${searchDisplay.matchCount}`
360
+ : 'No results'}
361
+ </span>
362
+ )}
363
+ <button
364
+ onClick={goToPrevMatch}
365
+ disabled={searchDisplay.matchCount === 0}
366
+ className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary disabled:opacity-30 rounded"
367
+ title="Previous (Shift+Enter)"
368
+ >
369
+ <ChevronUp className="w-3.5 h-3.5" />
370
+ </button>
371
+ <button
372
+ onClick={goToNextMatch}
373
+ disabled={searchDisplay.matchCount === 0}
374
+ className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary disabled:opacity-30 rounded"
375
+ title="Next (Enter)"
376
+ >
377
+ <ChevronDown className="w-3.5 h-3.5" />
378
+ </button>
379
+ <button
380
+ onClick={closeSearch}
381
+ className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary rounded"
382
+ title="Close (Escape)"
383
+ >
384
+ <X className="w-3.5 h-3.5" />
385
+ </button>
386
+ </div>
387
+ )}
388
+
389
+ <div
390
+ className="overflow-auto"
391
+ style={{ maxHeight }}
392
+ >
393
+ {highlighting ? (
394
+ <div className="p-4 text-theme-text-tertiary text-sm font-mono">Loading...</div>
395
+ ) : (
396
+ <div
397
+ ref={contentRef}
398
+ className={`shiki-viewer text-xs ${showLineNumbers ? 'with-line-numbers' : ''}`}
399
+ />
400
+ )}
401
+ </div>
402
+
403
+ <style>{`
404
+ :root[data-theme="light"] { --code-line-number: #8b949e; }
405
+ :root[data-theme="dark"] { --code-line-number: #484f58; }
406
+ .shiki-viewer pre {
407
+ margin: 0;
408
+ padding: 12px;
409
+ background: transparent !important;
410
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
411
+ font-size: 12px;
412
+ line-height: 1.5;
413
+ }
414
+ .shiki-viewer code {
415
+ font-family: inherit;
416
+ }
417
+ .shiki-viewer.with-line-numbers code {
418
+ counter-reset: line;
419
+ }
420
+ .shiki-viewer .line {
421
+ display: inline-block;
422
+ width: 100%;
423
+ }
424
+ .shiki-viewer.with-line-numbers .line::before {
425
+ counter-increment: line;
426
+ content: counter(line);
427
+ display: inline-block;
428
+ width: 3ch;
429
+ margin-right: 1.5ch;
430
+ text-align: right;
431
+ color: var(--code-line-number, #484f58);
432
+ user-select: none;
433
+ }
434
+ .shiki-viewer .line:hover {
435
+ background: rgba(99, 110, 123, 0.1);
436
+ }
437
+ mark.search-highlight {
438
+ background-color: rgba(250, 204, 21, 0.35);
439
+ color: inherit;
440
+ border-radius: 2px;
441
+ padding: 0;
442
+ }
443
+ mark.search-highlight.active {
444
+ background-color: rgba(250, 204, 21, 0.8);
445
+ outline: 2px solid rgba(250, 204, 21, 0.9);
446
+ border-radius: 2px;
447
+ }
448
+ `}</style>
449
+ </div>
450
+ )
451
+ }
@@ -0,0 +1,188 @@
1
+ import { useEffect, useRef, ReactNode } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { AlertTriangle, X } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+ import { SEVERITY_TEXT, SEVERITY_BADGE_BORDERED } from '../../utils/badge-colors'
6
+ import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
7
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
8
+
9
+
10
+ interface ConfirmDialogProps {
11
+ open: boolean
12
+ onClose: () => void
13
+ onConfirm: () => void
14
+ title: string
15
+ message: string
16
+ details?: string
17
+ confirmLabel?: string
18
+ cancelLabel?: string
19
+ variant?: 'danger' | 'warning'
20
+ isLoading?: boolean
21
+ children?: ReactNode // Optional custom content (e.g., checkboxes)
22
+ }
23
+
24
+ export function ConfirmDialog({
25
+ open,
26
+ onClose,
27
+ onConfirm,
28
+ title,
29
+ message,
30
+ details,
31
+ confirmLabel = 'Confirm',
32
+ cancelLabel = 'Cancel',
33
+ variant = 'danger',
34
+ isLoading = false,
35
+ children,
36
+ }: ConfirmDialogProps) {
37
+ const dialogRef = useRef<HTMLDivElement>(null)
38
+ const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
39
+
40
+ // Handle ESC key
41
+ useEffect(() => {
42
+ if (!open) return
43
+
44
+ const handleKeyDown = (e: KeyboardEvent) => {
45
+ if (e.key === 'Escape' && !isLoading) {
46
+ e.stopPropagation()
47
+ onClose()
48
+ }
49
+ }
50
+ document.addEventListener('keydown', handleKeyDown, true)
51
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
52
+ }, [open, onClose, isLoading])
53
+
54
+ // Focus trap
55
+ useEffect(() => {
56
+ if (open && dialogRef.current) {
57
+ dialogRef.current.focus()
58
+ }
59
+ }, [open])
60
+
61
+ if (!shouldRender) return null
62
+
63
+ const isDanger = variant === 'danger'
64
+ const severity = isDanger ? 'error' : 'warning'
65
+
66
+ return createPortal(
67
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
68
+ {/* Backdrop */}
69
+ <div
70
+ className={clsx(
71
+ 'absolute inset-0 bg-black/60 backdrop-blur-sm',
72
+ TRANSITION_BACKDROP,
73
+ isOpen ? 'opacity-100' : 'opacity-0'
74
+ )}
75
+ onClick={isLoading ? undefined : onClose}
76
+ />
77
+
78
+ {/* Dialog */}
79
+ <div
80
+ ref={dialogRef}
81
+ tabIndex={-1}
82
+ className={clsx(
83
+ 'relative bg-theme-surface border border-theme-border rounded-lg shadow-2xl max-w-md w-full mx-4 outline-none',
84
+ TRANSITION_PANEL,
85
+ isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
86
+ )}
87
+ >
88
+ {/* Header */}
89
+ <div className="flex items-start gap-3 p-4 border-b border-theme-border">
90
+ <div
91
+ className={clsx(
92
+ 'flex items-center justify-center w-10 h-10 rounded-full shrink-0',
93
+ isDanger ? 'bg-red-500/20' : 'bg-amber-500/20'
94
+ )}
95
+ >
96
+ <AlertTriangle className={clsx('w-5 h-5', SEVERITY_TEXT[severity])} />
97
+ </div>
98
+ <div className="flex-1 min-w-0">
99
+ <h3 className="text-lg font-semibold text-theme-text-primary">{title}</h3>
100
+ <p className="text-sm text-theme-text-secondary mt-1">{message}</p>
101
+ </div>
102
+ <button
103
+ onClick={onClose}
104
+ disabled={isLoading}
105
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
106
+ >
107
+ <X className="w-5 h-5" />
108
+ </button>
109
+ </div>
110
+
111
+ {/* Details */}
112
+ {details && (
113
+ <div className="p-4 border-b border-theme-border">
114
+ <pre className="text-xs text-theme-text-secondary bg-theme-base/50 rounded p-3 overflow-auto max-h-32 whitespace-pre-wrap font-mono">
115
+ {details}
116
+ </pre>
117
+ </div>
118
+ )}
119
+
120
+ {/* Custom content */}
121
+ {children && (
122
+ <div className="px-4 pt-4">
123
+ {children}
124
+ </div>
125
+ )}
126
+
127
+ {/* Warning message */}
128
+ <div className="p-4">
129
+ <div
130
+ className={clsx(
131
+ 'flex items-start gap-2 p-3 rounded text-sm',
132
+ SEVERITY_BADGE_BORDERED[severity]
133
+ )}
134
+ >
135
+ <AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
136
+ <span>
137
+ {isDanger
138
+ ? 'This action cannot be undone. Please make sure you want to proceed.'
139
+ : 'Please confirm you want to proceed with this action.'}
140
+ </span>
141
+ </div>
142
+ </div>
143
+
144
+ {/* Actions */}
145
+ <div className="flex items-center justify-end gap-3 p-4 border-t border-theme-border">
146
+ <button
147
+ onClick={onClose}
148
+ disabled={isLoading}
149
+ className="px-4 py-2 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors disabled:opacity-50"
150
+ >
151
+ {cancelLabel}
152
+ </button>
153
+ <button
154
+ onClick={onConfirm}
155
+ disabled={isLoading}
156
+ className={clsx(
157
+ 'px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2',
158
+ isDanger
159
+ ? 'bg-red-600 hover:bg-red-700 text-theme-text-primary'
160
+ : 'bg-amber-600 hover:bg-amber-700 text-theme-text-primary'
161
+ )}
162
+ >
163
+ {isLoading && (
164
+ <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
165
+ <circle
166
+ className="opacity-25"
167
+ cx="12"
168
+ cy="12"
169
+ r="10"
170
+ stroke="currentColor"
171
+ strokeWidth="4"
172
+ fill="none"
173
+ />
174
+ <path
175
+ className="opacity-75"
176
+ fill="currentColor"
177
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
178
+ />
179
+ </svg>
180
+ )}
181
+ {confirmLabel}
182
+ </button>
183
+ </div>
184
+ </div>
185
+ </div>,
186
+ document.body
187
+ )
188
+ }
@@ -0,0 +1,57 @@
1
+ import { useState } from 'react'
2
+ import { ConfirmDialog } from './ConfirmDialog'
3
+
4
+ interface ForceDeleteConfirmDialogProps {
5
+ open: boolean
6
+ onClose: () => void
7
+ onConfirm: (force: boolean) => void
8
+ resourceName: string
9
+ resourceKind: string
10
+ namespaceName: string
11
+ isLoading: boolean
12
+ }
13
+
14
+ export function ForceDeleteConfirmDialog({
15
+ open,
16
+ onClose,
17
+ onConfirm,
18
+ resourceName,
19
+ resourceKind,
20
+ namespaceName,
21
+ isLoading,
22
+ }: ForceDeleteConfirmDialogProps) {
23
+ const [forceDelete, setForceDelete] = useState(false)
24
+
25
+ function handleClose() {
26
+ onClose()
27
+ setForceDelete(false)
28
+ }
29
+
30
+ function handleConfirm() {
31
+ onConfirm(forceDelete)
32
+ }
33
+
34
+ return (
35
+ <ConfirmDialog
36
+ open={open}
37
+ onClose={handleClose}
38
+ onConfirm={handleConfirm}
39
+ title="Delete Resource"
40
+ message={`Are you sure you want to delete "${resourceName}"?`}
41
+ details={`This will permanently delete the ${resourceKind} "${resourceName}" from the "${namespaceName}" namespace.`}
42
+ confirmLabel={forceDelete ? 'Force Delete' : 'Delete'}
43
+ variant="danger"
44
+ isLoading={isLoading}
45
+ >
46
+ <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
47
+ <input
48
+ type="checkbox"
49
+ checked={forceDelete}
50
+ onChange={(e) => setForceDelete(e.target.checked)}
51
+ className="w-4 h-4 rounded border-theme-border bg-theme-base text-red-600 focus:ring-red-500 focus:ring-offset-0"
52
+ />
53
+ <span>Force delete (strips finalizers and bypasses grace period)</span>
54
+ </label>
55
+ </ConfirmDialog>
56
+ )
57
+ }