mirror of https://github.com/jetkvm/kvm.git
feat(ui): Add local authentication route
This commit is contained in:
parent
bb4ee2a6c7
commit
c6868c1427
|
@ -1,551 +0,0 @@
|
||||||
import Card, { GridCard } from "@/components/Card";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
|
||||||
import Modal from "@components/Modal";
|
|
||||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
|
||||||
import LoadingSpinner from "./LoadingSpinner";
|
|
||||||
|
|
||||||
export interface SystemVersionInfo {
|
|
||||||
local: { appVersion: string; systemVersion: string };
|
|
||||||
remote: { appVersion: string; systemVersion: string };
|
|
||||||
systemUpdateAvailable: boolean;
|
|
||||||
appUpdateAvailable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdateDialog({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
}) {
|
|
||||||
// We need to keep track of the update state in the dialog even if the dialog is minimized
|
|
||||||
const { setModalView } = useUpdateStore();
|
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
|
||||||
send("tryUpdate", {});
|
|
||||||
setModalView("updating");
|
|
||||||
}, [send, setModalView]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal open={open} onClose={() => setOpen(false)}>
|
|
||||||
<Dialog setOpen={setOpen} onConfirmUpdate={onConfirmUpdate} />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Dialog({
|
|
||||||
setOpen,
|
|
||||||
onConfirmUpdate,
|
|
||||||
}: {
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
onConfirmUpdate: () => void;
|
|
||||||
}) {
|
|
||||||
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
|
||||||
const { modalView, setModalView, otaState } = useUpdateStore();
|
|
||||||
|
|
||||||
const onFinishedLoading = useCallback(
|
|
||||||
async (versionInfo: SystemVersionInfo) => {
|
|
||||||
const hasUpdate =
|
|
||||||
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
|
|
||||||
|
|
||||||
setVersionInfo(versionInfo);
|
|
||||||
|
|
||||||
if (hasUpdate) {
|
|
||||||
setModalView("updateAvailable");
|
|
||||||
} else {
|
|
||||||
setModalView("upToDate");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setModalView],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset modal view when dialog is opened
|
|
||||||
useEffect(() => {
|
|
||||||
setVersionInfo(null);
|
|
||||||
}, [setModalView]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GridCard cardClassName="mx-auto relative max-w-md text-left pointer-events-auto">
|
|
||||||
<div className="p-10">
|
|
||||||
{modalView === "error" && (
|
|
||||||
<UpdateErrorState
|
|
||||||
errorMessage={otaState.error}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onRetryUpdate={() => setModalView("loading")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "loading" && (
|
|
||||||
<LoadingState
|
|
||||||
onFinished={onFinishedLoading}
|
|
||||||
onCancelCheck={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "updateAvailable" && (
|
|
||||||
<UpdateAvailableState
|
|
||||||
onConfirmUpdate={onConfirmUpdate}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
versionInfo={versionInfo!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "updating" && (
|
|
||||||
<UpdatingDeviceState
|
|
||||||
otaState={otaState}
|
|
||||||
onMinimizeUpgradeDialog={() => {
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "upToDate" && (
|
|
||||||
<SystemUpToDateState
|
|
||||||
checkUpdate={() => setModalView("loading")}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "updateCompleted" && (
|
|
||||||
<UpdateCompletedState onClose={() => setOpen(false)} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GridCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoadingState({
|
|
||||||
onFinished,
|
|
||||||
onCancelCheck,
|
|
||||||
}: {
|
|
||||||
onFinished: (versionInfo: SystemVersionInfo) => void;
|
|
||||||
onCancelCheck: () => void;
|
|
||||||
}) {
|
|
||||||
const [progressWidth, setProgressWidth] = useState("0%");
|
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
|
||||||
const [send] = useJsonRpc();
|
|
||||||
|
|
||||||
const getVersionInfo = useCallback(() => {
|
|
||||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
|
||||||
send("getUpdateStatus", {}, async resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error("Failed to check for updates");
|
|
||||||
reject(new Error("Failed to check for updates"));
|
|
||||||
} else {
|
|
||||||
const result = resp.result as SystemVersionInfo;
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
setProgressWidth("0%");
|
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
|
||||||
const signal = abortControllerRef.current.signal;
|
|
||||||
|
|
||||||
const animationTimer = setTimeout(() => {
|
|
||||||
setProgressWidth("100%");
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
getVersionInfo()
|
|
||||||
.then(versionInfo => {
|
|
||||||
if (progressBarRef.current) {
|
|
||||||
progressBarRef.current?.classList.add("!duration-1000");
|
|
||||||
}
|
|
||||||
return new Promise(resolve => setTimeout(() => resolve(versionInfo), 1000));
|
|
||||||
})
|
|
||||||
.then(versionInfo => {
|
|
||||||
if (!signal.aborted) {
|
|
||||||
onFinished(versionInfo as SystemVersionInfo);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (!signal.aborted) {
|
|
||||||
console.error("LoadingState: Error fetching version info", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(animationTimer);
|
|
||||||
abortControllerRef.current?.abort();
|
|
||||||
};
|
|
||||||
}, [getVersionInfo, onFinished]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-sm space-y-4">
|
|
||||||
<div className="space-y-0">
|
|
||||||
<p className="text-base font-semibold text-black dark:text-white">
|
|
||||||
Checking for updates...
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
We{"'"}re ensuring your device has the latest features and improvements.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
|
|
||||||
<div
|
|
||||||
ref={progressBarRef}
|
|
||||||
style={{ width: progressWidth }}
|
|
||||||
className="h-2.5 bg-blue-700 transition-all duration-[4s] ease-in-out"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="light"
|
|
||||||
text="Cancel"
|
|
||||||
onClick={() => {
|
|
||||||
onCancelCheck();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdatingDeviceState({
|
|
||||||
otaState,
|
|
||||||
onMinimizeUpgradeDialog,
|
|
||||||
}: {
|
|
||||||
otaState: UpdateState["otaState"];
|
|
||||||
onMinimizeUpgradeDialog: () => void;
|
|
||||||
}) {
|
|
||||||
const formatProgress = (progress: number) => `${Math.round(progress)}%`;
|
|
||||||
|
|
||||||
const calculateOverallProgress = (type: "system" | "app") => {
|
|
||||||
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
|
|
||||||
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
|
|
||||||
const verificationProgress = Math.round(
|
|
||||||
(otaState[`${type}VerificationProgress`] || 0) * 100,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!downloadProgress && !updateProgress && !verificationProgress) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`For ${type}:\n` +
|
|
||||||
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
|
|
||||||
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
|
|
||||||
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (type === "app") {
|
|
||||||
// App: 65% download, 34% verification, 1% update(There is no "real" update for the app)
|
|
||||||
return Math.min(
|
|
||||||
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// System: 10% download, 90% update
|
|
||||||
return Math.min(
|
|
||||||
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUpdateStatus = (type: "system" | "app") => {
|
|
||||||
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
|
|
||||||
const verfiedAt = otaState[`${type}VerifiedAt`];
|
|
||||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
|
||||||
|
|
||||||
if (!otaState.metadataFetchedAt) {
|
|
||||||
return "Fetching update information...";
|
|
||||||
} else if (!downloadFinishedAt) {
|
|
||||||
return `Downloading ${type} update...`;
|
|
||||||
} else if (!verfiedAt) {
|
|
||||||
return `Verifying ${type} update...`;
|
|
||||||
} else if (!updatedAt) {
|
|
||||||
return `Installing ${type} update...`;
|
|
||||||
} else {
|
|
||||||
return `Awaiting reboot`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isUpdateComplete = (type: "system" | "app") => {
|
|
||||||
return !!otaState[`${type}UpdatedAt`];
|
|
||||||
};
|
|
||||||
|
|
||||||
const areAllUpdatesComplete = () => {
|
|
||||||
if (otaState.systemUpdatePending && otaState.appUpdatePending) {
|
|
||||||
return isUpdateComplete("system") && isUpdateComplete("app");
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
(otaState.systemUpdatePending && isUpdateComplete("system")) ||
|
|
||||||
(otaState.appUpdatePending && isUpdateComplete("app"))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-sm space-y-4">
|
|
||||||
<div className="space-y-0">
|
|
||||||
<p className="text-base font-semibold text-black dark:text-white">
|
|
||||||
Updating your device
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
Please don{"'"}t turn off your device. This process may take a few minutes.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Card className="p-4 space-y-4">
|
|
||||||
{areAllUpdatesComplete() ? (
|
|
||||||
<div className="flex flex-col items-center my-2 space-y-2 text-center">
|
|
||||||
<LoadingSpinner className="w-6 h-6 text-blue-700 dark:text-blue-500" />
|
|
||||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
<span className="font-medium text-black dark:text-white">
|
|
||||||
Rebooting to complete the update...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
|
|
||||||
<div className="flex flex-col items-center my-2 space-y-2 text-center">
|
|
||||||
<LoadingSpinner className="w-6 h-6 text-blue-700 dark:text-blue-500" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{otaState.systemUpdatePending && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm font-semibold text-black dark:text-white">
|
|
||||||
Linux System Update
|
|
||||||
</p>
|
|
||||||
{calculateOverallProgress("system") < 100 ? (
|
|
||||||
<LoadingSpinner className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
|
||||||
) : (
|
|
||||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
|
||||||
<div
|
|
||||||
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
|
||||||
style={{
|
|
||||||
width: formatProgress(calculateOverallProgress("system")),
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
<span>{getUpdateStatus("system")}</span>
|
|
||||||
{calculateOverallProgress("system") < 100 ? (
|
|
||||||
<span>{formatProgress(calculateOverallProgress("system"))}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{otaState.appUpdatePending && (
|
|
||||||
<>
|
|
||||||
{otaState.systemUpdatePending && (
|
|
||||||
<hr className="dark:border-slate-600" />
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm font-semibold text-black dark:text-white">
|
|
||||||
App Update
|
|
||||||
</p>
|
|
||||||
{calculateOverallProgress("app") < 100 ? (
|
|
||||||
<LoadingSpinner className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
|
||||||
) : (
|
|
||||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
|
||||||
<div
|
|
||||||
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
|
||||||
style={{
|
|
||||||
width: formatProgress(calculateOverallProgress("app")),
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
<span>{getUpdateStatus("app")}</span>
|
|
||||||
{calculateOverallProgress("system") < 100 ? (
|
|
||||||
<span>{formatProgress(calculateOverallProgress("app"))}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
<div className="flex justify-start mt-4 text-white gap-x-2">
|
|
||||||
<Button
|
|
||||||
size="XS"
|
|
||||||
theme="light"
|
|
||||||
text="Update in Background"
|
|
||||||
onClick={onMinimizeUpgradeDialog}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SystemUpToDateState({
|
|
||||||
checkUpdate,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
checkUpdate: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-base font-semibold text-black dark:text-white">
|
|
||||||
System is up to date
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
Your system is running the latest version. No updates are currently available.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex mt-4 gap-x-2">
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="light"
|
|
||||||
text="Check Again"
|
|
||||||
onClick={() => {
|
|
||||||
checkUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="blank"
|
|
||||||
text="Close"
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateAvailableState({
|
|
||||||
versionInfo,
|
|
||||||
onConfirmUpdate,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
versionInfo: SystemVersionInfo;
|
|
||||||
onConfirmUpdate: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-base font-semibold text-black dark:text-white">
|
|
||||||
Update available
|
|
||||||
</p>
|
|
||||||
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
A new update is available to enhance system performance and improve
|
|
||||||
compatibility. We recommend updating to ensure everything runs smoothly.
|
|
||||||
</p>
|
|
||||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
{versionInfo?.systemUpdateAvailable ? (
|
|
||||||
<>
|
|
||||||
<span className="font-semibold">System:</span>{" "}
|
|
||||||
{versionInfo?.remote.systemVersion}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
{versionInfo?.appUpdateAvailable ? (
|
|
||||||
<>
|
|
||||||
<span className="font-semibold">App:</span> {versionInfo?.remote.appVersion}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-start gap-x-2">
|
|
||||||
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirmUpdate} />
|
|
||||||
<Button size="SM" theme="light" text="Do it later" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-base font-semibold dark:text-white">
|
|
||||||
Update Completed Successfully
|
|
||||||
</p>
|
|
||||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Your device has been successfully updated to the latest version. Enjoy the new
|
|
||||||
features and improvements!
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-start">
|
|
||||||
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateErrorState({
|
|
||||||
errorMessage,
|
|
||||||
onClose,
|
|
||||||
onRetryUpdate,
|
|
||||||
}: {
|
|
||||||
errorMessage: string | null;
|
|
||||||
onClose: () => void;
|
|
||||||
onRetryUpdate: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
|
||||||
<div>
|
|
||||||
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
|
||||||
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-base font-semibold dark:text-white">Update Error</p>
|
|
||||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
An error occurred while updating your device. Please try again later.
|
|
||||||
</p>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400">
|
|
||||||
Error details: {errorMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-start gap-x-2">
|
|
||||||
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
|
||||||
<Button size="SM" theme="primary" text="Retry" onClick={onRetryUpdate} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -18,16 +18,15 @@ import { isOnDevice } from "@/main";
|
||||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { SelectMenuBasic } from "../SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import { SystemVersionInfo } from "@components/UpdateDialog";
|
import { SystemVersionInfo } from "@/routes/devices.$id.update";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import api from "../../api";
|
import api from "../../api";
|
||||||
import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
|
||||||
import { LocalDevice } from "@routes/devices.$id";
|
import { LocalDevice } from "@routes/devices.$id";
|
||||||
import { useRevalidator, useNavigate } from "react-router-dom";
|
import { useRevalidator, useNavigate } from "react-router-dom";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||||
import { CLOUD_APP, DEVICE_API } from "@/ui.config";
|
import { CLOUD_APP, DEVICE_API } from "@/ui.config";
|
||||||
import { InputFieldWithLabel } from "../InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
|
||||||
export function SettingsItem({
|
export function SettingsItem({
|
||||||
title,
|
title,
|
||||||
|
@ -241,7 +240,7 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
setBacklightSettings(settings);
|
setBacklightSettings(settings);
|
||||||
handleBacklightSettingsSave();
|
handleBacklightSettingsSave();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleBacklightSettingsSave = () => {
|
const handleBacklightSettingsSave = () => {
|
||||||
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
|
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
|
||||||
|
@ -385,7 +384,7 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
const result = resp.result as BacklightSettings;
|
const result = resp.result as BacklightSettings;
|
||||||
setBacklightSettings(result);
|
setBacklightSettings(result);
|
||||||
})
|
});
|
||||||
|
|
||||||
send("getDevModeState", {}, resp => {
|
send("getDevModeState", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
|
@ -431,8 +430,9 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore();
|
const { setModalView: setLocalAuthModalView, modalView: localAuthModalView } =
|
||||||
const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false);
|
useLocalAuthModalStore();
|
||||||
|
const [isLocalAuthDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOnDevice) getDevice();
|
if (isOnDevice) getDevice();
|
||||||
|
@ -440,13 +440,13 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOnDevice) return;
|
if (!isOnDevice) return;
|
||||||
// Refresh device status when the local auth dialog is closed
|
// Refresh device status when the local auth dialog succeeds
|
||||||
if (!isLocalAuthDialogOpen) {
|
if (
|
||||||
|
["creationSuccess", "deleteSuccess", "updateSuccess"].includes(localAuthModalView)
|
||||||
|
) {
|
||||||
getDevice();
|
getDevice();
|
||||||
}
|
}
|
||||||
}, [getDevice, isLocalAuthDialogOpen]);
|
}, [getDevice, isLocalAuthDialogOpen, localAuthModalView]);
|
||||||
|
|
||||||
const revalidator = useRevalidator();
|
|
||||||
|
|
||||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||||
return localStorage.theme || "system";
|
return localStorage.theme || "system";
|
||||||
|
@ -484,18 +484,18 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid h-full shadow-sm grid-rows-headerBody"
|
className="grid h-full grid-rows-headerBody shadow-sm"
|
||||||
// Prevent the keyboard entries from propagating to the document where they are listened for and sent to the KVM
|
// Prevent the keyboard entries from propagating to the document where they are listened for and sent to the KVM
|
||||||
onKeyDown={e => e.stopPropagation()}
|
onKeyDown={e => e.stopPropagation()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<SidebarHeader title="Settings" setSidebarView={setSidebarView} />
|
<SidebarHeader title="Settings" setSidebarView={setSidebarView} />
|
||||||
<div
|
<div
|
||||||
className="h-full px-4 py-2 space-y-4 overflow-y-scroll bg-white dark:bg-slate-900"
|
className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 dark:bg-slate-900"
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between mt-2 gap-x-2">
|
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Check for Updates"
|
title="Check for Updates"
|
||||||
description={
|
description={
|
||||||
|
@ -552,17 +552,17 @@ export default function SettingsSidebar() {
|
||||||
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="block group grow"
|
className="group block grow"
|
||||||
onClick={() => console.log("Absolute mouse mode clicked")}
|
onClick={() => console.log("Absolute mouse mode clicked")}
|
||||||
>
|
>
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 group gap-x-4">
|
<div className="group flex items-center gap-x-4 px-4 py-3">
|
||||||
<img
|
<img
|
||||||
className="w-6 shrink-0 dark:invert"
|
className="w-6 shrink-0 dark:invert"
|
||||||
src={PointingFinger}
|
src={PointingFinger}
|
||||||
alt="Finger touching a screen"
|
alt="Finger touching a screen"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between grow">
|
<div className="flex grow items-center justify-between">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||||
Absolute
|
Absolute
|
||||||
|
@ -571,19 +571,23 @@ export default function SettingsSidebar() {
|
||||||
Most convenient
|
Most convenient
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="block opacity-50 cursor-not-allowed group grow"
|
className="group block grow cursor-not-allowed opacity-50"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
<div className="flex items-center gap-x-4 px-4 py-3">
|
||||||
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" />
|
<img
|
||||||
<div className="flex items-center justify-between grow">
|
className="w-6 shrink-0 dark:invert"
|
||||||
|
src={MouseIcon}
|
||||||
|
alt="Mouse icon"
|
||||||
|
/>
|
||||||
|
<div className="flex grow items-center justify-between">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||||
Relative
|
Relative
|
||||||
|
@ -601,7 +605,7 @@ export default function SettingsSidebar() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="space-y-4 pb-2">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Video"
|
title="Video"
|
||||||
description="Configure display settings and EDID for optimal compatibility"
|
description="Configure display settings and EDID for optimal compatibility"
|
||||||
|
@ -680,15 +684,15 @@ export default function SettingsSidebar() {
|
||||||
{isOnDevice && (
|
{isOnDevice && (
|
||||||
<>
|
<>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-4 space-y-4">
|
<div className="space-y-4 pb-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="JetKVM Cloud"
|
title="JetKVM Cloud"
|
||||||
description="Connect your device to the cloud for secure remote access and management"
|
description="Connect your device to the cloud for secure remote access and management"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-start p-4 gap-x-4">
|
<div className="flex items-start gap-x-4 p-4">
|
||||||
<ShieldCheckIcon className="w-8 h-8 mt-1 text-blue-600 shrink-0 dark:text-blue-500" />
|
<ShieldCheckIcon className="mt-1 h-8 w-8 shrink-0 text-blue-600 dark:text-blue-500" />
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
@ -780,7 +784,7 @@ export default function SettingsSidebar() {
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
{isOnDevice ? (
|
{isOnDevice ? (
|
||||||
<>
|
<>
|
||||||
<div className="pb-2 space-y-4">
|
<div className="space-y-4 pb-2">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Local Access"
|
title="Local Access"
|
||||||
description="Manage the mode of local access to the device"
|
description="Manage the mode of local access to the device"
|
||||||
|
@ -798,7 +802,7 @@ export default function SettingsSidebar() {
|
||||||
text="Disable Protection"
|
text="Disable Protection"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLocalAuthModalView("deletePassword");
|
setLocalAuthModalView("deletePassword");
|
||||||
setIsLocalAuthDialogOpen(true);
|
navigate("local-auth");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -808,7 +812,7 @@ export default function SettingsSidebar() {
|
||||||
text="Enable Password"
|
text="Enable Password"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLocalAuthModalView("createPassword");
|
setLocalAuthModalView("createPassword");
|
||||||
setIsLocalAuthDialogOpen(true);
|
navigate("local-auth");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -825,7 +829,7 @@ export default function SettingsSidebar() {
|
||||||
text="Change Password"
|
text="Change Password"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLocalAuthModalView("updatePassword");
|
setLocalAuthModalView("updatePassword");
|
||||||
setIsLocalAuthDialogOpen(true);
|
navigate("local-auth");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
@ -835,7 +839,7 @@ export default function SettingsSidebar() {
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="pb-2 space-y-4">
|
<div className="space-y-4 pb-2">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Updates"
|
title="Updates"
|
||||||
description="Manage software updates and version information"
|
description="Manage software updates and version information"
|
||||||
|
@ -889,13 +893,13 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="space-y-4 pb-2">
|
||||||
<SectionHeader
|
<SectionHeader title="Hardware" description="Configure the JetKVM Hardware" />
|
||||||
title="Hardware"
|
|
||||||
description="Configure the JetKVM Hardware"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<SettingsItem title="Display Brightness" description="Set the brightness of the display">
|
<SettingsItem
|
||||||
|
title="Display Brightness"
|
||||||
|
description="Set the brightness of the display"
|
||||||
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
label=""
|
label=""
|
||||||
|
@ -907,14 +911,17 @@ export default function SettingsSidebar() {
|
||||||
{ value: "64", label: "High" },
|
{ value: "64", label: "High" },
|
||||||
]}
|
]}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
settings.backlightSettings.max_brightness = parseInt(e.target.value)
|
settings.backlightSettings.max_brightness = parseInt(e.target.value);
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{settings.backlightSettings.max_brightness != 0 && (
|
{settings.backlightSettings.max_brightness != 0 && (
|
||||||
<>
|
<>
|
||||||
<SettingsItem title="Dim Display After" description="Set how long to wait before dimming the display">
|
<SettingsItem
|
||||||
|
title="Dim Display After"
|
||||||
|
description="Set how long to wait before dimming the display"
|
||||||
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
label=""
|
label=""
|
||||||
|
@ -928,12 +935,15 @@ export default function SettingsSidebar() {
|
||||||
{ value: "3600", label: "1 Hour" },
|
{ value: "3600", label: "1 Hour" },
|
||||||
]}
|
]}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
settings.backlightSettings.dim_after = parseInt(e.target.value)
|
settings.backlightSettings.dim_after = parseInt(e.target.value);
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<SettingsItem title="Turn off Display After" description="Set how long to wait before turning off the display">
|
<SettingsItem
|
||||||
|
title="Turn off Display After"
|
||||||
|
description="Set how long to wait before turning off the display"
|
||||||
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
label=""
|
label=""
|
||||||
|
@ -946,7 +956,7 @@ export default function SettingsSidebar() {
|
||||||
{ value: "3600", label: "1 Hour" },
|
{ value: "3600", label: "1 Hour" },
|
||||||
]}
|
]}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
settings.backlightSettings.off_after = parseInt(e.target.value)
|
settings.backlightSettings.off_after = parseInt(e.target.value);
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -957,13 +967,13 @@ export default function SettingsSidebar() {
|
||||||
The display will wake up when the connection state changes, or when touched.
|
The display will wake up when the connection state changes, or when touched.
|
||||||
</p>
|
</p>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="space-y-4 pb-2">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
description="Access additional settings for troubleshooting and customization"
|
description="Access additional settings for troubleshooting and customization"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pb-4 space-y-4">
|
<div className="space-y-4 pb-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Developer Mode"
|
title="Developer Mode"
|
||||||
description="Enable advanced features for developers"
|
description="Enable advanced features for developers"
|
||||||
|
@ -1079,14 +1089,6 @@ export default function SettingsSidebar() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LocalAuthPasswordDialog
|
|
||||||
open={isLocalAuthDialogOpen}
|
|
||||||
setOpen={x => {
|
|
||||||
// Revalidate the current route to refresh the local device status and dependent UI components
|
|
||||||
revalidator.revalidate();
|
|
||||||
setIsLocalAuthDialogOpen(x);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||||
import { CLOUD_API } from "./ui.config";
|
import { CLOUD_API } from "./ui.config";
|
||||||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||||
import UpdateRoute from "./routes/devices.$id.update";
|
import UpdateRoute from "./routes/devices.$id.update";
|
||||||
|
import LocalAuthRoute from "./routes/devices.$id.local-auth";
|
||||||
|
|
||||||
export const isOnDevice = import.meta.env.MODE === "device";
|
export const isOnDevice = import.meta.env.MODE === "device";
|
||||||
export const isInCloud = !isOnDevice;
|
export const isInCloud = !isOnDevice;
|
||||||
|
@ -86,6 +87,10 @@ if (isOnDevice) {
|
||||||
path: "update",
|
path: "update",
|
||||||
element: <UpdateRoute />,
|
element: <UpdateRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "local-auth",
|
||||||
|
element: <LocalAuthRoute />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -138,6 +143,10 @@ if (isOnDevice) {
|
||||||
path: "update",
|
path: "update",
|
||||||
element: <UpdateRoute />,
|
element: <UpdateRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "local-auth",
|
||||||
|
element: <LocalAuthRoute />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,365 @@
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
import api from "@/api";
|
||||||
|
import { useLocalAuthModalStore } from "@/hooks/stores";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function LocalAuthRoute() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="relative mx-auto max-w-md text-left pointer-events-auto">
|
||||||
|
{/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */}
|
||||||
|
<Dialog
|
||||||
|
// Effectively fetch the loaders from devices.$id.tsx to get new settings
|
||||||
|
onClose={open => {
|
||||||
|
if (!open) navigate("..");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dialog({ onClose }: { onClose: (open: boolean) => void }) {
|
||||||
|
const { modalView, setModalView } = useLocalAuthModalStore();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleCreatePassword = async (password: string, confirmPassword: string) => {
|
||||||
|
if (password === "") {
|
||||||
|
setError("Please enter a password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.POST("/auth/password-local", { password });
|
||||||
|
if (res.ok) {
|
||||||
|
setModalView("creationSuccess");
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
setError(data.error || "An error occurred while setting the password");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError("An error occurred while setting the password");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePassword = async (
|
||||||
|
oldPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
confirmNewPassword: string,
|
||||||
|
) => {
|
||||||
|
if (newPassword !== confirmNewPassword) {
|
||||||
|
setError("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldPassword === "") {
|
||||||
|
setError("Please enter your old password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword === "") {
|
||||||
|
setError("Please enter a new password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.PUT("/auth/password-local", {
|
||||||
|
oldPassword,
|
||||||
|
newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setModalView("updateSuccess");
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
setError(data.error || "An error occurred while changing the password");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError("An error occurred while changing the password");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePassword = async (password: string) => {
|
||||||
|
if (password === "") {
|
||||||
|
setError("Please enter your current password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.DELETE("/auth/local-password", { password });
|
||||||
|
if (res.ok) {
|
||||||
|
setModalView("deleteSuccess");
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
setError(data.error || "An error occurred while disabling the password");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError("An error occurred while disabling the password");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="relative max-w-lg mx-auto text-left pointer-events-auto dark:bg-slate-800">
|
||||||
|
<div className="p-10">
|
||||||
|
{modalView === "createPassword" && (
|
||||||
|
<CreatePasswordModal
|
||||||
|
onSetPassword={handleCreatePassword}
|
||||||
|
onCancel={() => onClose(false)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "deletePassword" && (
|
||||||
|
<DeletePasswordModal
|
||||||
|
onDeletePassword={handleDeletePassword}
|
||||||
|
onCancel={() => onClose(false)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "updatePassword" && (
|
||||||
|
<UpdatePasswordModal
|
||||||
|
onUpdatePassword={handleUpdatePassword}
|
||||||
|
onCancel={() => onClose(false)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "creationSuccess" && (
|
||||||
|
<SuccessModal
|
||||||
|
headline="Password Set Successfully"
|
||||||
|
description="You've successfully set up local device protection. Your device is now secure against unauthorized local access."
|
||||||
|
onClose={() => onClose(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "deleteSuccess" && (
|
||||||
|
<SuccessModal
|
||||||
|
headline="Password Protection Disabled"
|
||||||
|
description="You've successfully disabled the password protection for local access. Remember, your device is now less secure."
|
||||||
|
onClose={() => onClose(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "updateSuccess" && (
|
||||||
|
<SuccessModal
|
||||||
|
headline="Password Updated Successfully"
|
||||||
|
description="You've successfully changed your local device protection password. Make sure to remember your new password for future access."
|
||||||
|
onClose={() => onClose(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreatePasswordModal({
|
||||||
|
onSetPassword,
|
||||||
|
onCancel,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
onSetPassword: (password: string, confirmPassword: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
error: string | null;
|
||||||
|
}) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="hidden h-[24px] dark:block" />
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold dark:text-white">
|
||||||
|
Local Device Protection
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Create a password to protect your device from unauthorized local access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter a strong password"
|
||||||
|
value={password}
|
||||||
|
autoFocus
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Confirm New Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Re-enter your password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
text="Secure Device"
|
||||||
|
onClick={() => onSetPassword(password, confirmPassword)}
|
||||||
|
/>
|
||||||
|
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeletePasswordModal({
|
||||||
|
onDeletePassword,
|
||||||
|
onCancel,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
onDeletePassword: (password: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
error: string | null;
|
||||||
|
}) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="hidden h-[24px] dark:block" />
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold dark:text-white">
|
||||||
|
Disable Local Device Protection
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Enter your current password to disable local device protection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your current password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="danger"
|
||||||
|
text="Disable Protection"
|
||||||
|
onClick={() => onDeletePassword(password)}
|
||||||
|
/>
|
||||||
|
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdatePasswordModal({
|
||||||
|
onUpdatePassword,
|
||||||
|
onCancel,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
onUpdatePassword: (
|
||||||
|
oldPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
confirmNewPassword: string,
|
||||||
|
) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
error: string | null;
|
||||||
|
}) {
|
||||||
|
const [oldPassword, setOldPassword] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmNewPassword, setConfirmNewPassword] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="hidden h-[24px] dark:block" />
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold dark:text-white">
|
||||||
|
Change Local Device Password
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Enter your current password and a new password to update your local device
|
||||||
|
protection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your current password"
|
||||||
|
value={oldPassword}
|
||||||
|
onChange={e => setOldPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter a new strong password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Confirm New Password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Re-enter your new password"
|
||||||
|
value={confirmNewPassword}
|
||||||
|
onChange={e => setConfirmNewPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
text="Update Password"
|
||||||
|
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
|
||||||
|
/>
|
||||||
|
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SuccessModal({
|
||||||
|
headline,
|
||||||
|
description,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
headline: string;
|
||||||
|
description: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full max-w-lg flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="hidden h-[24px] dark:block" />
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ import { useInterval } from "usehooks-ts";
|
||||||
import SettingsSidebar from "@/components/sidebar/settings";
|
import SettingsSidebar from "@/components/sidebar/settings";
|
||||||
import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
|
import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
|
||||||
import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import UpdateDialog from "@components/UpdateDialog";
|
|
||||||
import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard";
|
import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
@ -478,16 +477,31 @@ export default function KvmIdRoute() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onKeyUp={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (location.pathname !== "/other-session") {
|
||||||
|
navigate("..");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Modal
|
<Modal
|
||||||
open={outlet !== null}
|
open={outlet !== null}
|
||||||
onClose={() => location.pathname !== "/other-session" && navigate("..")}
|
onClose={() => location.pathname !== "/other-session" && navigate("..")}
|
||||||
>
|
>
|
||||||
<Outlet context={{ connectWebRTC }} />
|
<Outlet context={{ connectWebRTC }} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
|
|
||||||
{kvmTerminal && (
|
{kvmTerminal && (
|
||||||
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{serialConsole && (
|
{serialConsole && (
|
||||||
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { GridCard } from "@/components/Card";
|
import Card, { GridCard } from "@/components/Card";
|
||||||
import { useUpdateStore } from "@/hooks/stores";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Dialog } from "@/components/UpdateDialog";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useCallback } from "react";
|
import { Button } from "@components/Button";
|
||||||
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
|
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
|
||||||
export default function UpdateRoute() {
|
export default function UpdateRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -29,3 +34,519 @@ export default function UpdateRoute() {
|
||||||
</GridCard>
|
</GridCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemVersionInfo {
|
||||||
|
local: { appVersion: string; systemVersion: string };
|
||||||
|
remote: { appVersion: string; systemVersion: string };
|
||||||
|
systemUpdateAvailable: boolean;
|
||||||
|
appUpdateAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
setOpen,
|
||||||
|
onConfirmUpdate,
|
||||||
|
}: {
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
onConfirmUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
||||||
|
const { modalView, setModalView, otaState } = useUpdateStore();
|
||||||
|
|
||||||
|
const onFinishedLoading = useCallback(
|
||||||
|
async (versionInfo: SystemVersionInfo) => {
|
||||||
|
const hasUpdate =
|
||||||
|
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
|
||||||
|
|
||||||
|
setVersionInfo(versionInfo);
|
||||||
|
|
||||||
|
if (hasUpdate) {
|
||||||
|
setModalView("updateAvailable");
|
||||||
|
} else {
|
||||||
|
setModalView("upToDate");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setModalView],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset modal view when dialog is opened
|
||||||
|
useEffect(() => {
|
||||||
|
setVersionInfo(null);
|
||||||
|
}, [setModalView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="mx-auto relative max-w-md text-left pointer-events-auto">
|
||||||
|
<div className="p-10">
|
||||||
|
{modalView === "error" && (
|
||||||
|
<UpdateErrorState
|
||||||
|
errorMessage={otaState.error}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onRetryUpdate={() => setModalView("loading")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "loading" && (
|
||||||
|
<LoadingState
|
||||||
|
onFinished={onFinishedLoading}
|
||||||
|
onCancelCheck={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "updateAvailable" && (
|
||||||
|
<UpdateAvailableState
|
||||||
|
onConfirmUpdate={onConfirmUpdate}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
versionInfo={versionInfo!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "updating" && (
|
||||||
|
<UpdatingDeviceState
|
||||||
|
otaState={otaState}
|
||||||
|
onMinimizeUpgradeDialog={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "upToDate" && (
|
||||||
|
<SystemUpToDateState
|
||||||
|
checkUpdate={() => setModalView("loading")}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalView === "updateCompleted" && (
|
||||||
|
<UpdateCompletedState onClose={() => setOpen(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingState({
|
||||||
|
onFinished,
|
||||||
|
onCancelCheck,
|
||||||
|
}: {
|
||||||
|
onFinished: (versionInfo: SystemVersionInfo) => void;
|
||||||
|
onCancelCheck: () => void;
|
||||||
|
}) {
|
||||||
|
const [progressWidth, setProgressWidth] = useState("0%");
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const getVersionInfo = useCallback(() => {
|
||||||
|
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||||
|
send("getUpdateStatus", {}, async resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error("Failed to check for updates");
|
||||||
|
reject(new Error("Failed to check for updates"));
|
||||||
|
} else {
|
||||||
|
const result = resp.result as SystemVersionInfo;
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setProgressWidth("0%");
|
||||||
|
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
const signal = abortControllerRef.current.signal;
|
||||||
|
|
||||||
|
const animationTimer = setTimeout(() => {
|
||||||
|
setProgressWidth("100%");
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
getVersionInfo()
|
||||||
|
.then(versionInfo => {
|
||||||
|
if (progressBarRef.current) {
|
||||||
|
progressBarRef.current?.classList.add("!duration-1000");
|
||||||
|
}
|
||||||
|
return new Promise(resolve => setTimeout(() => resolve(versionInfo), 1000));
|
||||||
|
})
|
||||||
|
.then(versionInfo => {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
onFinished(versionInfo as SystemVersionInfo);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
console.error("LoadingState: Error fetching version info", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(animationTimer);
|
||||||
|
abortControllerRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, [getVersionInfo, onFinished]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-sm space-y-4">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<p className="text-base font-semibold text-black dark:text-white">
|
||||||
|
Checking for updates...
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
We{"'"}re ensuring your device has the latest features and improvements.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
|
||||||
|
<div
|
||||||
|
ref={progressBarRef}
|
||||||
|
style={{ width: progressWidth }}
|
||||||
|
className="h-2.5 bg-blue-700 transition-all duration-[4s] ease-in-out"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Cancel"
|
||||||
|
onClick={() => {
|
||||||
|
onCancelCheck();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdatingDeviceState({
|
||||||
|
otaState,
|
||||||
|
onMinimizeUpgradeDialog,
|
||||||
|
}: {
|
||||||
|
otaState: UpdateState["otaState"];
|
||||||
|
onMinimizeUpgradeDialog: () => void;
|
||||||
|
}) {
|
||||||
|
const formatProgress = (progress: number) => `${Math.round(progress)}%`;
|
||||||
|
|
||||||
|
const calculateOverallProgress = (type: "system" | "app") => {
|
||||||
|
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
|
||||||
|
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
|
||||||
|
const verificationProgress = Math.round(
|
||||||
|
(otaState[`${type}VerificationProgress`] || 0) * 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!downloadProgress && !updateProgress && !verificationProgress) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`For ${type}:\n` +
|
||||||
|
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
|
||||||
|
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
|
||||||
|
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (type === "app") {
|
||||||
|
// App: 65% download, 34% verification, 1% update(There is no "real" update for the app)
|
||||||
|
return Math.min(
|
||||||
|
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// System: 10% download, 90% update
|
||||||
|
return Math.min(
|
||||||
|
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdateStatus = (type: "system" | "app") => {
|
||||||
|
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
|
||||||
|
const verfiedAt = otaState[`${type}VerifiedAt`];
|
||||||
|
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||||
|
|
||||||
|
if (!otaState.metadataFetchedAt) {
|
||||||
|
return "Fetching update information...";
|
||||||
|
} else if (!downloadFinishedAt) {
|
||||||
|
return `Downloading ${type} update...`;
|
||||||
|
} else if (!verfiedAt) {
|
||||||
|
return `Verifying ${type} update...`;
|
||||||
|
} else if (!updatedAt) {
|
||||||
|
return `Installing ${type} update...`;
|
||||||
|
} else {
|
||||||
|
return `Awaiting reboot`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUpdateComplete = (type: "system" | "app") => {
|
||||||
|
return !!otaState[`${type}UpdatedAt`];
|
||||||
|
};
|
||||||
|
|
||||||
|
const areAllUpdatesComplete = () => {
|
||||||
|
if (otaState.systemUpdatePending && otaState.appUpdatePending) {
|
||||||
|
return isUpdateComplete("system") && isUpdateComplete("app");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(otaState.systemUpdatePending && isUpdateComplete("system")) ||
|
||||||
|
(otaState.appUpdatePending && isUpdateComplete("app"))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-sm space-y-4">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<p className="text-base font-semibold text-black dark:text-white">
|
||||||
|
Updating your device
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
Please don{"'"}t turn off your device. This process may take a few minutes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card className="space-y-4 p-4">
|
||||||
|
{areAllUpdatesComplete() ? (
|
||||||
|
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||||
|
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||||
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
<span className="font-medium text-black dark:text-white">
|
||||||
|
Rebooting to complete the update...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
|
||||||
|
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||||
|
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{otaState.systemUpdatePending && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-semibold text-black dark:text-white">
|
||||||
|
Linux System Update
|
||||||
|
</p>
|
||||||
|
{calculateOverallProgress("system") < 100 ? (
|
||||||
|
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
||||||
|
<div
|
||||||
|
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||||
|
style={{
|
||||||
|
width: formatProgress(calculateOverallProgress("system")),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
<span>{getUpdateStatus("system")}</span>
|
||||||
|
{calculateOverallProgress("system") < 100 ? (
|
||||||
|
<span>{formatProgress(calculateOverallProgress("system"))}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{otaState.appUpdatePending && (
|
||||||
|
<>
|
||||||
|
{otaState.systemUpdatePending && (
|
||||||
|
<hr className="dark:border-slate-600" />
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-semibold text-black dark:text-white">
|
||||||
|
App Update
|
||||||
|
</p>
|
||||||
|
{calculateOverallProgress("app") < 100 ? (
|
||||||
|
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
||||||
|
<div
|
||||||
|
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||||
|
style={{
|
||||||
|
width: formatProgress(calculateOverallProgress("app")),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
<span>{getUpdateStatus("app")}</span>
|
||||||
|
{calculateOverallProgress("system") < 100 ? (
|
||||||
|
<span>{formatProgress(calculateOverallProgress("app"))}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
<div className="mt-4 flex justify-start gap-x-2 text-white">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Update in Background"
|
||||||
|
onClick={onMinimizeUpgradeDialog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SystemUpToDateState({
|
||||||
|
checkUpdate,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
checkUpdate: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold text-black dark:text-white">
|
||||||
|
System is up to date
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
Your system is running the latest version. No updates are currently available.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Check Again"
|
||||||
|
onClick={() => {
|
||||||
|
checkUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="blank"
|
||||||
|
text="Close"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateAvailableState({
|
||||||
|
versionInfo,
|
||||||
|
onConfirmUpdate,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
versionInfo: SystemVersionInfo;
|
||||||
|
onConfirmUpdate: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold text-black dark:text-white">
|
||||||
|
Update available
|
||||||
|
</p>
|
||||||
|
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
A new update is available to enhance system performance and improve
|
||||||
|
compatibility. We recommend updating to ensure everything runs smoothly.
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
{versionInfo?.systemUpdateAvailable ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold">System:</span>{" "}
|
||||||
|
{versionInfo?.remote.systemVersion}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{versionInfo?.appUpdateAvailable ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold">App:</span> {versionInfo?.remote.appVersion}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-start gap-x-2">
|
||||||
|
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirmUpdate} />
|
||||||
|
<Button size="SM" theme="light" text="Do it later" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold dark:text-white">
|
||||||
|
Update Completed Successfully
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Your device has been successfully updated to the latest version. Enjoy the new
|
||||||
|
features and improvements!
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-start">
|
||||||
|
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateErrorState({
|
||||||
|
errorMessage,
|
||||||
|
onClose,
|
||||||
|
onRetryUpdate,
|
||||||
|
}: {
|
||||||
|
errorMessage: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onRetryUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[140px] flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<img src={LogoBlueIcon} alt="" className="h-[24px] dark:hidden" />
|
||||||
|
<img src={LogoWhiteIcon} alt="" className="mt-0 hidden h-[24px] dark:block" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold dark:text-white">Update Error</p>
|
||||||
|
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
An error occurred while updating your device. Please try again later.
|
||||||
|
</p>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400">
|
||||||
|
Error details: {errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-start gap-x-2">
|
||||||
|
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||||
|
<Button size="SM" theme="primary" text="Retry" onClick={onRetryUpdate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue