From c505de6704ed532aba9ea36f4b0d00302ffe3142 Mon Sep 17 00:00:00 2001 From: Ivy Date: Fri, 14 Feb 2025 12:41:00 -0500 Subject: [PATCH] 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 --- .../app/components/composed/DataTable.tsx | 88 +++++++++++++++++-- ui/admin/app/components/composed/Filters.tsx | 8 ++ ui/admin/app/components/ui/calendar.tsx | 75 ++++++++++++++++ ui/admin/app/lib/service/routeService.ts | 4 + ui/admin/app/lib/utils/filter.ts | 22 +++++ .../app/routes/_auth.chat-threads._index.tsx | 40 ++++++++- .../app/routes/_auth.task-runs._index.tsx | 36 +++++++- ui/admin/app/routes/_auth.tasks._index.tsx | 39 +++++++- ui/admin/package.json | 3 +- ui/admin/pnpm-lock.yaml | 16 +++- 10 files changed, 313 insertions(+), 18 deletions(-) create mode 100644 ui/admin/app/components/ui/calendar.tsx create mode 100644 ui/admin/app/lib/utils/filter.ts diff --git a/ui/admin/app/components/composed/DataTable.tsx b/ui/admin/app/components/composed/DataTable.tsx index f3a78a988..1422807db 100644 --- a/ui/admin/app/components/composed/DataTable.tsx +++ b/ui/admin/app/components/composed/DataTable.tsx @@ -8,11 +8,20 @@ import { useReactTable, } from "@tanstack/react-table"; import { ListFilterIcon } from "lucide-react"; +import { useState } from "react"; +import { DateRange } from "react-day-picker"; import { useNavigate } from "react-router"; import { cn } from "~/lib/utils"; import { ComboBox } from "~/components/composed/ComboBox"; +import { Button } from "~/components/ui/button"; +import { Calendar } from "~/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; import { Table, TableBody, @@ -47,6 +56,9 @@ export function DataTable({ onCtrlClick, }: DataTableProps) { const table = useReactTable({ + enableColumnResizing: true, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", data, columns, state: { sorting: sort }, @@ -61,13 +73,33 @@ export function DataTable({ {headerGroup.headers.map((header) => { return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ )} + {header.column.getCanResize() && ( + + )} +
); })} @@ -115,6 +147,7 @@ export function DataTable({ onRowClick?.(cell.row.original); } }} + style={{ width: cell.column.getSize() }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -167,3 +200,44 @@ export const DataTableFilter = ({ /> ); }; + +export const DataTableTimeFilter = ({ + dateRange, + field, + onSelect, +}: { + dateRange: DateRange; + field: string; + onSelect: (range: DateRange) => void; +}) => { + const [range, setRange] = useState(dateRange); + return ( + + + + + + { + setRange(range); + if (range?.from && range?.to) { + onSelect(range); + } + }} + initialFocus + /> + + + ); +}; diff --git a/ui/admin/app/components/composed/Filters.tsx b/ui/admin/app/components/composed/Filters.tsx index b9878d0e9..06e33e5f7 100644 --- a/ui/admin/app/components/composed/Filters.tsx +++ b/ui/admin/app/components/composed/Filters.tsx @@ -14,6 +14,7 @@ type QueryParams = { agentId?: string; userId?: string; taskId?: string; + created?: string; }; export function Filters({ @@ -75,6 +76,13 @@ export function Filters({ value: workflowMap?.get(filters.taskId)?.name ?? filters.taskId, onRemove: () => updateFilters("taskId"), }, + "created" in filters && + filters.created && { + key: "created", + label: "Created", + value: new Date(filters.created).toLocaleDateString(), + onRemove: () => updateFilters("created"), + }, ].filter((x) => !!x); }, [navigate, searchParams, agentMap, userMap, workflowMap, url]); diff --git a/ui/admin/app/components/ui/calendar.tsx b/ui/admin/app/components/ui/calendar.tsx new file mode 100644 index 000000000..cfc34ac1d --- /dev/null +++ b/ui/admin/app/components/ui/calendar.tsx @@ -0,0 +1,75 @@ +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import * as React from "react"; +import { DayPicker } from "react-day-picker"; + +import { cn } from "~/lib/utils"; + +import { buttonVariants } from "~/components/ui/button"; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + .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" + : "[&:has([aria-selected])]:rounded-md" + ), + day: cn( + buttonVariants({ variant: "ghost" }), + "h-8 w-8 p-0 font-normal aria-selected:opacity-100" + ), + day_range_start: "day-range-start", + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ className, ...props }) => ( + + ), + IconRight: ({ className, ...props }) => ( + + ), + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/ui/admin/app/lib/service/routeService.ts b/ui/admin/app/lib/service/routeService.ts index c415669b1..b8ead09a2 100644 --- a/ui/admin/app/lib/service/routeService.ts +++ b/ui/admin/app/lib/service/routeService.ts @@ -17,6 +17,8 @@ const QuerySchemas = { userId: z.string().nullish(), taskId: z.string().nullish(), from: z.enum(["tasks", "agents", "users"]).nullish().catch(null), + createdStart: z.string().nullish(), + createdEnd: z.string().nullish(), }), taskSchema: z.object({ threadId: z.string().nullish(), @@ -25,6 +27,8 @@ const QuerySchemas = { agentId: z.string().nullish(), userId: z.string().nullish(), taskId: z.string().nullish(), + createdStart: z.string().nullish(), + createdEnd: z.string().nullish(), }), usersSchema: z.object({ userId: z.string().optional() }), } as const; diff --git a/ui/admin/app/lib/utils/filter.ts b/ui/admin/app/lib/utils/filter.ts new file mode 100644 index 000000000..6d2502e9a --- /dev/null +++ b/ui/admin/app/lib/utils/filter.ts @@ -0,0 +1,22 @@ +import { isAfter, isBefore, isSameDay } from "date-fns"; + +export const filterByCreatedRange = ( + items: T[], + start: string, + end?: string | null +) => { + const startDate = new Date(start); + const endDate = end ? new Date(end) : undefined; + return items.filter((item) => { + const createdDate = new Date(item.created); + + if (endDate) { + const withinStart = + isAfter(createdDate, startDate) || isSameDay(createdDate, startDate); + const withinEnd = + isBefore(createdDate, endDate) || isSameDay(createdDate, endDate); + return withinStart && withinEnd; + } + return isSameDay(createdDate, startDate); + }); +}; diff --git a/ui/admin/app/routes/_auth.chat-threads._index.tsx b/ui/admin/app/routes/_auth.chat-threads._index.tsx index 8bc76fa95..81abe39ad 100644 --- a/ui/admin/app/routes/_auth.chat-threads._index.tsx +++ b/ui/admin/app/routes/_auth.chat-threads._index.tsx @@ -16,10 +16,12 @@ import { UserService } from "~/lib/service/api/userService"; import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { timeSince } from "~/lib/utils"; +import { filterByCreatedRange } from "~/lib/utils/filter"; import { DataTable, DataTableFilter, + DataTableTimeFilter, useRowNavigate, } from "~/components/composed/DataTable"; import { Filters } from "~/components/composed/Filters"; @@ -61,7 +63,8 @@ export default function TaskRuns() { ? value : $path("/chat-threads/:id", { id: value.id }) ); - const { agentId, userId } = useLoaderData(); + const { agentId, userId, createdStart, createdEnd } = + useLoaderData(); const getThreads = useSWR(...ThreadsService.getThreads.swr({})); const getAgents = useSWR(...AgentService.getAgents.swr({})); @@ -86,8 +89,16 @@ export default function TaskRuns() { ); } + if (createdStart) { + filteredThreads = filterByCreatedRange( + filteredThreads, + createdStart, + createdEnd + ); + } + return filteredThreads; - }, [getThreads.data, agentId, userId]); + }, [getThreads.data, agentId, userId, createdStart, createdEnd]); const agentMap = useMemo( () => new Map(getAgents.data?.map((agent) => [agent.id, agent])), @@ -160,6 +171,8 @@ export default function TaskRuns() { $path("/chat-threads", { agentId: value, ...(userId && { userId }), + ...(createdStart && { createdStart }), + ...(createdEnd && { createdEnd }), }) ); }} @@ -183,6 +196,8 @@ export default function TaskRuns() { $path("/chat-threads", { userId: value, ...(agentId && { agentId }), + ...(createdStart && { createdStart }), + ...(createdEnd && { createdEnd }), }) ); }} @@ -191,7 +206,26 @@ export default function TaskRuns() { }), columnHelper.accessor("created", { id: "created", - header: "Created", + header: ({ column }) => ( + { + navigate.internal( + $path("/chat-threads", { + createdStart: range.from?.toDateString(), + createdEnd: range.to?.toDateString(), + ...(agentId && { agentId }), + ...(userId && { userId }), + }) + ); + }} + /> + ), cell: (info) => (

{timeSince(new Date(info.row.original.created))} ago

), diff --git a/ui/admin/app/routes/_auth.task-runs._index.tsx b/ui/admin/app/routes/_auth.task-runs._index.tsx index e2b3e1ed6..03185821d 100644 --- a/ui/admin/app/routes/_auth.task-runs._index.tsx +++ b/ui/admin/app/routes/_auth.task-runs._index.tsx @@ -18,10 +18,12 @@ import { WorkflowService } from "~/lib/service/api/workflowService"; import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { timeSince } from "~/lib/utils"; +import { filterByCreatedRange } from "~/lib/utils/filter"; import { DataTable, DataTableFilter, + DataTableTimeFilter, useRowNavigate, } from "~/components/composed/DataTable"; import { Filters } from "~/components/composed/Filters"; @@ -63,7 +65,8 @@ export default function TaskRuns() { ? value : $path("/task-runs/:id", { id: value.id }) ); - const { taskId, userId } = useLoaderData(); + const { taskId, userId, createdStart, createdEnd } = + useLoaderData(); const getThreads = useSWR(...ThreadsService.getThreads.swr({})); @@ -135,8 +138,16 @@ export default function TaskRuns() { filteredThreads = threads.filter((thread) => thread.userID === userId); } + if (createdStart) { + filteredThreads = filterByCreatedRange( + filteredThreads, + createdStart, + createdEnd + ); + } + return filteredThreads; - }, [threads, taskId, userId]); + }, [threads, taskId, userId, createdStart, createdEnd]); const namesCount = useMemo(() => { return ( @@ -232,7 +243,26 @@ export default function TaskRuns() { }), columnHelper.accessor("created", { id: "created", - header: "Created", + header: ({ column }) => ( + { + navigate.internal( + $path("/task-runs", { + createdStart: range.from?.toDateString(), + createdEnd: range.to?.toDateString(), + ...(taskId && { taskId }), + ...(userId && { userId }), + }) + ); + }} + /> + ), cell: (info) => (

{timeSince(new Date(info.row.original.created))} ago

), diff --git a/ui/admin/app/routes/_auth.tasks._index.tsx b/ui/admin/app/routes/_auth.tasks._index.tsx index 8a801bf80..dcf7bcace 100644 --- a/ui/admin/app/routes/_auth.tasks._index.tsx +++ b/ui/admin/app/routes/_auth.tasks._index.tsx @@ -15,11 +15,13 @@ import { UserService } from "~/lib/service/api/userService"; import { WorkflowService } from "~/lib/service/api/workflowService"; import { RouteHandle } from "~/lib/service/routeHandles"; import { RouteService } from "~/lib/service/routeService"; +import { filterByCreatedRange } from "~/lib/utils/filter"; import { timeSince } from "~/lib/utils/time"; import { DataTable, DataTableFilter, + DataTableTimeFilter, useRowNavigate, } from "~/components/composed/DataTable"; import { Filters } from "~/components/composed/Filters"; @@ -58,7 +60,8 @@ export default function Tasks() { const navigate = useRowNavigate((value: Workflow | string) => typeof value === "string" ? value : $path("/tasks/:id", { id: value.id }) ); - const { taskId, userId, agentId } = useLoaderData(); + const { taskId, userId, agentId, createdStart, createdEnd } = + useLoaderData(); const getAgents = useSWR(...AgentService.getAgents.swr({})); const getUsers = useSWR(...UserService.getUsers.swr({})); @@ -128,6 +131,14 @@ export default function Tasks() { filteredTasks = filteredTasks.filter((item) => item.id === taskId); } + if (createdStart) { + filteredTasks = filterByCreatedRange( + filteredTasks, + createdStart, + createdEnd + ); + } + filteredTasks = search ? filteredTasks.filter( (item) => @@ -138,7 +149,7 @@ export default function Tasks() { : filteredTasks; return filteredTasks; - }, [tasks, search, agentId, userId, taskId]); + }, [tasks, search, agentId, userId, taskId, createdStart, createdEnd]); const namesCount = useMemo(() => { return data.reduce>((acc, task) => { @@ -273,7 +284,29 @@ export default function Tasks() { }), columnHelper.accessor("created", { id: "created", - header: "Created", + header: ({ column }) => ( + { + navigate.internal( + $path("/tasks", { + ...(range.from && { + createdStart: range.from.toDateString(), + }), + ...(range.to && { createdEnd: range.to.toDateString() }), + ...(taskId && { taskId }), + ...(agentId && { agentId }), + ...(userId && { userId }), + }) + ); + }} + /> + ), cell: (info) => (

{timeSince(new Date(info.row.original.created))} ago

), diff --git a/ui/admin/package.json b/ui/admin/package.json index 49941ff76..6dd9e364c 100644 --- a/ui/admin/package.json +++ b/ui/admin/package.json @@ -38,7 +38,7 @@ "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.3", @@ -57,6 +57,7 @@ "next-themes": "^0.3.0", "query-string": "^9.1.0", "react": "^18.2.0", + "react-day-picker": "8.10.1", "react-dom": "^18.2.0", "react-hook-form": "^7.53.0", "react-icons": "^5.3.0", diff --git a/ui/admin/pnpm-lock.yaml b/ui/admin/pnpm-lock.yaml index 04ffaaada..6517582c6 100644 --- a/ui/admin/pnpm-lock.yaml +++ b/ui/admin/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: specifier: ^1.1.0 version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': - specifier: ^1.1.0 + specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.1.0 @@ -131,6 +131,9 @@ importers: react: specifier: ^18.2.0 version: 18.3.1 + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@4.1.0)(react@18.3.1) react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) @@ -3970,6 +3973,12 @@ packages: react-base16-styling@0.10.0: resolution: {integrity: sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==} + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -8955,6 +8964,11 @@ snapshots: csstype: 3.1.3 lodash-es: 4.17.21 + react-day-picker@8.10.1(date-fns@4.1.0)(react@18.3.1): + dependencies: + date-fns: 4.1.0 + react: 18.3.1 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0