refactor login form

This commit is contained in:
2024-07-13 19:15:21 +02:00
parent 7c562e8057
commit fd076517dc
10 changed files with 241 additions and 185 deletions

Binary file not shown.

View File

@@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "^5.0.19", "@fontsource/inter": "^5.0.19",
"@hookform/resolvers": "^3.9.0",
"@it-incubator/prettier-config": "^0.1.2", "@it-incubator/prettier-config": "^0.1.2",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
@@ -33,6 +34,7 @@
"lucide-react": "^0.400.0", "lucide-react": "^0.400.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@@ -0,0 +1,32 @@
import { Input, type InputProps } from "@/components/ui";
import {
type Control,
type FieldValues,
type UseControllerProps,
useController,
} from "react-hook-form";
type Props<T extends FieldValues> = Omit<
UseControllerProps<T>,
"control" | "defaultValue" | "rules"
> &
Omit<InputProps, "value" | "onChange"> & { control: Control<T> };
export const FormInput = <T extends FieldValues>({
control,
name,
disabled,
shouldUnregister,
...rest
}: Props<T>) => {
const {
field,
fieldState: { error },
} = useController({
control,
name,
disabled,
shouldUnregister,
});
return <Input errorMessage={error?.message} {...field} {...rest} />;
};

View File

@@ -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<T extends FieldValues> = Omit<
UseControllerProps<T>,
"control" | "defaultValue" | "rules"
> &
Omit<ComponentPropsWithoutRef<typeof Select>, "value" | "onValueChange"> & {
control: Control<T>;
};
export const FormSelect = <T extends FieldValues>({
control,
name,
disabled,
shouldUnregister,
...rest
}: Props<T>) => {
const {
field: { onChange, ...field },
} = useController({
control,
name,
disabled,
shouldUnregister,
});
return <Select {...field} {...rest} onValueChange={onChange} />;
};

View File

@@ -0,0 +1,2 @@
export * from "./form-select";
export * from "./form-input";

View File

@@ -4,6 +4,7 @@ export * from "./data-table-pagination";
export * from "./dialog"; export * from "./dialog";
export * from "./dropdown-menu"; export * from "./dropdown-menu";
export * from "./input"; export * from "./input";
export * from "./form";
export * from "./label"; export * from "./label";
export * from "./mode-toggle"; export * from "./mode-toggle";
export * from "./select"; export * from "./select";

View File

@@ -1,21 +1,33 @@
import * as React from "react"; import { Label } from "@/components/ui/label";
import { useAutoId } from "@/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type InputHTMLAttributes, forwardRef } from "react";
export interface InputProps export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
extends React.InputHTMLAttributes<HTMLInputElement> {} label?: string;
errorMessage?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, id, errorMessage, ...props }, ref) => {
const finalId = useAutoId(id);
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
return ( return (
<input <div className={"grid gap-1.5"}>
className={cn( {!!label && <Label htmlFor={finalId}>{label}</Label>}
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", <input
className, id={finalId}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
{!!errorMessage && (
<p className="text-red-500 text-xs">{errorMessage}</p>
)} )}
ref={ref} </div>
{...props}
/>
); );
}, },
); );

View File

@@ -0,0 +1 @@
export * from "./use-auto-id";

View File

@@ -0,0 +1,6 @@
import { useId } from 'react'
export const useAutoId = (id?: string) => {
const generatedId = useId()
return id ?? generatedId
}

View File

@@ -6,9 +6,9 @@ import {
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
Input, FormInput,
FormSelect,
Label, Label,
Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
@@ -16,30 +16,122 @@ import {
ToggleGroup, ToggleGroup,
ToggleGroupItem, ToggleGroupItem,
} from "@/components/ui"; } from "@/components/ui";
import { useLoginMutation } from "@/services/db"; import { type LoginArgs, useLoginMutation } from "@/services/db";
import { useSessionStore } from "@/state/db-session-store"; import { useSessionStore } from "@/state/db-session-store";
import { zodResolver } from "@hookform/resolvers/zod";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { type FormEventHandler, useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { type Control, useForm } from "react-hook-form";
import { z } from "zod";
export const Route = createFileRoute("/auth/login")({ export const Route = createFileRoute("/auth/login")({
component: LoginForm, 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<LoginWithConnectionStringFields>({
resolver: zodResolver(loginWithConnectionStringSchema),
defaultValues: {
type: "postgres",
connectionString: "",
},
});
return ( return (
<div className="grid gap-2"> <form
<Label htmlFor="dbType">Database type</Label> className="grid gap-2"
<Select defaultValue={"postgres"} name={"type"}> onSubmit={handleSubmit(onSubmit)}
<SelectTrigger className="w-full" id={"dbType"}> id={"login-form"}
<SelectValue /> >
</SelectTrigger> <DatabaseTypeSelector control={control} />
<SelectContent> <FormInput
<SelectItem value="postgres">Postgres</SelectItem> label={"Connection string"}
<SelectItem value="mysql">MySQL</SelectItem> name={"connectionString"}
</SelectContent> control={control}
</Select> placeholder={"postgres://postgres:postgres@localhost:5432/postgres"}
</div> />
</form>
);
}
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<LoginWithConnectionFields>({
resolver: zodResolver(loginWithConnectionFieldsSchema),
defaultValues: {
type: "postgres",
host: "",
port: "",
username: "",
password: "",
ssl: "prefer",
database: "",
},
});
return (
<form
className="grid gap-3"
onSubmit={handleSubmit(onSubmit)}
id={"login-form"}
>
<DatabaseTypeSelector control={control} />
<FormInput
name={"host"}
control={control}
label={"Host"}
placeholder={"127.0.0.1"}
/>
<FormInput name={"port"} control={control} label={"Port"} />
<FormInput name={"username"} control={control} label={"User"} />
<FormInput name={"password"} control={control} label={"Password"} />
<FormInput name={"database"} control={control} label={"Database"} />
<div className="grid gap-2">
<Label htmlFor="ssl">SSL mode</Label>
<FormSelect control={control} name={"ssl"}>
<SelectTrigger className="w-full" id={"ssl"}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="false">false</SelectItem>
<SelectItem value="true">true</SelectItem>
<SelectItem value="require">require</SelectItem>
<SelectItem value="allow">allow</SelectItem>
<SelectItem value="prefer">prefer</SelectItem>
<SelectItem value="verify-full">verify-full</SelectItem>
</SelectContent>
</FormSelect>
</div>
</form>
); );
} }
@@ -49,78 +141,9 @@ function LoginForm() {
const { mutateAsync } = useLoginMutation(); const { mutateAsync } = useLoginMutation();
const addSession = useSessionStore.use.addSession(); const addSession = useSessionStore.use.addSession();
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => { const onSubmit = async (args: LoginArgs) => {
e.preventDefault(); await mutateAsync(args);
const formData = new FormData(e.currentTarget); addSession(args);
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;
}
}; };
return ( return (
@@ -133,11 +156,7 @@ function LoginForm() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form <div className="grid gap-4">
className="grid gap-4"
id={"login-form"}
onSubmit={handleSubmit}
>
<ToggleGroup <ToggleGroup
type="single" type="single"
className="w-full border gap-0.5 rounded-md" className="w-full border gap-0.5 rounded-md"
@@ -158,85 +177,11 @@ function LoginForm() {
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
{connectionMethod === "fields" ? ( {connectionMethod === "fields" ? (
<> <ConnectionFieldsForm onSubmit={onSubmit} />
<DatabaseTypeSelector />
<div className="grid gap-2">
<Label htmlFor="host">Host</Label>
<Input
id="host"
name="host"
type="text"
required
placeholder={"127.0.0.1"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="port">Port</Label>
<Input
id="port"
name="port"
type="text"
required
defaultValue={"5432"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="username">User</Label>
<Input id="username" name="username" type="text" required />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
name="password"
id="password"
type="password"
required
placeholder={"********"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="database">Database</Label>
<Input
name="database"
id="database"
type="text"
defaultValue={"postgres"}
placeholder={"postgres"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="ssl">SSL mode</Label>
<Select defaultValue={"false"} name={"ssl"}>
<SelectTrigger className="w-full" id={"ssl"}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="false">false</SelectItem>
<SelectItem value="true">true</SelectItem>
<SelectItem value="require">require</SelectItem>
<SelectItem value="allow">allow</SelectItem>
<SelectItem value="prefer">prefer</SelectItem>
<SelectItem value="verify-full">verify-full</SelectItem>
</SelectContent>
</Select>
</div>
</>
) : ( ) : (
<div className="grid gap-2"> <ConnectionStringForm onSubmit={onSubmit} />
<DatabaseTypeSelector />
<Label htmlFor="connectionString">Connection string</Label>
<Input
name="connectionString"
id="connectionString"
type="text"
required
placeholder={
"postgres://postgres:postgres@localhost:5432/postgres"
}
/>
</div>
)} )}
</form> </div>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Button className="w-full" form={"login-form"}> <Button className="w-full" form={"login-form"}>
@@ -247,3 +192,24 @@ function LoginForm() {
</div> </div>
); );
} }
function DatabaseTypeSelector({
control,
}: {
control: Control<any>;
}) {
return (
<div className="grid gap-2">
<Label htmlFor="dbType">Database type</Label>
<FormSelect control={control} name={"type"}>
<SelectTrigger className="w-full" id={"dbType"}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="postgres">Postgres</SelectItem>
<SelectItem value="mysql">MySQL</SelectItem>
</SelectContent>
</FormSelect>
</div>
);
}