diff --git a/frontend/src/components/diagnostics/figureGallery.tsx b/frontend/src/components/diagnostics/figureGallery.tsx index 80f74c5..266b1ea 100644 --- a/frontend/src/components/diagnostics/figureGallery.tsx +++ b/frontend/src/components/diagnostics/figureGallery.tsx @@ -25,6 +25,10 @@ import { } from "@/components/ui/select.tsx"; import { FigureGalleryModal } from "./figureGalleryModal.tsx"; import { FigureGallerySkeleton } from "./figureGallerySkeleton.tsx"; +import { + matchesSelectorFilters, + SelectorFilterPanel, +} from "./selectorFilterPanel.tsx"; interface DiagnosticFigureGalleryProps { providerSlug: string; @@ -71,6 +75,9 @@ export function FigureGallery({ }: DiagnosticFigureGalleryProps) { const [filter, setFilter] = useState(""); const [selectedGroup, setSelectedGroup] = useState("all"); + const [selectorFilters, setSelectorFilters] = useState< + Record + >({}); const [selectedFigureIndex, setSelectedFigureIndex] = useState( null, ); @@ -117,6 +124,9 @@ export function FigureGallery({ ) { return false; } + if (!matchesSelectorFilters(executionGroup.selectors, selectorFilters)) { + return false; + } if (filter) { try { const regex = new RegExp(filter, "i"); @@ -189,6 +199,12 @@ export function FigureGallery({ + + {filteredFigures.length > 0 ? (
; + onFiltersChange: (filters: Record) => void; +} + +function extractSelectorFacets(groups: ExecutionGroup[]): SelectorFacet[] { + const facetMap = new Map>(); + for (const group of groups) { + for (const pairs of Object.values(group.selectors)) { + for (const [key, value] of pairs) { + if (!facetMap.has(key)) { + facetMap.set(key, new Set()); + } + facetMap.get(key)!.add(value); + } + } + } + return Array.from(facetMap.entries()) + .map(([key, values]) => ({ + key, + values: Array.from(values).sort(), + })) + .sort((a, b) => a.key.localeCompare(b.key)); +} + +export function matchesSelectorFilters( + selectors: ExecutionGroup["selectors"], + filters: Record, +): boolean { + for (const [filterKey, filterValues] of Object.entries(filters)) { + if (filterValues.length === 0) continue; + const groupValues = Object.values(selectors).flatMap((pairs) => + pairs.filter(([k]) => k === filterKey).map(([, v]) => v), + ); + if (!filterValues.some((v) => groupValues.includes(v))) { + return false; + } + } + return true; +} + +function MultiSelectFacet({ + facet, + selected, + onSelectionChange, +}: { + facet: SelectorFacet; + selected: string[]; + onSelectionChange: (values: string[]) => void; +}) { + return ( + + + + + + + + + No values found. + + {facet.values.map((value) => { + const isSelected = selected.includes(value); + return ( + { + onSelectionChange( + isSelected + ? selected.filter((v) => v !== value) + : [...selected, value], + ); + }} + > +
+ +
+ {value} +
+ ); + })} +
+
+
+
+
+ ); +} + +export function SelectorFilterPanel({ + executionGroups, + filters, + onFiltersChange, +}: SelectorFilterPanelProps) { + const facets = useMemo( + () => extractSelectorFacets(executionGroups), + [executionGroups], + ); + + const hasActiveFilters = Object.values(filters).some((v) => v.length > 0); + + if (facets.length === 0) return null; + + return ( +
+
+ Selectors: + {facets.map((facet) => ( + + onFiltersChange({ ...filters, [facet.key]: values }) + } + /> + ))} + {hasActiveFilters && ( + + )} +
+ {hasActiveFilters && ( +
+ {Object.entries(filters).flatMap(([key, values]) => + values.map((value) => ( + + {key}: {value} + + + )), + )} +
+ )} +
+ ); +}