Skip to content

Commit 484da8d

Browse files
committed
feat(query-section): replace Select components with SearchableSelect for improved instance and region selection
1 parent a01a3ac commit 484da8d

2 files changed

Lines changed: 143 additions & 56 deletions

File tree

frontend/src/components/query-section.tsx

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,10 @@
33
import React, { useEffect, useState } from "react"
44
import axios from "axios"
55
import { Button } from "@/components/ui/button"
6-
import {
7-
Select,
8-
SelectContent,
9-
SelectItem,
10-
SelectTrigger,
11-
SelectValue,
12-
} from "@/components/ui/select"
13-
import { Input } from "@/components/ui/input"
146
import { Label } from "@/components/ui/label"
157
import { Card, CardContent } from "@/components/ui/card"
168
import { DatePicker } from "@/components/ui/date-picker"
9+
import { SearchableSelect } from "@/components/ui/searchable-select"
1710

1811

1912
const AWS_INSTANCE: any = {}
@@ -467,68 +460,44 @@ export function QuerySection({ vendor, onDataFetch, setLoading }: QuerySectionPr
467460
<CardContent className="flex flex-wrap gap-4 p-1 items-end justify-center">
468461
<div className="flex flex-col space-y-1.5">
469462
<Label htmlFor="instance">Instance</Label>
470-
<Select
463+
<SearchableSelect
464+
id="instance"
471465
value={searchFilter.instance}
472-
onValueChange={(val) => handleFilterChange("instance", val)}
473-
>
474-
<SelectTrigger id="instance" className="w-[180px]">
475-
<SelectValue placeholder="Select Instance" />
476-
</SelectTrigger>
477-
<SelectContent>
478-
{(assoInstance || instance).map((e) => (
479-
<SelectItem key={e} value={e}>
480-
{e}
481-
</SelectItem>
482-
))}
483-
</SelectContent>
484-
</Select>
466+
onValueChange={(value) => handleFilterChange("instance", value)}
467+
options={assoInstance || instance}
468+
placeholder="Select Instance"
469+
searchPlaceholder="Search instance..."
470+
emptyMessage="No instance found."
471+
/>
485472
</div>
486473

487474
<div className="flex flex-col space-y-1.5">
488475
<Label htmlFor="region">Region</Label>
489-
<Select
476+
<SearchableSelect
477+
id="region"
490478
value={searchFilter.region}
491-
onValueChange={(val) => handleFilterChange("region", val)}
479+
onValueChange={(value) => handleFilterChange("region", value)}
480+
options={assoRegion || region}
481+
placeholder="Select Region"
482+
searchPlaceholder="Search region..."
483+
emptyMessage="No region found."
492484
disabled={vendor === "AWS" && !searchFilter.instance}
493-
>
494-
<SelectTrigger id="region" className="w-[180px]">
495-
<SelectValue placeholder="Select Region" />
496-
</SelectTrigger>
497-
<SelectContent>
498-
{(assoRegion || region).map((e) => (
499-
<SelectItem key={e} value={e}>
500-
{e}
501-
</SelectItem>
502-
))}
503-
</SelectContent>
504-
</Select>
485+
/>
505486
</div>
506487

507488
{(vendor === "AWS" || vendor === "AZURE") && (
508489
<div className="flex flex-col space-y-1.5">
509490
<Label htmlFor="az">AZ</Label>
510-
<Select
491+
<SearchableSelect
492+
id="az"
511493
value={searchFilter.az}
512-
onValueChange={(val) => handleFilterChange("az", val)}
494+
onValueChange={(value) => handleFilterChange("az", value)}
495+
options={vendor === "AZURE" ? ["ALL", "1", "2", "3", "Single"] : assoAZ || az}
496+
placeholder="Select AZ"
497+
searchPlaceholder="Search AZ..."
498+
emptyMessage="No AZ found."
513499
disabled={vendor === "AWS" && !searchFilter.region}
514-
>
515-
<SelectTrigger id="az" className="w-[180px]">
516-
<SelectValue placeholder="Select AZ" />
517-
</SelectTrigger>
518-
<SelectContent>
519-
{vendor === "AZURE"
520-
? ["ALL", "1", "2", "3", "Single"].map((e) => (
521-
<SelectItem key={e} value={e}>
522-
{e}
523-
</SelectItem>
524-
))
525-
: (assoAZ || az).map((e) => (
526-
<SelectItem key={e} value={e}>
527-
{e}
528-
</SelectItem>
529-
))}
530-
</SelectContent>
531-
</Select>
500+
/>
532501
</div>
533502
)}
534503

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"use client"
2+
3+
import { useId, useState } from "react"
4+
import { Check, ChevronsUpDown } from "lucide-react"
5+
6+
import { Button } from "@/components/ui/button"
7+
import { Input } from "@/components/ui/input"
8+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
9+
import { cn } from "@/lib/utils"
10+
11+
interface SearchableSelectProps {
12+
value: string
13+
onValueChange: (value: string) => void
14+
options: string[]
15+
placeholder: string
16+
searchPlaceholder?: string
17+
emptyMessage?: string
18+
disabled?: boolean
19+
id?: string
20+
className?: string
21+
}
22+
23+
export function SearchableSelect({
24+
value,
25+
onValueChange,
26+
options,
27+
placeholder,
28+
searchPlaceholder = "Search...",
29+
emptyMessage = "No results found.",
30+
disabled = false,
31+
id,
32+
className,
33+
}: SearchableSelectProps) {
34+
const generatedId = useId()
35+
const inputId = id ?? generatedId
36+
const [open, setOpen] = useState(false)
37+
const [query, setQuery] = useState("")
38+
39+
const normalizedQuery = query.trim().toLowerCase()
40+
const filteredOptions = normalizedQuery
41+
? options.filter((option) => option.toLowerCase().includes(normalizedQuery))
42+
: options
43+
44+
return (
45+
<Popover
46+
open={open}
47+
onOpenChange={(nextOpen) => {
48+
setOpen(nextOpen)
49+
if (!nextOpen) {
50+
setQuery("")
51+
}
52+
}}
53+
>
54+
<PopoverTrigger asChild>
55+
<Button
56+
id={inputId}
57+
type="button"
58+
variant="outline"
59+
role="combobox"
60+
aria-expanded={open}
61+
aria-controls={`${inputId}-listbox`}
62+
disabled={disabled}
63+
className={cn("w-[220px] justify-between font-normal", className)}
64+
>
65+
<span className={cn("truncate", !value && "text-muted-foreground")}>
66+
{value || placeholder}
67+
</span>
68+
<ChevronsUpDown className="size-4 opacity-50" />
69+
</Button>
70+
</PopoverTrigger>
71+
<PopoverContent className="w-[220px] p-0" align="start">
72+
<div className="border-b p-2">
73+
<Input
74+
value={query}
75+
onChange={(event) => setQuery(event.target.value)}
76+
placeholder={searchPlaceholder}
77+
autoFocus
78+
/>
79+
</div>
80+
<div
81+
id={`${inputId}-listbox`}
82+
role="listbox"
83+
className="max-h-64 overflow-y-auto p-1"
84+
>
85+
{filteredOptions.length > 0 ? (
86+
filteredOptions.map((option) => {
87+
const selected = option === value
88+
89+
return (
90+
<button
91+
key={option}
92+
type="button"
93+
role="option"
94+
aria-selected={selected}
95+
className={cn(
96+
"flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
97+
selected && "bg-accent/60"
98+
)}
99+
onClick={() => {
100+
onValueChange(option)
101+
setOpen(false)
102+
}}
103+
>
104+
<span className="truncate">{option}</span>
105+
<Check className={cn("size-4", selected ? "opacity-100" : "opacity-0")} />
106+
</button>
107+
)
108+
})
109+
) : (
110+
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
111+
{emptyMessage}
112+
</div>
113+
)}
114+
</div>
115+
</PopoverContent>
116+
</Popover>
117+
)
118+
}

0 commit comments

Comments
 (0)