once both databases are there, the form becomes two rows of actions
A text box invites an edit nobody is here to make. Named and created, the two things somebody wants are per-database actions, so the inputs and the button go and each database becomes a label, a table count, and what can be done to it. Push schema runs the WEBSITE's own `db:push`. Not drizzle-kit driven from here with a config this plugin assembles — the schema, the drizzle version and the push command all belong to that repository, and reimplementing any of them would be a second definition of the site's database going stale on its own schedule. This supplies the connection and runs their script. The connection goes in the environment, which works because src/env-file.ts consults .env files first and process.env last. So an ambient value is used only where no file in the checkout sets that key, and this cannot quietly override a connection somebody wrote down. Their name guard still applies and should: env.ts refuses to connect unless the database name matches what FOFOS_ENV expects. A plugin that worked around that would be defeating the one check standing between development and live data. "Schema pushed" is a COUNT of tables in public, not a comparison against the site's expected tables — that list would be a second copy of the site's schema, stale the first time it gains a table, and the question being asked is only whether anything is in there yet. Restore is present and disabled, gated on the schema being pushed. It is not wired: which backup it reads from is not decided, and guessing at the source of a restore is not a thing to do with a client's data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+82
-3
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Check, CircleCheck, CircleDashed, Database, Loader2, Plus } from 'lucide-react';
|
||||
import { ArrowUpFromLine, Check, CircleCheck, CircleDashed, Database, DatabaseBackup, Loader2, Plus } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -29,7 +29,7 @@ import { useFlash } from './useFlash';
|
||||
* typing a name actually has, and answering it about a different name would be worse than silence.
|
||||
*/
|
||||
|
||||
type Named = { name: string | null; exists: boolean | null };
|
||||
type Named = { name: string | null; exists: boolean | null; tables: number | null };
|
||||
type Status = { production: Named; development: Named; available: string[] };
|
||||
|
||||
export const DatabaseBlock = () => {
|
||||
@@ -61,6 +61,33 @@ export const DatabaseBlock = () => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const [output, setOutput] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Push the site's schema into one of the two.
|
||||
*
|
||||
* Runs the WEBSITE's own `db:push` — the schema and the drizzle version belong to that repository,
|
||||
* and a plugin that reimplemented either would be a second definition of the site's database going
|
||||
* stale on its own schedule.
|
||||
*/
|
||||
const push = async (target: 'production' | 'development') => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setOutput(null);
|
||||
try {
|
||||
const result = await clientRef.current.post<{ ok: boolean; output: string; status: Status }>(
|
||||
`/fofos-admin/_config/databases/${target}/push-schema`,
|
||||
{},
|
||||
);
|
||||
setStatus(result.status);
|
||||
setOutput(result.output);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** Records the names and creates whichever is missing. The server does both, in that order. */
|
||||
const apply = async () => {
|
||||
setBusy(true);
|
||||
@@ -123,6 +150,47 @@ export const DatabaseBlock = () => {
|
||||
|
||||
const label = missing.length ? `Create ${missing.length === 1 ? missing[0] : 'databases'}` : 'Save names';
|
||||
|
||||
/*
|
||||
Settled: both named, both there. The fields have nothing left to do — a text box invites an edit
|
||||
nobody is here to make, and the two things somebody DOES want at this point are per-database
|
||||
actions rather than a form.
|
||||
*/
|
||||
const settled = Boolean(
|
||||
status?.production.exists && status?.development.exists && !dirty && production.trim() && development.trim(),
|
||||
);
|
||||
|
||||
const row = (label: string, target: 'production' | 'development', state: Named) => {
|
||||
// "Schema pushed" is simply: something is in it. See the note on `tables`.
|
||||
const pushed = (state.tables ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="truncate font-mono text-xs">{state.name}</p>
|
||||
</div>
|
||||
|
||||
<span className="shrink-0 text-xs text-muted-foreground/70">
|
||||
{state.tables === null ? '' : pushed ? `${state.tables} tables` : 'empty'}
|
||||
</span>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => void push(target)} disabled={busy}>
|
||||
{busy ? <Loader2 className="size-3.5 animate-spin" /> : <ArrowUpFromLine className="size-3.5" />}
|
||||
Push schema
|
||||
</Button>
|
||||
|
||||
{/* Nothing to restore INTO until there are tables to restore into — a data-only load against
|
||||
an empty database fails on every insert, and the failure would read as a bad backup. */}
|
||||
<Button variant="outline" size="sm" disabled title={pushed ? undefined : 'Push the schema first'}>
|
||||
<DatabaseBackup className="size-3.5" />
|
||||
Restore from backup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Block
|
||||
icon={Database}
|
||||
@@ -137,7 +205,18 @@ export const DatabaseBlock = () => {
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{!status ? null : (
|
||||
{!status ? null : settled ? (
|
||||
<>
|
||||
{row('Production', 'production', status.production)}
|
||||
{row('Development', 'development', status.development)}
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
{output && (
|
||||
<pre className="max-h-48 overflow-auto rounded bg-black p-3 font-mono text-[11px] whitespace-pre-wrap text-white/90">
|
||||
{output}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{field('Production', 'what the live site reads', production, setProduction)}
|
||||
{field('Development', 'what you work against', development, setDevelopment)}
|
||||
|
||||
Reference in New Issue
Block a user