Compare commits
1 commit
main
...
02-19-refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebefe410f4 |
3 changed files with 90 additions and 25 deletions
|
|
@ -33,6 +33,32 @@ type SseErrorEvent = {
|
||||||
|
|
||||||
type SseEvent = SseOutputEvent | SseDoneEvent | 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) {
|
export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
const { data: repositories = [] } = useQuery({
|
const { data: repositories = [] } = useQuery({
|
||||||
...listRepositoriesOptions(),
|
...listRepositoriesOptions(),
|
||||||
|
|
@ -41,10 +67,11 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
|
|
||||||
const [selectedRepoId, setSelectedRepoId] = useState<string>("");
|
const [selectedRepoId, setSelectedRepoId] = useState<string>("");
|
||||||
const [commandLine, setCommandLine] = useState("snapshots");
|
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 [isRunning, setIsRunning] = useState(false);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const outputRef = useRef<HTMLDivElement>(null);
|
const outputRef = useRef<HTMLDivElement>(null);
|
||||||
|
const outputIdRef = useRef(0);
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
if (outputRef.current) {
|
if (outputRef.current) {
|
||||||
|
|
@ -55,7 +82,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
const appendOutput = useCallback(
|
const appendOutput = useCallback(
|
||||||
(line: string) => {
|
(line: string) => {
|
||||||
setOutput((prev) => {
|
setOutput((prev) => {
|
||||||
const newOutput = [...prev, line];
|
const newOutput = [...prev, { id: outputIdRef.current++, line }];
|
||||||
setTimeout(scrollToBottom, 0);
|
setTimeout(scrollToBottom, 0);
|
||||||
return newOutput;
|
return newOutput;
|
||||||
});
|
});
|
||||||
|
|
@ -69,6 +96,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutput([]);
|
setOutput([]);
|
||||||
|
outputIdRef.current = 0;
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
appendOutput(`$ restic ${commandLine}`.trim());
|
appendOutput(`$ restic ${commandLine}`.trim());
|
||||||
appendOutput("---");
|
appendOutput("---");
|
||||||
|
|
@ -80,6 +108,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
const command = parts[0];
|
const command = parts[0];
|
||||||
const argsArray = parts.slice(1);
|
const argsArray = parts.slice(1);
|
||||||
|
|
||||||
|
let success = false;
|
||||||
try {
|
try {
|
||||||
const result = await devPanelExec({
|
const result = await devPanelExec({
|
||||||
path: { shortId: selectedRepoId },
|
path: { shortId: selectedRepoId },
|
||||||
|
|
@ -87,25 +116,8 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const event of result.stream) {
|
await processStream(result.stream, appendOutput, abortControllerRef.current.signal);
|
||||||
if (abortControllerRef.current.signal.aborted) {
|
success = true;
|
||||||
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) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.name === "AbortError") {
|
if (err instanceof Error && err.name === "AbortError") {
|
||||||
appendOutput("---");
|
appendOutput("---");
|
||||||
|
|
@ -113,7 +125,9 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
} else {
|
} else {
|
||||||
appendOutput(`Error: ${parseError(err)?.message}`);
|
appendOutput(`Error: ${parseError(err)?.message}`);
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
|
||||||
|
if (success || !abortControllerRef.current?.signal.aborted) {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +139,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setOutput([]);
|
setOutput([]);
|
||||||
|
outputIdRef.current = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|
@ -198,7 +213,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
Output will appear here...
|
Output will appear here...
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("space-y-0.5", { hidden: output.length === 0 })}>
|
<div className={cn("space-y-0.5", { hidden: output.length === 0 })}>
|
||||||
{output.map((line, i) => {
|
{output.map(({ id, line }) => {
|
||||||
let displayLine = line;
|
let displayLine = line;
|
||||||
let isJson = false;
|
let isJson = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -210,7 +225,7 @@ export function DevPanel({ open, onOpenChange }: DevPanelProps) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<pre
|
<pre
|
||||||
key={`${i}-${line.slice(0, 20)}`}
|
key={id}
|
||||||
className={cn("whitespace-pre-wrap break-all text-xs", {
|
className={cn("whitespace-pre-wrap break-all text-xs", {
|
||||||
"wrap-break-word text-[10px] leading-tight": isJson,
|
"wrap-break-word text-[10px] leading-tight": isJson,
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
setAssignments(buildAssignments(currentMirrors));
|
queueMicrotask(() => {
|
||||||
|
setAssignments(buildAssignments(currentMirrors));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentMirrors, hasChanges]);
|
}, [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