From 6a35e4e047b694b7fd73cc0350e32cdb63a9bbad Mon Sep 17 00:00:00 2001 From: catalyst17 <37663786+catalyst17@users.noreply.github.com> Date: Mon, 26 May 2025 18:53:59 +0200 Subject: [PATCH 1/3] feat: enhance aggregate parameter input with multi-select presets --- .../aggregate-parameter-input.client.tsx | 209 ++++++++---------- 1 file changed, 93 insertions(+), 116 deletions(-) diff --git a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx index 536e585cef6..835b8c867fa 100644 --- a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx +++ b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx @@ -1,10 +1,15 @@ "use client"; -import { MultiSelect } from "@/components/blocks/multi-select"; -import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { ControllerRenderProps } from "react-hook-form"; +import { MultiSelect } from "@/components/blocks/multi-select"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"; interface Preset { label: string; @@ -147,130 +152,102 @@ interface AggregateParameterInputProps { } export function AggregateParameterInput(props: AggregateParameterInputProps) { - const { field, placeholder, endpointPath, showTip } = props; + const { field, placeholder, endpointPath } = props; const { value, onChange } = field; + const [searchQuery, setSearchQuery] = useState(''); + const inputRef = useRef(null); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const presets = useMemo( - () => getAggregatePresets(endpointPath), - [endpointPath], - ); - + const presets = useMemo(() => getAggregatePresets(endpointPath), [endpointPath]); + const selectedValues = useMemo(() => { if (!value) return []; - return Array.from( - new Set( - String(value) - .split(",") - .map((v) => v.trim()) // remove leading / trailing spaces - .filter(Boolean), - ), - ); + return String(value).split(',').filter(Boolean); }, [value]); - const handlePresetChange = useCallback( - (values: string[]) => { - onChange({ target: { value: values.join(",") } }); - }, - [onChange], - ); - - // Custom search function for the MultiSelect - const searchFunction = useCallback( - (option: { value: string; label: string }, searchTerm: string) => { - if (!searchTerm) return true; - const query = searchTerm.toLowerCase(); - return ( - option.label.toLowerCase().includes(query) || - option.value.toLowerCase().includes(query) - ); - }, - [], - ); - - // Get display values for the selected items - useCallback( - (value: string) => { - const preset = presets.find((p) => p.value === value); - return preset ? preset.label : value; - }, - [presets], - ); - - // Format selected values for display in the MultiSelect - useMemo(() => { - return selectedValues.map((value) => { - const preset = presets.find((p) => p.value === value); - return { - label: preset?.label || value, - value, - }; - }); - }, [selectedValues, presets]); - - // State for the manual input text - const [manualInput, setManualInput] = useState(""); - - // Update manual input when selected values change - useEffect(() => { - if (selectedValues.length === 0) { - setManualInput(""); - } else { - setManualInput(selectedValues.join(", ")); - } - }, [selectedValues]); - - // Handle manual input changes - const handleManualInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setManualInput(value); - - // Update selected values by splitting on commas and trimming whitespace - const newValues = value - .split(",") - .map((v) => v.trim()) - .filter(Boolean); - - onChange({ target: { value: newValues.join(",") } }); + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e); }; - return ( -
- {/* Editable formula text field */} -
- -
+ const handlePresetSelect = useCallback((preset: { value: string; label: string }) => { + const newValue = value ? `${value}, ${preset.value}` : preset.value; + onChange({ target: { value: newValue } }); + inputRef.current?.focus(); + }, [value, onChange]); - {/* MultiSelect for choosing aggregations */} - ( -
- {option.label} - - {option.value} - -
- )} + return ( +
+ {/* Main input field */} + + + {/* Preset selector */} + + + + + +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search aggregations..." + className="pl-8 h-9" + /> +
+
+
+ {presets + .filter(preset => + !searchQuery || + preset.label.toLowerCase().includes(searchQuery.toLowerCase()) || + preset.value.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map((preset) => ( + + ))} +
+
+
+ + {/* Selected presets as badges */} + {selectedValues.length > 0 && ( +
+ {selectedValues.map((val) => { + const preset = presets.find(p => p.value === val); + return ( + + {preset?.label || val} + + ); + })} +
+ )}
); } From 891e69913073a8cefee4d914ec7eec4a67ea6405 Mon Sep 17 00:00:00 2001 From: catalyst17 <37663786+catalyst17@users.noreply.github.com> Date: Tue, 27 May 2025 11:00:14 +0200 Subject: [PATCH 2/3] refactor: improve aggregate parameter input with MultiSelect --- .../aggregate-parameter-input.client.tsx | 202 ++++++++++-------- 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx index 835b8c867fa..e682ecb36f3 100644 --- a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx +++ b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx @@ -1,15 +1,10 @@ "use client"; +import { MultiSelect } from "@/components/blocks/multi-select"; +import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { ControllerRenderProps } from "react-hook-form"; -import { MultiSelect } from "@/components/blocks/multi-select"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"; interface Preset { label: string; @@ -152,102 +147,123 @@ interface AggregateParameterInputProps { } export function AggregateParameterInput(props: AggregateParameterInputProps) { - const { field, placeholder, endpointPath } = props; + const { field, placeholder, endpointPath, showTip } = props; const { value, onChange } = field; - const [searchQuery, setSearchQuery] = useState(''); - const inputRef = useRef(null); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const presets = useMemo(() => getAggregatePresets(endpointPath), [endpointPath]); - + const presets = useMemo( + () => getAggregatePresets(endpointPath), + [endpointPath], + ); + const selectedValues = useMemo(() => { if (!value) return []; - return String(value).split(',').filter(Boolean); + return String(value).split(",").filter(Boolean); }, [value]); - const handleInputChange = (e: React.ChangeEvent) => { - onChange(e); - }; + const handlePresetChange = useCallback( + (values: string[]) => { + onChange({ target: { value: values.join(",") } }); + }, + [onChange], + ); - const handlePresetSelect = useCallback((preset: { value: string; label: string }) => { - const newValue = value ? `${value}, ${preset.value}` : preset.value; - onChange({ target: { value: newValue } }); - inputRef.current?.focus(); - }, [value, onChange]); + // Custom search function for the MultiSelect + const searchFunction = useCallback( + (option: { value: string; label: string }, searchTerm: string) => { + if (!searchTerm) return true; + const query = searchTerm.toLowerCase(); + return ( + option.label.toLowerCase().includes(query) || + option.value.toLowerCase().includes(query) + ); + }, + [], + ); + + // Get display values for the selected items + useCallback( + (value: string) => { + const preset = presets.find((p) => p.value === value); + return preset ? preset.label : value; + }, + [presets], + ); + + // Format selected values for display in the MultiSelect + useMemo(() => { + return selectedValues.map((value) => { + const preset = presets.find((p) => p.value === value); + return { + label: preset?.label || value, + value, + }; + }); + }, [selectedValues, presets]); + + // State for the manual input text + const [manualInput, setManualInput] = useState(""); + + // Update manual input when selected values change + useEffect(() => { + if (selectedValues.length === 0) { + setManualInput(""); + } else { + setManualInput(selectedValues.join(", ")); + } + }, [selectedValues]); + + // Handle manual input changes + const handleManualInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setManualInput(value); + + // Update selected values by splitting on commas and trimming whitespace + const newValues = value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + + onChange({ target: { value: newValues.join(",") } }); + }; return ( -
- {/* Main input field */} - - - {/* Preset selector */} - - - - - -
-
- - setSearchQuery(e.target.value)} - placeholder="Search aggregations..." - className="pl-8 h-9" - /> -
-
-
- {presets - .filter(preset => - !searchQuery || - preset.label.toLowerCase().includes(searchQuery.toLowerCase()) || - preset.value.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map((preset) => ( - - ))} +
+ {/* Editable formula text field */} +
+ +
+ + {/* MultiSelect for choosing aggregations */} + ( +
+ {option.label} + + {option.value} +
- - - - {/* Selected presets as badges */} - {selectedValues.length > 0 && ( -
- {selectedValues.map((val) => { - const preset = presets.find(p => p.value === val); - return ( - - {preset?.label || val} - - ); - })} -
- )} + )} + />
); } From b1d6b81d9ce02e02e669c6e6b6b80f66efe9e635 Mon Sep 17 00:00:00 2001 From: catalyst17 <37663786+catalyst17@users.noreply.github.com> Date: Tue, 27 May 2025 11:21:09 +0200 Subject: [PATCH 3/3] fix: trim badge values --- .../aggregate-parameter-input.client.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx index e682ecb36f3..536e585cef6 100644 --- a/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx +++ b/apps/playground-web/src/app/insight/[blueprint_slug]/aggregate-parameter-input.client.tsx @@ -157,7 +157,14 @@ export function AggregateParameterInput(props: AggregateParameterInputProps) { const selectedValues = useMemo(() => { if (!value) return []; - return String(value).split(",").filter(Boolean); + return Array.from( + new Set( + String(value) + .split(",") + .map((v) => v.trim()) // remove leading / trailing spaces + .filter(Boolean), + ), + ); }, [value]); const handlePresetChange = useCallback(