in studio/src/components/analytics/data-table-faceted-filter.tsx [198:523]
export function DataTableFilterCommands<TData, TValue>({
onSelect,
selectedOptions,
title,
options,
customOptions,
}: DataTableFacetedFilter<TData, TValue>) {
const selectedValues = new Set(selectedOptions);
const [input, setInput] = useState("");
const [range, setRange] = useState<{ start: number; end: number }>({
start: 0,
end: 10,
});
let content: React.ReactNode;
// the options are filtered based on the search input
const [filteredOptions, setFilteredOptions] = useState(options);
const [shouldPrefixSearch, setShouldPrefixSearch] = useState(false);
const [searchValue, setSearchValue] = useState<string | undefined>(undefined);
useEffect(() => {
if (
!selectedOptions ||
selectedOptions.length === 0 ||
customOptions !== CustomOptions.Range
)
return;
const option1 = JSON.parse(selectedOptions[0]).value;
const option2 = JSON.parse(selectedOptions[1]).value;
const time1 = option1 / 10 ** 9;
const time2 = option2 / 10 ** 9;
if (time1 >= time2) {
setRange({ start: time2, end: time1 });
} else {
setRange({ start: time1, end: time2 });
}
}, [customOptions, selectedOptions]);
const updateRangeFilters = ({
rangeValue,
}: {
rangeValue: { start: number; end: number };
}) => {
selectedValues.clear();
setRange({
start: rangeValue.start,
end: rangeValue.end,
});
selectedValues.add(
JSON.stringify({
label: (rangeValue.start * 10 ** 9).toString(),
value: (rangeValue.start * 10 ** 9).toString(),
operator: 4,
}),
);
selectedValues.add(
JSON.stringify({
label: (rangeValue.end * 10 ** 9).toString(),
value: (rangeValue.end * 10 ** 9).toString(),
operator: 5,
}),
);
const filterValues = Array.from(selectedValues);
onSelect?.(filterValues);
};
switch (customOptions) {
case CustomOptions.Text:
content = (
<div className="flex flex-col py-2">
<p className="px-2 text-xs text-muted-foreground">Custom Input</p>
<div className="flex items-center gap-x-2 px-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={`Enter ${title}`}
className="border-none !bg-transparent p-0 focus-visible:ring-0"
/>
<Button
size="icon-sm"
variant="ghost"
className="flex-shrink-0"
disabled={!input}
onClick={() => {
selectedValues.add(
JSON.stringify({
label: input,
value: input,
operator: 0,
}),
);
const filterValues = Array.from(selectedValues);
onSelect?.(filterValues);
setInput("");
}}
>
<PlusCircleIcon className="h-5 w-5" />
</Button>
</div>
<Separator />
{selectedValues.size > 0 && (
<>
<div className="mt-2 flex flex-col px-2">
{Array.from(selectedValues).map((val) => {
const selected = JSON.parse(
val,
) as AnalyticsFilter["options"][number];
return (
<div
className="flex w-full items-center justify-between gap-x-4 text-sm"
key={selected.value}
>
<span className="w-full truncate">{selected.label}</span>
<Button
size="icon-sm"
variant="ghost"
className="flex-shrink-0 text-muted-foreground"
onClick={() => {
selectedValues.delete(JSON.stringify(selected));
const filterValues = Array.from(selectedValues);
onSelect?.(
filterValues.length ? filterValues : undefined,
);
}}
>
<XCircleIcon className="h-5 w-5" />
</Button>
</div>
);
})}
</div>
<Button
className="mx-1 mt-2"
variant="ghost"
size="sm"
onClick={() => {
onSelect?.(undefined);
setInput("");
}}
>
Clear Filters
</Button>
</>
)}
</div>
);
break;
case CustomOptions.Range:
content = (
<div className="flex flex-col py-2">
<p className="px-2 text-xs text-muted-foreground">{`Select ${title} (seconds)`}</p>
<SliderWithOptions
key={`slider-${range.start}-${range.end}`}
unit="sec"
defaultRange={{ start: range.start, end: range.end }}
onValueChange={updateRangeFilters}
/>
{selectedValues.size > 0 && (
<Button
className="mx-1 mt-2"
variant="ghost"
size="sm"
onClick={() => {
onSelect?.(undefined);
setInput("");
}}
>
Clear Filters
</Button>
)}
</div>
);
break;
default:
content = <></>;
break;
}
useEffect(() => {
if (!searchValue) {
setFilteredOptions(options);
return;
}
const filtered = options.filter((option) =>
shouldPrefixSearch
? option.label.toLowerCase().startsWith(searchValue.toLowerCase())
: option.label.toLowerCase().includes(searchValue.toLowerCase()),
);
setFilteredOptions(filtered);
}, [options, searchValue, shouldPrefixSearch]);
return (
<Command
className="w-72"
filter={shouldPrefixSearch ? prefixFilter : regularFilter}
key={shouldPrefixSearch ? "prefix" : "regular"}
>
{customOptions === undefined && (
<>
<div className="relative">
<CommandInput
placeholder={title}
disabled={options.length === 0}
value={searchValue}
onValueChange={(value) => {
setSearchValue(value);
}}
/>
<div className="absolute right-[2px] top-[2px]">
<Tooltip delayDuration={100}>
<TooltipTrigger>
<Toggle
size="sm"
pressed={shouldPrefixSearch}
onPressedChange={(pressed) =>
setShouldPrefixSearch(pressed)
}
>
<MdTextRotationNone className="h-4 w-4" />
</Toggle>
</TooltipTrigger>
<TooltipContent>Prefix Search</TooltipContent>
</Tooltip>
</div>
</div>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{options.length > 0 ? (
<CommandGroup>
{options.map((option, index) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value);
} else {
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
onSelect?.(
filterValues.length ? filterValues : undefined,
);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span className="truncate">{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
) : (
<div className="flex w-full items-center justify-center p-4 text-sm">
No filters.
</div>
)}
</CommandList>
<>
<Separator orientation="horizontal" />
<div className="flex justify-center gap-x-2 pt-1">
<Button
variant="ghost"
className="w-full justify-center text-center"
onClick={() => {
const filterValues = Array.from(selectedValues);
onSelect?.(
filterValues.length
? [
...filterValues,
...filteredOptions.map((option) => option.value),
]
: filteredOptions.map((option) => option.value),
);
}}
disabled={areAllFilteredOptionsSelected({
selectedValues,
filteredOptions,
options,
})}
>
{areAllFilteredOptionsSelected({
selectedValues,
filteredOptions,
options,
})
? "Selected All"
: "Select All"}
</Button>
{selectedValues.size > 0 && (
<>
<Separator orientation="vertical" className="h-8" />
<Button
variant="ghost"
className="w-full justify-center text-center"
onClick={() => {
onSelect?.(undefined);
}}
>
Clear Selection
</Button>
</>
)}
</div>
</>
</>
)}
{customOptions !== undefined && <>{content}</>}
</Command>
);
}