From 3d7508816ff46a911ff945cea420c6c55477904c Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Sun, 12 May 2024 06:00:04 +0700 Subject: [PATCH] feat: add frontend --- backend/drizzle.config.ts | 2 +- backend/src/db/index.ts | 4 +- backend/src/db/migrate.ts | 2 +- backend/src/lib/dbms/postgres.ts | 7 +- backend/src/routers/backup.router.ts | 4 +- backend/src/routers/index.ts | 6 +- backend/src/routers/server.router.ts | 13 +- backend/src/schedulers/process-backup.ts | 22 +- backend/src/schemas/server.schema.ts | 2 +- backend/src/services/backup.service.ts | 30 ++- backend/src/services/database.service.ts | 4 +- backend/src/services/server.service.ts | 18 +- backend/src/test.ts | 6 +- backend/src/utility/utils.ts | 4 + backend/tsconfig.json | 7 +- frontend/app/globals.css | 4 + frontend/bun.lockb | Bin 0 -> 153252 bytes frontend/components.json | 17 ++ frontend/package.json | 32 ++- frontend/postcss.config.js | 6 + frontend/src/App.css | 42 ---- frontend/src/App.tsx | 35 --- frontend/src/app/App.tsx | 22 ++ frontend/src/app/Router.tsx | 43 ++++ frontend/src/app/global.css | 3 + .../components/containers/confirm-dialog.tsx | 49 ++++ .../src/components/containers/sidebar.tsx | 40 ++++ frontend/src/components/layouts/dashboard.tsx | 18 ++ frontend/src/components/ui/alert.tsx | 78 ++++++ frontend/src/components/ui/back-button.tsx | 22 ++ frontend/src/components/ui/button.tsx | 84 +++++++ frontend/src/components/ui/card.tsx | 13 + frontend/src/components/ui/checkbox.tsx | 28 +++ frontend/src/components/ui/data-table.tsx | 15 ++ frontend/src/components/ui/dialog.tsx | 132 +++++++++++ frontend/src/components/ui/dropdown-menu.tsx | 198 ++++++++++++++++ frontend/src/components/ui/form.tsx | 93 ++++++++ frontend/src/components/ui/input.tsx | 51 ++++ frontend/src/components/ui/label.tsx | 24 ++ frontend/src/components/ui/nav-item.tsx | 35 +++ frontend/src/components/ui/page-title.tsx | 26 ++ frontend/src/components/ui/popover.tsx | 29 +++ frontend/src/components/ui/section-title.tsx | 13 + frontend/src/components/ui/select.tsx | 222 ++++++++++++++++++ frontend/src/components/ui/sonner.tsx | 30 +++ frontend/src/components/ui/tooltip.tsx | 28 +++ frontend/src/context/form-context.ts | 14 ++ frontend/src/hooks/usePartials.ts | 11 + frontend/src/hooks/useQueryParams.ts | 32 +++ frontend/src/index.css | 68 ------ frontend/src/lib/api.ts | 15 ++ frontend/src/lib/disclosure.ts | 38 +++ frontend/src/lib/queryClient.ts | 3 + frontend/src/lib/utils.ts | 30 +++ frontend/src/main.tsx | 13 +- .../pages/home/components/server-section.tsx | 44 ++++ frontend/src/pages/home/page.tsx | 11 + .../servers/components/add-server-dialog.tsx | 214 +++++++++++++++++ .../servers/components/backup-button.tsx | 46 ++++ .../servers/components/backup-status.tsx | 67 ++++++ .../servers/components/connection-status.tsx | 37 +++ .../pages/servers/components/server-list.tsx | 68 ++++++ frontend/src/pages/servers/page.tsx | 50 ++++ frontend/src/pages/servers/schema.ts | 21 ++ frontend/src/pages/servers/stores.ts | 4 + .../view/components/backups-section.tsx | 85 +++++++ .../view/components/databases-section.tsx | 25 ++ frontend/src/pages/servers/view/page.tsx | 65 +++++ frontend/src/pages/servers/view/table.tsx | 176 ++++++++++++++ frontend/tailwind.config.ts | 50 ++++ frontend/tsconfig.json | 7 +- frontend/vite.config.ts | 13 +- 72 files changed, 2558 insertions(+), 212 deletions(-) create mode 100644 frontend/app/globals.css create mode 100755 frontend/bun.lockb create mode 100644 frontend/components.json create mode 100644 frontend/postcss.config.js delete mode 100644 frontend/src/App.css delete mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/app/App.tsx create mode 100644 frontend/src/app/Router.tsx create mode 100644 frontend/src/app/global.css create mode 100644 frontend/src/components/containers/confirm-dialog.tsx create mode 100644 frontend/src/components/containers/sidebar.tsx create mode 100644 frontend/src/components/layouts/dashboard.tsx create mode 100644 frontend/src/components/ui/alert.tsx create mode 100644 frontend/src/components/ui/back-button.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx create mode 100644 frontend/src/components/ui/data-table.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/nav-item.tsx create mode 100644 frontend/src/components/ui/page-title.tsx create mode 100644 frontend/src/components/ui/popover.tsx create mode 100644 frontend/src/components/ui/section-title.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/sonner.tsx create mode 100644 frontend/src/components/ui/tooltip.tsx create mode 100644 frontend/src/context/form-context.ts create mode 100644 frontend/src/hooks/usePartials.ts create mode 100644 frontend/src/hooks/useQueryParams.ts delete mode 100644 frontend/src/index.css create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/disclosure.ts create mode 100644 frontend/src/lib/queryClient.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/pages/home/components/server-section.tsx create mode 100644 frontend/src/pages/home/page.tsx create mode 100644 frontend/src/pages/servers/components/add-server-dialog.tsx create mode 100644 frontend/src/pages/servers/components/backup-button.tsx create mode 100644 frontend/src/pages/servers/components/backup-status.tsx create mode 100644 frontend/src/pages/servers/components/connection-status.tsx create mode 100644 frontend/src/pages/servers/components/server-list.tsx create mode 100644 frontend/src/pages/servers/page.tsx create mode 100644 frontend/src/pages/servers/schema.ts create mode 100644 frontend/src/pages/servers/stores.ts create mode 100644 frontend/src/pages/servers/view/components/backups-section.tsx create mode 100644 frontend/src/pages/servers/view/components/databases-section.tsx create mode 100644 frontend/src/pages/servers/view/page.tsx create mode 100644 frontend/src/pages/servers/view/table.tsx create mode 100644 frontend/tailwind.config.ts diff --git a/backend/drizzle.config.ts b/backend/drizzle.config.ts index 3ab544e..f2beb92 100644 --- a/backend/drizzle.config.ts +++ b/backend/drizzle.config.ts @@ -1,4 +1,4 @@ -import { STORAGE_DIR } from "@/consts"; +import { STORAGE_DIR } from "../consts"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index 81568b1..55bd1ca 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -1,8 +1,8 @@ import path from "path"; import { drizzle } from "drizzle-orm/bun-sqlite"; import { Database } from "bun:sqlite"; -import { DATABASE_PATH } from "@/consts"; -import { mkdir } from "@/utility/utils"; +import { DATABASE_PATH } from "../consts"; +import { mkdir } from "../utility/utils"; import schema from "./schema"; // Create database directory if not exists diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index 8a300b9..408157d 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -1,6 +1,6 @@ import fs from "fs"; import { migrate } from "drizzle-orm/bun-sqlite/migrator"; -import { DATABASE_PATH } from "@/consts"; +import { DATABASE_PATH } from "../consts"; import db, { sqlite } from "."; import { seed } from "./seed"; diff --git a/backend/src/lib/dbms/postgres.ts b/backend/src/lib/dbms/postgres.ts index d9d2f52..1b39bf7 100644 --- a/backend/src/lib/dbms/postgres.ts +++ b/backend/src/lib/dbms/postgres.ts @@ -3,6 +3,7 @@ import type { PostgresConfig, } from "../../types/database.types"; import { exec } from "../../utility/process"; +import { urlencode } from "../../utility/utils"; import BaseDbms from "./base"; class PostgresDbms extends BaseDbms { @@ -18,7 +19,7 @@ class PostgresDbms extends BaseDbms { } async dump(dbName: string, path: string) { - return exec(["pg_dump", this.dbUrl + `/${dbName}`, "-Ftar", "-f", path]); + return exec(["pg_dump", this.dbUrl + `/${dbName}`, "-Z9", "-f", path]); } async restore(path: string) { @@ -29,7 +30,7 @@ class PostgresDbms extends BaseDbms { "-cC", "--if-exists", "--exit-on-error", - "-Ftar", + // "-Ftar", path, ]); } @@ -45,7 +46,7 @@ class PostgresDbms extends BaseDbms { private get dbUrl() { const { user, pass, host } = this.config; const port = this.config.port || 5432; - return `postgresql://${user}:${pass}@${host}:${port}`; + return `postgresql://${user}:${urlencode(pass)}@${host}:${port}`; } } diff --git a/backend/src/routers/backup.router.ts b/backend/src/routers/backup.router.ts index 1adab5c..5f5e1cf 100644 --- a/backend/src/routers/backup.router.ts +++ b/backend/src/routers/backup.router.ts @@ -2,8 +2,8 @@ import { createBackupSchema, getAllBackupQuery, restoreBackupSchema, -} from "@/schemas/backup.schema"; -import BackupService from "@/services/backup.service"; +} from "../schemas/backup.schema"; +import BackupService from "../services/backup.service"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; diff --git a/backend/src/routers/index.ts b/backend/src/routers/index.ts index 40cb87c..380f4b8 100644 --- a/backend/src/routers/index.ts +++ b/backend/src/routers/index.ts @@ -1,11 +1,13 @@ import { Hono } from "hono"; -import { handleError } from "@/middlewares/error-handler"; import server from "./server.router"; import backup from "./backup.router"; +import { cors } from "hono/cors"; +import { handleError } from "../middlewares/error-handler"; const routers = new Hono() // Middlewares .onError(handleError) + .use(cors()) // App health check .get("/health-check", (c) => c.text("OK")) @@ -14,4 +16,6 @@ const routers = new Hono() .route("/servers", server) .route("/backups", backup); +export type AppRouter = typeof routers; + export default routers; diff --git a/backend/src/routers/server.router.ts b/backend/src/routers/server.router.ts index 9901452..174c79e 100644 --- a/backend/src/routers/server.router.ts +++ b/backend/src/routers/server.router.ts @@ -1,9 +1,12 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import { checkServerSchema, createServerSchema } from "@/schemas/server.schema"; import { HTTPException } from "hono/http-exception"; -import DatabaseUtil from "@/lib/database-util"; -import ServerService from "@/services/server.service"; +import ServerService from "../services/server.service"; +import { + checkServerSchema, + createServerSchema, +} from "../schemas/server.schema"; +import DatabaseUtil from "../lib/database-util"; const serverService = new ServerService(); const router = new Hono() @@ -27,7 +30,7 @@ const router = new Hono() return c.json({ success: true, databases }); } catch (err) { throw new HTTPException(400, { - message: "Cannot connect to the database.", + message: (err as any).message || "Cannot connect to the database.", }); } }) @@ -49,7 +52,7 @@ const router = new Hono() .get("/:id", async (c) => { const { id } = c.req.param(); - const server = await serverService.getOrFail(id); + const server = await serverService.getById(id); return c.json(server); }); diff --git a/backend/src/schedulers/process-backup.ts b/backend/src/schedulers/process-backup.ts index ad11c9b..c822eb3 100644 --- a/backend/src/schedulers/process-backup.ts +++ b/backend/src/schedulers/process-backup.ts @@ -1,13 +1,13 @@ -import db from "@/db"; import fs from "fs"; import path from "path"; -import { backupModel, databaseModel } from "@/db/models"; -import DatabaseUtil from "@/lib/database-util"; -import ServerService from "@/services/server.service"; import { and, asc, eq, sql } from "drizzle-orm"; -import { BACKUP_DIR } from "@/consts"; -import { mkdir } from "@/utility/utils"; -import { hashFile } from "@/utility/hash"; +import ServerService from "../services/server.service"; +import db from "../db"; +import { backupModel, databaseModel } from "../db/models"; +import DatabaseUtil from "../lib/database-util"; +import { BACKUP_DIR } from "../consts"; +import { mkdir } from "../utility/utils"; +import { hashFile } from "../utility/hash"; let isRunning = false; const serverService = new ServerService(); @@ -19,16 +19,12 @@ const runBackup = async (task: PendingTasks[number]) => { .set({ status: "running" }) .where(eq(backupModel.id, task.id)); - const server = serverService.parse(task.server as never); + const server = serverService.parse(task.server); const dbName = task.database.name; const dbUtil = new DatabaseUtil(server.connection); if (task.type === "backup") { - const key = path.join( - server.connection.host, - dbName, - `${Date.now()}.tar` - ); + const key = path.join(server.connection.host, dbName, `${Date.now()}`); const outFile = path.join(BACKUP_DIR, key); mkdir(path.dirname(outFile)); diff --git a/backend/src/schemas/server.schema.ts b/backend/src/schemas/server.schema.ts index 190e2ce..62d6dcd 100644 --- a/backend/src/schemas/server.schema.ts +++ b/backend/src/schemas/server.schema.ts @@ -14,7 +14,7 @@ const sshSchema = z const postgresSchema = z.object({ type: z.literal("postgres"), host: z.string(), - port: z.number().optional(), + port: z.coerce.number().int().optional(), user: z.string(), pass: z.string(), }); diff --git a/backend/src/services/backup.service.ts b/backend/src/services/backup.service.ts index 75d1365..a4dfd6c 100644 --- a/backend/src/services/backup.service.ts +++ b/backend/src/services/backup.service.ts @@ -1,11 +1,11 @@ -import db from "@/db"; -import { backupModel, serverModel } from "@/db/models"; +import db from "../db"; +import { backupModel, serverModel } from "../db/models"; import type { CreateBackupSchema, GetAllBackupQuery, RestoreBackupSchema, -} from "@/schemas/backup.schema"; -import { and, desc, eq, inArray } from "drizzle-orm"; +} from "../schemas/backup.schema"; +import { and, count, desc, eq, inArray } from "drizzle-orm"; import DatabaseService from "./database.service"; import { HTTPException } from "hono/http-exception"; @@ -20,18 +20,28 @@ export default class BackupService { const page = query.page || 1; const limit = query.limit || 10; + const where = and( + serverId ? eq(backupModel.serverId, serverId) : undefined, + databaseId ? eq(backupModel.databaseId, databaseId) : undefined + ); + + const [totalRows] = await db + .select({ count: count() }) + .from(backupModel) + .where(where) + .limit(1); + const backups = await db.query.backup.findMany({ - where: (i) => - and( - serverId ? eq(i.serverId, serverId) : undefined, - databaseId ? eq(i.databaseId, databaseId) : undefined - ), + where, + with: { + database: { columns: { name: true } }, + }, orderBy: desc(serverModel.createdAt), limit, offset: (page - 1) * limit, }); - return backups; + return { count: totalRows.count, rows: backups }; } async getOrFail(id: string) { diff --git a/backend/src/services/database.service.ts b/backend/src/services/database.service.ts index ba8dfe4..729c272 100644 --- a/backend/src/services/database.service.ts +++ b/backend/src/services/database.service.ts @@ -1,5 +1,5 @@ -import db from "@/db"; -import { databaseModel } from "@/db/models"; +import db from "../db"; +import { databaseModel } from "../db/models"; import { desc, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; diff --git a/backend/src/services/server.service.ts b/backend/src/services/server.service.ts index 2a9408b..48df92e 100644 --- a/backend/src/services/server.service.ts +++ b/backend/src/services/server.service.ts @@ -1,6 +1,6 @@ -import db from "@/db"; -import { databaseModel, serverModel, type ServerModel } from "@/db/models"; -import type { CreateServerSchema } from "@/schemas/server.schema"; +import db from "../db"; +import { databaseModel, serverModel, type ServerModel } from "../db/models"; +import type { CreateServerSchema } from "../schemas/server.schema"; import { asc, desc, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; @@ -36,7 +36,15 @@ export default class ServerService { databases: true, }, }); - return server; + + if (!server) { + return null; + } + + const result = this.parse(server); + delete result.connection.pass; + + return result; } async create(data: CreateServerSchema) { @@ -73,7 +81,7 @@ export default class ServerService { }); } - parse(data: ServerModel) { + parse>(data: T) { const result = { ...data, connection: data.connection ? JSON.parse(data.connection) : null, diff --git a/backend/src/test.ts b/backend/src/test.ts index 2fff5dd..771f8f2 100644 --- a/backend/src/test.ts +++ b/backend/src/test.ts @@ -1,6 +1,6 @@ -import DatabaseUtil from "@/lib/database-util"; -import { DOCKER_HOST, BACKUP_DIR } from "@/consts"; -import { mkdir } from "@/utility/utils"; +import DatabaseUtil from "../lib/database-util"; +import { DOCKER_HOST, BACKUP_DIR } from "../consts"; +import { mkdir } from "../utility/utils"; import path from "path"; const main = async () => { diff --git a/backend/src/utility/utils.ts b/backend/src/utility/utils.ts index a42188d..4bef682 100644 --- a/backend/src/utility/utils.ts +++ b/backend/src/utility/utils.ts @@ -5,3 +5,7 @@ export const mkdir = (dir: string) => { fs.mkdirSync(dir, { recursive: true }); } }; + +export const urlencode = (str: string) => { + return encodeURIComponent(str); +}; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 6138132..238655f 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -22,11 +22,6 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } + "noPropertyAccessFromIndexSignature": false } } diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..91f55f0 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,4 @@ +@tailwind base; + @tailwind components; + @tailwind utilities; + \ No newline at end of file diff --git a/frontend/bun.lockb b/frontend/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..61d63c0fdae75ce2bcac9b13776080bddcb27f3b GIT binary patch literal 153252 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 - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx new file mode 100644 index 0000000..0cbe0b9 --- /dev/null +++ b/frontend/src/app/App.tsx @@ -0,0 +1,22 @@ +import { QueryClientProvider } from "react-query"; +import Router from "./Router"; +import { queryClient } from "@/lib/queryClient"; +import { lazy } from "react"; +import "./global.css"; + +const Toaster = lazy(() => import("@/components/ui/sonner")); +const ConfirmDialog = lazy( + () => import("@/components/containers/confirm-dialog") +); + +const App = () => { + return ( + + + + + + ); +}; + +export default App; diff --git a/frontend/src/app/Router.tsx b/frontend/src/app/Router.tsx new file mode 100644 index 0000000..cf84158 --- /dev/null +++ b/frontend/src/app/Router.tsx @@ -0,0 +1,43 @@ +import DashboardLayout from "@/components/layouts/dashboard"; +import { Suspense, lazy, useMemo } from "react"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; + +const HomePage = lazy(() => import("@/pages/home/page")); +const ServerPage = lazy(() => import("@/pages/servers/page")); +const ViewServerPage = lazy(() => import("@/pages/servers/view/page")); + +const router = createBrowserRouter([ + { + Component: DashboardLayout, + children: [ + { index: true, Component: HomePage }, + { + path: "servers", + children: [ + { + index: true, + Component: ServerPage, + }, + { + path: ":id", + Component: ViewServerPage, + }, + ], + }, + ], + }, +]); + +const Router = () => { + const routerData = useMemo(() => { + return router; + }, []); + + return ( + + + + ); +}; + +export default Router; diff --git a/frontend/src/app/global.css b/frontend/src/app/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/app/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/components/containers/confirm-dialog.tsx b/frontend/src/components/containers/confirm-dialog.tsx new file mode 100644 index 0000000..17e38ab --- /dev/null +++ b/frontend/src/components/containers/confirm-dialog.tsx @@ -0,0 +1,49 @@ +import { createDisclosureStore } from "@/lib/disclosure"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "../ui/dialog"; +import Button from "../ui/button"; + +type ConfirmDialogData = { + onConfirm: () => void; + title?: string; + description?: string; +}; + +// eslint-disable-next-line react-refresh/only-export-components +export const confirmDlg = createDisclosureStore({ + onConfirm: () => {}, +}); + +const ConfirmDialog = () => { + const { isOpen, data } = confirmDlg.useState(); + + return ( + + + {data?.title || "Are you sure?"} + {data?.description} + + + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/frontend/src/components/containers/sidebar.tsx b/frontend/src/components/containers/sidebar.tsx new file mode 100644 index 0000000..90899f1 --- /dev/null +++ b/frontend/src/components/containers/sidebar.tsx @@ -0,0 +1,40 @@ +import { + IoCogOutline, + IoLogOutOutline, + IoPieChartOutline, + IoServerOutline, +} from "react-icons/io5"; +import Nav from "../ui/nav-item"; +import Button from "../ui/button"; +import { cn } from "@/lib/utils"; + +const Sidebar = () => { + const isOpen = false; + + return ( +
+
+

Serep

+
+ +
+ ); +}; + +export default Sidebar; diff --git a/frontend/src/components/layouts/dashboard.tsx b/frontend/src/components/layouts/dashboard.tsx new file mode 100644 index 0000000..859a18b --- /dev/null +++ b/frontend/src/components/layouts/dashboard.tsx @@ -0,0 +1,18 @@ +import { Outlet } from "react-router-dom"; +import Sidebar from "../containers/sidebar"; +import { Suspense } from "react"; + +const DashboardLayout = () => { + return ( +
+ +
+ + + +
+
+ ); +}; + +export default DashboardLayout; diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..4af1d07 --- /dev/null +++ b/frontend/src/components/ui/alert.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border border-slate-200 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:border-slate-800 dark:[&>svg]:text-slate-50", + { + variants: { + variant: { + default: "bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50", + destructive: + "border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +type AlertProps = React.HTMLAttributes & + VariantProps; + +const Alert = React.forwardRef( + ({ className, variant, ...props }, ref) => ( +
+ ) +); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +const ErrorAlert = React.forwardRef< + HTMLDivElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Omit & { error?: any } +>(({ error, ...props }, ref) => { + if (!error?.message) { + return null; + } + + return ( + + {error?.message} + + ); +}); +ErrorAlert.displayName = "ErrorAlert"; + +export { Alert, AlertTitle, AlertDescription, ErrorAlert }; diff --git a/frontend/src/components/ui/back-button.tsx b/frontend/src/components/ui/back-button.tsx new file mode 100644 index 0000000..597c6ad --- /dev/null +++ b/frontend/src/components/ui/back-button.tsx @@ -0,0 +1,22 @@ +import Button from "./button"; +import { IoChevronBack } from "react-icons/io5"; + +type BackButtonProps = { + to: string; +}; + +const BackButton = ({ to }: BackButtonProps) => { + return ( + + ); +}; + +export default BackButton; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..dec20ea --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Loader2 } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { IconType } from "react-icons"; +import { Link } from "react-router-dom"; + +const buttonVariants = cva( + "inline-flex gap-1 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-primary-950 dark:focus-visible:ring-primary-300", + { + variants: { + variant: { + default: + "bg-primary-500 text-white hover:bg-primary-500/90 dark:bg-primary-50 dark:text-gray-900 dark:hover:bg-primary-50/90", + destructive: + "bg-red-500 text-primary-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-primary-50 dark:hover:bg-red-900/90", + outline: + "border border-primary-200 bg-white hover:bg-primary-100 hover:text-primary-900 dark:border-primary-800 dark:bg-primary-950 dark:hover:bg-primary-800 dark:hover:text-primary-50", + secondary: + "bg-primary-100 text-primary-900 hover:bg-primary-100/80 dark:bg-primary-800 dark:text-primary-50 dark:hover:bg-primary-800/80", + ghost: + "hover:bg-primary-100 hover:text-primary-900 dark:hover:bg-primary-800 dark:hover:text-primary-50", + link: "text-primary-900 underline-offset-4 hover:underline dark:text-primary-50", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + icon?: IconType; + isLoading?: boolean; + href?: string; +} + +const Button = React.forwardRef( + ( + { + className, + variant, + size, + type = "button", + href, + icon: Icon, + children, + isLoading, + disabled, + ...props + }, + ref + ) => { + const Comp = href ? Link : "button"; + return ( + + {Icon && !isLoading ? : null} + {isLoading && } + {children} + + ); + } +); +Button.displayName = "Button"; + +export default Button; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..3594640 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils"; +import React from "react"; + +const Card = ({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) => { + return ( +
+ ); +}; + +export default Card; diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0830223 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx new file mode 100644 index 0000000..29a5e0d --- /dev/null +++ b/frontend/src/components/ui/data-table.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import DataTableComponent, { TableColumn } from "react-data-table-component"; + +type DataTableProps = React.ComponentPropsWithoutRef; + +const DataTable = ({ ...props }: DataTableProps) => { + return ( +
+ +
+ ); +}; + +export type { TableColumn }; +export default DataTable; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..b1240f6 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,132 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogBody = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogBody.displayName = "DialogBody"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogBody, + DialogDescription, +}; diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..d1ef990 --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..d41a678 --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,93 @@ +import FormContext from "@/context/form-context"; +import { cn } from "@/lib/utils"; +import React, { useId } from "react"; +import { + Controller, + ControllerFieldState, + ControllerRenderProps, + FieldPath, + FieldValues, + UseFormReturn, + UseFormStateReturn, +} from "react-hook-form"; + +type FormProps = + React.ComponentPropsWithoutRef<"form"> & { + form: UseFormReturn; + }; + +const Form = ({ form, ...props }: FormProps) => { + return ( + +
+ + ); +}; + +type FormControlRenderFn< + T extends FieldValues, + FieldName extends FieldPath +> = ({ + field, + fieldState, + formState, +}: { + field: ControllerRenderProps; + fieldState: ControllerFieldState; + formState: UseFormStateReturn; + id: string; +}) => React.ReactElement; + +export type FormControlProps< + TValues extends FieldValues, + TName extends FieldPath = FieldPath +> = { + form: UseFormReturn; + name: TName; + render: FormControlRenderFn; + className?: string; + label?: string; +}; + +export const FormControl = ({ + form, + name, + render, + className, + label, +}: FormControlProps) => { + const fieldId = useId(); + + return ( + { + const { fieldState } = props; + + return ( +
+ {label != null && ( + + )} + + {render({ ...props, id: fieldId })} + + {fieldState.error != null && ( +

+ {fieldState.error.message} +

+ )} +
+ ); + }} + /> + ); +}; + +export default Form; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..890c156 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { FieldPath, FieldValues, UseFormReturn } from "react-hook-form"; +import { FormControl } from "./form"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +type InputFieldProps = Omit & { + form: UseFormReturn; + name: FieldPath; + label?: string; +}; + +const InputField = ({ + form, + name, + label, + className, + ...props +}: InputFieldProps) => ( + } + /> +); +InputField.displayName = "InputField"; + +export { InputField }; +export default Input; diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..7c96064 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/frontend/src/components/ui/nav-item.tsx b/frontend/src/components/ui/nav-item.tsx new file mode 100644 index 0000000..b283579 --- /dev/null +++ b/frontend/src/components/ui/nav-item.tsx @@ -0,0 +1,35 @@ +import { cn } from "@/lib/utils"; +import type { IconType } from "react-icons"; +import { Link, useLocation } from "react-router-dom"; + +type NavProps = { + path: string; + title: string; + exact?: boolean; + icon: IconType; +}; + +const Nav = ({ path, title, exact, icon: Icon }: NavProps) => { + const { pathname } = useLocation(); + const isActive = exact ? pathname === path : pathname.startsWith(path); + + return ( + + {Icon ? ( +
+ +
+ ) : null} + {title} + + ); +}; +export default Nav; diff --git a/frontend/src/components/ui/page-title.tsx b/frontend/src/components/ui/page-title.tsx new file mode 100644 index 0000000..26c31ca --- /dev/null +++ b/frontend/src/components/ui/page-title.tsx @@ -0,0 +1,26 @@ +import { cn } from "@/lib/utils"; +import Helmet from "react-helmet"; + +type Props = { + children?: string; + setTitle?: boolean; + className?: string; +}; + +const PageTitle = ({ children, setTitle = true, className }: Props) => { + return ( + <> + {setTitle && ( + + {children} + + )} + +

+ {children} +

+ + ); +}; + +export default PageTitle; diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 0000000..d628782 --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/frontend/src/components/ui/section-title.tsx b/frontend/src/components/ui/section-title.tsx new file mode 100644 index 0000000..04d26db --- /dev/null +++ b/frontend/src/components/ui/section-title.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils"; +import React from "react"; + +const SectionTitle = ({ + className, + ...props +}: React.ComponentPropsWithoutRef<"p">) => { + return ( +

+ ); +}; + +export default SectionTitle; diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000..09ab358 --- /dev/null +++ b/frontend/src/components/ui/select.tsx @@ -0,0 +1,222 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { FieldPath, FieldValues, UseFormReturn } from "react-hook-form"; +import { FormControl } from "./form"; + +const SelectRoot = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export type SelectOption = { + label: string; + value: string; +}; + +type SelectProps = { + id?: string; + value?: string; + placeholder?: string; + onChange?: (value: string) => void; + options?: SelectOption[] | null; + className?: string; +}; + +const Select = React.forwardRef< + React.ElementRef, + SelectProps +>(({ id, className, placeholder, value, options, onChange }, ref) => ( + + + + + + + {/* {placeholder != null && {placeholder}} */} + + {options?.map((option) => ( + + {option.label} + + ))} + + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +type SelectFieldProps = SelectProps & { + form: UseFormReturn; + name: FieldPath; + label?: string; +}; + +const SelectField = ({ + form, + name, + label, + className, + ...props +}: SelectFieldProps) => ( + ({ label: i.name, value: i.id }))} + onChange={(i) => setQuery({ databaseId: i })} + value={query.databaseId} + placeholder="Select Database" + className="min-w-[120px]" + /> +

+ + setQuery({ limit })} + onChangePage={(page) => setQuery({ page })} + /> + +
+ ); +}; + +export default BackupSection; diff --git a/frontend/src/pages/servers/view/components/databases-section.tsx b/frontend/src/pages/servers/view/components/databases-section.tsx new file mode 100644 index 0000000..c7016aa --- /dev/null +++ b/frontend/src/pages/servers/view/components/databases-section.tsx @@ -0,0 +1,25 @@ +import Card from "@/components/ui/card"; +import DataTable from "@/components/ui/data-table"; +import SectionTitle from "@/components/ui/section-title"; +import { DatabaseType, databaseColumns } from "../table"; + +type DatabaseSectionProps = { + databases: DatabaseType[]; +}; + +const DatabaseSection = ({ databases }: DatabaseSectionProps) => { + return ( +
+ Databases + + + +
+ ); +}; + +export default DatabaseSection; diff --git a/frontend/src/pages/servers/view/page.tsx b/frontend/src/pages/servers/view/page.tsx new file mode 100644 index 0000000..4687957 --- /dev/null +++ b/frontend/src/pages/servers/view/page.tsx @@ -0,0 +1,65 @@ +import BackButton from "@/components/ui/back-button"; +import PageTitle from "@/components/ui/page-title"; +import api, { parseJson } from "@/lib/api"; +import { IoServer } from "react-icons/io5"; +import { useQuery } from "react-query"; +import { useParams } from "react-router-dom"; +import ConnectionStatus, { + getConnectionLabel, +} from "../components/connection-status"; +import Card from "@/components/ui/card"; +import BackupSection from "./components/backups-section"; +import DatabaseSection from "./components/databases-section"; + +const ViewServerPage = () => { + const id = useParams().id!; + + const { data, isLoading, error } = useQuery({ + queryKey: ["servers"], + queryFn: () => api.servers[":id"].$get({ param: { id } }).then(parseJson), + }); + + const check = useQuery({ + queryKey: ["server", id], + queryFn: async () => { + return api.servers.check[":id"].$get({ param: { id } }).then(parseJson); + }, + refetchInterval: 30000, + }); + + if (isLoading || error) { + return null; + } + + return ( +
+ + Server Information + + + +
+

{data?.name}

+ + {data?.connection?.host ? ( + + {data?.connection?.host} + + ) : null} +
+ +
+ +

{getConnectionLabel(check.data?.success, check.error)}

+
+
+ +
+ + +
+
+ ); +}; + +export default ViewServerPage; diff --git a/frontend/src/pages/servers/view/table.tsx b/frontend/src/pages/servers/view/table.tsx new file mode 100644 index 0000000..72b2a0e --- /dev/null +++ b/frontend/src/pages/servers/view/table.tsx @@ -0,0 +1,176 @@ +import { TableColumn } from "@/components/ui/data-table"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import BackupButton from "../components/backup-button"; +import { copyToClipboard, formatBytes } from "@/lib/utils"; +import BackupStatus from "../components/backup-status"; +import { queryClient } from "@/lib/queryClient"; +import Button from "@/components/ui/button"; +import { IoCloudDownload, IoCopy, IoEllipsisVertical } from "react-icons/io5"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { confirmDlg } from "@/components/containers/confirm-dialog"; +import api, { parseJson } from "@/lib/api"; +import { toast } from "sonner"; + +dayjs.extend(relativeTime); + +export type DatabaseType = { + id: string; + name: string; + isActive: boolean; + createdAt: string; + serverId: string; + lastBackupAt: string | null; +}; + +export const databaseColumns: TableColumn[] = [ + { + name: "Name", + selector: (i) => i.name, + }, + { + name: "Last Backup", + selector: (i) => + i.lastBackupAt ? dayjs(i.lastBackupAt).format("YYYY-MM-DD HH:mm") : "", + cell: (i) => (i.lastBackupAt ? dayjs(i.lastBackupAt).fromNow() : "never"), + }, + { + selector: (i) => i.id, + width: "160px", + cell: (i) => ( +
+ { + queryClient.invalidateQueries(["backups/by-server", i.serverId]); + }} + /> +
+ ), + }, +]; + +export type BackupType = { + id: string; + serverId: string; + databaseId: string; + type: string; + status: string; + output: string; + key: string | null; + hash: string | null; + size: number | null; + createdAt: Date; + database?: DatabaseType; +}; + +const onRestoreBackup = async (row: BackupType) => { + try { + const res = await api.backups.restore.$post({ + json: { backupId: row.id }, + }); + await parseJson(res); + toast.success("Queueing database restore!"); + queryClient.invalidateQueries(["backups/by-server", row.serverId]); + } catch (err) { + toast.error("Failed to restore backup! " + (err as Error).message); + } +}; + +export const backupsColumns: TableColumn[] = [ + { + name: "Type", + selector: (i) => i.type, + cell: (i) => (i.type === "backup" ? "Backup" : "Restore"), + }, + { + name: "Database Name", + selector: (i) => i.database?.name || "-", + }, + { + name: "Status", + selector: (i) => i.status, + cell: (i) => , + }, + { + name: "Data", + selector: (i) => i.key || "-", + center: true, + cell: (i) => + i.key ? ( + + ) : ( + "-" + ), + }, + { + name: "SHA256", + selector: (i) => i.hash || "", + cell: (i) => + i.hash ? ( + + ) : ( + "-" + ), + }, + { + name: "Timestamp", + selector: (i) => dayjs(i.createdAt).format("YYYY-MM-DD HH:mm"), + cell: (i) => { + const diff = dayjs().diff(dayjs(i.createdAt), "days"); + return diff < 3 + ? dayjs(i.createdAt).fromNow() + : dayjs(i.createdAt).format("DD/MM/YY HH:mm"); + }, + }, + { + selector: (i) => i.id, + width: "40px", + style: { padding: 0 }, + cell: (row) => { + return ( + + + + + + { + confirmDlg.onOpen({ + title: "Restore Backup", + description: "Are you sure want to restore this backup?", + onConfirm: () => onRestoreBackup(row), + }); + }} + > + Restore + + + + ); + }, + }, +]; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..b7a4d01 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,50 @@ +import { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: ["./src/**/*.{ts,tsx}"], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + primary: { + "50": "#eef2ff", + "100": "#e0e7ff", + "200": "#c7d2fe", + "300": "#a5b4fc", + "400": "#818cf8", + "500": "#6366f1", + "600": "#4f46e5", + "700": "#4338ca", + "800": "#3730a3", + "900": "#312e81", + "950": "#1e1b4b", + }, + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; + +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a7fc6fb..7d5cc6a 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,7 +18,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@backend/*": ["../backend/src/*"], + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 861b04b..3e02254 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,14 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "@backend": path.resolve(__dirname, "../backend/src"), + }, + }, +});