refactor: react-doctor
This commit is contained in:
parent
a0a813ed09
commit
ebefe410f4
3 changed files with 90 additions and 25 deletions
|
|
@ -33,6 +33,32 @@ type SseErrorEvent = {
|
|||
|
||||
type SseEvent = SseOutputEvent | SseDoneEvent | SseErrorEvent;
|
||||
|
||||
async function processStream(
|
||||
stream: AsyncIterable<unknown>,
|
||||
appendOutput: (line: string) => void,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<void> {
|
||||
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<string>("");
|
||||
const [commandLine, setCommandLine] = useState("snapshots");
|
||||
const [output, setOutput] = useState<string[]>([]);
|
||||
const [output, setOutput] = useState<Array<{ id: number; line: string }>>([]);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const outputRef = useRef<HTMLDivElement>(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...
|
||||
</div>
|
||||
<div className={cn("space-y-0.5", { hidden: output.length === 0 })}>
|
||||
{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 (
|
||||
<pre
|
||||
key={`${i}-${line.slice(0, 20)}`}
|
||||
key={id}
|
||||
className={cn("whitespace-pre-wrap break-all text-xs", {
|
||||
"wrap-break-word text-[10px] leading-tight": isJson,
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
|
|||
|
||||
useEffect(() => {
|
||||
if (!hasChanges) {
|
||||
setAssignments(buildAssignments(currentMirrors));
|
||||
queueMicrotask(() => {
|
||||
setAssignments(buildAssignments(currentMirrors));
|
||||
});
|
||||
}
|
||||
}, [currentMirrors, hasChanges]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel
|
||||
formatter={(value, name) => [<ByteSize key={name} bytes={value as number} />, name]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Pie data={chartData} dataKey="value" nameKey="name" innerRadius={65} strokeWidth={5}>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
||||
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
|
||||
{compressionRatio > 0 ? `${compressionRatio.toFixed(1)}x` : "—"}
|
||||
</tspan>
|
||||
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
|
||||
Compression
|
||||
</tspan>
|
||||
</text>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue