import { useEffect, useState } from 'react'; import { useParams } from 'react-router'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Database, ExternalLink, Plus, RefreshCw, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { ApiError, api } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; interface ManagedDatabase { id: string; name: string; databaseName: string; username: string; password: string; host: string; port: number; phpMyAdminUrl: string | null; createdAt: string; updatedAt: string; } function extractApiMessage(error: unknown, fallback: string): string { if (error instanceof ApiError && error.data && typeof error.data === 'object') { const maybeMessage = (error.data as { message?: unknown }).message; if (typeof maybeMessage === 'string' && maybeMessage.trim()) { return maybeMessage; } } return fallback; } function InfoRow({ label, value }: { label: string; value: string }) { return (

{label}

{value}
); } export function DatabasesPage() { const { orgId, serverId } = useParams(); const queryClient = useQueryClient(); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(''); const [createPassword, setCreatePassword] = useState(''); const [editingDatabase, setEditingDatabase] = useState(null); const [editName, setEditName] = useState(''); const [editPassword, setEditPassword] = useState(''); const { data, isLoading } = useQuery({ queryKey: ['server-databases', orgId, serverId], queryFn: () => api.get<{ data: ManagedDatabase[] }>( `/organizations/${orgId}/servers/${serverId}/databases`, ), }); useEffect(() => { if (!editingDatabase) return; setEditName(editingDatabase.name); setEditPassword(''); }, [editingDatabase]); const databases = data?.data ?? []; const resetCreateForm = () => { setCreateName(''); setCreatePassword(''); }; const createMutation = useMutation({ mutationFn: (body: { name: string; password?: string }) => api.post(`/organizations/${orgId}/servers/${serverId}/databases`, body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['server-databases', orgId, serverId] }); setCreateOpen(false); resetCreateForm(); toast.success('Database created'); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to create database')); }, }); const updateMutation = useMutation({ mutationFn: (body: { name?: string; password?: string }) => api.patch( `/organizations/${orgId}/servers/${serverId}/databases/${editingDatabase!.id}`, body, ), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['server-databases', orgId, serverId] }); setEditingDatabase(null); setEditPassword(''); toast.success('Database updated'); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to update database')); }, }); const deleteMutation = useMutation({ mutationFn: (databaseId: string) => api.delete(`/organizations/${orgId}/servers/${serverId}/databases/${databaseId}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['server-databases', orgId, serverId] }); toast.success('Database deleted'); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to delete database')); }, }); return (

Databases

Unlimited MySQL databases for this server, with password rotation and phpMyAdmin links.

{ setCreateOpen(open); if (!open) resetCreateForm(); }} > Create MySQL Database
{ event.preventDefault(); createMutation.mutate({ name: createName, password: createPassword.trim() || undefined, }); }} >
setCreateName(event.target.value)} placeholder="LuckPerms" required />
setCreatePassword(event.target.value)} minLength={8} placeholder="Leave empty to auto-generate" />

If left empty, the panel generates a strong password automatically.

{ if (!open) { setEditingDatabase(null); setEditPassword(''); } }} > Edit Database
{ event.preventDefault(); updateMutation.mutate({ name: editName !== editingDatabase?.name ? editName : undefined, password: editPassword.trim() || undefined, }); }} >
setEditName(event.target.value)} required />
setEditPassword(event.target.value)} minLength={8} placeholder="Leave empty to keep the current password" />

Entering a value rotates the MySQL user password immediately.

{isLoading ? (
) : databases.length === 0 ? ( No databases yet. Create one for plugins, web panels, or server-side data. ) : (
{databases.map((database) => (
{database.name}

Created {new Date(database.createdAt).toLocaleString()}

{database.phpMyAdminUrl ? ( ) : null}
{!database.phpMyAdminUrl ? (

phpMyAdmin link is not configured. Set `managed_mysql.phpmyadmin_url` in the daemon config for this node.

) : null}
))}
)}
); }