diff --git a/frontend/bun.lockb b/frontend/bun.lockb index c031b92..ab4ef86 100644 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index 38ae999..20005bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "clsx": "^2.1.1", "ky": "^1.4.0", "lucide-react": "^0.400.0", + "primereact": "^10.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", "sonner": "^1.5.0", diff --git a/frontend/src/components/db-table-view/data-table-prime.tsx b/frontend/src/components/db-table-view/data-table-prime.tsx new file mode 100644 index 0000000..076baf3 --- /dev/null +++ b/frontend/src/components/db-table-view/data-table-prime.tsx @@ -0,0 +1,111 @@ +import { + Button, + DataTablePagination, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, + ScrollArea, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui"; +import { cn, isImageUrl, isUrl } from "@/lib/utils"; +import { useTableColumnsQuery, useTableDataQuery } from "@/services/db"; +import { useSettingsStore } from "@/state"; +import { + type ColumnDef, + type OnChangeFn, + type PaginationState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ArrowUp, Rows3 } from "lucide-react"; +import { Column } from "primereact/column"; +import { DataTable } from "primereact/datatable"; +import { usePassThrough } from "primereact/passthrough"; +import { useMemo, useState } from "react"; +export const DataTablePrime = ({ + tableName, + dbName, + offset, + pageSize, + onPageSizeChange, + onPageIndexChange, +}: { + tableName: string; + offset: number; + dbName: string; + pageSize: number; + onPageIndexChange: (pageIndex: number) => void; + onPageSizeChange: (pageSize: number) => void; +}) => { + const formatDates = useSettingsStore.use.formatDates(); + const showImagesPreview = useSettingsStore.use.showImagesPreview(); + const paginationOptions = useSettingsStore.use.paginationOptions(); + const [sorting, setSorting] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + + const { data: columns } = useTableColumnsQuery({ dbName, tableName }); + const { data } = useTableDataQuery({ + tableName, + dbName, + perPage: pageSize, + page: Math.floor(offset / pageSize), + sortDesc: sorting[0]?.desc, + sortField: sorting[0]?.id, + }); + + return ( +
+
+

+ {tableName} +

+

+ Rows: {data?.count} +

+
+
+ { + if (e.rows !== pageSize) { + onPageSizeChange(e.rows); + } else if (e.first !== offset) { + onPageIndexChange(e.first); + } + }} + paginator + totalRecords={data?.count} + rowsPerPageOptions={paginationOptions} + stripedRows + columnResizeMode="expand" + resizableColumns + showGridlines + size="small" + value={data?.data} + tableStyle={{ minWidth: "100%" }} + scrollable + scrollHeight="flex" + > + {columns?.map((col) => ( + + ))} + +
+
+ ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 0943dc8..1316ae5 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,7 @@ @tailwind components; @tailwind utilities; + @layer base { input[type=number] { -moz-appearance:textfield; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 0df4cb6..6bb61e1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,10 @@ import { RouterProvider, createRouter } from "@tanstack/react-router"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; + +import { PrimeReactProvider } from "primereact/api"; +import Tailwind from "primereact/passthrough/tailwind"; +import { twMerge } from "tailwind-merge"; import "@fontsource/inter/300.css"; import "@fontsource/inter/400.css"; import "@fontsource/inter/500.css"; @@ -9,6 +13,7 @@ import "@fontsource/inter/700.css"; import "@fontsource/inter/800.css"; import "./index.css"; import { Toaster } from "@/components/ui"; +import { datatable } from "@/styles/datatable"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Import the generated route tree @@ -31,6 +36,11 @@ const queryClient = new QueryClient({ }, }); +const PrimeStyles = { + ...Tailwind, + datatable, +}; + // Render the app const rootElement = document.getElementById("root"); if (rootElement && !rootElement.innerHTML) { @@ -42,8 +52,20 @@ if (rootElement && !rootElement.innerHTML) { initialIsOpen={false} buttonPosition={"bottom-left"} /> - - + + + + , ); diff --git a/frontend/src/routes/db/$dbName/tables/$tableName/data.tsx b/frontend/src/routes/db/$dbName/tables/$tableName/data.tsx index 5937aaa..e3a72d8 100644 --- a/frontend/src/routes/db/$dbName/tables/$tableName/data.tsx +++ b/frontend/src/routes/db/$dbName/tables/$tableName/data.tsx @@ -1,4 +1,5 @@ import { DataTable } from "@/components/db-table-view/data-table"; +import { DataTablePrime } from "@/components/db-table-view/data-table-prime"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { z } from "zod"; @@ -18,22 +19,25 @@ function TableView() { const navigate = useNavigate({ from: Route.fullPath }); const updatePageSize = (value: number) => { + console.log(value); return void navigate({ search: (prev) => ({ ...prev, pageSize: value, pageIndex: 0 }), }); }; const updatePageIndex = (pageIndex: number) => { + console.log(pageIndex); + return void navigate({ search: (prev) => ({ ...prev, pageIndex }) }); }; return (
- diff --git a/frontend/src/styles/datatable.tsx b/frontend/src/styles/datatable.tsx new file mode 100644 index 0000000..bab2952 --- /dev/null +++ b/frontend/src/styles/datatable.tsx @@ -0,0 +1,379 @@ +import { cn } from "@/lib/utils"; +import type { DataTablePassThroughOptions } from "primereact/datatable"; + +const TRANSITIONS = { + overlay: { + timeout: 150, + classNames: { + enter: "opacity-0 scale-75", + enterActive: + "opacity-100 !scale-100 transition-transform transition-opacity duration-150 ease-in", + exit: "opacity-100", + exitActive: "!opacity-0 transition-opacity duration-150 ease-linear", + }, + }, +}; + +export const datatable: DataTablePassThroughOptions = { + root: ({ props }) => ({ + className: cn("relative", { + "flex flex-col h-full": props.scrollable && props.scrollHeight === "flex", + }), + }), + loadingoverlay: { + className: cn( + "fixed w-full h-full t-0 l-0 bg-gray-100/40", + "transition duration-200", + "absolute flex items-center justify-center z-2", + "dark:bg-gray-950/40", // Dark Mode + ), + }, + loadingicon: "w-8 h-8", + wrapper: ({ props }) => ({ + className: cn({ + relative: props.scrollable, + "flex flex-col grow h-full": + props.scrollable && props.scrollHeight === "flex", + }), + }), + header: ({ props }) => ({ + className: cn( + "bg-slate-50 text-slate-700 border-gray-300 font-bold p-4", + "dark:border-blue-900/40 dark:text-white/80 dark:bg-gray-900", // Dark Mode + props.showGridlines + ? "border-x border-t border-b-0" + : "border-y border-x-0", + ), + }), + table: "w-full border-spacing-0 text-sm", + thead: ({ context }) => ({ + className: cn({ + "bg-slate-50 top-0 z-[1]": context.scrollable, + }), + }), + tbody: ({ props, context }) => ({ + className: cn({ + "sticky z-[1]": props.frozenRow && context.scrollable, + }), + }), + tfoot: ({ context }) => ({ + className: cn({ + "bg-slate-50 bottom-0 z-[1]": context.scrollable, + }), + }), + footer: { + className: cn( + "bg-slate-50 text-slate-700 border-t-0 border-b border-x-0 border-gray-300 font-bold p-4", + "dark:border-blue-900/40 dark:text-white/80 dark:bg-gray-900", // Dark Mode + ), + }, + column: { + headercell: ({ context, props }) => ({ + className: cn( + "text-left border-0 border-b border-solid border-gray-300 dark:border-blue-900/40 font-bold", + "transition duration-200", + context?.size === "small" + ? "p-2" + : context?.size === "large" + ? "p-5" + : "p-4", // Size + context.sorted + ? "bg-blue-50 text-blue-700" + : "bg-slate-50 text-slate-700", // Sort + context.sorted + ? "dark:text-white/80 dark:bg-blue-300" + : "dark:text-white/80 dark:bg-gray-900", // Dark Mode + { + "sticky z-[1]": props.frozen || props.frozen === "", // Frozen Columns + "border-x border-y": context?.showGridlines, + "overflow-hidden space-nowrap border-y relative bg-clip-padding": + context.resizable, // Resizable + }, + ), + }), + headercontent: "flex items-center font-medium text-muted-foreground", + bodycell: ({ props, context }) => ({ + className: cn( + "text-left border-0 border-b border-solid border-gray-300", + context?.size === "small" + ? "p-2" + : context?.size === "large" + ? "p-5" + : "p-4", // Size + "dark:text-white/80 dark:border-blue-900/40", // Dark Mode + { + "sticky bg-inherit": props && (props.frozen || props.frozen === ""), // Frozen Columns + "border-x border-y": context.showGridlines, + }, + ), + }), + footercell: ({ context }) => ({ + className: cn( + "text-left border-0 border-b border-solid border-gray-300 font-bold", + "bg-slate-50 text-slate-700", + "transition duration-200", + context?.size === "small" + ? "p-2" + : context?.size === "large" + ? "p-5" + : "p-4", // Size + "dark:text-white/80 dark:bg-gray-900 dark:border-blue-900/40", // Dark Mode + { + "border-x border-y": context.showGridlines, + }, + ), + }), + sorticon: ({ context }) => ({ + className: cn( + "ml-2", + context.sorted + ? "text-blue-700 dark:text-white/80" + : "text-slate-700 dark:text-white/70", + ), + }), + sortbadge: { + className: cn( + "flex items-center justify-center align-middle", + "rounded-[50%] w-[1.143rem] leading-[1.143rem] ml-2", + "text-blue-700 bg-blue-50", + "dark:text-white/80 dark:bg-blue-400", // Dark Mode + ), + }, + columnfilter: "inline-flex items-center ml-auto", + filteroverlay: { + className: cn( + "bg-white text-gray-600 border-0 rounded-md min-w-[12.5rem]", + "dark:bg-gray-900 dark:border-blue-900/40 dark:text-white/80", //Dark Mode + ), + }, + filtermatchmodedropdown: { + root: "min-[0px]:flex mb-2", + }, + filterrowitems: "m-0 p-0 py-3 list-none ", + filterrowitem: ({ context }) => ({ + className: cn( + "m-0 py-3 px-5 bg-transparent", + "transition duration-200", + context?.highlighted + ? "text-blue-700 bg-blue-100 dark:text-white/80 dark:bg-blue-300" + : "text-gray-600 bg-transparent dark:text-white/80 dark:bg-transparent", + ), + }), + filteroperator: { + className: cn( + "px-5 py-3 border-b border-solid border-gray-300 text-slate-700 bg-slate-50 rounded-t-md", + "dark:border-blue-900/40 dark:text-white/80 dark:bg-gray-900", // Dark Mode + ), + }, + filteroperatordropdown: { + root: "min-[0px]:flex", + }, + filterconstraint: + "p-5 border-b border-solid border-gray-300 dark:border-blue-900/40", + filteraddrule: "py-3 px-5", + filteraddrulebutton: { + root: "justify-center w-full min-[0px]:text-sm", + label: "flex-auto grow-0", + icon: "mr-2", + }, + filterremovebutton: { + root: "ml-2", + label: "grow-0", + }, + filterbuttonbar: "flex items-center justify-between p-5", + filterclearbutton: { + root: "w-auto min-[0px]:text-sm border-blue-500 text-blue-500 px-4 py-3", + }, + filterapplybutton: { + root: "w-auto min-[0px]:text-sm px-4 py-3", + }, + filtermenubutton: ({ context }) => ({ + className: cn( + "inline-flex justify-center items-center cursor-pointer no-underline overflow-hidden relative ml-2", + "w-8 h-8 rounded-[50%]", + "transition duration-200", + "hover:text-slate-700 hover:bg-gray-300/20", // Hover + "focus:outline-0 focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)]", // Focus + "dark:text-white/70 dark:hover:text-white/80 dark:bg-gray-900", // Dark Mode + { + "bg-blue-50 text-blue-700": context.active, + }, + ), + }), + headerfilterclearbutton: ({ context }) => ({ + className: cn( + "inline-flex justify-center items-center cursor-pointer no-underline overflow-hidden relative", + "text-left bg-transparent m-0 p-0 border-none select-none ml-2", + { + invisible: !context.hidden, + }, + ), + }), + columnresizer: + "block absolute top-0 right-0 m-0 w-2 h-full p-0 cursor-col-resize border border-transparent", + rowreordericon: "cursor-move", + roweditorinitbutton: { + className: cn( + "inline-flex items-center justify-center overflow-hidden relative", + "text-left cursor-pointer select-none", + "w-8 h-8 border-0 rounded-[50%]", + "transition duration-200", + "text-slate-700 border-transparent", + "focus:outline-0 focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)]", //Focus + "hover:text-slate-700 hover:bg-gray-300/20", //Hover + "dark:text-white/70", // Dark Mode + ), + }, + roweditorsavebutton: { + className: cn( + "inline-flex items-center justify-center overflow-hidden relative", + "text-left cursor-pointer select-none", + "w-8 h-8 border-0 rounded-[50%]", + "transition duration-200", + "text-slate-700 border-transparent", + "focus:outline-0 focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)]", //Focus + "hover:text-slate-700 hover:bg-gray-300/20", //Hover + "dark:text-white/70", // Dark Mode + ), + }, + roweditorcancelbutton: { + className: cn( + "inline-flex items-center justify-center overflow-hidden relative", + "text-left cursor-pointer select-none", + "w-8 h-8 border-0 rounded-[50%]", + "transition duration-200", + "text-slate-700 border-transparent", + "focus:outline-0 focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)]", //Focus + "hover:text-slate-700 hover:bg-gray-300/20", //Hover + "dark:text-white/70", // Dark Mode + ), + }, + radioButton: { + className: cn( + "relative inline-flex cursor-pointer select-none align-bottom", + "w-6 h-6", + ), + }, + radioButtonInput: { + className: cn( + "w-full h-full top-0 left-0 absolute appearance-none select-none", + "p-0 m-0 opacity-0 z-[1] rounded-[50%] outline-none", + "cursor-pointer peer", + ), + }, + radioButtonBox: ({ context }) => ({ + className: cn( + "flex items-center justify-center", + "h-6 w-6 rounded-full border-2 text-gray-700 transition duration-200 ease-in-out", + context.checked + ? "border-blue-500 bg-blue-500 dark:border-blue-400 dark:bg-blue-400 peer-hover:bg-blue-700 peer-hover:border-blue-700" + : "border-gray-300 bg-white dark:border-blue-900/40 dark:bg-gray-900 peer-hover:border-blue-500", + { + "hover:border-blue-500 focus:shadow-input-focus focus:outline-none focus:outline-offset-0 dark:hover:border-blue-400 dark:focus:shadow-[inset_0_0_0_0.2rem_rgba(147,197,253,0.5)]": + !context.disabled, + "cursor-default opacity-60": context.disabled, + }, + ), + }), + radioButtonIcon: ({ context }) => ({ + className: cn( + "transform rounded-full", + "block h-3 w-3 bg-white transition duration-200 dark:bg-gray-900", + { + "backface-hidden scale-10 invisible": context.checked === false, + "visible scale-100 transform": context.checked === true, + }, + ), + }), + headercheckboxwrapper: { + className: cn( + "cursor-pointer inline-flex relative select-none align-bottom", + "w-6 h-6", + ), + }, + headercheckbox: ({ context }) => ({ + className: cn( + "flex items-center justify-center", + "border-2 w-6 h-6 text-gray-600 rounded-lg transition-colors duration-200", + context.checked + ? "border-blue-500 bg-blue-500 dark:border-blue-400 dark:bg-blue-400" + : "border-gray-300 bg-white dark:border-blue-900/40 dark:bg-gray-900", + { + "hover:border-blue-500 dark:hover:border-blue-400 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[inset_0_0_0_0.2rem_rgba(147,197,253,0.5)]": + !context.disabled, + "cursor-default opacity-60": context.disabled, + }, + ), + }), + headercheckboxicon: + "w-4 h-4 transition-all duration-200 text-white text-base dark:text-gray-900", + checkboxwrapper: { + className: cn( + "cursor-pointer inline-flex relative select-none align-bottom", + "w-6 h-6", + ), + }, + checkbox: ({ context }) => ({ + className: cn( + "flex items-center justify-center", + "border-2 w-6 h-6 text-gray-600 rounded-lg transition-colors duration-200", + context.checked + ? "border-blue-500 bg-blue-500 dark:border-blue-400 dark:bg-blue-400" + : "border-gray-300 bg-white dark:border-blue-900/40 dark:bg-gray-900", + { + "hover:border-blue-500 dark:hover:border-blue-400 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[inset_0_0_0_0.2rem_rgba(147,197,253,0.5)]": + !context.disabled, + "cursor-default opacity-60": context.disabled, + }, + ), + }), + checkboxicon: + "w-4 h-4 transition-all duration-200 text-white text-base dark:text-gray-900", + transition: TRANSITIONS.overlay, + }, + bodyrow: ({ context }) => ({ + className: cn( + context.selected + ? "bg-blue-50 text-blue-700 dark:bg-blue-300" + : "bg-white text-foreground dark:bg-gray-900", + context.stripedRows && + (context.index % 2 === 0 + ? "bg-white text-foreground dark:bg-gray-900" + : "bg-muted/50 text-foreground dark:bg-gray-950"), + "transition duration-200", + "focus:outline focus:outline-[0.15rem] focus:outline-blue-200 focus:outline-offset-[-0.15rem]", // Focus + "dark:text-white/80 dark:focus:outline dark:focus:outline-[0.15rem] dark:focus:outline-blue-300 dark:focus:outline-offset-[-0.15rem]", // Dark Mode + { + "cursor-pointer": context.selectable, + "hover:bg-gray-300/20 hover:text-gray-600": + context.selectable && !context.selected, // Hover + }, + ), + }), + rowexpansion: "bg-white text-gray-600 dark:bg-gray-900 dark:text-white/80", + rowgroupheader: { + className: cn( + "sticky z-[1]", + "bg-white text-gray-600", + "transition duration-200", + ), + }, + rowgroupfooter: { + className: cn( + "sticky z-[1]", + "bg-white text-gray-600", + "transition duration-200", + ), + }, + rowgrouptoggler: { + className: cn( + "text-left m-0 p-0 cursor-pointer select-none", + "inline-flex items-center justify-center overflow-hidden relative", + "w-8 h-8 text-gray-500 border-0 bg-transparent rounded-[50%]", + "transition duration-200", + "dark:text-white/70", // Dark Mode + ), + }, + rowgrouptogglericon: "inline-block w-4 h-4", + resizehelper: "absolute hidden w-px z-10 bg-blue-500 dark:bg-blue-300", +}; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index a6dbb40..59afa9f 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,6 +7,7 @@ module.exports = { "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", + "./node_modules/primereact/**/*.{js,ts,jsx,tsx}", ], prefix: "", theme: {