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 (
);
}
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.
{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}
))}
)}
);
}