mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-19 07:40:16 -06:00
816 lines
26 KiB
TypeScript
816 lines
26 KiB
TypeScript
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../ui/tabs';
|
|
import { ToolbarFilters } from './data-table-toolbar';
|
|
import {
|
|
ToolbarType,
|
|
FilterOption,
|
|
TimeRangeConfig,
|
|
} from './data-table-toolbar';
|
|
import { DateTimePicker } from '@/components/v1/molecules/time-picker/date-time-picker';
|
|
import { Badge } from '@/components/v1/ui/badge';
|
|
import { Button } from '@/components/v1/ui/button';
|
|
import { Checkbox } from '@/components/v1/ui/checkbox';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/v1/ui/dropdown-menu';
|
|
import { Input } from '@/components/v1/ui/input';
|
|
import { Label } from '@/components/v1/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/v1/ui/select';
|
|
import { V1TaskStatus } from '@/lib/api';
|
|
import {
|
|
flattenDAGsKey,
|
|
createdAfterKey,
|
|
finishedBeforeKey,
|
|
statusKey,
|
|
isCustomTimeRangeKey,
|
|
timeWindowKey,
|
|
} from '@/pages/main/v1/workflow-runs-v1/components/v1/task-runs-columns';
|
|
import { XCircleIcon } from '@heroicons/react/24/outline';
|
|
import { Cross2Icon, MixerHorizontalIcon } from '@radix-ui/react-icons';
|
|
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
|
import { ColumnFiltersState, Table } from '@tanstack/react-table';
|
|
import { Column } from '@tanstack/react-table';
|
|
import * as React from 'react';
|
|
|
|
interface FilterControlProps<TData> {
|
|
column?: Column<TData, any>;
|
|
filter: {
|
|
columnId: string;
|
|
title: string;
|
|
type?: ToolbarType;
|
|
options?: FilterOption[];
|
|
timeRangeConfig?: TimeRangeConfig;
|
|
};
|
|
}
|
|
|
|
function FilterControl<TData>({ column, filter }: FilterControlProps<TData>) {
|
|
const value = column?.getFilterValue();
|
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
const keyInputRef = React.useRef<HTMLInputElement>(null);
|
|
const valueInputRef = React.useRef<HTMLInputElement>(null);
|
|
const arrayValueInputRef = React.useRef<HTMLInputElement>(null);
|
|
const [newKey, setNewKey] = React.useState('');
|
|
const [newValue, setNewValue] = React.useState('');
|
|
const [newArrayValue, setNewArrayValue] = React.useState('');
|
|
|
|
if (!filter.type) {
|
|
return null;
|
|
}
|
|
|
|
switch (filter.type) {
|
|
case ToolbarType.TimeRange:
|
|
const config = filter.timeRangeConfig;
|
|
if (!config) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{config.isCustomTimeRange && (
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
Custom Range
|
|
</span>
|
|
<Button
|
|
onClick={config.onClearTimeRange}
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
<XCircleIcon className="h-3 w-3 mr-1" />
|
|
Clear
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="space-y-1 w-full">
|
|
<DateTimePicker
|
|
label="After"
|
|
date={
|
|
config.createdAfter
|
|
? new Date(config.createdAfter)
|
|
: undefined
|
|
}
|
|
setDate={(date) =>
|
|
config.onCreatedAfterChange?.(date?.toISOString())
|
|
}
|
|
triggerClassName="w-full"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1 w-full">
|
|
<DateTimePicker
|
|
label="Before"
|
|
date={
|
|
config.finishedBefore
|
|
? new Date(config.finishedBefore)
|
|
: undefined
|
|
}
|
|
setDate={(date) =>
|
|
config.onFinishedBeforeChange?.(date?.toISOString())
|
|
}
|
|
triggerClassName="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!config.isCustomTimeRange && (
|
|
<div className="space-y-1">
|
|
<Select
|
|
value={
|
|
config.isCustomTimeRange ? 'custom' : config.currentTimeWindow
|
|
}
|
|
onValueChange={(value) => config.onTimeWindowChange?.(value)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="Choose time range" />
|
|
</SelectTrigger>
|
|
<SelectContent className="z-[80]">
|
|
<SelectItem value="1h">1 hour</SelectItem>
|
|
<SelectItem value="6h">6 hours</SelectItem>
|
|
<SelectItem value="1d">1 day</SelectItem>
|
|
<SelectItem value="7d">7 days</SelectItem>
|
|
<SelectItem value="custom">Custom</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
case ToolbarType.Switch:
|
|
return (
|
|
<div className="flex items-center justify-between hover:bg-muted/50 rounded-md px-3 py-2 bg-muted/10 border">
|
|
<Label
|
|
htmlFor={`filter-${filter.columnId}`}
|
|
className="text-sm font-medium cursor-pointer flex-1"
|
|
>
|
|
{filter.title}
|
|
</Label>
|
|
<Checkbox
|
|
id={`filter-${filter.columnId}`}
|
|
checked={!!value}
|
|
onCheckedChange={(checked) =>
|
|
column?.setFilterValue(checked === true ? true : undefined)
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
case ToolbarType.KeyValue:
|
|
const currentKVPairs = Array.isArray(value)
|
|
? value
|
|
: value
|
|
? [value]
|
|
: [];
|
|
|
|
const addKeyValue = () => {
|
|
if (newKey.trim() && newValue.trim()) {
|
|
const keyValuePair = `${newKey.trim()}:${newValue.trim()}`;
|
|
column?.setFilterValue([...currentKVPairs, keyValuePair]);
|
|
setNewKey('');
|
|
setNewValue('');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{currentKVPairs.length > 0 && (
|
|
<div className="space-y-2">
|
|
{currentKVPairs.map((val: string, index: number) => {
|
|
const separator = val.includes(':') ? ':' : '=';
|
|
const [key, value] = val.split(separator);
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between bg-muted/50 rounded-md px-2 py-1 text-xs"
|
|
>
|
|
<div className="flex items-center gap-1 font-mono">
|
|
<span className="text-blue-600 font-medium">{key}</span>
|
|
<span className="text-muted-foreground">{separator}</span>
|
|
<span className="text-green-600">{value}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const newValues = currentKVPairs.filter(
|
|
(_, i) => i !== index,
|
|
);
|
|
column?.setFilterValue(
|
|
newValues.length > 0 ? newValues : undefined,
|
|
);
|
|
}}
|
|
className="h-5 w-5 p-0 hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
<Cross2Icon className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs text-muted-foreground">Key</label>
|
|
<Input
|
|
ref={keyInputRef}
|
|
placeholder="ENV"
|
|
value={newKey}
|
|
onChange={(e) => setNewKey(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
addKeyValue();
|
|
} else if (e.key === 'Tab' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
valueInputRef.current?.focus();
|
|
}
|
|
}}
|
|
className="h-8 text-xs placeholder:text-muted-foreground/50"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs text-muted-foreground">Value</label>
|
|
<Input
|
|
ref={valueInputRef}
|
|
placeholder="PRODUCTION"
|
|
value={newValue}
|
|
onChange={(e) => setNewValue(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
addKeyValue();
|
|
} else if (e.key === 'Tab' && e.shiftKey) {
|
|
e.preventDefault();
|
|
keyInputRef.current?.focus();
|
|
}
|
|
}}
|
|
className="h-8 text-xs placeholder:text-muted-foreground/50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={!newKey.trim() || !newValue.trim()}
|
|
onClick={addKeyValue}
|
|
className="w-full h-8 text-xs"
|
|
>
|
|
Add Filter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
case ToolbarType.Array:
|
|
const currentArrayValues = Array.isArray(value)
|
|
? value
|
|
: value
|
|
? [value]
|
|
: [];
|
|
|
|
const addArrayValue = () => {
|
|
if (newArrayValue.trim()) {
|
|
column?.setFilterValue([...currentArrayValues, newArrayValue]);
|
|
setNewArrayValue('');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{currentArrayValues.length > 0 && (
|
|
<div className="space-y-2">
|
|
{currentArrayValues.map((val: string, index: number) => {
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between bg-muted/50 rounded-md px-2 py-1 text-xs"
|
|
>
|
|
<div className="flex items-center gap-1 font-mono">
|
|
<span className="text-muted-foreground">{val}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const newValues = currentArrayValues.filter(
|
|
(_, i) => i !== index,
|
|
);
|
|
column?.setFilterValue(
|
|
newValues.length > 0 ? newValues : undefined,
|
|
);
|
|
}}
|
|
className="h-5 w-5 p-0 hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
<Cross2Icon className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<Input
|
|
ref={arrayValueInputRef}
|
|
placeholder="foobar"
|
|
value={newArrayValue}
|
|
onChange={(e) => setNewArrayValue(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
addArrayValue();
|
|
}
|
|
}}
|
|
className="h-8 text-xs placeholder:text-muted-foreground/50 w-full"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={!newArrayValue.trim()}
|
|
onClick={addArrayValue}
|
|
className="w-full h-8 text-xs"
|
|
>
|
|
Add Filter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
case ToolbarType.Checkbox:
|
|
case ToolbarType.Radio:
|
|
if (!filter.options) {
|
|
return null;
|
|
}
|
|
|
|
const selectedValues = Array.isArray(value)
|
|
? value
|
|
: value
|
|
? [value]
|
|
: [];
|
|
const filteredOptions = filter.options.filter((option) =>
|
|
option.label.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{filter.options.length > 5 && (
|
|
<Input
|
|
placeholder={`Search ${filter.title.toLowerCase()}...`}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="h-8 text-xs"
|
|
/>
|
|
)}
|
|
<div className="max-h-56 overflow-y-auto space-y-1 border rounded-md p-2 bg-muted/10">
|
|
{filteredOptions.length > 0 ? (
|
|
filteredOptions.map((option) => (
|
|
<div
|
|
key={option.value}
|
|
className="flex items-center space-x-2 hover:bg-muted/50 rounded-md px-2 py-1.5"
|
|
>
|
|
<Checkbox
|
|
id={`${filter.columnId}-${option.value}`}
|
|
checked={selectedValues.includes(option.value)}
|
|
onCheckedChange={(checked) => {
|
|
let newValue;
|
|
if (checked) {
|
|
newValue = [...selectedValues, option.value];
|
|
} else {
|
|
newValue = selectedValues.filter(
|
|
(v) => v !== option.value,
|
|
);
|
|
}
|
|
column?.setFilterValue(
|
|
newValue.length > 0 ? newValue : undefined,
|
|
);
|
|
}}
|
|
/>
|
|
<Label
|
|
htmlFor={`${filter.columnId}-${option.value}`}
|
|
className="text-sm cursor-pointer flex-1 truncate"
|
|
>
|
|
{option.label}
|
|
</Label>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-xs text-muted-foreground text-center py-3">
|
|
No options found
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
case ToolbarType.Search:
|
|
const currentSearchTerm = value ? String(value) || '' : '';
|
|
|
|
return (
|
|
<Input
|
|
ref={keyInputRef}
|
|
placeholder={`Search ${filter.title.toLowerCase()}...`}
|
|
value={currentSearchTerm}
|
|
onChange={(e) => column?.setFilterValue(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Escape') {
|
|
column?.setFilterValue(undefined);
|
|
}
|
|
}}
|
|
className="h-8 text-xs placeholder:text-muted-foreground/50 w-full"
|
|
/>
|
|
);
|
|
|
|
default:
|
|
const exhaustiveCheck: never = filter.type;
|
|
return exhaustiveCheck;
|
|
}
|
|
}
|
|
|
|
interface DataTableOptionsProps<TData> {
|
|
table: Table<TData>;
|
|
filters: ToolbarFilters;
|
|
hiddenFilters: string[];
|
|
columnKeyToName?: Record<string, string>;
|
|
onResetFilters?: () => void;
|
|
}
|
|
|
|
function arraysEqual<T>(a: T[], b: T[]) {
|
|
return (
|
|
Array.isArray(a) &&
|
|
Array.isArray(b) &&
|
|
a.length === b.length &&
|
|
a.every((val, index) => val === b[index])
|
|
);
|
|
}
|
|
|
|
export function DataTableOptions<TData>({
|
|
table,
|
|
filters,
|
|
hiddenFilters,
|
|
columnKeyToName,
|
|
onResetFilters,
|
|
}: DataTableOptionsProps<TData>) {
|
|
const cf: ColumnFiltersState | undefined = table.getState().columnFilters;
|
|
const activeFiltersCount = React.useMemo(
|
|
() =>
|
|
cf?.filter((f) => {
|
|
if (
|
|
f.id === statusKey &&
|
|
arraysEqual(f.value as V1TaskStatus[], Object.values(V1TaskStatus))
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
f.id === createdAfterKey ||
|
|
f.id === finishedBeforeKey ||
|
|
(f.id === isCustomTimeRangeKey && f.value !== true) ||
|
|
(f.id === timeWindowKey && f.value === '1d')
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (f.id === flattenDAGsKey && !f.value) {
|
|
return false;
|
|
}
|
|
|
|
if (hiddenFilters.includes(f.id)) {
|
|
return false;
|
|
}
|
|
|
|
if (f.value === undefined || f.value === null || f.value === '') {
|
|
return false;
|
|
}
|
|
|
|
if (Array.isArray(f.value) && f.value.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})?.length || 0,
|
|
[hiddenFilters, cf],
|
|
);
|
|
|
|
const visibleFilters = filters.filter((filter) => {
|
|
if (hiddenFilters.includes(filter.columnId)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const hasFilters = visibleFilters.length > 0;
|
|
const hasVisibleColumns =
|
|
table
|
|
.getAllColumns()
|
|
.filter(
|
|
(column) =>
|
|
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
|
).length > 0;
|
|
|
|
if (!hasFilters && !hasVisibleColumns) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" className="h-8 flex-shrink-0">
|
|
<MixerHorizontalIcon className="h-4 w-4" />
|
|
<span className="cq-xl:inline hidden ml-2 text-sm">Filters</span>
|
|
{activeFiltersCount > 0 && (
|
|
<Badge variant="secondary" className="ml-2 px-1 py-0 text-xs">
|
|
{activeFiltersCount}
|
|
</Badge>
|
|
)}
|
|
<ChevronDownIcon className="h-4 w-4 ml-2 hidden cq-xl:inline" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
className="w-96 max-h-[32rem] overflow-y-auto z-[70] shadow-lg p-0"
|
|
>
|
|
<DataTableOptionsContent
|
|
table={table}
|
|
filters={filters}
|
|
hiddenFilters={hiddenFilters}
|
|
columnKeyToName={columnKeyToName}
|
|
showColumnVisibility
|
|
onResetFilters={onResetFilters}
|
|
activeFiltersCount={activeFiltersCount}
|
|
/>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
export interface DataTableOptionsContentProps<TData> {
|
|
table: Table<TData>;
|
|
filters: ToolbarFilters;
|
|
columnKeyToName?: Record<string, string>;
|
|
hiddenFilters: string[];
|
|
showColumnVisibility: boolean;
|
|
onResetFilters?: () => void;
|
|
activeFiltersCount?: number;
|
|
}
|
|
|
|
interface FiltersContentProps<TData> {
|
|
table: Table<TData>;
|
|
filters: ToolbarFilters;
|
|
hiddenFilters: string[];
|
|
onResetFilters?: () => void;
|
|
activeFiltersCount?: number;
|
|
}
|
|
|
|
function FiltersContent<TData>({
|
|
table,
|
|
filters,
|
|
hiddenFilters,
|
|
onResetFilters,
|
|
activeFiltersCount = 0,
|
|
}: FiltersContentProps<TData>) {
|
|
const visibleFilters = filters.filter((filter) => {
|
|
if (hiddenFilters.includes(filter.columnId)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-0">
|
|
{onResetFilters && activeFiltersCount > 0 && (
|
|
<div className="p-3 border-b bg-muted/10">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onResetFilters}
|
|
className="w-full h-8 text-xs"
|
|
>
|
|
<Cross2Icon className="h-3 w-3 mr-2" />
|
|
Clear All Filters
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<div className="max-h-96 overflow-y-auto">
|
|
<div className="p-3 space-y-4">
|
|
{visibleFilters.map((filter, index) => (
|
|
<div key={filter.columnId} className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-foreground">
|
|
{filter.title}
|
|
</label>
|
|
{table.getColumn(filter.columnId)?.getFilterValue() !==
|
|
undefined &&
|
|
[
|
|
ToolbarType.Array,
|
|
ToolbarType.KeyValue,
|
|
ToolbarType.Checkbox,
|
|
ToolbarType.TimeRange,
|
|
ToolbarType.Radio,
|
|
].includes(filter.type) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() =>
|
|
table
|
|
.getColumn(filter.columnId)
|
|
?.setFilterValue(undefined)
|
|
}
|
|
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<Cross2Icon className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<FilterControl
|
|
column={table.getColumn(filter.columnId)}
|
|
filter={filter}
|
|
/>
|
|
{index < visibleFilters.length - 1 && (
|
|
<div className="border-t border-border/20 pt-3" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ColumnsContentProps<TData> {
|
|
table: Table<TData>;
|
|
columnKeyToName?: Record<string, string>;
|
|
}
|
|
|
|
function ColumnsContent<TData>({
|
|
table,
|
|
columnKeyToName,
|
|
}: ColumnsContentProps<TData>) {
|
|
return (
|
|
<div className="space-y-0">
|
|
<div className="max-h-80 overflow-y-auto">
|
|
<div className="p-3 space-y-1">
|
|
{table
|
|
.getAllColumns()
|
|
.filter(
|
|
(column) =>
|
|
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
|
)
|
|
.map((column) => {
|
|
const columnName =
|
|
(columnKeyToName ?? {})[
|
|
column.id as keyof typeof columnKeyToName
|
|
] || column.id;
|
|
|
|
return (
|
|
<div
|
|
key={column.id}
|
|
className="flex items-center space-x-3 hover:bg-muted/50 rounded-md px-2 py-2 transition-colors"
|
|
>
|
|
<Checkbox
|
|
id={`column-${column.id}`}
|
|
checked={column.getIsVisible()}
|
|
onCheckedChange={(value) =>
|
|
column.toggleVisibility(!!value)
|
|
}
|
|
/>
|
|
<Label
|
|
htmlFor={`column-${column.id}`}
|
|
className="text-sm cursor-pointer flex-1 truncate font-medium"
|
|
>
|
|
{columnName}
|
|
</Label>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="p-3 border-t">
|
|
<div className="flex gap-1 w-full">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => table.toggleAllColumnsVisible(false)}
|
|
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground flex-1"
|
|
>
|
|
Hide All
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => table.toggleAllColumnsVisible(true)}
|
|
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground flex-1"
|
|
>
|
|
Show All
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DataTableOptionsContent<TData>({
|
|
table,
|
|
filters,
|
|
columnKeyToName,
|
|
hiddenFilters,
|
|
showColumnVisibility,
|
|
onResetFilters,
|
|
activeFiltersCount = 0,
|
|
}: DataTableOptionsContentProps<TData>) {
|
|
const [selectedTab, setSelectedTab] = React.useState<'filters' | 'columns'>(
|
|
'filters',
|
|
);
|
|
|
|
const visibleFilters = filters.filter((filter) => {
|
|
if (hiddenFilters.includes(filter.columnId)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const hasFilters = visibleFilters.length > 0;
|
|
const hasVisibleColumns =
|
|
table
|
|
.getAllColumns()
|
|
.filter(
|
|
(column) =>
|
|
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
|
).length > 0;
|
|
|
|
const showBothSections =
|
|
hasFilters && hasVisibleColumns && showColumnVisibility;
|
|
|
|
if (!showBothSections) {
|
|
return (
|
|
<div className="w-full">
|
|
{hasFilters ? (
|
|
<FiltersContent
|
|
table={table}
|
|
filters={filters}
|
|
hiddenFilters={hiddenFilters}
|
|
onResetFilters={onResetFilters}
|
|
activeFiltersCount={activeFiltersCount}
|
|
/>
|
|
) : hasVisibleColumns && showColumnVisibility ? (
|
|
<ColumnsContent table={table} columnKeyToName={columnKeyToName} />
|
|
) : (
|
|
<div className="p-6 text-center text-sm text-muted-foreground">
|
|
No options available
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tabs
|
|
value={selectedTab}
|
|
onValueChange={(value) => setSelectedTab(value as 'filters' | 'columns')}
|
|
className="w-full rounded-none p-0"
|
|
>
|
|
<TabsList className="grid w-full grid-cols-2 bg-muted/30 rounded-none px-2">
|
|
<TabsTrigger
|
|
value="filters"
|
|
className="text-xs font-medium data-[state=active]:bg-background data-[state=active]:text-foreground"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
Filters
|
|
{activeFiltersCount > 0 && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="h-4 px-1.5 text-[10px] leading-none"
|
|
>
|
|
{activeFiltersCount}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="columns"
|
|
className="text-xs font-medium data-[state=active]:bg-background data-[state=active]:text-foreground"
|
|
>
|
|
Column Visibility
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="filters" className="mt-0 space-y-0">
|
|
<FiltersContent
|
|
table={table}
|
|
filters={filters}
|
|
hiddenFilters={hiddenFilters}
|
|
onResetFilters={onResetFilters}
|
|
activeFiltersCount={activeFiltersCount}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="columns" className="mt-0">
|
|
<ColumnsContent table={table} columnKeyToName={columnKeyToName} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
);
|
|
}
|