From 9f495339d913179cd862f882ed6cfd07f0fe711a Mon Sep 17 00:00:00 2001 From: jacob-buster Date: Mon, 6 Oct 2025 14:44:54 -0600 Subject: [PATCH] improved enum drop downs + multi select --- .../DashboardFilters/DashboardFilters.tsx | 187 ++++++++++++++--- .../metrics/MetricFilters/MetricFilters.tsx | 188 +++++++++++++++--- 2 files changed, 322 insertions(+), 53 deletions(-) diff --git a/apps/web/src/components/features/dashboards/DashboardFilters/DashboardFilters.tsx b/apps/web/src/components/features/dashboards/DashboardFilters/DashboardFilters.tsx index d3ddd187a..dbdc97c79 100644 --- a/apps/web/src/components/features/dashboards/DashboardFilters/DashboardFilters.tsx +++ b/apps/web/src/components/features/dashboards/DashboardFilters/DashboardFilters.tsx @@ -1,6 +1,7 @@ import type { MetricFilter } from '@buster/server-shared/metrics'; import React, { useState } from 'react'; import { Input } from '@/components/ui/inputs/Input'; +import { Popover } from '@/components/ui/popover/Popover'; import { cn } from '@/lib/utils'; interface DashboardFiltersProps { @@ -32,6 +33,7 @@ export const DashboardFilters: React.FC = ({ const [filterValues, setFilterValues] = useState>({}); const [filterOperators, setFilterOperators] = useState>({}); const [customMode, setCustomMode] = useState>({}); + const [multiSelectValues, setMultiSelectValues] = useState>({}); const hasCommonFilters = commonFilters && commonFilters.length > 0; @@ -49,24 +51,58 @@ export const DashboardFilters: React.FC = ({ setFilterValues(newFilterValues); // Immediately process the enum selection - processFilterValues(newFilterValues, filterOperators); + processFilterValues(newFilterValues, filterOperators, multiSelectValues); } }; + const handleMultiSelectToggle = (key: string, value: string) => { + if (value === CUSTOM_VALUE_KEY) { + setCustomMode({ ...customMode, [key]: true }); + setMultiSelectValues({ ...multiSelectValues, [key]: [] }); + return; + } + + const currentValues = multiSelectValues[key] || []; + const newValues = currentValues.includes(value) + ? currentValues.filter((v) => v !== value) + : [...currentValues, value]; + + const newMultiSelectValues = { ...multiSelectValues, [key]: newValues }; + setMultiSelectValues(newMultiSelectValues); + + // Immediately process the multi-select + processFilterValues(filterValues, filterOperators, newMultiSelectValues); + }; + const handleOperatorChange = (key: string, op: string) => { setFilterOperators({ ...filterOperators, [key]: op }); }; const processFilterValues = ( currentFilterValues: Record, - currentFilterOperators: Record + currentFilterOperators: Record, + currentMultiSelectValues: Record ) => { // Convert values to appropriate types const typedValues: Record = {}; + + // Process multi-select values first (for in_list mode) + Object.entries(currentMultiSelectValues).forEach(([filterKey, selectedValues]) => { + const filter = commonFilters.find((f) => f.key === filterKey); + if (!filter || !selectedValues || selectedValues.length === 0) return; + + // Multi-select is always an array + typedValues[filterKey] = selectedValues; + }); + + // Process regular filter values Object.entries(currentFilterValues).forEach(([filterKey, filterValue]) => { const filter = commonFilters.find((f) => f.key === filterKey); if (!filter || !filterValue) return; + // Skip if already processed as multi-select + if (filterKey in typedValues) return; + let parsedValue: unknown; // Parse values based on filter type @@ -99,7 +135,7 @@ export const DashboardFilters: React.FC = ({ }; const handleBlur = () => { - processFilterValues(filterValues, filterOperators); + processFilterValues(filterValues, filterOperators, multiSelectValues); }; return ( @@ -110,6 +146,7 @@ export const DashboardFilters: React.FC = ({ {commonFilters.map((filter) => { const hasEnum = filter.validate?.enum && filter.validate.enum.length > 0; const isCustomMode = customMode[filter.key]; + const isInListMode = filter.mode === 'in_list'; return (
@@ -130,31 +167,129 @@ export const DashboardFilters: React.FC = ({ ))} )} - {hasEnum && !isCustomMode ? ( - handleMultiSelectToggle(filter.key, String(enumValue))} + className="h-4 w-4 rounded border-gray-300" + /> + {String(enumValue)} + + ); + })} +
+ +
+
+ } + align="start" > - - {filter.validate!.enum!.map((enumValue) => ( - - ))} - - + + + ) : hasEnum && !isCustomMode ? ( + + + {filter.validate!.enum!.map((enumValue) => { + const isSelected = filterValues[filter.key] === String(enumValue); + return ( + + ); + })} +
+ +
+ + } + align="start" + > + +
) : ( - handleInputChange(filter.key, e.target.value)} - onBlur={handleBlur} - placeholder={getPlaceholder(filter)} - className="h-8 w-48" - /> +
+ handleInputChange(filter.key, e.target.value)} + onBlur={handleBlur} + placeholder={getPlaceholder(filter)} + className="h-8 w-48" + /> + {hasEnum && isCustomMode && ( + + )} +
)} ); diff --git a/apps/web/src/components/features/metrics/MetricFilters/MetricFilters.tsx b/apps/web/src/components/features/metrics/MetricFilters/MetricFilters.tsx index 516f42ae3..b76cb2e11 100644 --- a/apps/web/src/components/features/metrics/MetricFilters/MetricFilters.tsx +++ b/apps/web/src/components/features/metrics/MetricFilters/MetricFilters.tsx @@ -1,6 +1,7 @@ import type { MetricFilter } from '@buster/server-shared/metrics'; import React, { useState } from 'react'; import { Input } from '@/components/ui/inputs/Input'; +import { Popover } from '@/components/ui/popover/Popover'; import { cn } from '@/lib/utils'; interface MetricFiltersProps { @@ -28,6 +29,7 @@ export const MetricFilters: React.FC = ({ const [filterValues, setFilterValues] = useState>({}); const [filterOperators, setFilterOperators] = useState>({}); const [customMode, setCustomMode] = useState>({}); + const [multiSelectValues, setMultiSelectValues] = useState>({}); if (!filters || filters.length === 0) { return null; @@ -47,24 +49,58 @@ export const MetricFilters: React.FC = ({ setFilterValues(newFilterValues); // Immediately process the enum selection - processFilterValues(newFilterValues, filterOperators); + processFilterValues(newFilterValues, filterOperators, multiSelectValues); } }; + const handleMultiSelectToggle = (key: string, value: string) => { + if (value === CUSTOM_VALUE_KEY) { + setCustomMode({ ...customMode, [key]: true }); + setMultiSelectValues({ ...multiSelectValues, [key]: [] }); + return; + } + + const currentValues = multiSelectValues[key] || []; + const newValues = currentValues.includes(value) + ? currentValues.filter((v) => v !== value) + : [...currentValues, value]; + + const newMultiSelectValues = { ...multiSelectValues, [key]: newValues }; + setMultiSelectValues(newMultiSelectValues); + + // Immediately process the multi-select + processFilterValues(filterValues, filterOperators, newMultiSelectValues); + }; + const handleOperatorChange = (key: string, op: string) => { setFilterOperators({ ...filterOperators, [key]: op }); }; const processFilterValues = ( currentFilterValues: Record, - currentFilterOperators: Record + currentFilterOperators: Record, + currentMultiSelectValues: Record ) => { // Convert values to appropriate types const typedValues: Record = {}; + + // Process multi-select values first (for in_list mode) + Object.entries(currentMultiSelectValues).forEach(([filterKey, selectedValues]) => { + const filter = filters.find((f) => f.key === filterKey); + if (!filter || !selectedValues || selectedValues.length === 0) return; + + // Multi-select is always an array + typedValues[filterKey] = selectedValues; + }); + + // Process regular filter values Object.entries(currentFilterValues).forEach(([filterKey, filterValue]) => { const filter = filters.find((f) => f.key === filterKey); if (!filter || !filterValue) return; + // Skip if already processed as multi-select + if (filterKey in typedValues) return; + let parsedValue: unknown; // Parse values based on filter type @@ -97,7 +133,7 @@ export const MetricFilters: React.FC = ({ }; const handleBlur = () => { - processFilterValues(filterValues, filterOperators); + processFilterValues(filterValues, filterOperators, multiSelectValues); }; return ( @@ -105,7 +141,7 @@ export const MetricFilters: React.FC = ({ {filters.map((filter) => { const hasEnum = filter.validate?.enum && filter.validate.enum.length > 0; const isCustomMode = customMode[filter.key]; - const showInput = !hasEnum || isCustomMode; + const isInListMode = filter.mode === 'in_list'; return (
@@ -126,31 +162,129 @@ export const MetricFilters: React.FC = ({ ))} )} - {hasEnum && !isCustomMode ? ( - handleMultiSelectToggle(filter.key, String(enumValue))} + className="h-4 w-4 rounded border-gray-300" + /> + {String(enumValue)} + + ); + })} +
+ +
+
+ } + align="start" > - - {filter.validate!.enum!.map((enumValue) => ( - - ))} - - + + + ) : hasEnum && !isCustomMode ? ( + + + {filter.validate!.enum!.map((enumValue) => { + const isSelected = filterValues[filter.key] === String(enumValue); + return ( + + ); + })} +
+ +
+ + } + align="start" + > + +
) : ( - handleInputChange(filter.key, e.target.value)} - onBlur={handleBlur} - placeholder={getPlaceholder(filter)} - className="h-8 w-48" - /> +
+ handleInputChange(filter.key, e.target.value)} + onBlur={handleBlur} + placeholder={getPlaceholder(filter)} + className="h-8 w-48" + /> + {hasEnum && isCustomMode && ( + + )} +
)} );