move things around

This commit is contained in:
2024-07-14 13:52:15 +02:00
parent 709ac0296d
commit 1aee971c7c
7 changed files with 261 additions and 144 deletions

View File

@@ -0,0 +1,41 @@
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui";
import type { Table } from "@tanstack/react-table";
export function ColumnsDropdown<T>({
table,
}: {
table: Table<T>;
}) {
return (
<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>
);
}

View File

@@ -0,0 +1,99 @@
import { cn, isImageUrl, isUrl } from "@/lib/utils";
import { type TableColumns, useTableColumnsQuery } from "@/services/db";
import { useSettingsStore } from "@/state";
import type { ColumnDef } from "@tanstack/react-table";
import { useMemo } from "react";
const buildColumns = ({
columns,
formatDates,
showImagesPreview,
}: {
columns?: TableColumns;
formatDates: boolean;
showImagesPreview: boolean;
}): ColumnDef<any>[] => {
if (!columns) return [] as ColumnDef<any>[];
return columns.map(({ column_name, udt_name, data_type }) => ({
accessorKey: column_name,
title: column_name,
size: 300,
header: () => {
return (
<div
className={cn(
"whitespace-nowrap ",
data_type === "integer" && "text-right",
)}
>
{`${column_name} [${udt_name.toUpperCase()}]`}
</div>
);
},
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue(column_name) as any;
let finalValue = value;
if (
formatDates &&
["timestamp", "datetime"].includes(udt_name.toLowerCase())
) {
finalValue = new Date(value as string).toLocaleString();
}
if (showImagesPreview && typeof value === "string" && isUrl(value)) {
const isImage = isImageUrl(value);
return (
<a
href={value}
target={"_blank"}
className={cn("hover:underline")}
rel="noreferrer"
>
<div
className={"flex items-center justify-between break-all gap-4"}
>
{value}
{isImage && (
<img
src={value}
alt={"preview"}
className="size-20 object-cover"
/>
)}
</div>
</a>
);
}
return (
<div
className={cn(
"break-all",
["integer", "int", "tinyint", "double"].includes(data_type) &&
"text-right",
)}
>
{finalValue}
</div>
);
},
})) as ColumnDef<any>[];
};
export const useColumns = ({
dbName,
tableName,
}: { dbName: string; tableName: string }) => {
const { data: details } = useTableColumnsQuery({ dbName, tableName });
const formatDates = useSettingsStore.use.formatDates();
const showImagesPreview = useSettingsStore.use.showImagesPreview();
return useMemo(() => {
return buildColumns({
columns: details,
formatDates,
showImagesPreview,
});
}, [details, formatDates, showImagesPreview]);
};

View File

@@ -2,21 +2,9 @@ import {
TableScrollContainer,
TableView,
} from "@/components/db-table-view/table";
import { DataTablePagination } from "@/components/ui";
import { useTableDataQuery } from "@/services/db";
import {
Button,
DataTablePagination,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
FormInput,
Tooltip,
} 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,
@@ -24,9 +12,14 @@ import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Info, Rows3, Search } from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { Rows3 } from "lucide-react";
import { useCallback, useState } from "react";
import { useColumns } from "./columns";
import { ColumnsDropdown } from "./columns-dropdown";
import {
WhereClauseForm,
type WhereClauseFormValues,
} from "./where-clause-form";
export const DataTable = ({
tableName,
@@ -43,14 +36,11 @@ export const DataTable = ({
onPageIndexChange: (pageIndex: number) => void;
onPageSizeChange: (pageSize: number) => void;
}) => {
const whereQueryForm = useForm<{ whereQuery: string }>();
const formatDates = useSettingsStore.use.formatDates();
const showImagesPreview = useSettingsStore.use.showImagesPreview();
const columns = useColumns({ dbName, tableName });
const [whereQuery, setWhereQuery] = useState("");
const [sorting, setSorting] = useState<SortingState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const { data: details } = useTableColumnsQuery({ dbName, tableName });
const { data, refetch } = useTableDataQuery({
whereQuery,
tableName,
@@ -60,6 +50,18 @@ export const DataTable = ({
sortDesc: sorting[0]?.desc,
sortField: sorting[0]?.id,
});
const handleWhereClauseFormSubmit = useCallback(
({ whereClause }: WhereClauseFormValues) => {
if (whereClause === whereQuery) {
void refetch();
return;
}
setWhereQuery(whereClause);
},
[whereQuery, refetch],
);
const paginationUpdater: OnChangeFn<PaginationState> = (args) => {
if (typeof args === "function") {
const newArgs = args({
@@ -77,74 +79,6 @@ export const DataTable = ({
}
};
const columns = useMemo(() => {
if (!details) return [] as ColumnDef<any>[];
return details.map(({ column_name, udt_name, data_type }) => ({
accessorKey: column_name,
title: column_name,
size: 300,
header: () => {
return (
<div
className={cn(
"whitespace-nowrap ",
data_type === "integer" && "text-right",
)}
>
{`${column_name} [${udt_name.toUpperCase()}]`}
</div>
);
},
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue(column_name) as any;
let finalValue = value;
if (
formatDates &&
["timestamp", "datetime"].includes(udt_name.toLowerCase())
) {
finalValue = new Date(value as string).toLocaleString();
}
if (showImagesPreview && typeof value === "string" && isUrl(value)) {
const isImage = isImageUrl(value);
return (
<a
href={value}
target={"_blank"}
className={cn("hover:underline")}
rel="noreferrer"
>
<div
className={"flex items-center justify-between break-all gap-4"}
>
{value}
{isImage && (
<img
src={value}
alt={"preview"}
className="size-20 object-cover"
/>
)}
</div>
</a>
);
}
return (
<div
className={cn(
"break-all",
["integer", "int", "tinyint", "double"].includes(data_type) &&
"text-right",
)}
>
{finalValue}
</div>
);
},
})) as ColumnDef<any>[];
}, [details, formatDates, showImagesPreview]);
const table = useReactTable({
data: data?.data ?? [],
columns,
@@ -171,62 +105,9 @@ export const DataTable = ({
<div className={"flex gap-4 items-center justify-between"}>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Rows3 /> {tableName}
<form
className={"flex gap-2"}
onSubmit={whereQueryForm.handleSubmit((data) => {
if (data.whereQuery === whereQuery) {
void refetch();
return;
}
setWhereQuery(data.whereQuery);
})}
>
<FormInput
name={"whereQuery"}
control={whereQueryForm.control}
placeholder="WHERE clause"
/>
<Button variant={"outline"} size={"icon"} type={"submit"}>
<Search className={"size-[1.2rem] shrink-0"} />
</Button>
<Tooltip
content={
<div>
Your input will be prefixed with WHERE and appended to the raw
SQL query.
<div>You can use AND, OR, NOT, BETWEEN, LIKE, =, etc.</div>
<div>To remove the WHERE clause, submit an empty input.</div>
</div>
}
>
<Info className={"size-4 shrink-0 cursor-help"} />
</Tooltip>
</form>
<WhereClauseForm onSubmit={handleWhereClauseFormSubmit} />
</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>
<ColumnsDropdown table={table} />
<p>
Rows: <strong>{data?.count}</strong>
</p>

View File

@@ -0,0 +1,44 @@
import { Button, FormInput, Tooltip } from "@/components/ui";
import { Info, Search } from "lucide-react";
import { useForm } from "react-hook-form";
export interface WhereClauseFormValues {
whereClause: string;
}
export function WhereClauseForm({
onSubmit,
}: {
onSubmit: (values: WhereClauseFormValues) => void;
}) {
const { control, handleSubmit } = useForm<WhereClauseFormValues>({
defaultValues: {
whereClause: "",
},
});
return (
<form
className="flex gap-2 "
onSubmit={handleSubmit(onSubmit)}
id={"where-clause-form"}
>
<FormInput name={"whereClause"} control={control} />
<Button variant={"outline"} size={"icon"} type={"submit"}>
<Search className={"size-[1.2rem] shrink-0"} />
</Button>
<Tooltip
content={
<div>
Your input will be prefixed with WHERE and appended to the raw SQL
query.
<div>You can use AND, OR, NOT, BETWEEN, LIKE, =, etc.</div>
<div>To remove the WHERE clause, submit an empty input.</div>
</div>
}
>
<Info className={"size-4 shrink-0 cursor-help"} />
</Tooltip>
</form>
);
}

View File

@@ -0,0 +1,23 @@
import { cleanEmptyParams } from "@/lib/utils";
import {
type RegisteredRouter,
type RouteIds,
getRouteApi,
useNavigate,
} from "@tanstack/react-router";
export function useFilters<T extends RouteIds<RegisteredRouter["routeTree"]>>(
routeId: T,
) {
const routeApi = getRouteApi<T>(routeId);
const navigate = useNavigate();
const filters = routeApi.useSearch();
const setFilters = (partialFilters: Partial<typeof filters>) =>
navigate({
search: (prev) => cleanEmptyParams({ ...prev, ...partialFilters }),
});
const resetFilters = () => navigate({ search: {} });
return { filters, setFilters, resetFilters };
}

View File

@@ -0,0 +1,2 @@
export const DEFAULT_PAGE_INDEX = 0;
export const DEFAULT_PAGE_SIZE = 10;

View File

@@ -1,3 +1,4 @@
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "@/lib/constants";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
@@ -42,3 +43,29 @@ const imageUrlRegex = new RegExp(
export function isImageUrl(value: string) {
return value.match(imageUrlRegex);
}
export const cleanEmptyParams = <T extends Record<string, unknown>>(
search: T,
) => {
const newSearch = { ...search };
for (const key of Object.keys(search)) {
const value = newSearch[key];
if (
value === undefined ||
value === "" ||
(typeof value === "number" && Number.isNaN(value))
)
delete newSearch[key];
}
if (search.pageIndex === DEFAULT_PAGE_INDEX) {
// @ts-ignore
newSearch.pageIndex = undefined;
}
if (search.pageSize === DEFAULT_PAGE_SIZE) {
// @ts-ignore
newSearch.pageSize = undefined;
}
return newSearch;
};