mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 05:09:26 +00:00
add sorting to table data
This commit is contained in:
@@ -32,12 +32,14 @@ const app = new Elysia({ prefix: "/api" })
|
||||
"databases/:dbName/tables/:tableName/data",
|
||||
async ({ params, query }) => {
|
||||
const { tableName, dbName } = params;
|
||||
const { perPage = "50", page = "0" } = query;
|
||||
const { perPage = "50", page = "0", sortField, sortDesc } = query;
|
||||
return getTableData({
|
||||
tableName,
|
||||
dbName,
|
||||
perPage: Number.parseInt(perPage, 10),
|
||||
page: Number.parseInt(page, 10),
|
||||
sortField,
|
||||
sortDesc: sortDesc === "true",
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -294,11 +296,15 @@ async function getTableData({
|
||||
dbName,
|
||||
perPage,
|
||||
page,
|
||||
sortDesc,
|
||||
sortField,
|
||||
}: {
|
||||
tableName: string;
|
||||
dbName: string;
|
||||
perPage: number;
|
||||
page: number;
|
||||
sortField?: string;
|
||||
sortDesc?: boolean;
|
||||
}) {
|
||||
const offset = (perPage * page).toString();
|
||||
const rows = sql`
|
||||
@@ -308,7 +314,13 @@ async function getTableData({
|
||||
const tables = sql`
|
||||
SELECT *
|
||||
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]);
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {
|
||||
Button,
|
||||
DataTablePagination,
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -13,12 +18,14 @@ import {
|
||||
type ColumnDef,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Rows3 } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ArrowUp, Rows3 } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
function isUrl(value: string) {
|
||||
@@ -48,12 +55,17 @@ export const DataTable = ({
|
||||
onPageIndexChange: (pageIndex: number) => void;
|
||||
onPageSizeChange: (pageSize: number) => void;
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
|
||||
const { data: details } = useTableColumnsQuery({ dbName, tableName });
|
||||
const { data } = useTableDataQuery({
|
||||
tableName,
|
||||
dbName,
|
||||
perPage: pageSize,
|
||||
page: pageIndex,
|
||||
sortDesc: sorting[0]?.desc,
|
||||
sortField: sorting[0]?.id,
|
||||
});
|
||||
|
||||
const paginationUpdater: OnChangeFn<PaginationState> = (args) => {
|
||||
@@ -137,8 +149,13 @@ export const DataTable = ({
|
||||
columns,
|
||||
columnResizeMode: "onChange",
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
rowCount: data?.count ?? 0,
|
||||
manualSorting: true,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
pagination: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
@@ -154,6 +171,30 @@ export const DataTable = ({
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Rows3 /> {tableName}
|
||||
</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>
|
||||
Rows: <strong>{data?.count}</strong>
|
||||
</p>
|
||||
@@ -172,20 +213,41 @@ export const DataTable = ({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const sorted = header.column.getIsSorted();
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
className={"relative"}
|
||||
className={"p-0 relative"}
|
||||
key={header.id}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
title={
|
||||
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
|
||||
{...{
|
||||
onDoubleClick: () => header.column.resetSize(),
|
||||
|
||||
@@ -27,7 +27,10 @@ if (rootElement && !rootElement.innerHTML) {
|
||||
<StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<ReactQueryDevtools
|
||||
initialIsOpen={false}
|
||||
buttonPosition={"top-right"}
|
||||
/>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -18,15 +18,9 @@ import {
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
||||
import { Database, Rows3, Table2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
|
||||
const searchSchema = z.object({
|
||||
dbSchema: z.string().optional().catch(""),
|
||||
});
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: Root,
|
||||
validateSearch: (search) => searchSchema.parse(search),
|
||||
});
|
||||
|
||||
function Root() {
|
||||
@@ -36,8 +30,8 @@ function Root() {
|
||||
const dbName = params.dbName ?? "";
|
||||
const navigate = useNavigate({ from: Route.fullPath });
|
||||
|
||||
const handleSelectedSchema = (schema: string) => {
|
||||
void navigate({ to: "/db/$dbName/tables", params: { dbName: schema } });
|
||||
const handleSelectedDb = (dbName: string) => {
|
||||
void navigate({ to: "/db/$dbName/tables", params: { dbName } });
|
||||
};
|
||||
|
||||
const { data: tables } = useTablesListQuery({ dbName });
|
||||
@@ -52,15 +46,15 @@ function Root() {
|
||||
</Link>
|
||||
</header>
|
||||
<aside className={"p-3"}>
|
||||
<Select value={dbName} onValueChange={handleSelectedSchema}>
|
||||
<Select value={dbName} onValueChange={handleSelectedDb}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Database Schema" />
|
||||
<SelectValue placeholder="Database" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data?.map((schema) => {
|
||||
{data?.map((db) => {
|
||||
return (
|
||||
<SelectItem value={schema} key={schema}>
|
||||
{schema}
|
||||
<SelectItem value={db} key={db}>
|
||||
{db}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -194,7 +194,7 @@ function Component() {
|
||||
)}
|
||||
<ArrowUp
|
||||
className={cn(
|
||||
"ml-2 size-4 opacity-0",
|
||||
"ml-2 size-4 opacity-0 transition-transform",
|
||||
sorted && "opacity-100",
|
||||
(sorted as string) === "desc" && "rotate-180",
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
});
|
||||
|
||||
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 (
|
||||
<div className="p-2">
|
||||
<h3>Table View</h3>
|
||||
{dbSchema}
|
||||
<div className="p-2 h-layout w-layout grid place-items-center">
|
||||
<div>
|
||||
<h1 className={"text-xl text-center font-semibold"}>Welcome!</h1>
|
||||
<p>Select a database to continue.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { dbService } from "./db.service";
|
||||
import type {
|
||||
@@ -13,7 +13,6 @@ export const useDatabasesListQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: [DB_QUERY_KEYS.DATABASES.ALL],
|
||||
queryFn: () => dbService.getDatabasesList(),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,7 +21,6 @@ export const useTablesListQuery = (args: GetTablesListArgs) => {
|
||||
queryKey: [DB_QUERY_KEYS.TABLES.ALL, args],
|
||||
queryFn: () => dbService.getTablesList(args),
|
||||
enabled: !!args.dbName,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,10 +27,17 @@ class DbService {
|
||||
.json<GetTablesListResponse>();
|
||||
}
|
||||
|
||||
getTableData({ dbName, tableName, page, perPage }: GetTableDataArgs) {
|
||||
getTableData({
|
||||
dbName,
|
||||
tableName,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortDesc,
|
||||
}: GetTableDataArgs) {
|
||||
return dbInstance
|
||||
.get(`api/databases/${dbName}/tables/${tableName}/data`, {
|
||||
searchParams: getValuable({ perPage, page }),
|
||||
searchParams: getValuable({ perPage, page, sortField, sortDesc }),
|
||||
})
|
||||
.json<GetTableDataResponse>();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export type GetTableDataArgs = {
|
||||
dbName: string;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
sortField?: string;
|
||||
sortDesc?: boolean;
|
||||
};
|
||||
|
||||
export type GetTableDataResponse = {
|
||||
|
||||
Reference in New Issue
Block a user