import { useRef, useState, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { Play, Loader2, Trash2, Terminal } from "lucide-react"; import { Button } from "./ui/button"; import { cn } from "~/client/lib/utils"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "./ui/sheet"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Label } from "./ui/label"; import { Textarea } from "./ui/textarea"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { devPanelExec } from "~/client/api-client/sdk.gen"; import { parseError } from "../lib/errors"; type DevPanelProps = { open: boolean; onOpenChange: (open: boolean) => void; }; type SseOutputEvent = { type: "stdout" | "stderr"; line: string; }; type SseDoneEvent = { type: "done"; exitCode: number; }; type SseErrorEvent = { type: "error"; message: string; }; type SseEvent = SseOutputEvent | SseDoneEvent | SseErrorEvent; export function DevPanel({ open, onOpenChange }: DevPanelProps) { const { data: repositories = [] } = useQuery({ ...listRepositoriesOptions(), enabled: open, }); const [selectedRepoId, setSelectedRepoId] = useState(""); const [commandLine, setCommandLine] = useState("snapshots"); const [output, setOutput] = useState([]); const [isRunning, setIsRunning] = useState(false); const abortControllerRef = useRef(null); const outputRef = useRef(null); const scrollToBottom = useCallback(() => { if (outputRef.current) { outputRef.current.scrollTop = outputRef.current.scrollHeight; } }, []); const appendOutput = useCallback( (line: string) => { setOutput((prev) => { const newOutput = [...prev, line]; setTimeout(scrollToBottom, 0); return newOutput; }); }, [scrollToBottom], ); const handleRun = async () => { if (!selectedRepoId || !commandLine.trim()) { return; } setOutput([]); setIsRunning(true); appendOutput(`$ restic ${commandLine}`.trim()); appendOutput("---"); abortControllerRef.current = new AbortController(); const trimmedLine = commandLine.trim(); const parts = trimmedLine.split(/\s+/); const command = parts[0]; const argsArray = parts.slice(1); try { const result = await devPanelExec({ path: { shortId: selectedRepoId }, body: { command, args: argsArray.length > 0 ? argsArray : undefined }, signal: abortControllerRef.current.signal, }); for await (const event of result.stream) { if (abortControllerRef.current.signal.aborted) { break; } const data = event as unknown as SseEvent; if (!data || typeof data !== "object") { continue; } if (data.type === "stdout" || data.type === "stderr") { appendOutput(data.line); } else if (data.type === "done") { appendOutput(`---`); appendOutput(`Command finished with exit code: ${data.exitCode}`); } else if (data.type === "error") { appendOutput(`Error: ${data.message}`); } } } catch (err) { if (err instanceof Error && err.name === "AbortError") { appendOutput("---"); appendOutput("Command cancelled"); } else { appendOutput(`Error: ${parseError(err)?.message}`); } } finally { setIsRunning(false); abortControllerRef.current = null; } }; const handleCancel = () => { abortControllerRef.current?.abort(); }; const handleClear = () => { setOutput([]); }; const handleClose = () => { if (isRunning) { handleCancel(); } onOpenChange(false); }; return ( Dev Panel Execute restic commands against a repository