From 7c562e8057642ee7187ad0736b65723c3da3857f Mon Sep 17 00:00:00 2001 From: andres Date: Sat, 13 Jul 2024 12:59:07 +0200 Subject: [PATCH 1/4] remove prime ng and fix table layout. Fix select --- frontend/bun.lockb | Bin 138157 -> 138157 bytes .../components/db-table-view/data-table.tsx | 193 +++++++++--------- .../{ => sidebar}/session-selector.tsx | 6 +- frontend/src/components/sidebar/sidebar.tsx | 119 +++++++++++ frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/table.tsx | 8 +- frontend/src/index.css | 4 +- frontend/src/routes/__root.tsx | 115 +---------- 8 files changed, 234 insertions(+), 213 deletions(-) rename frontend/src/components/{ => sidebar}/session-selector.tsx (90%) create mode 100644 frontend/src/components/sidebar/sidebar.tsx diff --git a/frontend/bun.lockb b/frontend/bun.lockb index c031b9245b4934b0ff82f804d0a3089665a4d4ec..d57c95af6dbcbbeb76fb6b8963b2abd2aa79d79f 100644 GIT binary patch delta 25 hcmZ3xon!5Gj)pCa`ibm}afW(^dIsCg5*f>P0RVON2yy@b delta 25 dcmZ3xon!5Gj)pCa`ibmJ3}CR`ERnH%7XWEG2PXgk diff --git a/frontend/src/components/db-table-view/data-table.tsx b/frontend/src/components/db-table-view/data-table.tsx index 9421b9d..e0d7ae7 100644 --- a/frontend/src/components/db-table-view/data-table.tsx +++ b/frontend/src/components/db-table-view/data-table.tsx @@ -5,7 +5,6 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, - ScrollArea, Table, TableBody, TableCell, @@ -167,7 +166,7 @@ export const DataTable = ({ }); return ( -
+

{tableName} @@ -201,103 +200,105 @@ export const DataTable = ({

- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const sorted = header.column.getIsSorted(); +
+
+
+ + {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" : ""}`, + return ( + - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} + > + +
header.column.resetSize(), + onMouseDown: header.getResizeHandler(), + onTouchStart: header.getResizeHandler(), + className: `resizer ${header.column.getIsResizing() ? "isResizing" : ""}`, + }} + /> + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
-
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + + +
+
); diff --git a/frontend/src/components/session-selector.tsx b/frontend/src/components/sidebar/session-selector.tsx similarity index 90% rename from frontend/src/components/session-selector.tsx rename to frontend/src/components/sidebar/session-selector.tsx index 7a648e8..54966f9 100644 --- a/frontend/src/components/session-selector.tsx +++ b/frontend/src/components/sidebar/session-selector.tsx @@ -49,8 +49,10 @@ function RawSessionSelector() { value={currentSessionId ? currentSessionId.toString() : ""} onValueChange={handleSessionSelected} > - - + + + + {mappedSessions} diff --git a/frontend/src/components/sidebar/sidebar.tsx b/frontend/src/components/sidebar/sidebar.tsx new file mode 100644 index 0000000..ae3bbb0 --- /dev/null +++ b/frontend/src/components/sidebar/sidebar.tsx @@ -0,0 +1,119 @@ +import { SessionSelector } from "@/components/sidebar/session-selector"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + buttonVariants, +} from "@/components/ui"; +import { cn } from "@/lib/utils"; +import { Route } from "@/routes/__root"; +import { useDatabasesListQuery, useTablesListQuery } from "@/services/db"; +import { useUiStore } from "@/state"; +import { Link, useNavigate, useParams } from "@tanstack/react-router"; +import { Database, Rows3 } from "lucide-react"; +import type { PropsWithChildren } from "react"; + +function SidebarContent() { + const { data } = useDatabasesListQuery(); + + const showSidebar = useUiStore.use.showSidebar(); + const params = useParams({ strict: false }); + const dbName = params.dbName ?? ""; + const { data: tables } = useTablesListQuery({ dbName }); + const navigate = useNavigate({ from: Route.fullPath }); + + const handleSelectedDb = (dbName: string) => { + void navigate({ to: "/db/$dbName/tables", params: { dbName } }); + }; + if (!showSidebar) return null; + + return ( + <> + + + + + ); +} + +function SidebarContainer({ children }: PropsWithChildren) { + return ; +} + +export function Sidebar() { + return ( + + + + ); +} diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index 3cfdb52..95ec012 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -21,7 +21,7 @@ const SelectTrigger = forwardRef< span]:line-clamp-1", + "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className, )} {...props} diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx index 2ad2e0b..a7b09ae 100644 --- a/frontend/src/components/ui/table.tsx +++ b/frontend/src/components/ui/table.tsx @@ -8,7 +8,7 @@ import { const Table = forwardRef>( ({ className, ...props }, ref) => ( -
+
>(({ className, ...props }, ref) => ( - + )); TableHeader.displayName = "TableHeader"; diff --git a/frontend/src/index.css b/frontend/src/index.css index 0943dc8..7841e0c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -74,11 +74,11 @@ text-underline-position: under; --sidebar-width: 264px; } - + .sidebar-closed { --sidebar-width: 0; } - + .grid-rows-layout { grid-template-rows: 60px 1fr; } diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 8e8271d..9869dfd 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,27 +1,11 @@ -import { SessionSelector } from "@/components/session-selector"; import { SettingsDialog } from "@/components/settings-dialog"; -import { - Button, - ModeToggle, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - buttonVariants, -} from "@/components/ui"; +import { Sidebar } from "@/components/sidebar/sidebar"; +import { Button, ModeToggle } from "@/components/ui"; import { cn } from "@/lib/utils"; -import { useDatabasesListQuery, useTablesListQuery } from "@/services/db"; import { useUiStore } from "@/state"; -import { - Link, - Outlet, - createRootRoute, - useNavigate, - useParams, -} from "@tanstack/react-router"; +import { Link, Outlet, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/router-devtools"; -import { Database, PanelLeft, PanelLeftClose, Rows3 } from "lucide-react"; +import { PanelLeft, PanelLeftClose } from "lucide-react"; export const Route = createRootRoute({ component: Root, @@ -31,16 +15,6 @@ function Root() { const showSidebar = useUiStore.use.showSidebar(); const toggleSidebar = useUiStore.use.toggleSidebar(); - const { data } = useDatabasesListQuery(); - const params = useParams({ strict: false }); - const dbName = params.dbName ?? ""; - const navigate = useNavigate({ from: Route.fullPath }); - - const handleSelectedDb = (dbName: string) => { - void navigate({ to: "/db/$dbName/tables", params: { dbName } }); - }; - - const { data: tables } = useTablesListQuery({ dbName }); return ( <>
- - + From fd076517dcf9de7c3ca68a8ae44ed19f938eeb07 Mon Sep 17 00:00:00 2001 From: andres Date: Sat, 13 Jul 2024 19:15:21 +0200 Subject: [PATCH 2/4] refactor login form --- frontend/bun.lockb | Bin 138157 -> 138990 bytes frontend/package.json | 2 + .../src/components/ui/form/form-input.tsx | 32 ++ .../src/components/ui/form/form-select.tsx | 34 ++ frontend/src/components/ui/form/index.ts | 2 + frontend/src/components/ui/index.ts | 1 + frontend/src/components/ui/input.tsx | 38 ++- frontend/src/hooks/index.ts | 1 + frontend/src/hooks/use-auto-id.ts | 6 + frontend/src/routes/auth/login.tsx | 310 ++++++++---------- 10 files changed, 241 insertions(+), 185 deletions(-) create mode 100644 frontend/src/components/ui/form/form-input.tsx create mode 100644 frontend/src/components/ui/form/form-select.tsx create mode 100644 frontend/src/components/ui/form/index.ts create mode 100644 frontend/src/hooks/index.ts create mode 100644 frontend/src/hooks/use-auto-id.ts diff --git a/frontend/bun.lockb b/frontend/bun.lockb index d57c95af6dbcbbeb76fb6b8963b2abd2aa79d79f..1e717ae147a1bc3f1f44f9cf52b6e6d9e72588f5 100644 GIT binary patch delta 23722 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
=EAqZZEjnC06zm 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 delta 23516 zcmeHvd3=q>_y03jE_p&EA*+Olh)76ekwqi-q8iJ!g>dZz35hJSP)kV^E!`}m#=evy zv8HIOwYO-swk{~8Xhj=aH&pfeKF^XHs-KU~@B90_zW?-j<^7yFGiPSbIdf*7=gz&E zn|tgxl-kb<4RY?)Bt80EFKOnO%;i77Hg!+nRKup8xBMqL4<6U-^%n=l)%7+=bak6; z^(~myi7AXErRHX5<)>r~8$UWPKRIidv=vewl2n+Rk~}mYGC5TwsRsDtpfc$A>|sjH zk&vqmelxi0pwo5V$ovs0S=6Sl#s`CvQSDF(%@!(_L*9rOAg5%eWu=Wu&dcixo{YKWE=l++{0=wb z^E_2uj)216g2D@0L9&-Ne4x}p=Q?VI`k>^Edm1fAImOouZ#8}aqzXk3`yK+P}U_pA7Z55Q!gAusn=X^V#Hh~;HxWu@dwQWDCkHHQF| zw&#g<9tB~@h1&{;X5@{fF`@~I;V8V;NF75Xc=GkGKy_?3fs(6MfKqR7fRemEWzRKM zZMluPP5Fm{)OOB*hxZE0gCz+~7p`j}Niesd@BDWHC!%d~paLCHyO$j7i17HjRqhN%Y5)%vjuSNZG6_mK+VMXlfnW&w4S z5~+?&eqKh}5O`!Gc(Sx7D2-h!P_pc9bJdS!8Vy1|DX<6a(AX{orL|!?C>fZm(dZbp zy?UUY1t^e_pay@6Rt5XTsvVVr)Awu=!y(P zZD9x~SyB^}TzAW?(vzSh_nyYj1Er28fRd$IpftZz@`j8~%NQnQc2I}H3QCr?)@T!r zx@lDEP@r1;V|&##UupD^MmKA8u|}tBG+(2GHQE`pHadDvqk$TA)u;iq7Rqn8Q|U#G zeo~+%c4~B&q^>ZlKxvvC=%!AmQcxO_b)b|#N2Ards}|xhmz~`bQaVB z^d)rY3_40Pz`u_gGkEOgj~kUDQQ(jvWynXpg2ES&K@Csrr)s1J+vu!O8KZ}%WsU8x zmZyQzBAGliJ2wk;hCrUio`I4AO$Vy6;{rShqK`Au4au)}WAQL!W${nFb_(!0mToCeUYII+J~bOfa~51~(Ltsq17?fSlap+ln%{oy|vTNtHSv^`r5iY1`l$e#vE<=LRr zd@oRn4KpY;AE=ePY>}&fR%Oejnyn058oTVXW0QE9o0)yggWS!`#M5w}$cx;~@)vk- zdkz)tc#d}rbLK%FW@hGTxEJ#x53{`3C`r9gUX_=$h~+_^X1PmMNotOqDpW&$8C(~o zez<20JHg95%|_R1lGKKWxW}15z{ zy2skpmZXln+$WCB=4B1dhTFAyaDa(r@U#Fk+rx_j%!Z#_`HcXR+y=9Nv|+r&9}WQ* z4Gu1b3l3D2-SLT$DRjwo2)cSPtPKweG_%D#4R>c=6lj)n5z4VBuSRys#o*$=IdIf9 zUIUjz11q<{L?CC$JO}-~4311whQsiMClB^C$#pO{NTdocagSj+yeP;lZ_)DXm8f~K zP9=}ayTQ@eVC?J1+GD~IqYjM;Tq|%0AoR8z98C)~fPMu>mfI^+qPdUC*^$0-0l11V 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 = Omit< + UseControllerProps, + "control" | "defaultValue" | "rules" +> & + Omit & { control: Control }; + +export const FormInput = ({ + control, + name, + disabled, + shouldUnregister, + ...rest +}: Props) => { + const { + field, + fieldState: { error }, + } = useController({ + control, + name, + disabled, + shouldUnregister, + }); + return ; +}; diff --git a/frontend/src/components/ui/form/form-select.tsx b/frontend/src/components/ui/form/form-select.tsx new file mode 100644 index 0000000..64776c0 --- /dev/null +++ b/frontend/src/components/ui/form/form-select.tsx @@ -0,0 +1,34 @@ +import { Select } from "@/components/ui"; +import type { ComponentPropsWithoutRef } from "react"; +import { + type Control, + type FieldValues, + type UseControllerProps, + useController, +} from "react-hook-form"; + +type Props = Omit< + UseControllerProps, + "control" | "defaultValue" | "rules" +> & + Omit, "value" | "onValueChange"> & { + control: Control; + }; + +export const FormSelect = ({ + control, + name, + disabled, + shouldUnregister, + ...rest +}: Props) => { + const { + field: { onChange, ...field }, + } = useController({ + control, + name, + disabled, + shouldUnregister, + }); + return + {!!label && } + + {!!errorMessage && ( +

{errorMessage}

)} - ref={ref} - {...props} - /> + ); }, ); diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..8d8add4 --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-auto-id"; diff --git a/frontend/src/hooks/use-auto-id.ts b/frontend/src/hooks/use-auto-id.ts new file mode 100644 index 0000000..8d0c60d --- /dev/null +++ b/frontend/src/hooks/use-auto-id.ts @@ -0,0 +1,6 @@ +import { useId } from 'react' + +export const useAutoId = (id?: string) => { + const generatedId = useId() + return id ?? generatedId +} diff --git a/frontend/src/routes/auth/login.tsx b/frontend/src/routes/auth/login.tsx index 47e3eaf..3ab04a3 100644 --- a/frontend/src/routes/auth/login.tsx +++ b/frontend/src/routes/auth/login.tsx @@ -6,9 +6,9 @@ import { CardFooter, CardHeader, CardTitle, - Input, + FormInput, + FormSelect, Label, - Select, SelectContent, SelectItem, SelectTrigger, @@ -16,30 +16,122 @@ import { ToggleGroup, ToggleGroupItem, } from "@/components/ui"; -import { useLoginMutation } from "@/services/db"; +import { type LoginArgs, useLoginMutation } from "@/services/db"; import { useSessionStore } from "@/state/db-session-store"; +import { zodResolver } from "@hookform/resolvers/zod"; import { createFileRoute } from "@tanstack/react-router"; -import { type FormEventHandler, useState } from "react"; -import { toast } from "sonner"; +import { useState } from "react"; +import { type Control, useForm } from "react-hook-form"; +import { z } from "zod"; export const Route = createFileRoute("/auth/login")({ component: LoginForm, }); -function DatabaseTypeSelector() { +const loginWithConnectionStringSchema = z.object({ + type: z.enum(["mysql", "postgres"]), + connectionString: z.string().trim().min(1, "Connection string is required"), +}); + +type LoginWithConnectionStringFields = z.infer< + typeof loginWithConnectionStringSchema +>; + +function ConnectionStringForm({ + onSubmit, +}: { + onSubmit: (values: LoginWithConnectionStringFields) => void; +}) { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(loginWithConnectionStringSchema), + defaultValues: { + type: "postgres", + connectionString: "", + }, + }); + return ( -
- - -
+
+ + + + ); +} + +const loginWithConnectionFieldsSchema = z.object({ + type: z.enum(["mysql", "postgres"]), + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required"), + host: z.string().min(1, "Host is required"), + port: z.string().min(1, "Port is required"), + database: z.string().min(1, "Database is required"), + ssl: z.enum(["false", "true", "require", "allow", "prefer", "verify-full"]), +}); +type LoginWithConnectionFields = z.infer< + typeof loginWithConnectionFieldsSchema +>; + +function ConnectionFieldsForm({ + onSubmit, +}: { + onSubmit: (values: LoginWithConnectionFields) => void; +}) { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(loginWithConnectionFieldsSchema), + defaultValues: { + type: "postgres", + host: "", + port: "", + username: "", + password: "", + ssl: "prefer", + database: "", + }, + }); + + return ( +
+ + + + + + +
+ + + + + + + false + true + require + allow + prefer + verify-full + + +
+ ); } @@ -49,78 +141,9 @@ function LoginForm() { const { mutateAsync } = useLoginMutation(); const addSession = useSessionStore.use.addSession(); - const handleSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const connectionString = formData.get("connectionString"); - const type = formData.get("type"); - if (connectionMethod === "connectionString") { - if ( - connectionString != null && - typeof connectionString === "string" && - type != null && - typeof type === "string" - ) { - try { - await mutateAsync({ connectionString, type }); - addSession({ connectionString, type }); - } catch (error) { - console.log(error); - toast.error("Invalid connection string"); - return; - } - } else { - toast.error("Please fill all fields"); - } - return; - } - - const username = formData.get("username"); - const password = formData.get("password"); - const host = formData.get("host"); - const port = formData.get("port"); - const database = formData.get("database"); - const ssl = formData.get("ssl"); - - if ( - database == null || - host == null || - password == null || - port == null || - ssl == null || - type == null || - username == null - ) { - toast.error("Please fill all fields"); - return; - } - if ( - typeof database !== "string" || - typeof host !== "string" || - typeof password !== "string" || - typeof port !== "string" || - typeof ssl !== "string" || - typeof type !== "string" || - typeof username !== "string" - ) { - return; - } - try { - await mutateAsync({ - username, - password, - host, - type, - port, - database, - ssl, - }); - addSession({ username, password, host, type, port, database, ssl }); - } catch (error) { - console.log(error); - toast.error("Invalid connection string"); - return; - } + const onSubmit = async (args: LoginArgs) => { + await mutateAsync(args); + addSession(args); }; return ( @@ -133,11 +156,7 @@ function LoginForm() { -
+
{connectionMethod === "fields" ? ( - <> - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- + ) : ( -
- - - -
+ )} - +
+ + Your input will be prefixed with WHERE and appended to the raw + SQL query. +
You can use AND, OR, NOT, BETWEEN, LIKE, =, etc.
+
To remove the WHERE clause, submit an empty input.
+ + } + > + +
+ @@ -227,6 +262,7 @@ export const DataTable = ({