From 7b9f93448b56bd44a3b16a5536e7e76e6fdab4bb Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 8 Jul 2024 12:47:15 +0200 Subject: [PATCH] add raw query editor and response table --- api/biome.json | 5 +- frontend/bun.lockb | Bin 134902 -> 135306 bytes frontend/package.json | 1 + .../components/db-table-view/data-table.tsx | 15 +-- frontend/src/components/ui/index.ts | 3 + frontend/src/components/ui/sonner.tsx | 29 +++++ .../src/components/ui/sql-data-table-cell.tsx | 55 ++++++++ frontend/src/components/ui/sql-data-table.tsx | 123 ++++++++++++++++++ frontend/src/lib/utils.ts | 44 +++++-- frontend/src/main.tsx | 2 + frontend/src/routes/raw/index.tsx | 71 +++++++++- frontend/src/services/db/db.hooks.ts | 21 ++- frontend/src/services/db/db.service.ts | 10 ++ frontend/src/services/db/db.types.ts | 8 ++ 14 files changed, 354 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/ui/sonner.tsx create mode 100644 frontend/src/components/ui/sql-data-table-cell.tsx create mode 100644 frontend/src/components/ui/sql-data-table.tsx diff --git a/api/biome.json b/api/biome.json index ec7a9f9..6f838ae 100644 --- a/api/biome.json +++ b/api/biome.json @@ -6,7 +6,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + } } }, "formatter": { diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 58237039cbc13f040b9ad9032548500390345dcb..5ca93d9b3ea95f527c595e591cd7caf9d2b718ae 100644 GIT binary patch delta 17814 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 delta 17646 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 diff --git a/frontend/package.json b/frontend/package.json index 3360e51..250c409 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "lucide-react": "^0.400.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8", diff --git a/frontend/src/components/db-table-view/data-table.tsx b/frontend/src/components/db-table-view/data-table.tsx index 2e901e7..e8a8d62 100644 --- a/frontend/src/components/db-table-view/data-table.tsx +++ b/frontend/src/components/db-table-view/data-table.tsx @@ -12,7 +12,7 @@ import { TableHeader, TableRow, } from "@/components/ui"; -import { cn } from "@/lib/utils"; +import { cn, isImageUrl, isUrl } from "@/lib/utils"; import { useTableColumnsQuery, useTableDataQuery } from "@/services/db"; import { useSettingsStore } from "@/state"; import { @@ -27,19 +27,6 @@ import { } from "@tanstack/react-table"; import { ArrowUp, Rows3 } from "lucide-react"; import { useMemo, useState } from "react"; -import { z } from "zod"; - -function isUrl(value: string) { - return z.string().url().safeParse(value).success; -} - -const imageUrlRegex = new RegExp( - /(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|svg|webp|bmp))/i, -); - -function isImageUrl(value: string) { - return value.match(imageUrlRegex); -} export const DataTable = ({ tableName, diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index c67c98f..eba1ad7 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -6,5 +6,8 @@ export * from "./input"; export * from "./label"; export * from "./mode-toggle"; export * from "./select"; +export * from "./sonner"; +export * from "./sql-data-table"; +export * from "./sql-data-table-cell"; export * from "./switch"; export * from "./table"; diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx new file mode 100644 index 0000000..243106a --- /dev/null +++ b/frontend/src/components/ui/sonner.tsx @@ -0,0 +1,29 @@ +import { useUiStore } from "@/state"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const theme = useUiStore.use.theme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/frontend/src/components/ui/sql-data-table-cell.tsx b/frontend/src/components/ui/sql-data-table-cell.tsx new file mode 100644 index 0000000..0f5dfad --- /dev/null +++ b/frontend/src/components/ui/sql-data-table-cell.tsx @@ -0,0 +1,55 @@ +import { cn, isImageUrl, isUrl } from "@/lib/utils"; +import { useSettingsStore } from "@/state"; +import type { Row } from "@tanstack/react-table"; +import { memo } from "react"; + +type Props = { + row: Row; + column_name: string; + isDate?: boolean; + isNumber?: boolean; +}; + +function RawSqlDataTableCell({ + isNumber, + row, + column_name, + isDate, +}: Props) { + const formatDates = useSettingsStore.use.formatDates(); + const showImagesPreview = useSettingsStore.use.showImagesPreview(); + const value = row.getValue(column_name) as any; + let finalValue = value; + if (formatDates && isDate) { + finalValue = new Date(value as string).toLocaleString(); + } + if (showImagesPreview && typeof value === "string" && isUrl(value)) { + const isImage = isImageUrl(value); + return ( + +
+ {value} + {isImage && ( + {"preview"} + )} +
+
+ ); + } + return ( +
+ {finalValue} +
+ ); +} + +const SqlDataTableCell = memo(RawSqlDataTableCell); + +SqlDataTableCell.displayName = "SqlDataTableCell"; + +export { SqlDataTableCell }; diff --git a/frontend/src/components/ui/sql-data-table.tsx b/frontend/src/components/ui/sql-data-table.tsx new file mode 100644 index 0000000..bde257c --- /dev/null +++ b/frontend/src/components/ui/sql-data-table.tsx @@ -0,0 +1,123 @@ +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; +import { flexRender } from "@tanstack/react-table"; +import type { Table as TableType } from "@tanstack/react-table"; +import { ArrowUp } from "lucide-react"; +import { type ComponentPropsWithoutRef, memo } from "react"; + +type Props = { + table: TableType; + columnsLength: number; +} & ComponentPropsWithoutRef; + +function RawSqlDataTable({ + table, + columnsLength, + style, + ...rest +}: Props) { + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const sorted = header.column.getIsSorted(); + + return ( + + +
header.column.resetSize(), + onMouseDown: header.getResizeHandler(), + onTouchStart: header.getResizeHandler(), + className: `resizer ${header.column.getIsResizing() ? "isResizing" : ""}`, + }} + /> + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ ); +} + +const SqlDataTable = memo(RawSqlDataTable); + +SqlDataTable.displayName = "SqlDataTable"; + +export { SqlDataTable }; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index b19bf57..27c565d 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,24 +1,44 @@ -import { type ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } -type Valuable = { [K in keyof T as T[K] extends null | undefined ? never : K]: T[K] } +type Valuable = { + [K in keyof T as T[K] extends null | undefined ? never : K]: T[K]; +}; export function getValuable>(obj: T): V { return Object.fromEntries( Object.entries(obj).filter( - ([, v]) => !((typeof v === 'string' && !v.length) || v === null || typeof v === 'undefined') - ) - ) as V + ([, v]) => + !( + (typeof v === "string" && !v.length) || + v === null || + typeof v === "undefined" + ), + ), + ) as V; } export function prettyBytes(bytes: number): string { - const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] - if (bytes === 0) return '0 B' - const i = Math.floor(Math.log(bytes) / Math.log(1024)) - const size = bytes / Math.pow(1024, i) - return `${size.toFixed(2)} ${units[i]}` + const units = ["B", "kB", "MB", "GB", "TB", "PB"]; + if (bytes === 0) return "0 B"; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const size = bytes / 1024 ** i; + return `${size.toFixed(2)} ${units[i]}`; +} + +export function isUrl(value: string) { + return z.string().url().safeParse(value).success; +} + +const imageUrlRegex = new RegExp( + /(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|svg|webp|bmp))/i, +); + +export function isImageUrl(value: string) { + return value.match(imageUrlRegex); } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c92c514..5154e6d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -8,6 +8,7 @@ import "@fontsource/inter/600.css"; import "@fontsource/inter/700.css"; import "@fontsource/inter/800.css"; import "./index.css"; +import { Toaster } from "@/components/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Import the generated route tree @@ -35,6 +36,7 @@ if (rootElement && !rootElement.innerHTML) { initialIsOpen={false} buttonPosition={"bottom-left"} /> + , diff --git a/frontend/src/routes/raw/index.tsx b/frontend/src/routes/raw/index.tsx index 358690b..2aad6d0 100644 --- a/frontend/src/routes/raw/index.tsx +++ b/frontend/src/routes/raw/index.tsx @@ -1,16 +1,77 @@ +import { Button, SqlDataTable, SqlDataTableCell } from "@/components/ui"; +import { useQueryRawSqlMutation } from "@/services/db"; +import { useUiStore } from "@/state"; import Editor from "@monaco-editor/react"; import { createFileRoute } from "@tanstack/react-router"; +import { + type ColumnDef, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ArrowRight } from "lucide-react"; +import { useMemo, useState } from "react"; export const Route = createFileRoute("/raw/")({ component: Component, }); function Component() { + const [query, setQuery] = useState("SELECT * FROM"); + const { mutate, data } = useQueryRawSqlMutation(); + const appTheme = useUiStore.use.theme(); + const themeToUse = appTheme === "light" ? "light" : "vs-dark"; + + const handleSubmit = () => { + if (!query) return; + + mutate({ query }); + }; + const columns = useMemo[]>(() => { + if (!data) return [] as ColumnDef[]; + + return Object.keys(data[0]).map((key) => ({ + header: key, + accessorKey: key, + cell: ({ row }) => , + })) as ColumnDef[]; + }, [data]); + + const table = useReactTable({ + data: data ?? [], + columns, + getCoreRowModel: getCoreRowModel(), + }); + return ( - +
+
+ +
+ {data &&

Result: {data.length} rows

} + +
+ {data && ( +
+ +
+ )} +
+
); } diff --git a/frontend/src/services/db/db.hooks.ts b/frontend/src/services/db/db.hooks.ts index 90aa1b0..4e2c000 100644 --- a/frontend/src/services/db/db.hooks.ts +++ b/frontend/src/services/db/db.hooks.ts @@ -1,4 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { HTTPError } from "ky"; +import { toast } from "sonner"; import { DB_QUERY_KEYS } from "./db.query-keys"; import { dbService } from "./db.service"; import type { @@ -7,6 +9,7 @@ import type { GetTableForeignKeysArgs, GetTableIndexesArgs, GetTablesListArgs, + QueryRawSqlArgs, } from "./db.types"; export const useDatabasesListQuery = () => { @@ -75,3 +78,19 @@ export const useTableForeignKeysQuery = (args: GetTableForeignKeysArgs) => { enabled: !!args.tableName && !!args.dbName, }); }; + +export const useQueryRawSqlMutation = () => { + return useMutation({ + onError: async (error) => { + if (error instanceof HTTPError) { + const errorJson = await error.response.json(); + console.log(errorJson); + toast.error(errorJson.message); + return; + } + toast.error(error.message); + }, + mutationFn: ({ query }: QueryRawSqlArgs) => + dbService.queryRawSql({ query }), + }); +}; diff --git a/frontend/src/services/db/db.service.ts b/frontend/src/services/db/db.service.ts index f2a3fc8..8e3b1a1 100644 --- a/frontend/src/services/db/db.service.ts +++ b/frontend/src/services/db/db.service.ts @@ -9,6 +9,8 @@ import type { GetTableIndexesArgs, GetTablesListArgs, GetTablesListResponse, + QueryRawSqlArgs, + QueryRawSqlResponse, TableColumns, TableForeignKeys, TableIndexes, @@ -59,6 +61,14 @@ class DbService { .get(`api/databases/${dbName}/tables/${tableName}/foreign-keys`) .json(); } + + queryRawSql({ query }: QueryRawSqlArgs) { + return dbInstance + .post("api/raw", { + json: { query }, + }) + .json(); + } } export const dbService = new DbService(); diff --git a/frontend/src/services/db/db.types.ts b/frontend/src/services/db/db.types.ts index 3371b54..27fc082 100644 --- a/frontend/src/services/db/db.types.ts +++ b/frontend/src/services/db/db.types.ts @@ -69,6 +69,13 @@ export type GetTableForeignKeysArgs = { tableName: string; dbName: string; }; + +export type QueryRawSqlArgs = { + query: string; +}; + +export type QueryRawSqlResponse = Array>; + export type TableForeignKey = { conname: string; deferrable: boolean; @@ -80,4 +87,5 @@ export type TableForeignKey = { on_delete: string; on_update: string; }; + export type TableForeignKeys = TableForeignKey[];