From 6797acc724aef2d4ab9a4ec034085f25d4deaa07 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:50:05 +0000 Subject: [PATCH] feat: modularize database manager UI --- .../managers/database/ActionToolbar.tsx | 38 ++++ .../managers/database/ConnectionForm.tsx | 115 ++++++++++++ .../managers/database/DatabaseManager.tsx | 176 +++++++++--------- .../managers/database/SchemaViewer.tsx | 81 ++++++++ 4 files changed, 325 insertions(+), 85 deletions(-) create mode 100644 frontends/nextjs/src/components/managers/database/ActionToolbar.tsx create mode 100644 frontends/nextjs/src/components/managers/database/ConnectionForm.tsx create mode 100644 frontends/nextjs/src/components/managers/database/SchemaViewer.tsx diff --git a/frontends/nextjs/src/components/managers/database/ActionToolbar.tsx b/frontends/nextjs/src/components/managers/database/ActionToolbar.tsx new file mode 100644 index 000000000..ddd2029cb --- /dev/null +++ b/frontends/nextjs/src/components/managers/database/ActionToolbar.tsx @@ -0,0 +1,38 @@ +import { Button } from '@/components/ui' +import { ArrowsClockwise, Export, UploadSimple, Trash } from '@phosphor-icons/react' + +interface ActionToolbarProps { + isLoading?: boolean + onRefresh: () => void + onExport: () => void + onImport: () => void + onClear: () => void +} + +export function ActionToolbar({ isLoading, onRefresh, onExport, onImport, onClear }: ActionToolbarProps) { + return ( +
+
+

Database Management

+

Manage all persistent data across the application

+
+
+ + + + +
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/database/ConnectionForm.tsx b/frontends/nextjs/src/components/managers/database/ConnectionForm.tsx new file mode 100644 index 000000000..ac3dc3ce8 --- /dev/null +++ b/frontends/nextjs/src/components/managers/database/ConnectionForm.tsx @@ -0,0 +1,115 @@ +import { useState, type FormEvent } from 'react' +import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label } from '@/components/ui' + +export interface ConnectionDetails { + driver: string + host: string + port: string + database: string + username: string + password: string +} + +interface ConnectionFormProps { + onConnect: (details: ConnectionDetails) => Promise | void + isConnecting?: boolean + status: 'disconnected' | 'connecting' | 'connected' + lastConnectedAt: Date | null +} + +export function ConnectionForm({ onConnect, isConnecting, status, lastConnectedAt }: ConnectionFormProps) { + const [details, setDetails] = useState({ + driver: 'prisma-client', + host: 'localhost', + port: '5432', + database: 'metabuilder', + username: 'admin', + password: '', + }) + + const handleChange = (key: keyof ConnectionDetails, value: string) => { + setDetails((prev) => ({ ...prev, [key]: value })) + } + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + await onConnect(details) + } + + const statusVariant = status === 'connected' ? 'default' : status === 'connecting' ? 'secondary' : 'outline' + const statusLabel = + status === 'connected' + ? 'Connected' + : status === 'connecting' + ? 'Connecting...' + : 'Not connected' + + return ( + + +
+ Connection + Initialize and validate access to the database layer +
+ {statusLabel} +
+ +
+
+ + handleChange('driver', e.target.value)} + placeholder="prisma-client" + /> +
+
+ + handleChange('database', e.target.value)} + placeholder="metabuilder" + /> +
+
+ + handleChange('host', e.target.value)} placeholder="localhost" /> +
+
+ + handleChange('port', e.target.value)} placeholder="5432" /> +
+
+ + handleChange('username', e.target.value)} + placeholder="admin" + /> +
+
+ + handleChange('password', e.target.value)} + placeholder="••••••••" + /> +
+
+
+ {lastConnectedAt ? `Last connected ${lastConnectedAt.toLocaleString()}` : 'No connection established yet.'} +
+ +
+
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/database/DatabaseManager.tsx b/frontends/nextjs/src/components/managers/database/DatabaseManager.tsx index ba65e5fd1..d951f226a 100644 --- a/frontends/nextjs/src/components/managers/database/DatabaseManager.tsx +++ b/frontends/nextjs/src/components/managers/database/DatabaseManager.tsx @@ -1,9 +1,7 @@ -import { useState, useEffect } from 'react' -import { Button } from '@/components/ui' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' -import { Badge } from '@/components/ui' -import { ScrollArea } from '@/components/ui' import { Database, DB_KEYS } from '@/lib/database' +import type { ModelSchema } from '@/lib/types/schema-types' import { toast } from 'sonner' import { Database as DatabaseIcon, @@ -16,12 +14,27 @@ import { ChatCircle, Tree, Gear, - Trash, - ArrowsClockwise, } from '@phosphor-icons/react' +import { ActionToolbar } from './ActionToolbar' +import { ConnectionForm, type ConnectionDetails } from './ConnectionForm' +import { SchemaViewer } from './SchemaViewer' + +interface DatabaseStats { + users: number + credentials: number + workflows: number + luaScripts: number + pages: number + schemas: number + comments: number + componentNodes: number + componentConfigs: number +} + +type ConnectionState = 'disconnected' | 'connecting' | 'connected' export function DatabaseManager() { - const [stats, setStats] = useState({ + const [stats, setStats] = useState({ users: 0, credentials: 0, workflows: 0, @@ -32,14 +45,12 @@ export function DatabaseManager() { componentNodes: 0, componentConfigs: 0, }) - + const [schemas, setSchemas] = useState([]) const [isLoading, setIsLoading] = useState(false) + const [connectionState, setConnectionState] = useState('disconnected') + const [lastConnectedAt, setLastConnectedAt] = useState(null) - useEffect(() => { - loadStats() - }, []) - - const loadStats = async () => { + const loadStats = useCallback(async () => { setIsLoading(true) try { const [ @@ -48,7 +59,7 @@ export function DatabaseManager() { workflows, luaScripts, pages, - schemas, + schemaData, comments, hierarchy, configs, @@ -70,19 +81,25 @@ export function DatabaseManager() { workflows: workflows.length, luaScripts: luaScripts.length, pages: pages.length, - schemas: schemas.length, + schemas: schemaData.length, comments: comments.length, componentNodes: Object.keys(hierarchy).length, componentConfigs: Object.keys(configs).length, }) + setSchemas(schemaData) } catch (error) { + console.error(error) toast.error('Failed to load database statistics') } finally { setIsLoading(false) } - } + }, []) - const handleClearDatabase = async () => { + useEffect(() => { + void loadStats() + }, [loadStats]) + + const handleClearDatabase = useCallback(async () => { if (!confirm('Are you sure you want to clear the entire database? This action cannot be undone!')) { return } @@ -97,11 +114,12 @@ export function DatabaseManager() { await loadStats() toast.success('Database cleared and reinitialized') } catch (error) { + console.error(error) toast.error('Failed to clear database') } - } + }, [loadStats]) - const handleExportDatabase = async () => { + const handleExportDatabase = useCallback(async () => { try { const data = await Database.exportDatabase() const blob = new Blob([data], { type: 'application/json' }) @@ -113,11 +131,12 @@ export function DatabaseManager() { URL.revokeObjectURL(url) toast.success('Database exported successfully') } catch (error) { + console.error(error) toast.error('Failed to export database') } - } + }, []) - const handleImportDatabase = () => { + const handleImportDatabase = useCallback(() => { const input = document.createElement('input') input.type = 'file' input.accept = 'application/json' @@ -131,51 +150,64 @@ export function DatabaseManager() { await loadStats() toast.success('Database imported successfully') } catch (error) { + console.error(error) toast.error('Failed to import database') } } input.click() - } + }, [loadStats]) - const totalRecords = Object.values(stats).reduce((a, b) => a + b, 0) + const handleConnect = useCallback( + async (details: ConnectionDetails) => { + setConnectionState('connecting') + try { + await Database.initializeDatabase() + setConnectionState('connected') + setLastConnectedAt(new Date()) + toast.success(`Connected to ${details.database || 'Metabuilder database'} via ${details.driver}`) + await loadStats() + } catch (error) { + console.error(error) + setConnectionState('disconnected') + toast.error('Failed to initialize database connection') + } + }, + [loadStats], + ) - const dbEntities = [ - { key: 'users', icon: Users, label: 'Users', count: stats.users, color: 'text-blue-500' }, - { key: 'credentials', icon: Key, label: 'Credentials (SHA-512)', count: stats.credentials, color: 'text-amber-500' }, - { key: 'workflows', icon: Lightning, label: 'Workflows', count: stats.workflows, color: 'text-purple-500' }, - { key: 'luaScripts', icon: Code, label: 'Lua Scripts', count: stats.luaScripts, color: 'text-indigo-500' }, - { key: 'pages', icon: FileText, label: 'Pages', count: stats.pages, color: 'text-cyan-500' }, - { key: 'schemas', icon: TableIcon, label: 'Data Schemas', count: stats.schemas, color: 'text-green-500' }, - { key: 'comments', icon: ChatCircle, label: 'Comments', count: stats.comments, color: 'text-pink-500' }, - { key: 'componentNodes', icon: Tree, label: 'Component Hierarchy', count: stats.componentNodes, color: 'text-teal-500' }, - { key: 'componentConfigs', icon: Gear, label: 'Component Configs', count: stats.componentConfigs, color: 'text-orange-500' }, - ] + const dbEntities = useMemo( + () => [ + { key: 'users', icon: Users, label: 'Users', count: stats.users, color: 'text-blue-500' }, + { key: 'credentials', icon: Key, label: 'Credentials (SHA-512)', count: stats.credentials, color: 'text-amber-500' }, + { key: 'workflows', icon: Lightning, label: 'Workflows', count: stats.workflows, color: 'text-purple-500' }, + { key: 'luaScripts', icon: Code, label: 'Lua Scripts', count: stats.luaScripts, color: 'text-indigo-500' }, + { key: 'pages', icon: FileText, label: 'Pages', count: stats.pages, color: 'text-cyan-500' }, + { key: 'schemas', icon: TableIcon, label: 'Data Schemas', count: stats.schemas, color: 'text-green-500' }, + { key: 'comments', icon: ChatCircle, label: 'Comments', count: stats.comments, color: 'text-pink-500' }, + { key: 'componentNodes', icon: Tree, label: 'Component Hierarchy', count: stats.componentNodes, color: 'text-teal-500' }, + { key: 'componentConfigs', icon: Gear, label: 'Component Configs', count: stats.componentConfigs, color: 'text-orange-500' }, + ], + [stats], + ) + + const totalRecords = useMemo(() => Object.values(stats).reduce((a, b) => a + b, 0), [stats]) return (
-
-
-

Database Management

-

- Manage all persistent data across the application -

-
-
- - - - -
-
+ void loadStats()} + onExport={handleExportDatabase} + onImport={handleImportDatabase} + onClear={handleClearDatabase} + /> + +
@@ -184,9 +216,7 @@ export function DatabaseManager() { Database Overview - - All data stored using SHA-512 password hashing and KV persistence - + All data stored using SHA-512 password hashing and KV persistence
{totalRecords}
@@ -210,31 +240,7 @@ export function DatabaseManager() { ))}
- - - Database Keys - - All KV storage keys used by the application - - - - -
- {Object.entries(DB_KEYS).map(([key, value]) => ( -
-
- {key} -

{value}

-
- - KV - -
- ))} -
-
-
-
+ diff --git a/frontends/nextjs/src/components/managers/database/SchemaViewer.tsx b/frontends/nextjs/src/components/managers/database/SchemaViewer.tsx new file mode 100644 index 000000000..f601641c0 --- /dev/null +++ b/frontends/nextjs/src/components/managers/database/SchemaViewer.tsx @@ -0,0 +1,81 @@ +import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, ScrollArea } from '@/components/ui' +import type { ModelSchema } from '@/lib/types/schema-types' + +interface SchemaViewerProps { + schemas: ModelSchema[] + dbKeys: Record +} + +export function SchemaViewer({ schemas, dbKeys }: SchemaViewerProps) { + return ( +
+ + + Schemas + Models and fields available in the database + + + {schemas.length === 0 ? ( +

No schemas configured yet.

+ ) : ( +
+ {schemas.map((schema) => ( +
+
+
+

+ {schema.icon && {schema.icon}} + {schema.label || schema.name} + {schema.name} +

+ {schema.labelPlural && ( +

Plural: {schema.labelPlural}

+ )} +
+ {schema.fields.length} fields +
+ {schema.fields.length > 0 && ( +
+ {schema.fields.slice(0, 6).map((field) => ( + + {field.label || field.name} ({field.type}) + + ))} + {schema.fields.length > 6 && ( + +{schema.fields.length - 6} more + )} +
+ )} +
+ ))} +
+ )} +
+
+ + + + Database Keys + All KV storage keys used by the application + + + +
+ {Object.entries(dbKeys).map(([key, value]) => ( +
+
+ {key} +

{value}

+
+ + KV + +
+ ))} +
+
+
+
+
+ ) +}