mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 20:59:23 +00:00
add login support (wonky still)
This commit is contained in:
Binary file not shown.
@@ -20,6 +20,9 @@
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@tanstack/react-query": "^5.50.1",
|
||||
"@tanstack/react-router": "^1.43.12",
|
||||
"@tanstack/react-table": "^8.19.2",
|
||||
|
||||
79
frontend/src/components/ui/card.tsx
Normal file
79
frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./button";
|
||||
export * from "./card";
|
||||
export * from "./data-table-pagination";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
@@ -11,3 +12,6 @@ export * from "./sql-data-table";
|
||||
export * from "./sql-data-table-cell";
|
||||
export * from "./switch";
|
||||
export * from "./table";
|
||||
export * from "./tabs";
|
||||
export * from "./toggle";
|
||||
export * from "./toggle-group";
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
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
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
53
frontend/src/components/ui/tabs.tsx
Normal file
53
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
59
frontend/src/components/ui/toggle-group.tsx
Normal file
59
frontend/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toggleVariants } from "./toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
43
frontend/src/components/ui/toggle.tsx
Normal file
43
frontend/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
@@ -13,6 +13,7 @@
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as RawIndexImport } from './routes/raw/index'
|
||||
import { Route as AuthLoginImport } from './routes/auth/login'
|
||||
import { Route as DbDbNameTablesIndexImport } from './routes/db/$dbName/tables/index'
|
||||
import { Route as DbDbNameTablesTableNameIndexImport } from './routes/db/$dbName/tables/$tableName/index'
|
||||
import { Route as DbDbNameTablesTableNameDataImport } from './routes/db/$dbName/tables/$tableName/data'
|
||||
@@ -29,6 +30,11 @@ const RawIndexRoute = RawIndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AuthLoginRoute = AuthLoginImport.update({
|
||||
path: '/auth/login',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const DbDbNameTablesIndexRoute = DbDbNameTablesIndexImport.update({
|
||||
path: '/db/$dbName/tables/',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -57,6 +63,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/auth/login': {
|
||||
id: '/auth/login'
|
||||
path: '/auth/login'
|
||||
fullPath: '/auth/login'
|
||||
preLoaderRoute: typeof AuthLoginImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/raw/': {
|
||||
id: '/raw/'
|
||||
path: '/raw'
|
||||
@@ -92,6 +105,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export const routeTree = rootRoute.addChildren({
|
||||
IndexRoute,
|
||||
AuthLoginRoute,
|
||||
RawIndexRoute,
|
||||
DbDbNameTablesIndexRoute,
|
||||
DbDbNameTablesTableNameDataRoute,
|
||||
@@ -107,6 +121,7 @@ export const routeTree = rootRoute.addChildren({
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/auth/login",
|
||||
"/raw/",
|
||||
"/db/$dbName/tables/",
|
||||
"/db/$dbName/tables/$tableName/data",
|
||||
@@ -116,6 +131,9 @@ export const routeTree = rootRoute.addChildren({
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/auth/login": {
|
||||
"filePath": "auth/login.tsx"
|
||||
},
|
||||
"/raw/": {
|
||||
"filePath": "raw/index.tsx"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDatabasesListQuery, useTablesListQuery } from "@/services/db";
|
||||
import { useUiStore } from "@/state";
|
||||
import { useSessionStore } from "@/state/db-session-store";
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
@@ -29,6 +30,8 @@ export const Route = createRootRoute({
|
||||
function Root() {
|
||||
const showSidebar = useUiStore.use.showSidebar();
|
||||
const toggleSidebar = useUiStore.use.toggleSidebar();
|
||||
const sessions = useSessionStore.use.sessions();
|
||||
const currentSessionId = useSessionStore.use.currentSessionId();
|
||||
|
||||
const { data } = useDatabasesListQuery();
|
||||
const params = useParams({ strict: false });
|
||||
@@ -40,7 +43,6 @@ function Root() {
|
||||
};
|
||||
|
||||
const { data: tables } = useTablesListQuery({ dbName });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -74,8 +76,33 @@ function Root() {
|
||||
<aside className={"p-3"}>
|
||||
{showSidebar && (
|
||||
<>
|
||||
{sessions.length > 0 && (
|
||||
<Select
|
||||
value={currentSessionId ? currentSessionId.toString() : ""}
|
||||
>
|
||||
<SelectTrigger className="max-w-full">
|
||||
<SelectValue placeholder="Select a Database" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sessions?.map((session) => {
|
||||
const text =
|
||||
"connectionString" in session
|
||||
? session.connectionString
|
||||
: `${session.host}:${session.port}/${session.database}`;
|
||||
return (
|
||||
<SelectItem
|
||||
value={session.id.toString()}
|
||||
key={session.id}
|
||||
>
|
||||
{text}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Select value={dbName} onValueChange={handleSelectedDb}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger className="w-full mt-4">
|
||||
<SelectValue placeholder="Select a Database" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
237
frontend/src/routes/auth/login.tsx
Normal file
237
frontend/src/routes/auth/login.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/components/ui";
|
||||
import { useLoginMutation } from "@/services/db";
|
||||
import { useSessionStore } from "@/state/db-session-store";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { type FormEventHandler, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/auth/login")({
|
||||
component: LoginForm,
|
||||
});
|
||||
|
||||
function LoginForm() {
|
||||
const [connectionMethod, setConnectionMethod] =
|
||||
useState<string>("connectionString");
|
||||
const { mutateAsync } = useLoginMutation();
|
||||
const addSession = useSessionStore.use.addSession();
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const connectionString = formData.get("connectionString");
|
||||
|
||||
if (connectionMethod === "connectionString") {
|
||||
if (connectionString != null && typeof connectionString === "string") {
|
||||
try {
|
||||
await mutateAsync({ connectionString });
|
||||
addSession({ connectionString });
|
||||
} 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 type = formData.get("type");
|
||||
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 (
|
||||
<div className={"flex w-layout h-layout items-center justify-center p-4"}>
|
||||
<Card className="w-full max-w-sm max-h-full overflow-y-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your database credentials below.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
className="grid gap-4"
|
||||
id={"login-form"}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
className="w-full border gap-0.5 rounded-md"
|
||||
value={connectionMethod}
|
||||
onValueChange={setConnectionMethod}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="fields"
|
||||
className={"w-full rounded-r-none"}
|
||||
>
|
||||
Fields
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="connectionString"
|
||||
className={"w-full rounded-l-none"}
|
||||
>
|
||||
Connection string
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
{connectionMethod === "fields" ? (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dbType">Database type</Label>
|
||||
<Select defaultValue={"postgres"} name={"type"}>
|
||||
<SelectTrigger className="w-full" id={"dbType"}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="postgres">Postgres</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<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">
|
||||
<Label htmlFor="connectionString">Connection string</Label>
|
||||
<Input
|
||||
name="connectionString"
|
||||
id="connectionString"
|
||||
type="text"
|
||||
required
|
||||
placeholder={
|
||||
"postgres://postgres:postgres@localhost:5432/postgres"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" form={"login-form"}>
|
||||
Sign in
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,9 +9,14 @@ import type {
|
||||
GetTableForeignKeysArgs,
|
||||
GetTableIndexesArgs,
|
||||
GetTablesListArgs,
|
||||
QueryRawSqlArgs,
|
||||
} from "./db.types";
|
||||
|
||||
export const useLoginMutation = () => {
|
||||
return useMutation({
|
||||
mutationFn: dbService.login,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDatabasesListQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: [DB_QUERY_KEYS.DATABASES.ALL],
|
||||
@@ -90,7 +95,6 @@ export const useQueryRawSqlMutation = () => {
|
||||
}
|
||||
toast.error(error.message);
|
||||
},
|
||||
mutationFn: ({ query }: QueryRawSqlArgs) =>
|
||||
dbService.queryRawSql({ query }),
|
||||
mutationFn: dbService.queryRawSql,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ky from 'ky'
|
||||
import ky from "ky";
|
||||
|
||||
export const dbInstance = ky.create({
|
||||
prefixUrl: 'http://localhost:3000'
|
||||
})
|
||||
credentials: "include",
|
||||
prefixUrl: "http://localhost:3000",
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
GetTableIndexesArgs,
|
||||
GetTablesListArgs,
|
||||
GetTablesListResponse,
|
||||
LoginArgs,
|
||||
LoginResponse,
|
||||
QueryRawSqlArgs,
|
||||
QueryRawSqlResponse,
|
||||
TableColumns,
|
||||
@@ -17,6 +19,12 @@ import type {
|
||||
} from "@/services/db/db.types";
|
||||
|
||||
class DbService {
|
||||
login(data: LoginArgs) {
|
||||
return dbInstance
|
||||
.post("api/auth/login", { json: data })
|
||||
.json<LoginResponse>();
|
||||
}
|
||||
|
||||
getDatabasesList() {
|
||||
return dbInstance.get("api/databases").json<DatabasesResponse>();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
export type LoginArgs =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
type: string;
|
||||
port: string;
|
||||
ssl: string;
|
||||
database: string;
|
||||
}
|
||||
| {
|
||||
connectionString: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export type DatabasesResponse = Array<string>;
|
||||
|
||||
// Tables List
|
||||
|
||||
82
frontend/src/state/db-session-store.ts
Normal file
82
frontend/src/state/db-session-store.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createSelectors } from "@/lib/create-selectors";
|
||||
import { toast } from "sonner";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
type SessionFields = {
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
type: string;
|
||||
port: string;
|
||||
database: string;
|
||||
ssl: string;
|
||||
id: number;
|
||||
};
|
||||
type SessionConnectionString = {
|
||||
id: number;
|
||||
connectionString: string;
|
||||
};
|
||||
type Session = SessionFields | SessionConnectionString;
|
||||
|
||||
type SesionState = {
|
||||
sessions: Session[];
|
||||
currentSessionId: number | null;
|
||||
setCurrentSessionId: (sessionId: number | null) => void;
|
||||
addSession: (
|
||||
session: Omit<SessionConnectionString, "id"> | Omit<SessionFields, "id">,
|
||||
) => void;
|
||||
removeSession: (sessionId: number) => void;
|
||||
};
|
||||
|
||||
const useSessionStoreBase = create<SesionState>()(
|
||||
persist(
|
||||
(set) => {
|
||||
return {
|
||||
currentSessionId: null,
|
||||
setCurrentSessionId: (sessionId) => {
|
||||
set(() => ({ currentSessionId: sessionId }));
|
||||
},
|
||||
sessions: [],
|
||||
addSession: (session) => {
|
||||
set((state) => {
|
||||
const id = state.sessions.length
|
||||
? Math.max(...state.sessions.map((s) => s.id)) + 1
|
||||
: 1;
|
||||
let isExisting = false;
|
||||
for (const s of state.sessions) {
|
||||
if (
|
||||
"connectionString" in s &&
|
||||
"connectionString" in session &&
|
||||
s.connectionString === session.connectionString
|
||||
) {
|
||||
isExisting = true;
|
||||
break;
|
||||
}
|
||||
if ("host" in s && "host" in session && s.host === session.host)
|
||||
isExisting = true;
|
||||
}
|
||||
if (isExisting) {
|
||||
toast.error("Session already exists");
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
sessions: [...state.sessions, { ...session, id }],
|
||||
currentSessionId: id,
|
||||
};
|
||||
});
|
||||
},
|
||||
removeSession: (sessionId) => {
|
||||
set((state) => ({
|
||||
sessions: state.sessions.filter((s) => s.id !== sessionId),
|
||||
}));
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
name: "db-session-storage",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useSessionStore = createSelectors(useSessionStoreBase);
|
||||
Reference in New Issue
Block a user