Merge pull request 'feat(scripts): כפתור "הרץ" מ-UI לסקריפטי read-only (קטגוריה B #4)' (#283) from worktree-scripts-run-ui into main
This commit was merged in pull request #283.
This commit is contained in:
@@ -3,10 +3,16 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronDown, ChevronLeft } from "lucide-react";
|
||||
import { ChevronDown, ChevronLeft, Play, Loader2 } from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -15,7 +21,11 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { fetchScriptsCatalog } from "@/lib/api/scripts";
|
||||
import {
|
||||
fetchScriptsCatalog,
|
||||
useRunScript,
|
||||
type ScriptRunResult,
|
||||
} from "@/lib/api/scripts";
|
||||
|
||||
/*
|
||||
* /scripts — catalog of everything under scripts/, rendered as the
|
||||
@@ -150,7 +160,15 @@ function StatusChip({ status }: { status: ScriptStatus }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ScriptTable({ rows, giteaBase }: { rows: ScriptRow[]; giteaBase: string | null }) {
|
||||
function ScriptTable({
|
||||
rows, giteaBase, runnable, runningName, onRun,
|
||||
}: {
|
||||
rows: ScriptRow[];
|
||||
giteaBase: string | null;
|
||||
runnable: Set<string>;
|
||||
runningName: string | null;
|
||||
onRun: (name: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -195,24 +213,42 @@ function ScriptTable({ rows, giteaBase }: { rows: ScriptRow[]; giteaBase: string
|
||||
<StatusChip status={s.status} />
|
||||
</TableCell>
|
||||
<TableCell className="px-5 py-3.5 text-end">
|
||||
{disabled || !href ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="rounded-lg border border-rule-soft px-4 py-1.5 text-[0.81rem] font-semibold text-ink-muted cursor-default"
|
||||
>
|
||||
מקור
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-block rounded-lg border border-rule px-4 py-1.5 text-[0.81rem] font-semibold text-gold-deep hover:bg-gold-wash hover:border-gold transition-colors"
|
||||
>
|
||||
מקור
|
||||
</a>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-2 justify-end">
|
||||
{/* "הרץ" — only on read-only/audit scripts in the host allowlist (#4) */}
|
||||
{!disabled && runnable.has(s.name) ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={runningName !== null}
|
||||
onClick={() => onRun(s.name)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-success bg-success px-3.5 py-1.5 text-[0.81rem] font-semibold text-white hover:bg-success/90 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{runningName === s.name ? (
|
||||
<Loader2 className="size-3.5 animate-spin" aria-hidden />
|
||||
) : (
|
||||
<Play className="size-3.5" aria-hidden />
|
||||
)}
|
||||
הרץ
|
||||
</button>
|
||||
) : null}
|
||||
{disabled || !href ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="rounded-lg border border-rule-soft px-4 py-1.5 text-[0.81rem] font-semibold text-ink-muted cursor-default"
|
||||
>
|
||||
מקור
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-block rounded-lg border border-rule px-4 py-1.5 text-[0.81rem] font-semibold text-gold-deep hover:bg-gold-wash hover:border-gold transition-colors"
|
||||
>
|
||||
מקור
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -225,8 +261,16 @@ function ScriptTable({ rows, giteaBase }: { rows: ScriptRow[]; giteaBase: string
|
||||
/** Collapsible sub-topic block (#11, mockup 16): parchment header with a
|
||||
* chevron + title + count, and the group's table beneath. */
|
||||
function ScriptGroup({
|
||||
title, rows, giteaBase, defaultOpen,
|
||||
}: { title: string; rows: ScriptRow[]; giteaBase: string | null; defaultOpen: boolean }) {
|
||||
title, rows, giteaBase, defaultOpen, runnable, runningName, onRun,
|
||||
}: {
|
||||
title: string;
|
||||
rows: ScriptRow[];
|
||||
giteaBase: string | null;
|
||||
defaultOpen: boolean;
|
||||
runnable: Set<string>;
|
||||
runningName: string | null;
|
||||
onRun: (name: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||
@@ -248,7 +292,15 @@ function ScriptGroup({
|
||||
{rows.length}
|
||||
</span>
|
||||
</button>
|
||||
{open ? <ScriptTable rows={rows} giteaBase={giteaBase} /> : null}
|
||||
{open ? (
|
||||
<ScriptTable
|
||||
rows={rows}
|
||||
giteaBase={giteaBase}
|
||||
runnable={runnable}
|
||||
runningName={runningName}
|
||||
onRun={onRun}
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -276,6 +328,36 @@ export default function ScriptsPage() {
|
||||
|
||||
const giteaBase = data?.gitea_url ?? null;
|
||||
|
||||
// #4 — read-only scripts the host allowlist permits running; the "הרץ" button
|
||||
// surfaces only for these. A run opens an output dialog with exit code + stdout.
|
||||
const runnable = useMemo(
|
||||
() => new Set(data?.runnable_scripts ?? []),
|
||||
[data],
|
||||
);
|
||||
const run = useRunScript();
|
||||
const [runState, setRunState] = useState<{
|
||||
name: string;
|
||||
running: boolean;
|
||||
result?: ScriptRunResult;
|
||||
error?: string;
|
||||
} | null>(null);
|
||||
const runningName = runState?.running ? runState.name : null;
|
||||
|
||||
async function handleRun(name: string) {
|
||||
if (!window.confirm(`להריץ את ${name}? (סקריפט קריאה-בלבד)`)) return;
|
||||
setRunState({ name, running: true });
|
||||
try {
|
||||
const result = await run.mutateAsync(name);
|
||||
setRunState({ name, running: false, result });
|
||||
} catch (e) {
|
||||
setRunState({
|
||||
name,
|
||||
running: false,
|
||||
error: e instanceof Error ? e.message : "שגיאה בהרצה",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
@@ -314,6 +396,9 @@ export default function ScriptsPage() {
|
||||
rows={g.rows}
|
||||
giteaBase={giteaBase}
|
||||
defaultOpen={g.title !== ARCHIVE_GROUP && g.title !== DELETED_GROUP}
|
||||
runnable={runnable}
|
||||
runningName={runningName}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
))}
|
||||
{lastModified ? (
|
||||
@@ -324,6 +409,46 @@ export default function ScriptsPage() {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* run-output dialog (#4) — opens when a run completes (exit code + output) */}
|
||||
<Dialog
|
||||
open={!!runState && !runState.running}
|
||||
onOpenChange={(o) => !o && setRunState(null)}
|
||||
>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-ink-soft text-sm font-normal">הרצה:</span>
|
||||
<code dir="ltr" className="font-mono text-sm text-navy">
|
||||
{runState?.name}
|
||||
</code>
|
||||
{runState?.result ? (
|
||||
<span
|
||||
className={`ms-auto rounded-full px-2.5 py-0.5 text-xs font-semibold ${
|
||||
runState.result.ok
|
||||
? "bg-success-bg text-success"
|
||||
: "bg-danger-bg text-danger"
|
||||
}`}
|
||||
>
|
||||
{runState.result.ok ? "✓" : "✗"} exit {runState.result.exit_code}
|
||||
</span>
|
||||
) : null}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{runState?.error ? (
|
||||
<p className="text-danger text-sm">{runState.error}</p>
|
||||
) : runState?.result ? (
|
||||
<pre
|
||||
dir="ltr"
|
||||
className="text-[0.72rem] font-mono whitespace-pre-wrap max-h-72 overflow-auto rounded-md border border-rule-soft bg-parchment p-3 leading-relaxed"
|
||||
>
|
||||
{(runState.result.stdout || "") +
|
||||
(runState.result.stderr ? `\n${runState.result.stderr}` : "") ||
|
||||
"—"}
|
||||
</pre>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user