Skip to content

Commit f7331f1

Browse files
committed
feat: enhance aggregate parameter input with multi-select presets (#7168)
## [Dashboard] Feature: Enhance Aggregate Parameter Input with Multi-Select Functionality ## Notes for the reviewer This PR replaces the simple dropdown select with a more robust multi-select component for aggregate parameter inputs. The new implementation allows users to select multiple presets and displays them as badges. ## How to test Test the new aggregate parameter input by: 1. Opening the insight blueprint page 2. Selecting multiple presets from the dropdown 3. Verifying that selected values appear as badges 4. Testing the search functionality within the preset selector 5. Confirming that manual input still works alongside preset selection <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced aggregate parameter input with combined manual entry and multi-select for presets. - Added search functionality within the multi-select component for easier preset selection. - Updated parameter descriptions and examples for "aggregate" to clarify multi-selection usage. - **Refactor** - Made input options optional and improved synchronization between manual input and multi-select. - Adjusted tooltip positioning for better UI alignment on aggregate parameters. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- start pr-codex --> --- ## PR-Codex overview This PR enhances the `ParameterSection` and `AggregateParameterInput` components to provide dynamic descriptions and examples based on the parameter type. It also refines the handling of selected values and manual input for aggregations, replacing the `Select` component with a `MultiSelect`. ### Detailed summary - Updated `description` and `exampleToShow` in `ParameterSection` based on `param.name`. - Introduced `MultiSelect` in `AggregateParameterInput` for selecting aggregation presets. - Added state management for manual input and selected values. - Removed the `Select` component in favor of a more flexible input system. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent a5d6e76 commit f7331f1

File tree

2 files changed

+133
-47
lines changed

2 files changed

+133
-47
lines changed

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

Lines changed: 123 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
"use client";
22

3+
import { MultiSelect } from "@/components/blocks/multi-select";
34
import { Input } from "@/components/ui/input";
4-
import {
5-
Select,
6-
SelectContent,
7-
SelectItem,
8-
SelectTrigger,
9-
SelectValue,
10-
} from "@/components/ui/select";
115
import { cn } from "@/lib/utils";
6+
import { useCallback, useEffect, useMemo, useState } from "react";
127
import type { ControllerRenderProps } from "react-hook-form";
138

149
interface Preset {
@@ -145,53 +140,137 @@ interface AggregateParameterInputProps {
145140
},
146141
string
147142
>;
148-
showTip: boolean;
149-
hasError: boolean;
150-
placeholder: string;
151-
endpointPath: string; // New prop
143+
showTip?: boolean;
144+
hasError?: boolean;
145+
placeholder?: string;
146+
endpointPath: string;
152147
}
153148

154149
export function AggregateParameterInput(props: AggregateParameterInputProps) {
155-
const { field, showTip, hasError, placeholder, endpointPath } = props;
150+
const { field, placeholder, endpointPath, showTip } = props;
156151
const { value, onChange } = field;
157152

158-
const presets = getAggregatePresets(endpointPath);
153+
const presets = useMemo(
154+
() => getAggregatePresets(endpointPath),
155+
[endpointPath],
156+
);
157+
158+
const selectedValues = useMemo(() => {
159+
if (!value) return [];
160+
return Array.from(
161+
new Set(
162+
String(value)
163+
.split(",")
164+
.map((v) => v.trim()) // remove leading / trailing spaces
165+
.filter(Boolean),
166+
),
167+
);
168+
}, [value]);
169+
170+
const handlePresetChange = useCallback(
171+
(values: string[]) => {
172+
onChange({ target: { value: values.join(",") } });
173+
},
174+
[onChange],
175+
);
176+
177+
// Custom search function for the MultiSelect
178+
const searchFunction = useCallback(
179+
(option: { value: string; label: string }, searchTerm: string) => {
180+
if (!searchTerm) return true;
181+
const query = searchTerm.toLowerCase();
182+
return (
183+
option.label.toLowerCase().includes(query) ||
184+
option.value.toLowerCase().includes(query)
185+
);
186+
},
187+
[],
188+
);
189+
190+
// Get display values for the selected items
191+
useCallback(
192+
(value: string) => {
193+
const preset = presets.find((p) => p.value === value);
194+
return preset ? preset.label : value;
195+
},
196+
[presets],
197+
);
198+
199+
// Format selected values for display in the MultiSelect
200+
useMemo(() => {
201+
return selectedValues.map((value) => {
202+
const preset = presets.find((p) => p.value === value);
203+
return {
204+
label: preset?.label || value,
205+
value,
206+
};
207+
});
208+
}, [selectedValues, presets]);
209+
210+
// State for the manual input text
211+
const [manualInput, setManualInput] = useState("");
212+
213+
// Update manual input when selected values change
214+
useEffect(() => {
215+
if (selectedValues.length === 0) {
216+
setManualInput("");
217+
} else {
218+
setManualInput(selectedValues.join(", "));
219+
}
220+
}, [selectedValues]);
221+
222+
// Handle manual input changes
223+
const handleManualInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
224+
const value = e.target.value;
225+
setManualInput(value);
226+
227+
// Update selected values by splitting on commas and trimming whitespace
228+
const newValues = value
229+
.split(",")
230+
.map((v) => v.trim())
231+
.filter(Boolean);
232+
233+
onChange({ target: { value: newValues.join(",") } });
234+
};
159235

160236
return (
161-
<div className="flex flex-col space-y-1">
162-
<Input
163-
{...field}
237+
<div className="w-full">
238+
{/* Editable formula text field */}
239+
<div className="relative">
240+
<Input
241+
value={manualInput}
242+
onChange={handleManualInputChange}
243+
placeholder={placeholder}
244+
className={cn(
245+
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
246+
showTip && "lg:pr-10",
247+
)}
248+
/>
249+
</div>
250+
251+
{/* MultiSelect for choosing aggregations */}
252+
<MultiSelect
253+
options={presets}
254+
selectedValues={selectedValues}
255+
onSelectedValuesChange={handlePresetChange}
256+
placeholder="Select presets (optional)"
257+
searchPlaceholder="Search aggregation presets"
164258
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",
259+
"rounded-none border-0 border-border border-t-2 border-dashed",
260+
"hover:bg-inherit",
261+
)}
262+
popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]"
263+
selectedBadgeClassName="font-normal"
264+
overrideSearchFn={searchFunction}
265+
renderOption={(option) => (
266+
<div className="flex w-full items-center justify-between">
267+
<span className="truncate">{option.label}</span>
268+
<span className="ml-2 truncate font-mono text-muted-foreground text-xs">
269+
{option.value}
270+
</span>
271+
</div>
168272
)}
169-
placeholder={placeholder}
170273
/>
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>
195274
</div>
196275
);
197276
}

apps/playground-web/src/app/insight/[blueprint_slug]/blueprint-playground.client.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,9 @@ function ParameterSection(props: {
527527
<div className="flex flex-col gap-2">
528528
{description && (
529529
<p className="text-foreground">
530-
{description}
530+
{param.name === "aggregate"
531+
? "Aggregation(s). You can type in multiple, separated by a comma, or select from the presets"
532+
: description}
531533
</p>
532534
)}
533535

@@ -536,7 +538,9 @@ function ParameterSection(props: {
536538
<p className="mb-1 text-muted-foreground">
537539
Example:{" "}
538540
<span className="font-mono">
539-
{exampleToShow}
541+
{param.name === "aggregate"
542+
? "count() AS count_all, countDistinct(address) AS unique_addresses"
543+
: exampleToShow}
540544
</span>
541545
</p>
542546
</div>
@@ -547,7 +551,10 @@ function ParameterSection(props: {
547551
<Button
548552
asChild
549553
variant="ghost"
550-
className="-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex"
554+
className={cn(
555+
"-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex",
556+
param.name === "aggregate" && "top-[21px]",
557+
)}
551558
>
552559
<div>
553560
<InfoIcon className="size-4" />

0 commit comments

Comments
 (0)