mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 12:33:05 +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",
|
"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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user