Skip to content

Commit c505de6

Browse files
authored
fix: created filter and resizable table columns (#1748)
* enhance: add resize columns * enhance: add created table filter * Update DataTable.tsx * use daterange instead * renamed start->createdStart, end->createdEnd
1 parent ebff364 commit c505de6

File tree

10 files changed

+313
-18
lines changed

10 files changed

+313
-18
lines changed

ui/admin/app/components/composed/DataTable.tsx

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@ import {
88
useReactTable,
99
} from "@tanstack/react-table";
1010
import { ListFilterIcon } from "lucide-react";
11+
import { useState } from "react";
12+
import { DateRange } from "react-day-picker";
1113
import { useNavigate } from "react-router";
1214

1315
import { cn } from "~/lib/utils";
1416

1517
import { ComboBox } from "~/components/composed/ComboBox";
18+
import { Button } from "~/components/ui/button";
19+
import { Calendar } from "~/components/ui/calendar";
20+
import {
21+
Popover,
22+
PopoverContent,
23+
PopoverTrigger,
24+
} from "~/components/ui/popover";
1625
import {
1726
Table,
1827
TableBody,
@@ -47,6 +56,9 @@ export function DataTable<TData, TValue>({
4756
onCtrlClick,
4857
}: DataTableProps<TData, TValue>) {
4958
const table = useReactTable({
59+
enableColumnResizing: true,
60+
columnResizeMode: "onChange",
61+
columnResizeDirection: "ltr",
5062
data,
5163
columns,
5264
state: { sorting: sort },
@@ -61,13 +73,33 @@ export function DataTable<TData, TValue>({
6173
<TableRow key={headerGroup.id} className="p-4">
6274
{headerGroup.headers.map((header) => {
6375
return (
64-
<TableHead key={header.id}>
65-
{header.isPlaceholder
66-
? null
67-
: flexRender(
68-
header.column.columnDef.header,
69-
header.getContext()
70-
)}
76+
<TableHead
77+
key={header.id}
78+
style={{ width: header.getSize() }}
79+
className="space-between group relative px-0"
80+
>
81+
<div className="flex h-full w-full items-center justify-between">
82+
{header.isPlaceholder ? null : (
83+
<div className="w-full px-2">
84+
{flexRender(
85+
header.column.columnDef.header,
86+
header.getContext()
87+
)}
88+
</div>
89+
)}
90+
{header.column.getCanResize() && (
91+
<button
92+
onMouseDown={header.getResizeHandler()}
93+
onTouchStart={header.getResizeHandler()}
94+
className={cn(
95+
"mx-2 h-full w-1 cursor-col-resize self-end group-hover:bg-muted-foreground/30",
96+
{
97+
isResizing: header.column.getIsResizing(),
98+
}
99+
)}
100+
></button>
101+
)}
102+
</div>
71103
</TableHead>
72104
);
73105
})}
@@ -115,6 +147,7 @@ export function DataTable<TData, TValue>({
115147
onRowClick?.(cell.row.original);
116148
}
117149
}}
150+
style={{ width: cell.column.getSize() }}
118151
>
119152
{flexRender(cell.column.columnDef.cell, cell.getContext())}
120153
</TableCell>
@@ -167,3 +200,44 @@ export const DataTableFilter = ({
167200
/>
168201
);
169202
};
203+
204+
export const DataTableTimeFilter = ({
205+
dateRange,
206+
field,
207+
onSelect,
208+
}: {
209+
dateRange: DateRange;
210+
field: string;
211+
onSelect: (range: DateRange) => void;
212+
}) => {
213+
const [range, setRange] = useState<DateRange | undefined>(dateRange);
214+
return (
215+
<Popover>
216+
<PopoverTrigger asChild>
217+
<Button
218+
variant="text"
219+
endContent={<ListFilterIcon />}
220+
className="w-full p-0"
221+
classNames={{
222+
content: "w-full justify-between",
223+
}}
224+
>
225+
{field}
226+
</Button>
227+
</PopoverTrigger>
228+
<PopoverContent>
229+
<Calendar
230+
mode="range"
231+
selected={range}
232+
onSelect={(range) => {
233+
setRange(range);
234+
if (range?.from && range?.to) {
235+
onSelect(range);
236+
}
237+
}}
238+
initialFocus
239+
/>
240+
</PopoverContent>
241+
</Popover>
242+
);
243+
};

ui/admin/app/components/composed/Filters.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type QueryParams = {
1414
agentId?: string;
1515
userId?: string;
1616
taskId?: string;
17+
created?: string;
1718
};
1819

1920
export function Filters({
@@ -75,6 +76,13 @@ export function Filters({
7576
value: workflowMap?.get(filters.taskId)?.name ?? filters.taskId,
7677
onRemove: () => updateFilters("taskId"),
7778
},
79+
"created" in filters &&
80+
filters.created && {
81+
key: "created",
82+
label: "Created",
83+
value: new Date(filters.created).toLocaleDateString(),
84+
onRemove: () => updateFilters("created"),
85+
},
7886
].filter((x) => !!x);
7987
}, [navigate, searchParams, agentMap, userMap, workflowMap, url]);
8088

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
2+
import * as React from "react";
3+
import { DayPicker } from "react-day-picker";
4+
5+
import { cn } from "~/lib/utils";
6+
7+
import { buttonVariants } from "~/components/ui/button";
8+
9+
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
10+
11+
function Calendar({
12+
className,
13+
classNames,
14+
showOutsideDays = true,
15+
...props
16+
}: CalendarProps) {
17+
return (
18+
<DayPicker
19+
showOutsideDays={showOutsideDays}
20+
className={cn("p-3", className)}
21+
classNames={{
22+
months: "flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0",
23+
month: "space-y-4",
24+
caption: "relative flex items-center justify-center pt-1",
25+
caption_label: "text-sm font-medium",
26+
nav: "flex items-center space-x-1",
27+
nav_button: cn(
28+
buttonVariants({ variant: "outline" }),
29+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
30+
),
31+
nav_button_previous: "absolute left-1",
32+
nav_button_next: "absolute right-1",
33+
table: "w-full border-collapse space-y-1",
34+
head_row: "flex",
35+
head_cell:
36+
"w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground",
37+
row: "mt-2 flex w-full",
38+
cell: cn(
39+
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
40+
props.mode === "range"
41+
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
42+
: "[&:has([aria-selected])]:rounded-md"
43+
),
44+
day: cn(
45+
buttonVariants({ variant: "ghost" }),
46+
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
47+
),
48+
day_range_start: "day-range-start",
49+
day_range_end: "day-range-end",
50+
day_selected:
51+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
52+
day_today: "bg-accent text-accent-foreground",
53+
day_outside:
54+
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
55+
day_disabled: "text-muted-foreground opacity-50",
56+
day_range_middle:
57+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
58+
day_hidden: "invisible",
59+
...classNames,
60+
}}
61+
components={{
62+
IconLeft: ({ className, ...props }) => (
63+
<ChevronLeftIcon className={cn("h-4 w-4", className)} {...props} />
64+
),
65+
IconRight: ({ className, ...props }) => (
66+
<ChevronRightIcon className={cn("h-4 w-4", className)} {...props} />
67+
),
68+
}}
69+
{...props}
70+
/>
71+
);
72+
}
73+
Calendar.displayName = "Calendar";
74+
75+
export { Calendar };

ui/admin/app/lib/service/routeService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const QuerySchemas = {
1717
userId: z.string().nullish(),
1818
taskId: z.string().nullish(),
1919
from: z.enum(["tasks", "agents", "users"]).nullish().catch(null),
20+
createdStart: z.string().nullish(),
21+
createdEnd: z.string().nullish(),
2022
}),
2123
taskSchema: z.object({
2224
threadId: z.string().nullish(),
@@ -25,6 +27,8 @@ const QuerySchemas = {
2527
agentId: z.string().nullish(),
2628
userId: z.string().nullish(),
2729
taskId: z.string().nullish(),
30+
createdStart: z.string().nullish(),
31+
createdEnd: z.string().nullish(),
2832
}),
2933
usersSchema: z.object({ userId: z.string().optional() }),
3034
} as const;

ui/admin/app/lib/utils/filter.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { isAfter, isBefore, isSameDay } from "date-fns";
2+
3+
export const filterByCreatedRange = <T extends { created: string }>(
4+
items: T[],
5+
start: string,
6+
end?: string | null
7+
) => {
8+
const startDate = new Date(start);
9+
const endDate = end ? new Date(end) : undefined;
10+
return items.filter((item) => {
11+
const createdDate = new Date(item.created);
12+
13+
if (endDate) {
14+
const withinStart =
15+
isAfter(createdDate, startDate) || isSameDay(createdDate, startDate);
16+
const withinEnd =
17+
isBefore(createdDate, endDate) || isSameDay(createdDate, endDate);
18+
return withinStart && withinEnd;
19+
}
20+
return isSameDay(createdDate, startDate);
21+
});
22+
};

ui/admin/app/routes/_auth.chat-threads._index.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { UserService } from "~/lib/service/api/userService";
1616
import { RouteHandle } from "~/lib/service/routeHandles";
1717
import { RouteQueryParams, RouteService } from "~/lib/service/routeService";
1818
import { timeSince } from "~/lib/utils";
19+
import { filterByCreatedRange } from "~/lib/utils/filter";
1920

2021
import {
2122
DataTable,
2223
DataTableFilter,
24+
DataTableTimeFilter,
2325
useRowNavigate,
2426
} from "~/components/composed/DataTable";
2527
import { Filters } from "~/components/composed/Filters";
@@ -61,7 +63,8 @@ export default function TaskRuns() {
6163
? value
6264
: $path("/chat-threads/:id", { id: value.id })
6365
);
64-
const { agentId, userId } = useLoaderData<typeof clientLoader>();
66+
const { agentId, userId, createdStart, createdEnd } =
67+
useLoaderData<typeof clientLoader>();
6568

6669
const getThreads = useSWR(...ThreadsService.getThreads.swr({}));
6770
const getAgents = useSWR(...AgentService.getAgents.swr({}));
@@ -86,8 +89,16 @@ export default function TaskRuns() {
8689
);
8790
}
8891

92+
if (createdStart) {
93+
filteredThreads = filterByCreatedRange(
94+
filteredThreads,
95+
createdStart,
96+
createdEnd
97+
);
98+
}
99+
89100
return filteredThreads;
90-
}, [getThreads.data, agentId, userId]);
101+
}, [getThreads.data, agentId, userId, createdStart, createdEnd]);
91102

92103
const agentMap = useMemo(
93104
() => new Map(getAgents.data?.map((agent) => [agent.id, agent])),
@@ -160,6 +171,8 @@ export default function TaskRuns() {
160171
$path("/chat-threads", {
161172
agentId: value,
162173
...(userId && { userId }),
174+
...(createdStart && { createdStart }),
175+
...(createdEnd && { createdEnd }),
163176
})
164177
);
165178
}}
@@ -183,6 +196,8 @@ export default function TaskRuns() {
183196
$path("/chat-threads", {
184197
userId: value,
185198
...(agentId && { agentId }),
199+
...(createdStart && { createdStart }),
200+
...(createdEnd && { createdEnd }),
186201
})
187202
);
188203
}}
@@ -191,7 +206,26 @@ export default function TaskRuns() {
191206
}),
192207
columnHelper.accessor("created", {
193208
id: "created",
194-
header: "Created",
209+
header: ({ column }) => (
210+
<DataTableTimeFilter
211+
key={column.id}
212+
field="Created"
213+
dateRange={{
214+
from: createdStart ? new Date(createdStart) : undefined,
215+
to: createdEnd ? new Date(createdEnd) : undefined,
216+
}}
217+
onSelect={(range) => {
218+
navigate.internal(
219+
$path("/chat-threads", {
220+
createdStart: range.from?.toDateString(),
221+
createdEnd: range.to?.toDateString(),
222+
...(agentId && { agentId }),
223+
...(userId && { userId }),
224+
})
225+
);
226+
}}
227+
/>
228+
),
195229
cell: (info) => (
196230
<p>{timeSince(new Date(info.row.original.created))} ago</p>
197231
),

0 commit comments

Comments
 (0)