import { useCallback, useEffect, useRef, useState } from 'react'; import { Check, CircleCheck, CircleDashed, Database, Loader2, Plus } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { readableError } from './errors'; import { Block } from './Block'; import { useFlash } from './useFlash'; /** * Where the fofos site's own data lives — two databases, named here. * * ── Naming and creating are separate ── * * Save stores what you typed; Create makes whichever of the two is not there yet. They are different * decisions and were worth two buttons: a database that already exists still needs its name recorded, * and making Create the only way to save would mean nobody could point the site at something that was * already set up. * * ── It reports what is THERE, per name ── * * A tick or a dashed circle beside each field, from `pg_database` rather than from what was saved. The * useful question about a database name is not whether somebody typed it — it is whether the thing it * names exists, and those two came apart the moment saving stopped creating. * * ── Create only offers itself when there is something to create ── * * Both present and the button goes. A button that does nothing is a thing to wonder about later. */ type Named = { name: string | null; exists: boolean | null }; type Status = { production: Named; development: Named; available: string[] }; export const DatabaseBlock = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [status, setStatus] = useState(null); const [production, setProduction] = useState(''); const [development, setDevelopment] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [flashing, flash] = useFlash(); const load = useCallback(async () => { try { const current = await clientRef.current.get('/fofos-admin/_config/databases'); setStatus(current); // Only seed the fields from the server when they are untouched, so a reload landing mid-typing // cannot replace what somebody is in the middle of writing. setProduction((typed) => typed || (current.production.name ?? '')); setDevelopment((typed) => typed || (current.development.name ?? '')); } catch { setStatus(null); } }, []); useEffect(() => { void load(); }, [load]); const save = async () => { setBusy(true); setError(null); try { setStatus( await clientRef.current.put('/fofos-admin/_config/databases', { production, development }), ); flash(); } catch (failure) { setError(readableError(failure)); } finally { setBusy(false); } }; const create = async () => { setBusy(true); setError(null); try { const result = await clientRef.current.post<{ created: string[]; status: Status }>( '/fofos-admin/_config/databases/create', {}, ); setStatus(result.status); } catch (failure) { setError(readableError(failure)); } finally { setBusy(false); } }; const field = ( label: string, hint: string, value: string, onChange: (next: string) => void, state: Named | undefined, ) => { // Only about what is SAVED. A tick next to a name somebody is still typing would be about a // different database from the one on screen. const settled = state?.name === value.trim() ? state : undefined; const Icon = settled?.exists ? CircleCheck : CircleDashed; return (
{hint}
onChange(event.target.value)} placeholder={label === 'Production' ? 'fofos_website' : 'fofos_website_dev'} className="w-full font-mono text-xs" autoComplete="off" spellCheck={false} /> {settled ? ( <> {settled.exists ? 'exists' : 'not yet'} ) : null}
); }; const dirty = production.trim() !== (status?.production.name ?? '') || development.trim() !== (status?.development.name ?? ''); // From the SAVED names, not the typed ones — Create acts on what is stored. const missing = [status?.production, status?.development].filter((entry) => entry?.name && entry.exists === false); return ( Saved ) : undefined } > {!status ? null : ( <> {field('Production', 'what the live site reads', production, setProduction, status.production)} {field('Development', 'what you work against', development, setDevelopment, status.development)} {error &&

{error}

}
{/* Only when a saved name has nothing behind it. Nothing to create, no button. */} {missing.length > 0 && ( )} {/* Save first. Create acts on what is stored, and a button that quietly used a different value from the one in the field would be the worst kind of surprise here. */} {missing.length > 0 && dirty && ( Save the names first. )}
)}
); };