add sorting to table data

This commit is contained in:
2024-07-07 13:10:31 +02:00
parent 5d1658c522
commit c1a31640a3
9 changed files with 117 additions and 49 deletions

View File

@@ -32,12 +32,14 @@ const app = new Elysia({ prefix: "/api" })
"databases/:dbName/tables/:tableName/data", "databases/:dbName/tables/:tableName/data",
async ({ params, query }) => { async ({ params, query }) => {
const { tableName, dbName } = params; const { tableName, dbName } = params;
const { perPage = "50", page = "0" } = query; const { perPage = "50", page = "0", sortField, sortDesc } = query;
return getTableData({ return getTableData({
tableName, tableName,
dbName, dbName,
perPage: Number.parseInt(perPage, 10), perPage: Number.parseInt(perPage, 10),
page: Number.parseInt(page, 10), page: Number.parseInt(page, 10),
sortField,
sortDesc: sortDesc === "true",
}); });
}, },
) )
@@ -294,11 +296,15 @@ async function getTableData({
dbName, dbName,
perPage, perPage,
page, page,
sortDesc,
sortField,
}: { }: {
tableName: string; tableName: string;
dbName: string; dbName: string;
perPage: number; perPage: number;
page: number; page: number;
sortField?: string;
sortDesc?: boolean;
}) { }) {
const offset = (perPage * page).toString(); const offset = (perPage * page).toString();
const rows = sql` const rows = sql`
@@ -308,7 +314,13 @@ async function getTableData({
const tables = sql` const tables = sql`
SELECT * SELECT *
FROM ${sql(dbName)}.${sql(tableName)} FROM ${sql(dbName)}.${sql(tableName)}
LIMIT ${perPage} OFFSET ${offset}`; ${
sortField
? sql`ORDER BY ${sql(sortField)} ${sortDesc ? sql`DESC` : sql`ASC`}`
: sql``
}
LIMIT ${perPage} OFFSET ${offset}
`;
const [[count], data] = await Promise.all([rows, tables]); const [[count], data] = await Promise.all([rows, tables]);

View File

@@ -1,5 +1,10 @@
import { import {
Button,
DataTablePagination, DataTablePagination,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
@@ -13,12 +18,14 @@ import {
type ColumnDef, type ColumnDef,
type OnChangeFn, type OnChangeFn,
type PaginationState, type PaginationState,
type SortingState,
type VisibilityState,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Rows3 } from "lucide-react"; import { ArrowUp, Rows3 } from "lucide-react";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { z } from "zod"; import { z } from "zod";
function isUrl(value: string) { function isUrl(value: string) {
@@ -48,12 +55,17 @@ export const DataTable = ({
onPageIndexChange: (pageIndex: number) => void; onPageIndexChange: (pageIndex: number) => void;
onPageSizeChange: (pageSize: number) => void; onPageSizeChange: (pageSize: number) => void;
}) => { }) => {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const { data: details } = useTableColumnsQuery({ dbName, tableName }); const { data: details } = useTableColumnsQuery({ dbName, tableName });
const { data } = useTableDataQuery({ const { data } = useTableDataQuery({
tableName, tableName,
dbName, dbName,
perPage: pageSize, perPage: pageSize,
page: pageIndex, page: pageIndex,
sortDesc: sorting[0]?.desc,
sortField: sorting[0]?.id,
}); });
const paginationUpdater: OnChangeFn<PaginationState> = (args) => { const paginationUpdater: OnChangeFn<PaginationState> = (args) => {
@@ -137,8 +149,13 @@ export const DataTable = ({
columns, columns,
columnResizeMode: "onChange", columnResizeMode: "onChange",
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onColumnVisibilityChange: setColumnVisibility,
rowCount: data?.count ?? 0, rowCount: data?.count ?? 0,
manualSorting: true,
onSortingChange: setSorting,
state: { state: {
sorting,
columnVisibility,
pagination: { pagination: {
pageIndex, pageIndex,
pageSize, pageSize,
@@ -154,6 +171,30 @@ export const DataTable = ({
<h1 className="text-2xl font-bold flex items-center gap-2"> <h1 className="text-2xl font-bold flex items-center gap-2">
<Rows3 /> {tableName} <Rows3 /> {tableName}
</h1> </h1>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
onSelect={(e) => e.preventDefault()}
checked={column.getIsVisible()}
onCheckedChange={column.toggleVisibility}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<p> <p>
Rows: <strong>{data?.count}</strong> Rows: <strong>{data?.count}</strong>
</p> </p>
@@ -172,20 +213,41 @@ export const DataTable = ({
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
const sorted = header.column.getIsSorted();
return ( return (
<TableHead <TableHead
className={"relative"} className={"p-0 relative"}
key={header.id} key={header.id}
style={{ style={{
width: header.getSize(), width: header.getSize(),
}} }}
> >
{header.isPlaceholder <Button
? null variant="ghost"
: flexRender( onClick={header.column.getToggleSortingHandler()}
header.column.columnDef.header, title={
header.getContext(), header.column.getNextSortingOrder() === "asc"
? "Sort ascending"
: header.column.getNextSortingOrder() === "desc"
? "Sort descending"
: "Clear sort"
}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
<ArrowUp
className={cn(
"ml-2 size-4 opacity-0 transition-transform",
sorted && "opacity-100",
(sorted as string) === "desc" && "rotate-180",
)} )}
/>
</Button>
<div <div
{...{ {...{
onDoubleClick: () => header.column.resetSize(), onDoubleClick: () => header.column.resetSize(),

View File

@@ -27,7 +27,10 @@ if (rootElement && !rootElement.innerHTML) {
<StrictMode> <StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools
initialIsOpen={false}
buttonPosition={"top-right"}
/>
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryClientProvider> </QueryClientProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -18,15 +18,9 @@ import {
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools"; import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { Database, Rows3, Table2 } from "lucide-react"; import { Database, Rows3, Table2 } from "lucide-react";
import { z } from "zod";
const searchSchema = z.object({
dbSchema: z.string().optional().catch(""),
});
export const Route = createRootRoute({ export const Route = createRootRoute({
component: Root, component: Root,
validateSearch: (search) => searchSchema.parse(search),
}); });
function Root() { function Root() {
@@ -36,8 +30,8 @@ function Root() {
const dbName = params.dbName ?? ""; const dbName = params.dbName ?? "";
const navigate = useNavigate({ from: Route.fullPath }); const navigate = useNavigate({ from: Route.fullPath });
const handleSelectedSchema = (schema: string) => { const handleSelectedDb = (dbName: string) => {
void navigate({ to: "/db/$dbName/tables", params: { dbName: schema } }); void navigate({ to: "/db/$dbName/tables", params: { dbName } });
}; };
const { data: tables } = useTablesListQuery({ dbName }); const { data: tables } = useTablesListQuery({ dbName });
@@ -52,15 +46,15 @@ function Root() {
</Link> </Link>
</header> </header>
<aside className={"p-3"}> <aside className={"p-3"}>
<Select value={dbName} onValueChange={handleSelectedSchema}> <Select value={dbName} onValueChange={handleSelectedDb}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Database Schema" /> <SelectValue placeholder="Database" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{data?.map((schema) => { {data?.map((db) => {
return ( return (
<SelectItem value={schema} key={schema}> <SelectItem value={db} key={db}>
{schema} {db}
</SelectItem> </SelectItem>
); );
})} })}

View File

@@ -194,7 +194,7 @@ function Component() {
)} )}
<ArrowUp <ArrowUp
className={cn( className={cn(
"ml-2 size-4 opacity-0", "ml-2 size-4 opacity-0 transition-transform",
sorted && "opacity-100", sorted && "opacity-100",
(sorted as string) === "desc" && "rotate-180", (sorted as string) === "desc" && "rotate-180",
)} )}

View File

@@ -1,26 +1,16 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute('/')({ export const Route = createFileRoute("/")({
component: Index, component: Index,
}) });
function Index() { function Index() {
const { dbSchema } = Route.useSearch()
if (!dbSchema) {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
)
}
return <TableView dbSchema={dbSchema} />
}
function TableView({ dbSchema }: { dbSchema: string }) {
return ( return (
<div className="p-2"> <div className="p-2 h-layout w-layout grid place-items-center">
<h3>Table View</h3> <div>
{dbSchema} <h1 className={"text-xl text-center font-semibold"}>Welcome!</h1>
<p>Select a database to continue.</p>
</div>
</div> </div>
) );
} }

View File

@@ -1,4 +1,4 @@
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { DB_QUERY_KEYS } from "./db.query-keys"; import { DB_QUERY_KEYS } from "./db.query-keys";
import { dbService } from "./db.service"; import { dbService } from "./db.service";
import type { import type {
@@ -13,7 +13,6 @@ export const useDatabasesListQuery = () => {
return useQuery({ return useQuery({
queryKey: [DB_QUERY_KEYS.DATABASES.ALL], queryKey: [DB_QUERY_KEYS.DATABASES.ALL],
queryFn: () => dbService.getDatabasesList(), queryFn: () => dbService.getDatabasesList(),
placeholderData: keepPreviousData,
}); });
}; };
@@ -22,7 +21,6 @@ export const useTablesListQuery = (args: GetTablesListArgs) => {
queryKey: [DB_QUERY_KEYS.TABLES.ALL, args], queryKey: [DB_QUERY_KEYS.TABLES.ALL, args],
queryFn: () => dbService.getTablesList(args), queryFn: () => dbService.getTablesList(args),
enabled: !!args.dbName, enabled: !!args.dbName,
placeholderData: keepPreviousData,
}); });
}; };

View File

@@ -27,10 +27,17 @@ class DbService {
.json<GetTablesListResponse>(); .json<GetTablesListResponse>();
} }
getTableData({ dbName, tableName, page, perPage }: GetTableDataArgs) { getTableData({
dbName,
tableName,
page,
perPage,
sortField,
sortDesc,
}: GetTableDataArgs) {
return dbInstance return dbInstance
.get(`api/databases/${dbName}/tables/${tableName}/data`, { .get(`api/databases/${dbName}/tables/${tableName}/data`, {
searchParams: getValuable({ perPage, page }), searchParams: getValuable({ perPage, page, sortField, sortDesc }),
}) })
.json<GetTableDataResponse>(); .json<GetTableDataResponse>();
} }

View File

@@ -26,6 +26,8 @@ export type GetTableDataArgs = {
dbName: string; dbName: string;
perPage?: number; perPage?: number;
page?: number; page?: number;
sortField?: string;
sortDesc?: boolean;
}; };
export type GetTableDataResponse = { export type GetTableDataResponse = {