Add update step tracking and progress bar component
This commit is contained in:
1 parent
eb1301df99
commit
f4d5e74c59
8 files changed
+631
-3
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import React from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { UpdateStep } from "@/stores/updateStore";
|
||||
|
||||
interface UpdateProgressBarProps {
|
||||
steps: UpdateStep[];
|
||||
overallProgress: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UpdateProgressBar({
|
||||
steps,
|
||||
overallProgress,
|
||||
className,
|
||||
}: UpdateProgressBarProps) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Overall Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Overall Progress</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
{overallProgress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
className="h-full rounded-full bg-gray-900 transition-all duration-500 ease-out dark:bg-gray-100"
|
||||
style={{ width: `${overallProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps List */}
|
||||
<div className="space-y-2">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded border px-3 py-2.5 transition-all duration-200",
|
||||
step.status === "in-progress" &&
|
||||
"border-gray-400 bg-gray-50 dark:border-gray-600 dark:bg-gray-900",
|
||||
step.status === "completed" &&
|
||||
"border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-950",
|
||||
step.status === "error" &&
|
||||
"border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/30",
|
||||
step.status === "pending" &&
|
||||
"border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950",
|
||||
)}
|
||||
>
|
||||
{/* Status Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{step.status === "pending" && (
|
||||
<div className="size-5 rounded-full border-2 border-gray-300 dark:border-gray-700" />
|
||||
)}
|
||||
{step.status === "in-progress" && (
|
||||
<Icon
|
||||
name="lu:Loader"
|
||||
className="size-5 animate-spin text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
)}
|
||||
{step.status === "completed" && (
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-gray-700 dark:bg-gray-300">
|
||||
<Icon
|
||||
name="lu:Check"
|
||||
className="size-3 text-white dark:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step.status === "error" && (
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-red-600 dark:bg-red-500">
|
||||
<Icon name="lu:X" className="size-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm",
|
||||
step.status === "completed" &&
|
||||
"text-gray-600 dark:text-gray-400",
|
||||
step.status === "error" &&
|
||||
"font-medium text-red-700 dark:text-red-400",
|
||||
step.status === "in-progress" &&
|
||||
"font-medium text-gray-900 dark:text-gray-100",
|
||||
step.status === "pending" &&
|
||||
"text-gray-500 dark:text-gray-500",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
{/* Subsector Badge - minimal inline display */}
|
||||
<span
|
||||
className={cn("text-xs text-gray-400 dark:text-gray-600")}
|
||||
>
|
||||
(
|
||||
{step.id === "rust-build"
|
||||
? "rust & electron"
|
||||
: step.subsector}
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
{/* Progress percentage for in-progress steps */}
|
||||
{step.status === "in-progress" &&
|
||||
step.progress !== undefined &&
|
||||
step.progress > 0 && (
|
||||
<span className="text-xs font-medium text-gray-600 tabular-nums dark:text-gray-400">
|
||||
{Math.round(step.progress)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for steps with detailed tracking */}
|
||||
{step.status === "in-progress" &&
|
||||
step.progress !== undefined &&
|
||||
step.progress > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
className="h-full rounded-full bg-gray-700 transition-all duration-500 dark:bg-gray-300"
|
||||
style={{ width: `${step.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export const UPDATE_EXECUTE = "update:execute";
|
||||
export const UPDATE_LOG = "update:log";
|
||||
export const UPDATE_END = "update:end";
|
||||
export const UPDATE_CANCEL = "update:cancel";
|
||||
export const UPDATE_STEP = "update:step";
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
UPDATE_END,
|
||||
UPDATE_EXECUTE,
|
||||
UPDATE_LOG,
|
||||
UPDATE_STEP,
|
||||
} from "./update-channels";
|
||||
|
||||
type UpdateExecuteInvokeParams = {
|
||||
@@ -16,11 +17,20 @@ type UpdateExecuteInvokeParams = {
|
||||
};
|
||||
};
|
||||
|
||||
type UpdateStepParams = {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
};
|
||||
|
||||
export function exposeUpdateContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
|
||||
let currentLogListener: ((event: any, log: string) => void) | null = null;
|
||||
let currentEndListener: ((event: any, params: any) => void) | null = null;
|
||||
let currentStepListener:
|
||||
| ((event: any, params: UpdateStepParams) => void)
|
||||
| null = null;
|
||||
|
||||
contextBridge.exposeInMainWorld("update", {
|
||||
execute: (params: UpdateExecuteInvokeParams) =>
|
||||
@@ -52,5 +62,17 @@ export function exposeUpdateContext() {
|
||||
|
||||
ipcRenderer.on(UPDATE_END, currentEndListener);
|
||||
},
|
||||
|
||||
onStep: (callback: (params: UpdateStepParams) => void) => {
|
||||
if (currentStepListener) {
|
||||
ipcRenderer.removeListener(UPDATE_STEP, currentStepListener);
|
||||
}
|
||||
|
||||
currentStepListener = (_event, params) => {
|
||||
callback(params);
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_STEP, currentStepListener);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UPDATE_END,
|
||||
UPDATE_EXECUTE,
|
||||
UPDATE_LOG,
|
||||
UPDATE_STEP,
|
||||
} from "./update-channels";
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import tkill from "@jub3i/tree-kill";
|
||||
@@ -116,6 +117,13 @@ async function update(
|
||||
commit,
|
||||
});
|
||||
|
||||
// Reset Rust build progress tracking
|
||||
rustBuildProgress = {
|
||||
totalDerivations: 0,
|
||||
builtDerivations: 0,
|
||||
maxPercent: 0,
|
||||
};
|
||||
|
||||
const qitechControlEnv = process.env.QITECH_CONTROL_ENV;
|
||||
const homeDir =
|
||||
qitechControlEnv === "control-os" ? "/home/qitech" : process.env.HOME;
|
||||
@@ -129,6 +137,8 @@ async function update(
|
||||
|
||||
// 1. first make sure the clone path is empty by deleting it if it containsa .git folder
|
||||
const repoDir = `${homeDir}/${githubRepoName}`;
|
||||
|
||||
// Clear repo (not tracked in progress UI)
|
||||
const clearResult = await clearRepoDirectory(
|
||||
`${homeDir}/${githubRepoName}`,
|
||||
event,
|
||||
@@ -139,6 +149,10 @@ async function update(
|
||||
}
|
||||
|
||||
// 2. clone the repository
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
});
|
||||
const cloneResult = await cloneRepository(
|
||||
{
|
||||
githubRepoOwner,
|
||||
@@ -151,11 +165,19 @@ async function update(
|
||||
event,
|
||||
);
|
||||
if (!cloneResult.success) {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_LOG, cloneResult.error);
|
||||
return;
|
||||
}
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "completed",
|
||||
});
|
||||
|
||||
// 3. make the nixos-install.sh script executable
|
||||
// 3. make the nixos-install.sh script executable (not tracked in progress UI)
|
||||
const chmodResult = await runCommand(
|
||||
"chmod",
|
||||
["+x", "nixos-install.sh"],
|
||||
@@ -168,17 +190,35 @@ async function update(
|
||||
}
|
||||
|
||||
// 4. run the nixos-install.sh script
|
||||
const installResult = await runCommand(
|
||||
// This script will handle rust-build, electron-build, and system-install
|
||||
// Start with rust-build (cargo builds)
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
});
|
||||
|
||||
const installResult = await runCommandWithStepTracking(
|
||||
"./nixos-install.sh",
|
||||
[],
|
||||
repoDir,
|
||||
event,
|
||||
);
|
||||
|
||||
if (!installResult.success) {
|
||||
// Mark current and remaining steps as error
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_LOG, installResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - steps are already marked as completed by runCommandWithStepTracking
|
||||
resolve();
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
@@ -244,7 +284,7 @@ async function cloneRepository(
|
||||
: `https://github.com/${githubRepoOwner}/${githubRepoName}.git`;
|
||||
|
||||
// Determine clone arguments based on whether tag, branch, or commit is specified
|
||||
const cloneArgs = ["clone", repoUrl];
|
||||
const cloneArgs = ["clone", "--progress", repoUrl];
|
||||
|
||||
if (tag) {
|
||||
// Clone a specific tag
|
||||
@@ -333,6 +373,11 @@ async function runCommand(
|
||||
const log = data.toString();
|
||||
console.error(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
|
||||
// Git outputs progress to stderr
|
||||
if (cmd === "git" && args.includes("--progress")) {
|
||||
parseGitProgress(log, event);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
@@ -386,6 +431,249 @@ async function runCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Git clone progress
|
||||
function parseGitProgress(
|
||||
log: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): void {
|
||||
// Git progress format: "Receiving objects: 45% (234/520)"
|
||||
// or "Resolving deltas: 100% (150/150)"
|
||||
const receivingMatch = log.match(/Receiving objects:\s*(\d+)%/);
|
||||
const resolvingMatch = log.match(/Resolving deltas:\s*(\d+)%/);
|
||||
|
||||
if (receivingMatch) {
|
||||
const percent = parseInt(receivingMatch[1], 10);
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
progress: Math.floor(percent * 0.8), // Receiving is 80% of clone
|
||||
});
|
||||
} else if (resolvingMatch) {
|
||||
const percent = parseInt(resolvingMatch[1], 10);
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
progress: Math.floor(80 + percent * 0.2), // Resolving is last 20%
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track Rust build progress
|
||||
let rustBuildProgress = {
|
||||
totalDerivations: 0,
|
||||
builtDerivations: 0,
|
||||
maxPercent: 0, // Track max to prevent backward movement
|
||||
};
|
||||
|
||||
// Parse Rust build output for progress
|
||||
function parseRustBuildOutput(
|
||||
log: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): void {
|
||||
// Track derivations to build
|
||||
const derivationsMatch = log.match(/these (\d+) derivations? will be built/i);
|
||||
if (derivationsMatch) {
|
||||
rustBuildProgress.totalDerivations = parseInt(derivationsMatch[1], 10);
|
||||
rustBuildProgress.builtDerivations = 0;
|
||||
rustBuildProgress.maxPercent = 0;
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Track building packages
|
||||
if (
|
||||
log.includes("building '/nix/store/") ||
|
||||
log.includes("building /nix/store/")
|
||||
) {
|
||||
rustBuildProgress.builtDerivations++;
|
||||
|
||||
// Check if this is the server-deps package (one of the last builds)
|
||||
const isServerDeps = log.includes("-server-deps");
|
||||
|
||||
let percent = 15;
|
||||
if (isServerDeps) {
|
||||
// server-deps indicates we're at 85%
|
||||
percent = 85;
|
||||
} else if (rustBuildProgress.totalDerivations > 0) {
|
||||
const derivationProgress =
|
||||
rustBuildProgress.builtDerivations / rustBuildProgress.totalDerivations;
|
||||
percent = 15 + Math.floor(derivationProgress * 70); // Map to 15-85%
|
||||
}
|
||||
|
||||
// Only move forward
|
||||
percent = Math.max(percent, rustBuildProgress.maxPercent);
|
||||
rustBuildProgress.maxPercent = percent;
|
||||
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: percent,
|
||||
});
|
||||
}
|
||||
|
||||
// Track installing phase - go up to 90%
|
||||
if (log.includes("installing") || log.includes("Installing")) {
|
||||
const percent = Math.max(90, rustBuildProgress.maxPercent);
|
||||
rustBuildProgress.maxPercent = percent;
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: percent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced version of runCommand that tracks build steps based on log output
|
||||
async function runCommandWithStepTracking(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
workingDir: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const completeCommand = `${cmd} ${args.join(" ")}`;
|
||||
const workingDirText = terminalGray(workingDir);
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
`🚀 ${workingDirText} ${terminalColor("blue", completeCommand)}`,
|
||||
);
|
||||
|
||||
const childProcess = spawn(cmd, args, {
|
||||
cwd: workingDir,
|
||||
});
|
||||
|
||||
// Store reference to current process for cancellation
|
||||
currentUpdateProcess = childProcess;
|
||||
|
||||
// Track which steps have been marked as in-progress
|
||||
// Note: rust-build is already marked as in-progress before calling this function
|
||||
let systemInstallStarted = false;
|
||||
|
||||
// Function to process log output and update steps
|
||||
const processLogForSteps = (log: string) => {
|
||||
const logLower = log.toLowerCase();
|
||||
|
||||
// Parse Rust build progress
|
||||
parseRustBuildOutput(log, event);
|
||||
|
||||
// Check for system install indicators
|
||||
// System install happens after Rust build completes (at ~90%)
|
||||
// Look for bootloader/activation messages that indicate final system installation
|
||||
if (
|
||||
!systemInstallStarted &&
|
||||
rustBuildProgress.maxPercent >= 90 &&
|
||||
(logLower.includes("updating grub") ||
|
||||
logLower.includes("installing bootloader") ||
|
||||
logLower.includes("updating bootloader") ||
|
||||
logLower.includes("activating the configuration") ||
|
||||
logLower.includes("building the system configuration") ||
|
||||
logLower.includes("these 0 derivations"))
|
||||
) {
|
||||
console.log("🟢 Detected system install start:", log.substring(0, 100));
|
||||
// Mark rust as complete, start system install
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "completed",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "in-progress",
|
||||
});
|
||||
systemInstallStarted = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Stream stdout logs back to renderer
|
||||
childProcess.stdout.on("data", (data) => {
|
||||
const log = data.toString();
|
||||
console.log(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
processLogForSteps(log);
|
||||
});
|
||||
|
||||
// Stream stderr logs back to renderer
|
||||
childProcess.stderr.on("data", (data) => {
|
||||
const log = data.toString();
|
||||
console.error(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
processLogForSteps(log);
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code, signal) => {
|
||||
// Clear process reference when completed
|
||||
if (currentUpdateProcess === childProcess) {
|
||||
currentUpdateProcess = null;
|
||||
}
|
||||
|
||||
if (signal === "SIGTERM" || signal === "SIGKILL") {
|
||||
event.sender.send(UPDATE_LOG, terminalInfo("Command was cancelled"));
|
||||
reject({
|
||||
success: false,
|
||||
error: "Command was cancelled",
|
||||
});
|
||||
} else if (code === 0) {
|
||||
// Mark all remaining steps as completed on success
|
||||
if (!systemInstallStarted) {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "completed",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "completed",
|
||||
});
|
||||
} else {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "completed",
|
||||
});
|
||||
}
|
||||
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalSuccess("Command completed successfully"),
|
||||
);
|
||||
resolve({ success: true, error: undefined });
|
||||
} else {
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalError(`Command failed with code ${code}`),
|
||||
);
|
||||
reject({
|
||||
success: false,
|
||||
error: terminalError(code?.toString() ?? "NO_CODE"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on("error", (err) => {
|
||||
// Clear process reference on error
|
||||
if (currentUpdateProcess === childProcess) {
|
||||
currentUpdateProcess = null;
|
||||
}
|
||||
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalError(`Command error: ${err.message}`),
|
||||
);
|
||||
reject({
|
||||
success: false,
|
||||
error: terminalError(err.message),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
event.sender.send(UPDATE_LOG, terminalError(`Error: ${error.toString()}`));
|
||||
return { success: false, error: error.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
function terminalColor(
|
||||
color: "blue" | "green" | "red" | "cyan" | "gray",
|
||||
text: string,
|
||||
|
||||
@@ -2,6 +2,13 @@ import type { UpdateInfo } from "@/stores/updateStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
|
||||
let currentLogListener: ((line: string) => void) | null = null;
|
||||
let currentStepListener:
|
||||
| ((params: {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
}) => void)
|
||||
| null = null;
|
||||
|
||||
export async function updateExecute(
|
||||
source: UpdateInfo,
|
||||
@@ -14,15 +21,32 @@ export async function updateExecute(
|
||||
currentLogListener = null;
|
||||
}
|
||||
|
||||
if (currentStepListener) {
|
||||
window.update.onStep(() => {}); // clear previous
|
||||
currentStepListener = null;
|
||||
}
|
||||
|
||||
currentLogListener = onLog;
|
||||
window.update.onLog(currentLogListener);
|
||||
|
||||
// Set up step listener
|
||||
currentStepListener = (params) => {
|
||||
const { setStepStatus, setStepProgress } = useUpdateStore.getState();
|
||||
setStepStatus(params.stepId, params.status);
|
||||
if (params.progress !== undefined) {
|
||||
setStepProgress(params.stepId, params.progress);
|
||||
}
|
||||
};
|
||||
window.update.onStep(currentStepListener);
|
||||
|
||||
window.update.execute(source);
|
||||
|
||||
window.update.onEnd((params) => {
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {});
|
||||
window.update.onStep(() => {}); // remove step listener
|
||||
currentLogListener = null;
|
||||
currentStepListener = null;
|
||||
resolve(params);
|
||||
});
|
||||
});
|
||||
@@ -55,6 +79,7 @@ export async function updateCancel(): Promise<{
|
||||
const result = await window.update.cancel();
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {}); // remove listener
|
||||
window.update.onStep(() => {}); // remove step listener
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
|
||||
import { SectionTitle } from "@/components/SectionTitle";
|
||||
import { Terminal } from "@/components/Terminal";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
|
||||
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
@@ -18,12 +19,15 @@ export function UpdateExecutePage() {
|
||||
isUpdating,
|
||||
terminalLines,
|
||||
currentUpdateInfo,
|
||||
steps,
|
||||
overallProgress,
|
||||
setUpdateInfo,
|
||||
startUpdate,
|
||||
stopUpdate,
|
||||
addTerminalLine,
|
||||
clearTerminalLines,
|
||||
resetUpdateState,
|
||||
initializeSteps,
|
||||
} = useUpdateStore();
|
||||
|
||||
// Set update info from search params when component mounts or search changes
|
||||
@@ -50,6 +54,7 @@ export function UpdateExecutePage() {
|
||||
commit: search.commit,
|
||||
};
|
||||
|
||||
initializeSteps();
|
||||
startUpdate();
|
||||
// Perhaps we just need to clear the logs ?
|
||||
const res = await updateExecute(updateInfo, addTerminalLine);
|
||||
@@ -138,6 +143,15 @@ export function UpdateExecutePage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{isUpdating && (
|
||||
<UpdateProgressBar
|
||||
steps={steps}
|
||||
overallProgress={overallProgress}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Terminal
|
||||
lines={terminalLines}
|
||||
className="h-160"
|
||||
|
||||
@@ -10,10 +10,21 @@ export type UpdateInfo = {
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
export type UpdateStep = {
|
||||
id: string;
|
||||
label: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
subsector: "nixos" | "rust" | "electron" | "general";
|
||||
progress?: number; // 0-100 for steps with detailed progress tracking
|
||||
};
|
||||
|
||||
export type UpdateState = {
|
||||
isUpdating: boolean;
|
||||
terminalLines: string[];
|
||||
currentUpdateInfo: UpdateInfo | null;
|
||||
steps: UpdateStep[];
|
||||
currentStepIndex: number;
|
||||
overallProgress: number; // 0-100
|
||||
};
|
||||
|
||||
export type UpdateActions = {
|
||||
@@ -23,14 +34,80 @@ export type UpdateActions = {
|
||||
addTerminalLine: (line: string) => void;
|
||||
clearTerminalLines: () => void;
|
||||
resetUpdateState: () => void;
|
||||
setStepStatus: (stepId: string, status: UpdateStep["status"]) => void;
|
||||
setStepProgress: (stepId: string, progress: number) => void;
|
||||
initializeSteps: () => void;
|
||||
updateProgress: (progress: number) => void;
|
||||
};
|
||||
|
||||
export type UpdateStore = UpdateState & UpdateActions;
|
||||
|
||||
const defaultSteps: UpdateStep[] = [
|
||||
{
|
||||
id: "clone-repo",
|
||||
label: "Clone repository",
|
||||
status: "pending",
|
||||
subsector: "general",
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: "rust-build",
|
||||
label: "Build system packages",
|
||||
status: "pending",
|
||||
subsector: "rust",
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: "system-install",
|
||||
label: "Install system update",
|
||||
status: "pending",
|
||||
subsector: "nixos",
|
||||
progress: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to calculate overall progress based on step weights and individual progress
|
||||
const calculateOverallProgress = (steps: UpdateStep[]): number => {
|
||||
// Define weights for each step (should sum to 100)
|
||||
const stepWeights: Record<string, number> = {
|
||||
"clone-repo": 10, // 10% for cloning
|
||||
"rust-build": 80, // 80% for building (goes up to 90% internally)
|
||||
"system-install": 10, // 10% for system installation
|
||||
};
|
||||
|
||||
let totalProgress = 0;
|
||||
|
||||
steps.forEach((step) => {
|
||||
const weight = stepWeights[step.id] || 0;
|
||||
|
||||
if (step.status === "completed") {
|
||||
// Completed steps contribute their full weight
|
||||
totalProgress += weight;
|
||||
} else if (step.status === "in-progress") {
|
||||
// In-progress steps: use progress value or 0 if not yet set
|
||||
const stepProgress = step.progress ?? 0;
|
||||
|
||||
if (step.id === "rust-build") {
|
||||
// Rust build progress is 0-90%, map it to 0-100% of its weight
|
||||
totalProgress += (stepProgress / 90) * weight;
|
||||
} else {
|
||||
// Other steps: progress is 0-100%
|
||||
totalProgress += (stepProgress / 100) * weight;
|
||||
}
|
||||
}
|
||||
// Pending and error steps contribute 0
|
||||
});
|
||||
|
||||
return Math.round(Math.min(100, Math.max(0, totalProgress)));
|
||||
};
|
||||
|
||||
const initialState: UpdateState = {
|
||||
isUpdating: false,
|
||||
terminalLines: [],
|
||||
currentUpdateInfo: null,
|
||||
steps: defaultSteps,
|
||||
currentStepIndex: 0,
|
||||
overallProgress: 0,
|
||||
};
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
@@ -105,6 +182,63 @@ export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
state.isUpdating = false;
|
||||
state.terminalLines = [];
|
||||
state.currentUpdateInfo = null;
|
||||
state.steps = defaultSteps.map((step) => ({ ...step }));
|
||||
state.currentStepIndex = 0;
|
||||
state.overallProgress = 0;
|
||||
}),
|
||||
),
|
||||
|
||||
initializeSteps: () =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.steps = defaultSteps.map((step) => ({ ...step }));
|
||||
state.currentStepIndex = 0;
|
||||
state.overallProgress = 0;
|
||||
}),
|
||||
),
|
||||
|
||||
setStepStatus: (stepId: string, status: UpdateStep["status"]) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
|
||||
if (stepIndex !== -1) {
|
||||
state.steps[stepIndex].status = status;
|
||||
|
||||
// Update current step index and ensure progress is initialized
|
||||
if (status === "in-progress") {
|
||||
state.currentStepIndex = stepIndex;
|
||||
// Initialize progress to 0 if not already set
|
||||
if (state.steps[stepIndex].progress === undefined) {
|
||||
state.steps[stepIndex].progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall progress based on step weights and progress
|
||||
state.overallProgress = calculateOverallProgress(state.steps);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
setStepProgress: (stepId: string, progress: number) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
|
||||
if (stepIndex !== -1) {
|
||||
state.steps[stepIndex].progress = Math.min(
|
||||
100,
|
||||
Math.max(0, progress),
|
||||
);
|
||||
|
||||
// Recalculate overall progress when step progress changes
|
||||
state.overallProgress = calculateOverallProgress(state.steps);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
updateProgress: (progress: number) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.overallProgress = Math.min(100, Math.max(0, progress));
|
||||
}),
|
||||
),
|
||||
}));
|
||||
Vendored
+7
@@ -46,6 +46,13 @@ interface UpdateContext {
|
||||
onEnd: (
|
||||
callback: (params: { success: boolean; error?: string }) => void,
|
||||
) => void;
|
||||
onStep: (
|
||||
callback: (params: {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
}) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface TroubleshootContext {
|
||||
|
||||
Reference in new issue
Block a user