@skyhook-io/radar-app 0.2.2 → 1.0.1
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 +5 -5
- package/src/App.tsx +143 -36
- package/src/api/client.ts +121 -4
- package/src/components/ContextSwitcher.tsx +49 -16
- package/src/components/NamespaceSwitcher.tsx +298 -0
- package/src/components/audit/AuditSettingsDialog.tsx +49 -10
- package/src/components/helm/ChartBrowser.tsx +11 -11
- package/src/components/helm/HelmReleaseDrawer.tsx +35 -19
- package/src/components/helm/HelmView.tsx +33 -7
- package/src/components/helm/InstallWizard.tsx +79 -22
- package/src/components/home/HomeView.tsx +13 -1
- package/src/components/portforward/PortForwardButton.tsx +37 -16
- package/src/components/portforward/PortForwardManager.tsx +152 -111
- package/src/components/resources/ResourcesView.tsx +2 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +17 -18
- package/src/components/ui/DiagnosticsOverlay.tsx +93 -2
- package/src/components/ui/UpdateNotification.tsx +7 -7
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -111,9 +111,9 @@ export function UpdateNotification() {
|
|
|
111
111
|
const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
|
|
112
112
|
|
|
113
113
|
return (
|
|
114
|
-
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-
|
|
114
|
+
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
|
|
115
115
|
<div className="flex items-start gap-3">
|
|
116
|
-
<div className="flex items-center justify-center w-8 h-8 bg-
|
|
116
|
+
<div className="flex items-center justify-center w-8 h-8 bg-accent-muted rounded-full shrink-0">
|
|
117
117
|
<UpdateIcon state={effectiveState} />
|
|
118
118
|
</div>
|
|
119
119
|
<div className="flex-1 min-w-0">
|
|
@@ -154,7 +154,7 @@ export function UpdateNotification() {
|
|
|
154
154
|
href={versionInfo.releaseUrl}
|
|
155
155
|
target="_blank"
|
|
156
156
|
rel="noopener noreferrer"
|
|
157
|
-
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-
|
|
157
|
+
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-accent-text hover:underline"
|
|
158
158
|
>
|
|
159
159
|
Download from GitHub →
|
|
160
160
|
</a>
|
|
@@ -186,11 +186,11 @@ function UpdateIcon({ state }: { state: DesktopUpdateState }) {
|
|
|
186
186
|
switch (state) {
|
|
187
187
|
case 'downloading':
|
|
188
188
|
case 'applying':
|
|
189
|
-
return <Loader2 className="w-4 h-4 text-
|
|
189
|
+
return <Loader2 className="w-4 h-4 text-accent animate-spin" />
|
|
190
190
|
case 'ready':
|
|
191
191
|
return <ArrowDownToLine className="w-4 h-4 text-green-400" />
|
|
192
192
|
default:
|
|
193
|
-
return <Download className="w-4 h-4 text-
|
|
193
|
+
return <Download className="w-4 h-4 text-accent" />
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -247,7 +247,7 @@ function DesktopUpdateControls({
|
|
|
247
247
|
<div className="mt-2 space-y-1">
|
|
248
248
|
<div className="w-full bg-theme-elevated rounded-full h-1.5 overflow-hidden">
|
|
249
249
|
<div
|
|
250
|
-
className="bg-
|
|
250
|
+
className="bg-accent h-full rounded-full transition-all duration-300"
|
|
251
251
|
style={{ width: `${Math.round((progress ?? 0) * 100)}%` }}
|
|
252
252
|
/>
|
|
253
253
|
</div>
|
|
@@ -272,7 +272,7 @@ function DesktopUpdateControls({
|
|
|
272
272
|
case 'applying':
|
|
273
273
|
return (
|
|
274
274
|
<div className="mt-2 flex items-center gap-2">
|
|
275
|
-
<Loader2 className="w-3.5 h-3.5 text-
|
|
275
|
+
<Loader2 className="w-3.5 h-3.5 text-accent animate-spin" />
|
|
276
276
|
<p className="text-xs text-theme-text-secondary">Applying update...</p>
|
|
277
277
|
</div>
|
|
278
278
|
)
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type RendererOverrides,
|
|
9
9
|
} from '@skyhook-io/k8s-ui'
|
|
10
10
|
import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
|
|
11
|
-
import type
|
|
11
|
+
import { kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
12
12
|
import {
|
|
13
13
|
useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
|
|
14
14
|
useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
|
|
@@ -383,7 +383,7 @@ export function WorkloadView({
|
|
|
383
383
|
initialYaml={duplicateYaml}
|
|
384
384
|
title="Duplicate Resource"
|
|
385
385
|
onCreated={(result) => {
|
|
386
|
-
rest.onNavigateToResource?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
386
|
+
rest.onNavigateToResource?.({ kind: kindToPlural(result.kind), namespace: result.namespace, name: result.name, group: '' })
|
|
387
387
|
}}
|
|
388
388
|
/>
|
|
389
389
|
</>
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import { clsx } from 'clsx'
|
|
4
|
-
import { ChevronDown, Search, X, Check, Shield } from 'lucide-react'
|
|
5
|
-
import { Tooltip } from './Tooltip'
|
|
6
|
-
import { isForbiddenError } from '../../api/client'
|
|
7
|
-
|
|
8
|
-
interface Namespace {
|
|
9
|
-
name: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface NamespaceSelectorProps {
|
|
13
|
-
value: string[]
|
|
14
|
-
onChange: (value: string[]) => void
|
|
15
|
-
namespaces: Namespace[] | undefined
|
|
16
|
-
namespacesError?: Error | null
|
|
17
|
-
className?: string
|
|
18
|
-
disabled?: boolean
|
|
19
|
-
disabledTooltip?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function NamespaceSelector({
|
|
23
|
-
value,
|
|
24
|
-
onChange,
|
|
25
|
-
namespaces,
|
|
26
|
-
namespacesError,
|
|
27
|
-
className,
|
|
28
|
-
disabled,
|
|
29
|
-
disabledTooltip,
|
|
30
|
-
}: NamespaceSelectorProps) {
|
|
31
|
-
const [isOpen, setIsOpen] = useState(false)
|
|
32
|
-
const [search, setSearch] = useState('')
|
|
33
|
-
const [manualInput, setManualInput] = useState('')
|
|
34
|
-
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
|
35
|
-
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 })
|
|
36
|
-
|
|
37
|
-
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
38
|
-
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
39
|
-
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
40
|
-
|
|
41
|
-
const isForbidden = isForbiddenError(namespacesError)
|
|
42
|
-
|
|
43
|
-
// Convert value to Set for efficient lookups
|
|
44
|
-
const selectedSet = useMemo(() => new Set(value), [value])
|
|
45
|
-
|
|
46
|
-
// Sort and filter namespaces
|
|
47
|
-
const sortedNamespaces = useMemo(() => {
|
|
48
|
-
if (!namespaces) return []
|
|
49
|
-
return [...namespaces].sort((a, b) => a.name.localeCompare(b.name))
|
|
50
|
-
}, [namespaces])
|
|
51
|
-
|
|
52
|
-
const filteredNamespaces = useMemo(() => {
|
|
53
|
-
if (!search.trim()) return sortedNamespaces
|
|
54
|
-
const searchLower = search.toLowerCase()
|
|
55
|
-
return sortedNamespaces.filter((ns) =>
|
|
56
|
-
ns.name.toLowerCase().includes(searchLower)
|
|
57
|
-
)
|
|
58
|
-
}, [sortedNamespaces, search])
|
|
59
|
-
|
|
60
|
-
// Update position when dropdown opens
|
|
61
|
-
const updatePosition = useCallback(() => {
|
|
62
|
-
if (!triggerRef.current) return
|
|
63
|
-
const rect = triggerRef.current.getBoundingClientRect()
|
|
64
|
-
const dropdownWidth = Math.max(rect.width, 220) // Minimum width for checkboxes
|
|
65
|
-
// Align dropdown to the right edge of the button
|
|
66
|
-
const left = rect.right - dropdownWidth
|
|
67
|
-
setDropdownPosition({
|
|
68
|
-
top: rect.bottom + 4,
|
|
69
|
-
left: Math.max(8, left), // Ensure at least 8px from screen edge
|
|
70
|
-
width: dropdownWidth,
|
|
71
|
-
})
|
|
72
|
-
}, [])
|
|
73
|
-
|
|
74
|
-
// Open dropdown
|
|
75
|
-
const openDropdown = useCallback(() => {
|
|
76
|
-
setIsOpen(true)
|
|
77
|
-
setSearch('')
|
|
78
|
-
setHighlightedIndex(0)
|
|
79
|
-
updatePosition()
|
|
80
|
-
}, [updatePosition])
|
|
81
|
-
|
|
82
|
-
// Close dropdown
|
|
83
|
-
const closeDropdown = useCallback(() => {
|
|
84
|
-
setIsOpen(false)
|
|
85
|
-
setSearch('')
|
|
86
|
-
}, [])
|
|
87
|
-
|
|
88
|
-
// Toggle a namespace selection
|
|
89
|
-
const toggleNamespace = useCallback((ns: string) => {
|
|
90
|
-
if (selectedSet.has(ns)) {
|
|
91
|
-
onChange(value.filter((v) => v !== ns))
|
|
92
|
-
} else {
|
|
93
|
-
onChange([...value, ns])
|
|
94
|
-
}
|
|
95
|
-
}, [selectedSet, value, onChange])
|
|
96
|
-
|
|
97
|
-
// Select all visible namespaces
|
|
98
|
-
const selectAll = useCallback(() => {
|
|
99
|
-
const allNames = sortedNamespaces.map((ns) => ns.name)
|
|
100
|
-
onChange(allNames)
|
|
101
|
-
}, [sortedNamespaces, onChange])
|
|
102
|
-
|
|
103
|
-
// Clear all selections (shows all namespaces)
|
|
104
|
-
const clearAll = useCallback(() => {
|
|
105
|
-
onChange([])
|
|
106
|
-
}, [onChange])
|
|
107
|
-
|
|
108
|
-
// Add a manually typed namespace
|
|
109
|
-
const addManualNamespace = useCallback(() => {
|
|
110
|
-
const ns = manualInput.trim()
|
|
111
|
-
if (ns && !selectedSet.has(ns)) {
|
|
112
|
-
onChange([...value, ns])
|
|
113
|
-
}
|
|
114
|
-
setManualInput('')
|
|
115
|
-
}, [manualInput, selectedSet, value, onChange])
|
|
116
|
-
|
|
117
|
-
// Focus search input when dropdown opens
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (isOpen) {
|
|
120
|
-
// Small delay to ensure the dropdown is rendered
|
|
121
|
-
requestAnimationFrame(() => {
|
|
122
|
-
searchInputRef.current?.focus()
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
}, [isOpen])
|
|
126
|
-
|
|
127
|
-
// Reset highlighted index when filtered options change
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
setHighlightedIndex(0)
|
|
130
|
-
}, [filteredNamespaces])
|
|
131
|
-
|
|
132
|
-
// Handle click outside
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
if (!isOpen) return
|
|
135
|
-
|
|
136
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
137
|
-
const target = e.target as Node
|
|
138
|
-
if (
|
|
139
|
-
triggerRef.current?.contains(target) ||
|
|
140
|
-
dropdownRef.current?.contains(target)
|
|
141
|
-
) {
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
closeDropdown()
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Small delay to prevent immediate close on open click
|
|
148
|
-
const timeoutId = setTimeout(() => {
|
|
149
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
150
|
-
}, 0)
|
|
151
|
-
|
|
152
|
-
return () => {
|
|
153
|
-
clearTimeout(timeoutId)
|
|
154
|
-
document.removeEventListener('mousedown', handleClickOutside)
|
|
155
|
-
}
|
|
156
|
-
}, [isOpen, closeDropdown])
|
|
157
|
-
|
|
158
|
-
// Handle keyboard navigation
|
|
159
|
-
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
160
|
-
switch (e.key) {
|
|
161
|
-
case 'ArrowDown':
|
|
162
|
-
e.preventDefault()
|
|
163
|
-
setHighlightedIndex((prev) =>
|
|
164
|
-
prev < filteredNamespaces.length - 1 ? prev + 1 : prev
|
|
165
|
-
)
|
|
166
|
-
break
|
|
167
|
-
case 'ArrowUp':
|
|
168
|
-
e.preventDefault()
|
|
169
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0))
|
|
170
|
-
break
|
|
171
|
-
case 'Enter':
|
|
172
|
-
case ' ':
|
|
173
|
-
e.preventDefault()
|
|
174
|
-
if (filteredNamespaces[highlightedIndex]) {
|
|
175
|
-
toggleNamespace(filteredNamespaces[highlightedIndex].name)
|
|
176
|
-
}
|
|
177
|
-
break
|
|
178
|
-
case 'Escape':
|
|
179
|
-
e.preventDefault()
|
|
180
|
-
closeDropdown()
|
|
181
|
-
break
|
|
182
|
-
case 'Tab':
|
|
183
|
-
closeDropdown()
|
|
184
|
-
break
|
|
185
|
-
}
|
|
186
|
-
}, [filteredNamespaces, highlightedIndex, toggleNamespace, closeDropdown])
|
|
187
|
-
|
|
188
|
-
// Scroll highlighted item into view
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
if (!isOpen || !dropdownRef.current) return
|
|
191
|
-
const highlighted = dropdownRef.current.querySelector('[data-highlighted="true"]')
|
|
192
|
-
if (highlighted) {
|
|
193
|
-
highlighted.scrollIntoView({ block: 'nearest' })
|
|
194
|
-
}
|
|
195
|
-
}, [highlightedIndex, isOpen])
|
|
196
|
-
|
|
197
|
-
// Get display value
|
|
198
|
-
const displayValue = useMemo(() => {
|
|
199
|
-
if (value.length === 0) return 'All Namespaces'
|
|
200
|
-
if (value.length === 1) return value[0]
|
|
201
|
-
return `${value.length} namespaces`
|
|
202
|
-
}, [value])
|
|
203
|
-
|
|
204
|
-
const allSelected = sortedNamespaces.length > 0 && value.length === sortedNamespaces.length
|
|
205
|
-
|
|
206
|
-
return (
|
|
207
|
-
<>
|
|
208
|
-
<Tooltip content={disabledTooltip} disabled={!disabled} position="bottom">
|
|
209
|
-
<button
|
|
210
|
-
ref={triggerRef}
|
|
211
|
-
type="button"
|
|
212
|
-
disabled={disabled}
|
|
213
|
-
onClick={() => (isOpen ? closeDropdown() : openDropdown())}
|
|
214
|
-
className={clsx(
|
|
215
|
-
'appearance-none bg-theme-elevated text-theme-text-primary text-xs rounded px-2 py-1 pr-6 border border-theme-border-light',
|
|
216
|
-
'focus:outline-none focus:ring-1 focus:ring-blue-500 min-w-[100px] text-left relative',
|
|
217
|
-
'transition-colors',
|
|
218
|
-
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-theme-hover',
|
|
219
|
-
className
|
|
220
|
-
)}
|
|
221
|
-
>
|
|
222
|
-
<span className="block truncate">{displayValue}</span>
|
|
223
|
-
<ChevronDown
|
|
224
|
-
className={clsx(
|
|
225
|
-
'absolute right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-secondary transition-transform',
|
|
226
|
-
isOpen && 'rotate-180'
|
|
227
|
-
)}
|
|
228
|
-
/>
|
|
229
|
-
</button>
|
|
230
|
-
</Tooltip>
|
|
231
|
-
|
|
232
|
-
{isOpen &&
|
|
233
|
-
createPortal(
|
|
234
|
-
<>
|
|
235
|
-
{/* Backdrop to capture clicks outside the dropdown */}
|
|
236
|
-
<div
|
|
237
|
-
className="fixed inset-0 z-[9998]"
|
|
238
|
-
onClick={closeDropdown}
|
|
239
|
-
/>
|
|
240
|
-
<div
|
|
241
|
-
ref={dropdownRef}
|
|
242
|
-
className="fixed z-[9999] bg-theme-elevated border border-theme-border rounded-md shadow-lg overflow-hidden"
|
|
243
|
-
style={{
|
|
244
|
-
top: dropdownPosition.top,
|
|
245
|
-
left: dropdownPosition.left,
|
|
246
|
-
width: dropdownPosition.width,
|
|
247
|
-
}}
|
|
248
|
-
onKeyDown={handleKeyDown}
|
|
249
|
-
>
|
|
250
|
-
{isForbidden ? (
|
|
251
|
-
<>
|
|
252
|
-
{/* Manual namespace input when listing is forbidden */}
|
|
253
|
-
<div className="p-2 border-b border-theme-border">
|
|
254
|
-
<div className="flex items-center gap-1.5 text-[10px] text-amber-400 mb-2">
|
|
255
|
-
<Shield className="w-3 h-3" />
|
|
256
|
-
<span>Cannot list namespaces — type a name</span>
|
|
257
|
-
</div>
|
|
258
|
-
<div className="flex gap-1">
|
|
259
|
-
<input
|
|
260
|
-
ref={searchInputRef}
|
|
261
|
-
type="text"
|
|
262
|
-
value={manualInput}
|
|
263
|
-
onChange={(e) => setManualInput(e.target.value)}
|
|
264
|
-
onKeyDown={(e) => {
|
|
265
|
-
if (e.key === 'Enter') {
|
|
266
|
-
e.preventDefault()
|
|
267
|
-
addManualNamespace()
|
|
268
|
-
} else if (e.key === 'Escape') {
|
|
269
|
-
closeDropdown()
|
|
270
|
-
}
|
|
271
|
-
}}
|
|
272
|
-
placeholder="Namespace name..."
|
|
273
|
-
className="flex-1 bg-theme-base text-theme-text-primary text-xs rounded px-2 py-1.5 border border-theme-border-light focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-theme-text-tertiary"
|
|
274
|
-
/>
|
|
275
|
-
<button
|
|
276
|
-
type="button"
|
|
277
|
-
onClick={addManualNamespace}
|
|
278
|
-
disabled={!manualInput.trim()}
|
|
279
|
-
className="px-2 py-1 text-xs btn-brand rounded"
|
|
280
|
-
>
|
|
281
|
-
Add
|
|
282
|
-
</button>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
|
|
286
|
-
{/* Show currently selected namespaces with remove button */}
|
|
287
|
-
<div className="max-h-[200px] overflow-y-auto">
|
|
288
|
-
{value.length === 0 ? (
|
|
289
|
-
<div className="px-3 py-4 text-center text-xs text-theme-text-tertiary">
|
|
290
|
-
All namespaces (type a name to filter)
|
|
291
|
-
</div>
|
|
292
|
-
) : (
|
|
293
|
-
value.map((ns) => (
|
|
294
|
-
<div
|
|
295
|
-
key={ns}
|
|
296
|
-
className="w-full text-left px-3 py-1.5 text-xs flex items-center justify-between gap-2 text-theme-text-primary hover:bg-theme-hover"
|
|
297
|
-
>
|
|
298
|
-
<span className="truncate">{ns}</span>
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onClick={() => onChange(value.filter(v => v !== ns))}
|
|
302
|
-
className="text-theme-text-tertiary hover:text-red-400 flex-shrink-0"
|
|
303
|
-
>
|
|
304
|
-
<X className="w-3 h-3" />
|
|
305
|
-
</button>
|
|
306
|
-
</div>
|
|
307
|
-
))
|
|
308
|
-
)}
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
{value.length > 0 && (
|
|
312
|
-
<div className="px-3 py-1.5 text-[10px] text-theme-text-tertiary border-t border-theme-border bg-theme-base flex justify-between">
|
|
313
|
-
<span>{value.length} selected</span>
|
|
314
|
-
<button type="button" onClick={clearAll} className="text-blue-400 hover:text-blue-300">
|
|
315
|
-
Clear all
|
|
316
|
-
</button>
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
319
|
-
</>
|
|
320
|
-
) : (
|
|
321
|
-
<>
|
|
322
|
-
{/* Search input */}
|
|
323
|
-
<div className="p-2 border-b border-theme-border">
|
|
324
|
-
<div className="relative">
|
|
325
|
-
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
326
|
-
<input
|
|
327
|
-
ref={searchInputRef}
|
|
328
|
-
type="text"
|
|
329
|
-
value={search}
|
|
330
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
331
|
-
placeholder="Search namespaces..."
|
|
332
|
-
className="w-full bg-theme-base text-theme-text-primary text-xs rounded px-2 py-1.5 pl-7 pr-7 border border-theme-border-light focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-theme-text-tertiary"
|
|
333
|
-
/>
|
|
334
|
-
{search && (
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
onClick={() => setSearch('')}
|
|
338
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
339
|
-
>
|
|
340
|
-
<X className="w-3.5 h-3.5" />
|
|
341
|
-
</button>
|
|
342
|
-
)}
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
|
|
346
|
-
{/* Select All / Clear All buttons */}
|
|
347
|
-
<div className="flex gap-1 px-2 py-1.5 border-b border-theme-border bg-theme-base">
|
|
348
|
-
<button
|
|
349
|
-
type="button"
|
|
350
|
-
onClick={selectAll}
|
|
351
|
-
disabled={allSelected}
|
|
352
|
-
className={clsx(
|
|
353
|
-
'flex-1 text-[10px] px-2 py-1 rounded transition-colors',
|
|
354
|
-
allSelected
|
|
355
|
-
? 'text-theme-text-tertiary cursor-not-allowed'
|
|
356
|
-
: 'text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary'
|
|
357
|
-
)}
|
|
358
|
-
>
|
|
359
|
-
Select All
|
|
360
|
-
</button>
|
|
361
|
-
<button
|
|
362
|
-
type="button"
|
|
363
|
-
onClick={clearAll}
|
|
364
|
-
disabled={value.length === 0}
|
|
365
|
-
className={clsx(
|
|
366
|
-
'flex-1 text-[10px] px-2 py-1 rounded transition-colors',
|
|
367
|
-
value.length === 0
|
|
368
|
-
? 'text-theme-text-tertiary cursor-not-allowed'
|
|
369
|
-
: 'text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary'
|
|
370
|
-
)}
|
|
371
|
-
>
|
|
372
|
-
Clear All
|
|
373
|
-
</button>
|
|
374
|
-
</div>
|
|
375
|
-
|
|
376
|
-
{/* Options list with checkboxes */}
|
|
377
|
-
<div className="max-h-[240px] overflow-y-auto">
|
|
378
|
-
{filteredNamespaces.length === 0 ? (
|
|
379
|
-
<div className="px-3 py-6 text-center text-xs text-theme-text-tertiary">
|
|
380
|
-
No namespaces match "{search}"
|
|
381
|
-
</div>
|
|
382
|
-
) : (
|
|
383
|
-
filteredNamespaces.map((ns, index) => {
|
|
384
|
-
const isSelected = selectedSet.has(ns.name)
|
|
385
|
-
return (
|
|
386
|
-
<button
|
|
387
|
-
key={ns.name}
|
|
388
|
-
type="button"
|
|
389
|
-
data-highlighted={index === highlightedIndex}
|
|
390
|
-
onClick={() => toggleNamespace(ns.name)}
|
|
391
|
-
onMouseEnter={() => setHighlightedIndex(index)}
|
|
392
|
-
className={clsx(
|
|
393
|
-
'w-full text-left px-3 py-1.5 text-xs transition-colors flex items-center gap-2',
|
|
394
|
-
'text-theme-text-primary',
|
|
395
|
-
index === highlightedIndex && 'bg-theme-hover'
|
|
396
|
-
)}
|
|
397
|
-
>
|
|
398
|
-
<div
|
|
399
|
-
className={clsx(
|
|
400
|
-
'w-3.5 h-3.5 rounded border flex items-center justify-center flex-shrink-0',
|
|
401
|
-
isSelected
|
|
402
|
-
? 'bg-blue-500 border-blue-500'
|
|
403
|
-
: 'border-theme-border-light bg-theme-base'
|
|
404
|
-
)}
|
|
405
|
-
>
|
|
406
|
-
{isSelected && <Check className="w-2.5 h-2.5 text-white" />}
|
|
407
|
-
</div>
|
|
408
|
-
<span className="truncate">{ns.name}</span>
|
|
409
|
-
</button>
|
|
410
|
-
)
|
|
411
|
-
})
|
|
412
|
-
)}
|
|
413
|
-
</div>
|
|
414
|
-
|
|
415
|
-
{/* Namespace count and selection info */}
|
|
416
|
-
{sortedNamespaces.length > 0 && (
|
|
417
|
-
<div className="px-3 py-1.5 text-[10px] text-theme-text-tertiary border-t border-theme-border bg-theme-base flex justify-between">
|
|
418
|
-
<span>
|
|
419
|
-
{filteredNamespaces.length === sortedNamespaces.length
|
|
420
|
-
? `${sortedNamespaces.length} namespaces`
|
|
421
|
-
: `${filteredNamespaces.length} of ${sortedNamespaces.length} namespaces`}
|
|
422
|
-
</span>
|
|
423
|
-
{value.length > 0 && (
|
|
424
|
-
<span className="text-blue-400">{value.length} selected</span>
|
|
425
|
-
)}
|
|
426
|
-
</div>
|
|
427
|
-
)}
|
|
428
|
-
</>
|
|
429
|
-
)}
|
|
430
|
-
</div>
|
|
431
|
-
</>,
|
|
432
|
-
document.body
|
|
433
|
-
)}
|
|
434
|
-
</>
|
|
435
|
-
)
|
|
436
|
-
}
|