@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,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, '<').replace(/>/g, '>')
|
|
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
|
+
}
|