Skip to content

Commit cbffdc6

Browse files
DuraCHYoDanila Epishev
andauthored
refactor(ui): rewrite NamespaceSelector to Combobox pattern (#416)
Co-authored-by: Danila Epishev <depishev@alfabank.ru>
1 parent 33189e6 commit cbffdc6

File tree

2 files changed

+98
-34
lines changed

2 files changed

+98
-34
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*.so
66
*.dylib
77

8+
#SQLite dev.db
9+
dev.db
10+
811
# Test binary, built with `go test -c`
912
*.test
1013

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import { useMemo, useState } from 'react'
2+
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
13
import { Namespace } from 'kubernetes-types/core/v1'
24

35
import { useResources } from '@/lib/api'
6+
import { cn } from '@/lib/utils'
7+
import { Button } from '@/components/ui/button'
48
import {
5-
Select,
6-
SelectContent,
7-
SelectItem,
8-
SelectTrigger,
9-
SelectValue,
10-
} from '@/components/ui/select'
9+
Command,
10+
CommandEmpty,
11+
CommandGroup,
12+
CommandInput,
13+
CommandItem,
14+
CommandList,
15+
} from '@/components/ui/command'
16+
import {
17+
Popover,
18+
PopoverContent,
19+
PopoverTrigger,
20+
} from '@/components/ui/popover'
1121

1222
export function NamespaceSelector({
1323
selectedNamespace,
@@ -18,36 +28,87 @@ export function NamespaceSelector({
1828
handleNamespaceChange: (namespace: string) => void
1929
showAll?: boolean
2030
}) {
31+
const [open, setOpen] = useState(false)
2132
const { data, isLoading } = useResources('namespaces')
2233

23-
const sortedNamespaces = data?.sort((a, b) => {
24-
const nameA = a.metadata?.name?.toLowerCase() || ''
25-
const nameB = b.metadata?.name?.toLowerCase() || ''
26-
return nameA.localeCompare(nameB)
27-
}) || [{ metadata: { name: 'default' } }]
34+
const sortedNamespaces = useMemo(() => {
35+
if (!data) return []
36+
return [...data].sort((a, b) => {
37+
const nameA = a.metadata?.name?.toLowerCase() || ''
38+
const nameB = b.metadata?.name?.toLowerCase() || ''
39+
return nameA.localeCompare(nameB)
40+
})
41+
}, [data])
2842

2943
return (
30-
<Select value={selectedNamespace} onValueChange={handleNamespaceChange}>
31-
<SelectTrigger className="max-w-48">
32-
<SelectValue placeholder="Select a namespace" />
33-
</SelectTrigger>
34-
<SelectContent>
35-
{isLoading && (
36-
<SelectItem disabled value="_loading">
37-
Loading namespaces...
38-
</SelectItem>
39-
)}
40-
{showAll && (
41-
<SelectItem key="all" value="_all">
42-
All Namespaces
43-
</SelectItem>
44-
)}
45-
{sortedNamespaces?.map((ns: Namespace) => (
46-
<SelectItem key={ns.metadata!.name} value={ns.metadata!.name!}>
47-
{ns.metadata!.name}
48-
</SelectItem>
49-
))}
50-
</SelectContent>
51-
</Select>
44+
<Popover open={open} onOpenChange={setOpen}>
45+
<PopoverTrigger asChild>
46+
<Button
47+
variant="outline"
48+
role="combobox"
49+
aria-expanded={open}
50+
className="w-[200px] justify-between shadow-sm"
51+
>
52+
<span className="truncate">
53+
{selectedNamespace === '_all'
54+
? 'All Namespaces'
55+
: (selectedNamespace || "Select namespace...")}
56+
</span>
57+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
58+
</Button>
59+
</PopoverTrigger>
60+
61+
<PopoverContent className="w-[200px] p-0" align="start">
62+
<Command>
63+
<CommandInput placeholder="Search..." className="h-9" />
64+
<CommandList className="max-h-[300px] overflow-x-hidden overflow-y-auto [ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
65+
{isLoading ? (
66+
<div className="flex items-center justify-center p-6 text-sm">
67+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
68+
Loading...
69+
</div>
70+
) : (
71+
<>
72+
<CommandEmpty>No results.</CommandEmpty>
73+
<CommandGroup>
74+
{showAll && (
75+
<CommandItem
76+
value="_all"
77+
onSelect={() => {
78+
handleNamespaceChange('_all')
79+
setOpen(false)
80+
}}
81+
>
82+
<Check className={cn("mr-2 h-4 w-4 shrink-0", selectedNamespace === '_all' ? "opacity-100" : "opacity-0")} />
83+
<span className="truncate">All Namespaces</span>
84+
</CommandItem>
85+
)}
86+
87+
{sortedNamespaces.map((ns: Namespace) => {
88+
const name = ns.metadata?.name || ''
89+
return (
90+
<CommandItem
91+
key={name}
92+
value={name}
93+
onSelect={(val) => {
94+
handleNamespaceChange(val)
95+
setOpen(false)
96+
}}
97+
className="flex items-center"
98+
>
99+
<Check className={cn("mr-2 h-4 w-4 shrink-0", selectedNamespace === name ? "opacity-100" : "opacity-0")} />
100+
<span className="truncate flex-1 min-w-0" title={name}>
101+
{name}
102+
</span>
103+
</CommandItem>
104+
)
105+
})}
106+
</CommandGroup>
107+
</>
108+
)}
109+
</CommandList>
110+
</Command>
111+
</PopoverContent>
112+
</Popover>
52113
)
53-
}
114+
}

0 commit comments

Comments
 (0)