one spinner per action, one button for both, and the shared console
Four things, all of them the panel telling a small lie. A single `busy` boolean put BOTH rows into a spinner while only the pressed one was doing anything — the other looked started, then finished without having done a thing. It is now which action is running, so a row spins when it is the one running or when both are. Pushing both is the common case and was two clicks. It is one, running production then development sequentially: they run the same drizzle against one Postgres, and two at once buys nothing but an interleaved transcript. The status is re-read after every push regardless of outcome. It was taken from the route's reply, which only exists on the path where the push SUCCEEDED — a failure answers 400 and throws, and a push that failed halfway can still have changed the table count. That is the difference between the panel being right and the panel being right when nothing went wrong. And the output is the same console the checkout uses, extracted rather than copied: black, copyable, gone a few seconds after a success and never after a failure. WebsiteCheckout keeps its behaviour and loses the state that used to implement it. Restore stays disabled. What a backup IS here is still undecided, and a button that guesses at the source of a restore does not belong near a client's data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+5
-34
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Check, CircleAlert, CircleCheck, CircleDashed, Copy, DownloadCloud, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { CircleAlert, CircleCheck, CircleDashed, DownloadCloud, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { copyToClipboard } from 'helpers/clipboard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { readableError } from './errors';
|
||||
import { useFlash } from './useFlash';
|
||||
import { Console } from './Console';
|
||||
|
||||
/**
|
||||
* The website checkout under the root location: is it there, clone it, update it.
|
||||
@@ -67,8 +66,6 @@ export const WebsiteCheckout = ({
|
||||
const [busy, setBusy] = useState<'clone' | 'update' | null>(null);
|
||||
const [output, setOutput] = useState<string | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [showing, show] = useFlash();
|
||||
const [copied, flashCopied] = useFlash(1500);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@@ -101,9 +98,6 @@ export const WebsiteCheckout = ({
|
||||
setStatus(result.status);
|
||||
setOutput(result.output || null);
|
||||
setFailed(!result.ok);
|
||||
// Only on the way out. A failure has no timer at all rather than a long one — nothing should be
|
||||
// able to take it off the screen except the next attempt.
|
||||
if (result.ok) show();
|
||||
} catch (failure) {
|
||||
// A 400 carries git's own words in the body; `readableError` surfaces them.
|
||||
setOutput(readableError(failure));
|
||||
@@ -165,32 +159,9 @@ export const WebsiteCheckout = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* git's own words, in git's own shape — a console, so it looks like the thing it is quoting
|
||||
rather than like a paragraph of the form. Scrolled, not truncated: the useful line of a
|
||||
failure is usually the last one. */}
|
||||
{output && (failed || showing) && (
|
||||
<div className="relative">
|
||||
<pre className="max-h-40 overflow-auto rounded-md border border-border bg-black p-3 pr-10 font-mono text-[11px] leading-relaxed whitespace-pre-wrap">
|
||||
<code className={failed ? 'text-red-400' : 'text-neutral-200'}>{output}</code>
|
||||
</pre>
|
||||
|
||||
{/* Pinned to the corner of the BOX, not scrolled with the text — a copy button that leaves
|
||||
the viewport when the transcript is long is missing exactly when the transcript is worth
|
||||
copying. `pr-10` above keeps the text out from under it. Its own dark background because
|
||||
it sits over the output rather than beside it. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (await copyToClipboard(output)) flashCopied();
|
||||
}}
|
||||
aria-label="Copy this output"
|
||||
title="Copy"
|
||||
className="absolute top-1.5 right-1.5 rounded border border-border/60 bg-black/80 p-1.5 text-neutral-400 transition-colors hover:text-neutral-100"
|
||||
>
|
||||
{copied ? <Check className="size-3.5 text-success" /> : <Copy className="size-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* git's own words, in git's own shape — see `Console`, which owns the timing and the copy
|
||||
button. Shared with the schema pushes in Database, which need exactly the same thing. */}
|
||||
<Console output={output} failed={failed} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user