initial commit

This commit is contained in:
2024-07-07 00:48:39 +02:00
commit cc7d7d71b9
49 changed files with 3017 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import {
ModeToggle,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
buttonVariants,
} from "@/components/ui";
import { cn } from "@/lib/utils";
import { useDatabasesListQuery, useTablesListQuery } from "@/services/db";
import {
Link,
Outlet,
createRootRoute,
useNavigate,
useParams,
} 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() {
const { data } = useDatabasesListQuery();
const params = useParams({ strict: false });
const dbName = params.dbName ?? "";
const navigate = useNavigate({ from: Route.fullPath });
const handleSelectedSchema = (schema: string) => {
void navigate({ to: "/db/$dbName/tables", params: { dbName: schema } });
};
const { data: tables } = useTablesListQuery({ dbName });
return (
<>
<div className="h-screen grid grid-rows-layout grid-cols-layout">
<header className="p-2 flex gap-2 border-b items-center col-span-full">
<ModeToggle />
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
</header>
<aside className={"p-3"}>
<Select value={dbName} onValueChange={handleSelectedSchema}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Database Schema" />
</SelectTrigger>
<SelectContent>
{data?.map((schema) => {
return (
<SelectItem value={schema} key={schema}>
{schema}
</SelectItem>
);
})}
</SelectContent>
</Select>
<nav className="flex flex-col gap-1 mt-4">
{dbName && (
<Link
to={"/db/$dbName/tables"}
params={{ dbName }}
activeOptions={{ exact: true }}
className={cn(
"flex items-center gap-2 rounded py-1.5 pl-1.5",
"hover:bg-muted",
"[&.active]:bg-muted [&.active]:font-semibold",
)}
>
<Database className={"size-4"} /> {dbName}
</Link>
)}
{tables?.map((table) => {
return (
<div
key={table.table_name}
className={cn(
"flex items-center gap-2 px-2.5 rounded py-1.5 justify-between w-full",
)}
>
<Link
className={cn(
"w-full flex gap-2 items-center",
"hover:underline",
"[&.active]:font-semibold",
)}
to={"/db/$dbName/tables/$tableName"}
params={{ tableName: table.table_name, dbName: dbName }}
>
<Table2 className={"size-4 shrink-0"} />
{table.table_name}
</Link>
<Link
className={cn(
"hover:underline shrink-0",
buttonVariants({ variant: "ghost", size: "iconSm" }),
"[&.active]:bg-muted",
)}
title={"Explore Data"}
aria-label={"Explore Data"}
to={"/db/$dbName/tables/$tableName/data"}
params={{ tableName: table.table_name, dbName: dbName }}
search={{ pageIndex: 0, pageSize: 10 }}
>
<Rows3 className={"size-4 shrink-0"} />
</Link>
</div>
);
})}
</nav>
</aside>
<Outlet />
</div>
<TanStackRouterDevtools />
</>
);
}

View File

@@ -0,0 +1,41 @@
import { DataTable } from "@/components/db-table-view/data-table";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { z } from "zod";
const tableSearchSchema = z.object({
pageSize: z.number().catch(10),
pageIndex: z.number().catch(0),
});
export const Route = createFileRoute("/db/$dbName/tables/$tableName/data")({
component: TableView,
validateSearch: (search) => tableSearchSchema.parse(search),
});
function TableView() {
const { tableName, dbName } = Route.useParams();
const { pageSize, pageIndex } = Route.useSearch();
const navigate = useNavigate({ from: Route.fullPath });
const updatePageSize = (value: number) => {
return void navigate({
search: (prev) => ({ ...prev, pageSize: value, pageIndex: 0 }),
});
};
const updatePageIndex = (pageIndex: number) => {
return void navigate({ search: (prev) => ({ ...prev, pageIndex }) });
};
return (
<div className="p-3 h-layout w-layout">
<DataTable
dbName={dbName}
tableName={tableName}
pageSize={pageSize}
pageIndex={pageIndex}
onPageIndexChange={updatePageIndex}
onPageSizeChange={updatePageSize}
/>
</div>
);
}

View File

@@ -0,0 +1,161 @@
import { DataTable } from "@/components/ui/data-table";
import { cn } from "@/lib/utils";
import {
type TableColumn,
type TableForeignKey,
type TableIndexEntry,
useTableColumnsQuery,
useTableForeignKeysQuery,
useTableIndexesQuery,
} from "@/services/db";
import { Link, createFileRoute } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { ArrowRight, Table2 } from "lucide-react";
export const Route = createFileRoute("/db/$dbName/tables/$tableName/")({
component: TableDetailsTable,
});
const columnHelper = createColumnHelper<TableColumn>();
const columns = [
columnHelper.accessor("column_name", {
header: "Column",
}),
columnHelper.accessor("data_type", {
header: "Type",
}),
columnHelper.accessor("udt_name", {
header: "UDT",
}),
columnHelper.accessor("column_comment", {
header: "Comment",
}),
];
const tableIndexesColumnHelper = createColumnHelper<TableIndexEntry>();
const tableIndexesColumns = [
tableIndexesColumnHelper.accessor("type", {
header: "Type",
}),
tableIndexesColumnHelper.accessor("columns", {
header: "Columns",
cell: (props) => props.getValue().join(", "),
}),
tableIndexesColumnHelper.accessor("key", {
header: "Key",
}),
];
const tableForeignKeysColumnHelper = createColumnHelper<TableForeignKey>();
const tableForeignKeysColumns = [
tableForeignKeysColumnHelper.accessor("source", {
header: "Source",
cell: (props) => props.getValue().join(", "),
}),
tableForeignKeysColumnHelper.accessor("target", {
header: "Target",
cell: (props) => {
const { table, target } = props.row.original;
return (
<Link
from={Route.fullPath}
to={"../$tableName"}
params={{ tableName: table }}
className={"hover:underline"}
>
{table} ({target.join(", ")})
</Link>
);
},
}),
tableForeignKeysColumnHelper.accessor("on_delete", {
header: "On Delete",
}),
tableForeignKeysColumnHelper.accessor("on_update", {
header: "On Update",
}),
];
function TableDetailsTable() {
const { tableName: name } = Route.useParams();
const { data: tableColumns } = useTableColumnsQuery({ name });
const { data: tableIndexes } = useTableIndexesQuery({ name });
const { data: tableForeignKeys } = useTableForeignKeysQuery({ name });
return (
<div className={"p-3 w-layout"}>
<div className={"flex flex-col gap-4 flex-1 max-h-full pb-3"}>
<div className={"flex gap-4 items-center justify-between"}>
<h1 className={"text-2xl font-bold flex items-center gap-2"}>
<Table2 /> {name}
</h1>
<Link
from={Route.fullPath}
to={"./data"}
search={{
pageIndex: 0,
pageSize: 10,
}}
className={cn("flex gap-2 items-center", "hover:underline")}
>
Explore data <ArrowRight />
</Link>
</div>
<div className={"flex gap-4 items-center justify-between"}>
<h2 className={"text-xl font-bold flex items-center gap-2"}>
Columns
</h2>
</div>
<div
className={
"overflow-auto rounded-md border min-h-0 h-full min-w-0 w-full"
}
>
<DataTable columns={columns} data={tableColumns ?? []} />
</div>
<div className={"flex gap-4 items-center justify-between"}>
<h2 className={"text-xl font-bold flex items-center gap-2"}>
Indexes
</h2>
</div>
<div
className={
"overflow-auto rounded-md border min-h-0 h-full min-w-0 w-full"
}
>
{tableIndexes?.length ? (
<DataTable
columns={tableIndexesColumns}
data={tableIndexes ?? []}
/>
) : (
<div className="text-center p-4">No indexes.</div>
)}
</div>
<div className={"flex gap-4 items-center justify-between"}>
<h2 className={"text-xl font-bold flex items-center gap-2"}>
Foreign Keys
</h2>
</div>
<div
className={
"overflow-auto rounded-md border min-h-0 h-full min-w-0 w-full"
}
>
{tableForeignKeys?.length ? (
<DataTable
columns={tableForeignKeysColumns}
data={tableForeignKeys ?? []}
/>
) : (
<div className="text-center p-4">No foreign keys.</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,271 @@
import {
Button,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui";
import { cn, prettyBytes } from "@/lib/utils";
import { type TableInfo, useTablesListQuery } from "@/services/db";
import { Link, createFileRoute } from "@tanstack/react-router";
import {
type SortingState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUp, Database } from "lucide-react";
import { useMemo, useState } from "react";
export const Route = createFileRoute("/db/$dbName/tables/")({
component: Component,
});
const columnHelper = createColumnHelper<TableInfo>();
const createColumns = (dbName: string) => {
return [
columnHelper.accessor("table_name", {
header: "Name",
cell: (props) => {
const tableName = props.getValue();
return (
<div className={"flex w-full"}>
<Link
className={"hover:underline w-full"}
to={"/db/$dbName/tables/$tableName"}
params={{ dbName, tableName }}
>
{tableName}
</Link>
</div>
);
},
}),
columnHelper.accessor("table_size", {
header: "Table Size",
cell: (props) => prettyBytes(props.getValue()),
meta: {
className: "text-end",
},
}),
columnHelper.accessor("index_size", {
header: "Index Size",
cell: (props) => prettyBytes(props.getValue()),
meta: {
className: "text-end",
},
}),
columnHelper.accessor("total_size", {
header: "Total Size",
cell: (props) => prettyBytes(props.getValue()),
meta: {
className: "text-end",
},
}),
columnHelper.accessor("row_count", {
header: "Rows",
meta: {
className: "text-end",
},
}),
columnHelper.accessor("primary_key", {
header: "Primary key",
}),
columnHelper.accessor("indexes", {
header: "Indexes",
cell: (props) => {
const indexes = props.getValue();
if (!indexes) return null;
return (
<div>
{indexes?.split(",").map((index) => (
<div key={index}>{index}</div>
))}
</div>
);
},
}),
columnHelper.accessor("comments", {
header: "Comment",
}),
columnHelper.accessor("schema_name", {
header: "Schema",
}),
];
};
function Component() {
const { dbName } = Route.useParams();
const [sorting, setSorting] = useState<SortingState>([]);
const { data } = useTablesListQuery({
dbName,
sortDesc: sorting[0]?.desc,
sortField: sorting[0]?.id,
});
const details = useMemo(() => {
if (!data) {
return [];
}
return data;
}, [data]);
const columns = useMemo(() => createColumns(dbName), [dbName]);
const table = useReactTable({
data: details,
columns,
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
state: {
sorting,
},
onSortingChange: setSorting,
});
return (
<div className={"p-3 h-layout w-layout"}>
<div className={"flex flex-col gap-4 flex-1 max-h-full pb-3"}>
<div className={"flex gap-4 items-center justify-between"}>
<h1 className={"text-2xl font-bold flex items-center gap-2"}>
<Database /> {dbName}
</h1>
<p>
Tables: <strong>{details?.length ?? 0}</strong>
</p>
</div>
<div
className={
"overflow-auto rounded-md border min-h-0 h-full min-w-0 w-full"
}
>
<Table
className={"min-w-full"}
{...{
style: {
width: table.getCenterTotalSize(),
},
}}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const sorted = header.column.getIsSorted();
if (header.column.getCanSort()) {
return (
<TableHead
key={header.id}
className={cn(
"p-0",
header.column.columnDef.meta?.className,
)}
>
<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",
sorted && "opacity-100",
(sorted as string) === "desc" && "rotate-180",
)}
/>
</Button>
</TableHead>
);
}
return (
<TableHead
className={cn(
"text-nowrap",
header.column.columnDef.meta?.className,
)}
key={header.id}
style={{
width: header.getSize(),
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell
className={cn(
"text-nowrap",
cell.column?.columnDef?.meta?.className,
)}
key={cell.id}
style={{
width: cell.column.getSize(),
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { createFileRoute } from '@tanstack/react-router'
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>
)
}