diff --git a/app/client/components/data-table-sort-header.tsx b/app/client/components/data-table-sort-header.tsx new file mode 100644 index 00000000..0a888e49 --- /dev/null +++ b/app/client/components/data-table-sort-header.tsx @@ -0,0 +1,54 @@ +import type { Column } from "@tanstack/react-table"; +import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; +import { Button } from "~/client/components/ui/button"; +import { cn } from "~/client/lib/utils"; + +export function DataTableSortHeader({ + column, + title, + sortDirection, + center = false, +}: { + column: Column; + title: string; + sortDirection: false | "asc" | "desc"; + center?: boolean; +}) { + const icon = + sortDirection === "desc" ? ( + + ) : sortDirection === "asc" ? ( + + ) : ( + + ); + const iconVisibility = sortDirection ? "" : "lg:invisible lg:group-hover/sort:visible"; + + if (center) { + return ( + + ); + } + + return ( + + ); +} diff --git a/app/client/components/status-dot.tsx b/app/client/components/status-dot.tsx index 5a9c8544..b1118967 100644 --- a/app/client/components/status-dot.tsx +++ b/app/client/components/status-dot.tsx @@ -54,7 +54,7 @@ export const StatusDot = ({ variant, label, animated }: StatusDotProps) => { -

{label}

+

{label}

); diff --git a/app/client/modules/notifications/routes/notifications.tsx b/app/client/modules/notifications/routes/notifications.tsx index 836a7d41..6f6b91a5 100644 --- a/app/client/modules/notifications/routes/notifications.tsx +++ b/app/client/modules/notifications/routes/notifications.tsx @@ -1,6 +1,18 @@ import { useSuspenseQuery } from "@tanstack/react-query"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; import { Bell, Plus, RotateCcw } from "lucide-react"; import { useState } from "react"; +import { listNotificationDestinationsOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { DataTableSortHeader } from "~/client/components/data-table-sort-header"; import { EmptyState } from "~/client/components/empty-state"; import { StatusDot } from "~/client/components/status-dot"; import { Button } from "~/client/components/ui/button"; @@ -8,20 +20,47 @@ import { Card } from "~/client/components/ui/card"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; -import { listNotificationDestinationsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { useNavigate } from "@tanstack/react-router"; import { cn } from "~/client/lib/utils"; -export function NotificationsPage() { - const [searchQuery, setSearchQuery] = useState(""); - const [typeFilter, setTypeFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); +type NotificationRow = { + id: number; + name: string; + type: "email" | "slack" | "discord" | "gotify" | "ntfy" | "pushover" | "telegram" | "custom" | "generic"; + enabled: boolean; +}; - const clearFilters = () => { - setSearchQuery(""); - setTypeFilter(""); - setStatusFilter(""); - }; +const notificationColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => row.original.name, + }, + { + accessorKey: "type", + header: ({ column }) => , + cell: ({ row }) => row.original.type, + filterFn: (row, id, value) => row.getValue(id) === value, + }, + { + accessorFn: (row) => (row.enabled ? "enabled" : "disabled"), + id: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + filterFn: (row, id, value) => row.getValue(id) === value, + }, +]; + +export function NotificationsPage() { + const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); const navigate = useNavigate(); @@ -29,15 +68,24 @@ export function NotificationsPage() { ...listNotificationDestinationsOptions(), }); - const filteredNotifications = data.filter((notification) => { - const matchesSearch = notification.name.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesType = !typeFilter || notification.type === typeFilter; - const matchesStatus = !statusFilter || (statusFilter === "enabled" ? notification.enabled : !notification.enabled); - return matchesSearch && matchesType && matchesStatus; + const table = useReactTable({ + data, + columns: notificationColumns, + state: { columnFilters, sorting }, + onColumnFiltersChange: setColumnFilters, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), }); + const rows = table.getRowModel().rows; + const hasFilters = columnFilters.length > 0; + + const clearFilters = () => table.resetColumnFilters(); + const hasNoNotifications = data.length === 0; - const hasNoFilteredNotifications = filteredNotifications.length === 0 && !hasNoNotifications; + const hasNoFilteredNotifications = rows.length === 0 && !hasNoNotifications; if (hasNoNotifications) { return ( @@ -55,6 +103,10 @@ export function NotificationsPage() { ); } + const search = (table.getColumn("name")?.getFilterValue() as string) ?? ""; + const type = (table.getColumn("type")?.getFilterValue() as string) ?? ""; + const status = (table.getColumn("status")?.getFilterValue() as string) ?? ""; + return (
@@ -62,10 +114,10 @@ export function NotificationsPage() { setSearchQuery(e.target.value)} + value={search} + onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value)} /> - table.getColumn("type")?.setFilterValue(value)}> @@ -77,10 +129,11 @@ export function NotificationsPage() { Ntfy Pushover Telegram + Generic Custom - table.getColumn("status")?.setFilterValue(value)}> @@ -89,7 +142,7 @@ export function NotificationsPage() { Disabled - {(searchQuery || typeFilter || statusFilter) && ( + {hasFilters && (