Skip to content

Commit 891e699

Browse files
committed
refactor: improve aggregate parameter input with MultiSelect
1 parent 6a35e4e commit 891e699

File tree

1 file changed

+109
-93
lines changed

1 file changed

+109
-93
lines changed

apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx

Lines changed: 109 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
"use client";
22

3+
import { MultiSelect } from "@/components/blocks/multi-select";
4+
import { Input } from "@/components/ui/input";
35
import { cn } from "@/lib/utils";
46
import { useCallback, useEffect, useMemo, useState } from "react";
57
import type { ControllerRenderProps } from "react-hook-form";
6-
import { MultiSelect } from "@/components/blocks/multi-select";
7-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8-
import { Input } from "@/components/ui/input";
9-
import { Badge } from "@/components/ui/badge";
10-
import { Button } from "@/components/ui/button";
11-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
12-
import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react";
138

149
interface Preset {
1510
label: string;
@@ -152,102 +147,123 @@ interface AggregateParameterInputProps {
152147
}
153148

154149
export function AggregateParameterInput(props: AggregateParameterInputProps) {
155-
const { field, placeholder, endpointPath } = props;
150+
const { field, placeholder, endpointPath, showTip } = props;
156151
const { value, onChange } = field;
157-
const [searchQuery, setSearchQuery] = useState('');
158-
const inputRef = useRef<HTMLInputElement>(null);
159-
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
160152

161-
const presets = useMemo(() => getAggregatePresets(endpointPath), [endpointPath]);
162-
153+
const presets = useMemo(
154+
() => getAggregatePresets(endpointPath),
155+
[endpointPath],
156+
);
157+
163158
const selectedValues = useMemo(() => {
164159
if (!value) return [];
165-
return String(value).split(',').filter(Boolean);
160+
return String(value).split(",").filter(Boolean);
166161
}, [value]);
167162

168-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
169-
onChange(e);
170-
};
163+
const handlePresetChange = useCallback(
164+
(values: string[]) => {
165+
onChange({ target: { value: values.join(",") } });
166+
},
167+
[onChange],
168+
);
171169

172-
const handlePresetSelect = useCallback((preset: { value: string; label: string }) => {
173-
const newValue = value ? `${value}, ${preset.value}` : preset.value;
174-
onChange({ target: { value: newValue } });
175-
inputRef.current?.focus();
176-
}, [value, onChange]);
170+
// Custom search function for the MultiSelect
171+
const searchFunction = useCallback(
172+
(option: { value: string; label: string }, searchTerm: string) => {
173+
if (!searchTerm) return true;
174+
const query = searchTerm.toLowerCase();
175+
return (
176+
option.label.toLowerCase().includes(query) ||
177+
option.value.toLowerCase().includes(query)
178+
);
179+
},
180+
[],
181+
);
182+
183+
// Get display values for the selected items
184+
useCallback(
185+
(value: string) => {
186+
const preset = presets.find((p) => p.value === value);
187+
return preset ? preset.label : value;
188+
},
189+
[presets],
190+
);
191+
192+
// Format selected values for display in the MultiSelect
193+
useMemo(() => {
194+
return selectedValues.map((value) => {
195+
const preset = presets.find((p) => p.value === value);
196+
return {
197+
label: preset?.label || value,
198+
value,
199+
};
200+
});
201+
}, [selectedValues, presets]);
202+
203+
// State for the manual input text
204+
const [manualInput, setManualInput] = useState("");
205+
206+
// Update manual input when selected values change
207+
useEffect(() => {
208+
if (selectedValues.length === 0) {
209+
setManualInput("");
210+
} else {
211+
setManualInput(selectedValues.join(", "));
212+
}
213+
}, [selectedValues]);
214+
215+
// Handle manual input changes
216+
const handleManualInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
217+
const value = e.target.value;
218+
setManualInput(value);
219+
220+
// Update selected values by splitting on commas and trimming whitespace
221+
const newValues = value
222+
.split(",")
223+
.map((v) => v.trim())
224+
.filter(Boolean);
225+
226+
onChange({ target: { value: newValues.join(",") } });
227+
};
177228

178229
return (
179-
<div className="w-full space-y-2">
180-
{/* Main input field */}
181-
<Input
182-
ref={inputRef}
183-
value={value || ''}
184-
onChange={handleInputChange}
185-
placeholder={placeholder || "Enter aggregation formula..."}
186-
className="w-full font-mono text-sm"
187-
/>
188-
189-
{/* Preset selector */}
190-
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
191-
<PopoverTrigger asChild>
192-
<Button
193-
variant="outline"
194-
size="sm"
195-
className="w-full justify-between text-muted-foreground"
196-
type="button"
197-
>
198-
<span>Select from presets</span>
199-
<ChevronDownIcon className="h-4 w-4" />
200-
</Button>
201-
</PopoverTrigger>
202-
<PopoverContent className="w-[500px] p-0" align="start">
203-
<div className="p-2 border-b">
204-
<div className="relative">
205-
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
206-
<Input
207-
value={searchQuery}
208-
onChange={(e) => setSearchQuery(e.target.value)}
209-
placeholder="Search aggregations..."
210-
className="pl-8 h-9"
211-
/>
212-
</div>
213-
</div>
214-
<div className="max-h-[300px] overflow-auto p-1">
215-
{presets
216-
.filter(preset =>
217-
!searchQuery ||
218-
preset.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
219-
preset.value.toLowerCase().includes(searchQuery.toLowerCase())
220-
)
221-
.map((preset) => (
222-
<button
223-
key={preset.value}
224-
className="w-full text-left p-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md flex items-center justify-between"
225-
onClick={() => handlePresetSelect(preset)}
226-
type="button"
227-
>
228-
<span>{preset.label}</span>
229-
<span className="text-xs text-muted-foreground font-mono ml-2">
230-
{preset.value}
231-
</span>
232-
</button>
233-
))}
230+
<div className="w-full">
231+
{/* Editable formula text field */}
232+
<div className="relative">
233+
<Input
234+
value={manualInput}
235+
onChange={handleManualInputChange}
236+
placeholder={placeholder}
237+
className={cn(
238+
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
239+
showTip && "lg:pr-10",
240+
)}
241+
/>
242+
</div>
243+
244+
{/* MultiSelect for choosing aggregations */}
245+
<MultiSelect
246+
options={presets}
247+
selectedValues={selectedValues}
248+
onSelectedValuesChange={handlePresetChange}
249+
placeholder="Select presets (optional)"
250+
searchPlaceholder="Search aggregation presets"
251+
className={cn(
252+
"rounded-none border-0 border-border border-t-2 border-dashed",
253+
"hover:bg-inherit",
254+
)}
255+
popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]"
256+
selectedBadgeClassName="font-normal"
257+
overrideSearchFn={searchFunction}
258+
renderOption={(option) => (
259+
<div className="flex w-full items-center justify-between">
260+
<span className="truncate">{option.label}</span>
261+
<span className="ml-2 truncate font-mono text-muted-foreground text-xs">
262+
{option.value}
263+
</span>
234264
</div>
235-
</PopoverContent>
236-
</Popover>
237-
238-
{/* Selected presets as badges */}
239-
{selectedValues.length > 0 && (
240-
<div className="flex flex-wrap gap-1">
241-
{selectedValues.map((val) => {
242-
const preset = presets.find(p => p.value === val);
243-
return (
244-
<Badge key={val} variant="secondary" className="font-normal">
245-
{preset?.label || val}
246-
</Badge>
247-
);
248-
})}
249-
</div>
250-
)}
265+
)}
266+
/>
251267
</div>
252268
);
253269
}

0 commit comments

Comments
 (0)