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 MouseIcon from "@/assets/mouse-icon.svg";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "../SelectMenuBasic";
|
||||
import { SystemVersionInfo } from "@components/UpdateDialog";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { SystemVersionInfo } from "@/routes/devices.$id.update";
|
||||
import notifications from "@/notifications";
|
||||
import api from "../../api";
|
||||
import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
||||
import { LocalDevice } from "@routes/devices.$id";
|
||||
import { useRevalidator, useNavigate } from "react-router-dom";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { CLOUD_APP, DEVICE_API } from "@/ui.config";
|
||||
import { InputFieldWithLabel } from "../InputField";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
|
||||
export function SettingsItem({
|
||||
title,
|
||||
|
@ -241,7 +240,7 @@ export default function SettingsSidebar() {
|
|||
|
||||
setBacklightSettings(settings);
|
||||
handleBacklightSettingsSave();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBacklightSettingsSave = () => {
|
||||
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
|
||||
|
@ -385,7 +384,7 @@ export default function SettingsSidebar() {
|
|||
}
|
||||
const result = resp.result as BacklightSettings;
|
||||
setBacklightSettings(result);
|
||||
})
|
||||
});
|
||||
|
||||
send("getDevModeState", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
|
@ -431,8 +430,9 @@ export default function SettingsSidebar() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore();
|
||||
const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false);
|
||||
const { setModalView: setLocalAuthModalView, modalView: localAuthModalView } =
|
||||
useLocalAuthModalStore();
|
||||
const [isLocalAuthDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnDevice) getDevice();
|
||||
|
@ -440,13 +440,13 @@ export default function SettingsSidebar() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!isOnDevice) return;
|
||||
// Refresh device status when the local auth dialog is closed
|
||||
if (!isLocalAuthDialogOpen) {
|
||||
// Refresh device status when the local auth dialog succeeds
|
||||
if (
|
||||
["creationSuccess", "deleteSuccess", "updateSuccess"].includes(localAuthModalView)
|
||||
) {
|
||||
getDevice();
|
||||
}
|
||||
}, [getDevice, isLocalAuthDialogOpen]);
|
||||
|
||||
const revalidator = useRevalidator();
|
||||
}, [getDevice, isLocalAuthDialogOpen, localAuthModalView]);
|
||||
|
||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||
return localStorage.theme || "system";
|
||||
|
@ -484,18 +484,18 @@ export default function SettingsSidebar() {
|
|||
|
||||
return (
|
||||
<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
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
>
|
||||
<SidebarHeader title="Settings" setSidebarView={setSidebarView} />
|
||||
<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}
|
||||
>
|
||||
<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
|
||||
title="Check for Updates"
|
||||
description={
|
||||
|
@ -552,17 +552,17 @@ export default function SettingsSidebar() {
|
|||
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="block group grow"
|
||||
className="group block grow"
|
||||
onClick={() => console.log("Absolute mouse mode clicked")}
|
||||
>
|
||||
<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
|
||||
className="w-6 shrink-0 dark:invert"
|
||||
src={PointingFinger}
|
||||
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">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Absolute
|
||||
|
@ -571,19 +571,23 @@ export default function SettingsSidebar() {
|
|||
Most convenient
|
||||
</p>
|
||||
</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>
|
||||
</GridCard>
|
||||
</button>
|
||||
<button
|
||||
className="block opacity-50 cursor-not-allowed group grow"
|
||||
className="group block grow cursor-not-allowed opacity-50"
|
||||
disabled
|
||||
>
|
||||
<GridCard>
|
||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
||||
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" />
|
||||
<div className="flex items-center justify-between grow">
|
||||
<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"
|
||||
/>
|
||||
<div className="flex grow items-center justify-between">
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Relative
|
||||
|
@ -601,7 +605,7 @@ export default function SettingsSidebar() {
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
title="Video"
|
||||
description="Configure display settings and EDID for optimal compatibility"
|
||||
|
@ -680,15 +684,15 @@ export default function SettingsSidebar() {
|
|||
{isOnDevice && (
|
||||
<>
|
||||
<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
|
||||
title="JetKVM Cloud"
|
||||
description="Connect your device to the cloud for secure remote access and management"
|
||||
/>
|
||||
|
||||
<GridCard>
|
||||
<div className="flex items-start p-4 gap-x-4">
|
||||
<ShieldCheckIcon className="w-8 h-8 mt-1 text-blue-600 shrink-0 dark:text-blue-500" />
|
||||
<div className="flex items-start gap-x-4 p-4">
|
||||
<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-2">
|
||||
<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" />
|
||||
{isOnDevice ? (
|
||||
<>
|
||||
<div className="pb-2 space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<SectionHeader
|
||||
title="Local Access"
|
||||
description="Manage the mode of local access to the device"
|
||||
|
@ -798,7 +802,7 @@ export default function SettingsSidebar() {
|
|||
text="Disable Protection"
|
||||
onClick={() => {
|
||||
setLocalAuthModalView("deletePassword");
|
||||
setIsLocalAuthDialogOpen(true);
|
||||
navigate("local-auth");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -808,7 +812,7 @@ export default function SettingsSidebar() {
|
|||
text="Enable Password"
|
||||
onClick={() => {
|
||||
setLocalAuthModalView("createPassword");
|
||||
setIsLocalAuthDialogOpen(true);
|
||||
navigate("local-auth");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -825,7 +829,7 @@ export default function SettingsSidebar() {
|
|||
text="Change Password"
|
||||
onClick={() => {
|
||||
setLocalAuthModalView("updatePassword");
|
||||
setIsLocalAuthDialogOpen(true);
|
||||
navigate("local-auth");
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
@ -835,7 +839,7 @@ export default function SettingsSidebar() {
|
|||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
</>
|
||||
) : null}
|
||||
<div className="pb-2 space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<SectionHeader
|
||||
title="Updates"
|
||||
description="Manage software updates and version information"
|
||||
|
@ -889,13 +893,13 @@ export default function SettingsSidebar() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<div className="pb-2 space-y-4">
|
||||
<SectionHeader
|
||||
title="Hardware"
|
||||
description="Configure the JetKVM Hardware"
|
||||
/>
|
||||
<div className="space-y-4 pb-2">
|
||||
<SectionHeader title="Hardware" description="Configure the JetKVM Hardware" />
|
||||
</div>
|
||||
<SettingsItem title="Display Brightness" description="Set the brightness of the display">
|
||||
<SettingsItem
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
|
@ -907,63 +911,69 @@ export default function SettingsSidebar() {
|
|||
{ value: "64", label: "High" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.max_brightness = parseInt(e.target.value)
|
||||
settings.backlightSettings.max_brightness = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{settings.backlightSettings.max_brightness != 0 && (
|
||||
<>
|
||||
<SettingsItem title="Dim Display After" description="Set how long to wait before dimming the display">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 Minute" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.dim_after = parseInt(e.target.value)
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem title="Turn off Display After" description="Set how long to wait before turning off the display">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.off_after = parseInt(e.target.value)
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Dim Display After"
|
||||
description="Set how long to wait before dimming the display"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 Minute" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.dim_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Turn off Display After"
|
||||
description="Set how long to wait before turning off the display"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.off_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The display will wake up when the connection state changes, or when touched.
|
||||
</p>
|
||||
<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
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
/>
|
||||
|
||||
<div className="pb-4 space-y-4">
|
||||
<div className="space-y-4 pb-4">
|
||||
<SettingsItem
|
||||
title="Developer Mode"
|
||||
description="Enable advanced features for developers"
|
||||
|
@ -1079,14 +1089,6 @@ export default function SettingsSidebar() {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
|||
import { CLOUD_API } from "./ui.config";
|
||||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||
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 isInCloud = !isOnDevice;
|
||||
|
@ -86,6 +87,10 @@ if (isOnDevice) {
|
|||
path: "update",
|
||||
element: <UpdateRoute />,
|
||||
},
|
||||
{
|
||||
path: "local-auth",
|
||||
element: <LocalAuthRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -138,6 +143,10 @@ if (isOnDevice) {
|
|||
path: "update",
|
||||
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 ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
|
||||
import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import UpdateDialog from "@components/UpdateDialog";
|
||||
import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard";
|
||||
import api from "../api";
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
@ -478,16 +477,31 @@ export default function KvmIdRoute() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={outlet !== null}
|
||||
onClose={() => location.pathname !== "/other-session" && navigate("..")}
|
||||
<div
|
||||
onKeyUp={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Escape") {
|
||||
if (location.pathname !== "/other-session") {
|
||||
navigate("..");
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Outlet context={{ connectWebRTC }} />
|
||||
</Modal>
|
||||
<Modal
|
||||
open={outlet !== null}
|
||||
onClose={() => location.pathname !== "/other-session" && navigate("..")}
|
||||
>
|
||||
<Outlet context={{ connectWebRTC }} />
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
{kvmTerminal && (
|
||||
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||
)}
|
||||
|
||||
{serialConsole && (
|
||||
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { useUpdateStore } from "@/hooks/stores";
|
||||
import { Dialog } from "@/components/UpdateDialog";
|
||||
import Card, { GridCard } from "@/components/Card";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
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() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -29,3 +34,519 @@ export default function UpdateRoute() {
|
|||
</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