added ability to copy bot token
This commit is contained in:
parent
bb1f680d3b
commit
8aad71643f
16 changed files with 294 additions and 23 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { CheckCircle2, CircleAlert, FileJson, Loader2, Play, X } from "lucide-react";
|
||||
import { Check, CheckCircle2, CircleAlert, Copy, FileJson, Loader2, Play, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
|
@ -17,7 +17,8 @@ export function ActionButton({
|
|||
tone = "danger",
|
||||
disabled = false,
|
||||
onDone,
|
||||
onError
|
||||
onError,
|
||||
secretField
|
||||
}: {
|
||||
label: string;
|
||||
path: string;
|
||||
|
|
@ -34,17 +35,24 @@ export function ActionButton({
|
|||
// form — an optimistic-locking 409, say — and replace the raw backend text with
|
||||
// an explanation by returning it.
|
||||
onError?: (error: unknown) => string | undefined;
|
||||
// secretField names a key in result.details that holds a one-time secret
|
||||
// (e.g. a bot token) -- when present, it's pulled out of the generic JSON
|
||||
// dump and rendered instead as its own copy-to-clipboard callout, so it
|
||||
// doesn't get lost among the other fields.
|
||||
secretField?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [reason, setReason] = useState("");
|
||||
const [result, setResult] = useState<CommandResult | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [secretCopied, setSecretCopied] = useState(false);
|
||||
|
||||
function reset() {
|
||||
setReason("");
|
||||
setResult(null);
|
||||
setError("");
|
||||
setSecretCopied(false);
|
||||
}
|
||||
|
||||
async function run(confirm: boolean) {
|
||||
|
|
@ -78,6 +86,18 @@ export function ActionButton({
|
|||
}
|
||||
}, [open, payload]);
|
||||
|
||||
const secretValue = secretField && result?.details && typeof result.details[secretField] === "string"
|
||||
? (result.details[secretField] as string)
|
||||
: "";
|
||||
const visibleDetails = secretValue && result?.details
|
||||
? Object.fromEntries(Object.entries(result.details).filter(([key]) => key !== secretField))
|
||||
: result?.details;
|
||||
|
||||
async function copySecret() {
|
||||
await navigator.clipboard.writeText(secretValue);
|
||||
setSecretCopied(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -133,7 +153,19 @@ export function ActionButton({
|
|||
<div className="result-line"><span>{"Status"}</span><strong>{result.status}</strong></div>
|
||||
<div className="result-line"><span>{"Dry-run"}</span><strong>{result.dry_run ? "Yes" : "No"}</strong></div>
|
||||
<div className="result-message">{result.message || result.error}</div>
|
||||
{result.details && <JsonBlock value={JSON.stringify(result.details, null, 2)} />}
|
||||
{secretValue && (
|
||||
<div className="secret-reveal">
|
||||
<div className="secret-reveal-label">{"One-time secret — copy it now, it won't be shown again"}</div>
|
||||
<div className="secret-reveal-row">
|
||||
<code className="secret-reveal-value">{"•".repeat(Math.min(secretValue.length, 40))}</code>
|
||||
<button className="btn icon-text" type="button" onClick={() => void copySecret()}>
|
||||
{secretCopied ? <Check size={15} /> : <Copy size={15} />}
|
||||
{secretCopied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{visibleDetails && Object.keys(visibleDetails).length > 0 && <JsonBlock value={JSON.stringify(visibleDetails, null, 2)} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue