mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 12:33:05 +00:00
refactor login form
This commit is contained in:
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
32
frontend/src/components/ui/form/form-input.tsx
Normal file
32
frontend/src/components/ui/form/form-input.tsx
Normal 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} />;
|
||||||
|
};
|
||||||
34
frontend/src/components/ui/form/form-select.tsx
Normal file
34
frontend/src/components/ui/form/form-select.tsx
Normal 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} />;
|
||||||
|
};
|
||||||
2
frontend/src/components/ui/form/index.ts
Normal file
2
frontend/src/components/ui/form/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./form-select";
|
||||||
|
export * from "./form-input";
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
1
frontend/src/hooks/index.ts
Normal file
1
frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./use-auto-id";
|
||||||
6
frontend/src/hooks/use-auto-id.ts
Normal file
6
frontend/src/hooks/use-auto-id.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
export const useAutoId = (id?: string) => {
|
||||||
|
const generatedId = useId()
|
||||||
|
return id ?? generatedId
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user