import { Button } from "@/src/components/ui/button"; import { Input } from "@/src/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { DatePicker } from "@/src/components/date-picker"; import { useState, type Dispatch, type SetStateAction } from "react"; import { Check, ChevronDown, Plus, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { MultiSelect } from "@/src/features/filters/components/multi-select"; import { type WipFilterState, type WipFilterCondition, type FilterState, type FilterCondition, type ColumnDefinition, filterOperators, singleFilter, } from "@langfuse/shared"; import { NonEmptyString } from "@langfuse/shared"; import { cn } from "@/src/utils/tailwind"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { InputCommand, InputCommandEmpty, InputCommandGroup, InputCommandInput, InputCommandItem, InputCommandList, } from "@/src/components/ui/input-command"; // Has WipFilterState, passes all valid filters to parent onChange export function PopoverFilterBuilder({ columns, filterState, onChange, columnsWithCustomSelect = [], }: { columns: ColumnDefinition[]; filterState: FilterState; onChange: | Dispatch> | ((newState: FilterState) => void); columnsWithCustomSelect?: string[]; }) { const capture = usePostHogClientCapture(); const [wipFilterState, _setWipFilterState] = useState(filterState); const addNewFilter = () => { setWipFilterState((prev) => [ ...prev, { column: undefined, type: undefined, operator: undefined, value: undefined, key: undefined, }, ]); }; const getValidFilters = (state: WipFilterState): FilterCondition[] => { const valid = state.filter( (f) => singleFilter.safeParse(f).success, ) as FilterCondition[]; return valid; }; const setWipFilterState = ( state: ((prev: WipFilterState) => WipFilterState) | WipFilterState, ) => { _setWipFilterState((prev) => { const newState = state instanceof Function ? state(prev) : state; const validFilters = getValidFilters(newState); onChange(validFilters); return newState; }); }; return (
{ if (open) { capture("table:filter_builder_open"); } // Create empty filter when opening popover if (open && filterState.length === 0) addNewFilter(); // Discard all wip filters when closing popover if (!open) { capture("table:filter_builder_close", { filter: filterState, }); setWipFilterState(filterState); } }} > {filterState.length > 0 ? ( ) : null}
); } export function InlineFilterState({ filterState, className, }: { filterState: FilterState; className?: string; }) { return filterState.map((filter, i) => { return ( {filter.column} {filter.type === "stringObject" || filter.type === "numberObject" ? `.${filter.key}` : ""}{" "} {filter.operator}{" "} {filter.type === "datetime" ? new Date(filter.value).toLocaleString() : filter.type === "stringOptions" || filter.type === "arrayOptions" ? filter.value.length > 2 ? `${filter.value.length} selected` : filter.value.join(", ") : filter.type === "number" || filter.type === "numberObject" ? filter.value : filter.type === "boolean" ? `${filter.value}` : `"${filter.value}"`} ); }); } export function InlineFilterBuilder({ columns, filterState, onChange, disabled, columnsWithCustomSelect, }: { columns: ColumnDefinition[]; filterState: FilterState; onChange: | Dispatch> | ((newState: FilterState) => void); disabled?: boolean; columnsWithCustomSelect?: string[]; }) { const [wipFilterState, _setWipFilterState] = useState(filterState); const setWipFilterState = ( state: ((prev: WipFilterState) => WipFilterState) | WipFilterState, ) => { _setWipFilterState((prev) => { const newState = state instanceof Function ? state(prev) : state; const validFilters = newState.filter( (f) => singleFilter.safeParse(f).success, ) as FilterState; onChange(validFilters); return newState; }); }; return (
); } const getOperator = ( type: NonNullable, ): WipFilterCondition["operator"] => { return filterOperators[type]?.length > 0 ? filterOperators[type][0] : undefined; }; function FilterBuilderForm({ columns, filterState, onChange, disabled, columnsWithCustomSelect = [], }: { columns: ColumnDefinition[]; filterState: WipFilterState; onChange: Dispatch>; disabled?: boolean; columnsWithCustomSelect?: string[]; }) { const handleFilterChange = (filter: WipFilterCondition, i: number) => { onChange((prev) => { const newState = [...prev]; newState[i] = filter; return newState; }); }; const addNewFilter = () => { onChange((prev) => [ ...prev, { column: undefined, operator: undefined, value: undefined, type: undefined, key: undefined, }, ]); }; const removeFilter = (i: number) => { onChange((prev) => { const newState = [...prev]; newState.splice(i, 1); return newState; }); }; return ( <> {filterState.map((filter, i) => { const column = columns.find( (c) => c.id === filter.column || c.name === filter.column, ); return ( ); })}
{i === 0 ? "Where" : "And"} {/* selector of the column to be filtered */} { e.stopPropagation(); }} onTouchMove={(e) => { e.stopPropagation(); }} > (e.target.style.border = "none")} /> No options found. {columns.map((option) => ( { const col = columns.find( (c) => c.id === value, ); const defaultOperator = col?.type ? getOperator(col.type) : undefined; handleFilterChange( { column: col?.name, type: col?.type, operator: defaultOperator, value: undefined, key: undefined, } as WipFilterCondition, i, ); }} > {option.name} ))} {filter.type && (filter.type === "numberObject" || filter.type === "stringObject") && (column?.type === "numberObject" || column?.type === "stringObject") ? ( column.keyOptions ? ( // Case 1: object with keyOptions - selector of the key of the object ) : ( // Case 2: object without keyOptions - text input handleFilterChange( { ...filter, key: e.target.value }, i, ) } /> ) ) : filter.type === "categoryOptions" && column?.type === "categoryOptions" ? ( // Case 3: categoryOptions ) : null} {filter.type === "string" || filter.type === "stringObject" ? ( handleFilterChange( { ...filter, value: e.target.value }, i, ) } /> ) : filter.type === "number" || filter.type === "numberObject" ? ( handleFilterChange( { ...filter, value: isNaN(Number(e.target.value)) ? e.target.value : Number(e.target.value), }, i, ) } /> ) : filter.type === "datetime" ? ( { handleFilterChange( { ...filter, value: date, }, i, ); }} includeTimePicker /> ) : filter.type === "stringOptions" || filter.type === "arrayOptions" ? ( handleFilterChange({ ...filter, value }, i) } values={Array.isArray(filter.value) ? filter.value : []} disabled={disabled} isCustomSelectEnabled={ column?.type === filter.type && columnsWithCustomSelect.includes(column.id) } /> ) : filter.type === "categoryOptions" && column?.type === "categoryOptions" ? ( o.label === filter.key) ?.values?.map((v) => ({ value: v })) ?? [] } onValueChange={(value) => handleFilterChange({ ...filter, value }, i) } values={Array.isArray(filter.value) ? filter.value : []} disabled={disabled} isCustomSelectEnabled={ column?.type === filter.type && columnsWithCustomSelect.includes(column.id) } /> ) : filter.type === "boolean" ? ( ) : ( )}
{!disabled ? ( ) : null} ); }