Skip to content

Commit a82e7b1

Browse files
committed
feat: enhance aggregate parameter input with multi-select presets
1 parent 9479d25 commit a82e7b1

File tree

1 file changed

+101
-46
lines changed

1 file changed

+101
-46
lines changed

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

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"use client";
22

3-
import { Input } from "@/components/ui/input";
4-
import {
5-
Select,
6-
SelectContent,
7-
SelectItem,
8-
SelectTrigger,
9-
SelectValue,
10-
} from "@/components/ui/select";
113
import { cn } from "@/lib/utils";
124
import type { ControllerRenderProps } from "react-hook-form";
5+
import { MultiSelect } from "@/components/blocks/multi-select";
6+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7+
import { Input } from "@/components/ui/input";
8+
import { Badge } from "@/components/ui/badge";
9+
import { Button } from "@/components/ui/button";
10+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
11+
import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react";
1312

1413
interface Preset {
1514
label: string;
@@ -145,53 +144,109 @@ interface AggregateParameterInputProps {
145144
},
146145
string
147146
>;
148-
showTip: boolean;
149-
hasError: boolean;
150-
placeholder: string;
151-
endpointPath: string; // New prop
147+
showTip?: boolean;
148+
hasError?: boolean;
149+
placeholder?: string;
150+
endpointPath: string;
152151
}
153152

154153
export function AggregateParameterInput(props: AggregateParameterInputProps) {
155-
const { field, showTip, hasError, placeholder, endpointPath } = props;
154+
const { field, placeholder, endpointPath } = props;
156155
const { value, onChange } = field;
156+
const [searchQuery, setSearchQuery] = useState('');
157+
const inputRef = useRef<HTMLInputElement>(null);
158+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
159+
160+
const presets = useMemo(() => getAggregatePresets(endpointPath), [endpointPath]);
161+
162+
const selectedValues = useMemo(() => {
163+
if (!value) return [];
164+
return String(value).split(',').filter(Boolean);
165+
}, [value]);
166+
167+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
168+
onChange(e);
169+
};
157170

158-
const presets = getAggregatePresets(endpointPath);
171+
const handlePresetSelect = useCallback((preset: { value: string; label: string }) => {
172+
const newValue = value ? `${value}, ${preset.value}` : preset.value;
173+
onChange({ target: { value: newValue } });
174+
inputRef.current?.focus();
175+
}, [value, onChange]);
159176

160177
return (
161-
<div className="flex flex-col space-y-1">
178+
<div className="w-full space-y-2">
179+
{/* Main input field */}
162180
<Input
163-
{...field}
164-
className={cn(
165-
"h-auto truncate rounded-none border-0 bg-transparent py-5 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
166-
showTip && "lg:pr-10",
167-
hasError && "text-destructive-text",
168-
)}
169-
placeholder={placeholder}
181+
ref={inputRef}
182+
value={value || ''}
183+
onChange={handleInputChange}
184+
placeholder={placeholder || "Enter aggregation formula..."}
185+
className="w-full font-mono text-sm"
170186
/>
171-
<Select
172-
value={presets.find((p) => p.value === value)?.value || ""}
173-
onValueChange={(selectedValue) => {
174-
if (selectedValue) {
175-
onChange({ target: { value: selectedValue } });
176-
}
177-
}}
178-
>
179-
<SelectTrigger
180-
className={cn(
181-
"h-8 border-dashed bg-transparent text-xs focus:ring-0 focus:ring-offset-0",
182-
!presets.find((p) => p.value === value) && "text-muted-foreground",
183-
)}
184-
>
185-
<SelectValue placeholder="Select a preset (optional)" />
186-
</SelectTrigger>
187-
<SelectContent className="font-mono">
188-
{presets.map((preset) => (
189-
<SelectItem key={preset.value} value={preset.value}>
190-
{preset.label}
191-
</SelectItem>
192-
))}
193-
</SelectContent>
194-
</Select>
187+
188+
{/* Preset selector */}
189+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
190+
<PopoverTrigger asChild>
191+
<Button
192+
variant="outline"
193+
size="sm"
194+
className="w-full justify-between text-muted-foreground"
195+
type="button"
196+
>
197+
<span>Select from presets</span>
198+
<ChevronDownIcon className="h-4 w-4" />
199+
</Button>
200+
</PopoverTrigger>
201+
<PopoverContent className="w-[500px] p-0" align="start">
202+
<div className="p-2 border-b">
203+
<div className="relative">
204+
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
205+
<Input
206+
value={searchQuery}
207+
onChange={(e) => setSearchQuery(e.target.value)}
208+
placeholder="Search aggregations..."
209+
className="pl-8 h-9"
210+
/>
211+
</div>
212+
</div>
213+
<div className="max-h-[300px] overflow-auto p-1">
214+
{presets
215+
.filter(preset =>
216+
!searchQuery ||
217+
preset.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
218+
preset.value.toLowerCase().includes(searchQuery.toLowerCase())
219+
)
220+
.map((preset) => (
221+
<button
222+
key={preset.value}
223+
className="w-full text-left p-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md flex items-center justify-between"
224+
onClick={() => handlePresetSelect(preset)}
225+
type="button"
226+
>
227+
<span>{preset.label}</span>
228+
<span className="text-xs text-muted-foreground font-mono ml-2">
229+
{preset.value}
230+
</span>
231+
</button>
232+
))}
233+
</div>
234+
</PopoverContent>
235+
</Popover>
236+
237+
{/* Selected presets as badges */}
238+
{selectedValues.length > 0 && (
239+
<div className="flex flex-wrap gap-1">
240+
{selectedValues.map((val) => {
241+
const preset = presets.find(p => p.value === val);
242+
return (
243+
<Badge key={val} variant="secondary" className="font-normal">
244+
{preset?.label || val}
245+
</Badge>
246+
);
247+
})}
248+
</div>
249+
)}
195250
</div>
196251
);
197252
}

0 commit comments

Comments
 (0)