From ebefe410f4e6a84f21d27aad39401270069c18b1 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 19 Feb 2026 19:05:17 +0100 Subject: [PATCH] refactor: react-doctor --- app/client/components/dev-panel.tsx | 63 ++++++++++++------- .../components/schedule-mirrors-config.tsx | 4 +- .../compression-stats-chart-inner.tsx | 48 ++++++++++++++ 3 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 app/client/modules/repositories/components/compression-stats-chart-inner.tsx diff --git a/app/client/components/dev-panel.tsx b/app/client/components/dev-panel.tsx index eb242923..d8b1e8ac 100644 --- a/app/client/components/dev-panel.tsx +++ b/app/client/components/dev-panel.tsx @@ -33,6 +33,32 @@ type SseErrorEvent = { type SseEvent = SseOutputEvent | SseDoneEvent | SseErrorEvent; +async function processStream( + stream: AsyncIterable, + appendOutput: (line: string) => void, + abortSignal: AbortSignal, +): Promise { + for await (const event of stream) { + if (abortSignal.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}`); + } + } +} + export function DevPanel({ open, onOpenChange }: DevPanelProps) { const { data: repositories = [] } = useQuery({ ...listRepositoriesOptions(), @@ -41,10 +67,11 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { const [selectedRepoId, setSelectedRepoId] = useState(""); const [commandLine, setCommandLine] = useState("snapshots"); - const [output, setOutput] = useState([]); + const [output, setOutput] = useState>([]); const [isRunning, setIsRunning] = useState(false); const abortControllerRef = useRef(null); const outputRef = useRef(null); + const outputIdRef = useRef(0); const scrollToBottom = useCallback(() => { if (outputRef.current) { @@ -55,7 +82,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { const appendOutput = useCallback( (line: string) => { setOutput((prev) => { - const newOutput = [...prev, line]; + const newOutput = [...prev, { id: outputIdRef.current++, line }]; setTimeout(scrollToBottom, 0); return newOutput; }); @@ -69,6 +96,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { } setOutput([]); + outputIdRef.current = 0; setIsRunning(true); appendOutput(`$ restic ${commandLine}`.trim()); appendOutput("---"); @@ -80,6 +108,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { const command = parts[0]; const argsArray = parts.slice(1); + let success = false; try { const result = await devPanelExec({ path: { shortId: selectedRepoId }, @@ -87,25 +116,8 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { 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}`); - } - } + await processStream(result.stream, appendOutput, abortControllerRef.current.signal); + success = true; } catch (err) { if (err instanceof Error && err.name === "AbortError") { appendOutput("---"); @@ -113,7 +125,9 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { } else { appendOutput(`Error: ${parseError(err)?.message}`); } - } finally { + } + + if (success || !abortControllerRef.current?.signal.aborted) { setIsRunning(false); abortControllerRef.current = null; } @@ -125,6 +139,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { const handleClear = () => { setOutput([]); + outputIdRef.current = 0; }; const handleClose = () => { @@ -198,7 +213,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { Output will appear here...
- {output.map((line, i) => { + {output.map(({ id, line }) => { let displayLine = line; let isJson = false; try { @@ -210,7 +225,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) { } return (
 {
 		if (!hasChanges) {
-			setAssignments(buildAssignments(currentMirrors));
+			queueMicrotask(() => {
+				setAssignments(buildAssignments(currentMirrors));
+			});
 		}
 	}, [currentMirrors, hasChanges]);
 
diff --git a/app/client/modules/repositories/components/compression-stats-chart-inner.tsx b/app/client/modules/repositories/components/compression-stats-chart-inner.tsx
new file mode 100644
index 00000000..65e85a56
--- /dev/null
+++ b/app/client/modules/repositories/components/compression-stats-chart-inner.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { Label, Pie, PieChart } from "recharts";
+import { ByteSize } from "~/client/components/bytes-size";
+import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/client/components/ui/chart";
+import type { ChartConfig } from "~/client/components/ui/chart";
+
+type CompressionChartProps = {
+	chartData: Array<{ name: string; value: number; fill: string }>;
+	chartConfig: ChartConfig;
+	compressionRatio: number;
+};
+
+export default function CompressionChart({ chartData, chartConfig, compressionRatio }: CompressionChartProps) {
+	return (
+		
+			
+				 [, name]}
+						/>
+					}
+				/>
+				
+					
+			
+		
+	);
+}