feat(ui): Add dedicated update route and refactor update dialog state management

This commit is contained in:
Adam Shiervani 2025-02-26 00:13:02 +01:00
parent a2652c5265
commit bb4ee2a6c7
6 changed files with 62 additions and 38 deletions

View File

@ -2,25 +2,21 @@ import { cx } from "@/cva.config";
import { Button } from "./Button"; import { Button } from "./Button";
import { GridCard } from "./Card"; import { GridCard } from "./Card";
import LoadingSpinner from "./LoadingSpinner"; import LoadingSpinner from "./LoadingSpinner";
import { UpdateState } from "@/hooks/stores"; import { useNavigate } from "react-router-dom";
import { useUpdateStore } from "@/hooks/stores";
interface UpdateInProgressStatusCardProps { export default function UpdateInProgressStatusCard() {
setIsUpdateDialogOpen: (isOpen: boolean) => void; const navigate = useNavigate();
setModalView: (view: UpdateState["modalView"]) => void; const { setModalView } = useUpdateStore();
}
export default function UpdateInProgressStatusCard({
setIsUpdateDialogOpen,
setModalView,
}: UpdateInProgressStatusCardProps) {
return ( return (
<div className="w-full transition-all duration-300 ease-in-out opacity-100 select-none"> <div className="w-full select-none opacity-100 transition-all duration-300 ease-in-out">
<GridCard cardClassName="!shadow-xl"> <GridCard cardClassName="!shadow-xl">
<div className="flex items-center justify-between gap-x-3 px-2.5 py-2.5 text-black dark:text-white"> <div className="flex items-center justify-between gap-x-3 px-2.5 py-2.5 text-black dark:text-white">
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} /> <LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-semibold leading-none transition text-ellipsis"> <div className="text-ellipsis text-sm font-semibold leading-none transition">
Update in Progress Update in Progress
</div> </div>
<div className="text-sm leading-none"> <div className="text-sm leading-none">
@ -39,7 +35,7 @@ export default function UpdateInProgressStatusCard({
text="View Details" text="View Details"
onClick={() => { onClick={() => {
setModalView("updating"); setModalView("updating");
setIsUpdateDialogOpen(true); navigate("update");
}} }}
/> />
</div> </div>

View File

@ -24,7 +24,7 @@ import notifications from "@/notifications";
import api from "../../api"; import api from "../../api";
import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog"; import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
import { LocalDevice } from "@routes/devices.$id"; import { LocalDevice } from "@routes/devices.$id";
import { useRevalidator } 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 "../InputField";
@ -267,14 +267,16 @@ export default function SettingsSidebar() {
}); });
}, [send, sshKey]); }, [send, sshKey]);
const { setIsUpdateDialogOpen, setModalView, otaState } = useUpdateStore(); const { setModalView, otaState } = useUpdateStore();
const navigate = useNavigate();
const handleCheckForUpdates = () => { const handleCheckForUpdates = () => {
if (otaState.updating) { if (otaState.updating) {
setModalView("updating"); setModalView("updating");
setIsUpdateDialogOpen(true); navigate("update");
} else { } else {
setModalView("loading"); setModalView("loading");
setIsUpdateDialogOpen(true); navigate("update");
} }
}; };

View File

@ -303,7 +303,8 @@ export const useSettingsStore = create(
dim_after: 10000, dim_after: 10000,
off_after: 50000, off_after: 50000,
}, },
setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }), setBacklightSettings: (settings: BacklightSettings) =>
set({ backlightSettings: settings }),
}), }),
{ {
name: "settings", name: "settings",
@ -484,8 +485,6 @@ export interface UpdateState {
| "updateCompleted" | "updateCompleted"
| "error"; | "error";
setModalView: (view: UpdateState["modalView"]) => void; setModalView: (view: UpdateState["modalView"]) => void;
isUpdateDialogOpen: boolean;
setIsUpdateDialogOpen: (isOpen: boolean) => void;
setUpdateErrorMessage: (errorMessage: string) => void; setUpdateErrorMessage: (errorMessage: string) => void;
updateErrorMessage: string | null; updateErrorMessage: string | null;
} }
@ -520,8 +519,6 @@ export const useUpdateStore = create<UpdateState>(set => ({
set({ updateDialogHasBeenMinimized: hasBeenMinimized }), set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
modalView: "loading", modalView: "loading",
setModalView: view => set({ modalView: view }), setModalView: view => set({ modalView: view }),
isUpdateDialogOpen: false,
setIsUpdateDialogOpen: isOpen => set({ isUpdateDialogOpen: isOpen }),
updateErrorMessage: null, updateErrorMessage: null,
setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }),
})); }));

View File

@ -29,6 +29,7 @@ import WelcomeRoute from "./routes/welcome-local";
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password"; 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";
export const isOnDevice = import.meta.env.MODE === "device"; export const isOnDevice = import.meta.env.MODE === "device";
export const isInCloud = !isOnDevice; export const isInCloud = !isOnDevice;
@ -81,6 +82,10 @@ if (isOnDevice) {
path: "other-session", path: "other-session",
element: <OtherSessionRoute />, element: <OtherSessionRoute />,
}, },
{
path: "update",
element: <UpdateRoute />,
},
], ],
}, },
@ -129,6 +134,10 @@ if (isOnDevice) {
path: "other-session", path: "other-session",
element: <OtherSessionRoute />, element: <OtherSessionRoute />,
}, },
{
path: "update",
element: <UpdateRoute />,
},
], ],
}, },
{ {

View File

@ -125,13 +125,7 @@ export default function KvmIdRoute() {
const setTransceiver = useRTCStore(state => state.setTransceiver); const setTransceiver = useRTCStore(state => state.setTransceiver);
const navigate = useNavigate(); const navigate = useNavigate();
const { const { otaState, setOtaState, setModalView } = useUpdateStore();
otaState,
setOtaState,
isUpdateDialogOpen,
setIsUpdateDialogOpen,
setModalView,
} = useUpdateStore();
const sdp = useCallback( const sdp = useCallback(
async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => { async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
@ -356,7 +350,7 @@ export default function KvmIdRoute() {
if (otaState.error) { if (otaState.error) {
setModalView("error"); setModalView("error");
setIsUpdateDialogOpen(true); navigate("update");
return; return;
} }
@ -387,10 +381,10 @@ export default function KvmIdRoute() {
useEffect(() => { useEffect(() => {
if (queryParams.get("updateSuccess")) { if (queryParams.get("updateSuccess")) {
setModalView("updateCompleted"); setModalView("updateCompleted");
setIsUpdateDialogOpen(true); navigate("update");
setQueryParams({}); setQueryParams({});
} }
}, [queryParams, setIsUpdateDialogOpen, setModalView, setQueryParams]); }, [navigate, queryParams, setModalView, setQueryParams]);
const diskChannel = useRTCStore(state => state.diskChannel)!; const diskChannel = useRTCStore(state => state.diskChannel)!;
const file = useMountMediaStore(state => state.localFile)!; const file = useMountMediaStore(state => state.localFile)!;
@ -444,16 +438,13 @@ export default function KvmIdRoute() {
}, [kvmTerminal]); }, [kvmTerminal]);
const outlet = useOutlet(); const outlet = useOutlet();
const isUpdateDialogOpen = location.pathname.includes("/update");
return ( return (
<> <>
<Transition show={!isUpdateDialogOpen && otaState.updating}> <Transition show={!isUpdateDialogOpen && otaState.updating}>
<div className="pointer-events-none fixed inset-0 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center"> <div className="pointer-events-none fixed inset-0 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center">
<div className="transition duration-1000 ease-in data-[closed]:opacity-0"> <div className="transition duration-1000 ease-in data-[closed]:opacity-0">
<UpdateInProgressStatusCard <UpdateInProgressStatusCard />
setIsUpdateDialogOpen={setIsUpdateDialogOpen}
setModalView={setModalView}
/>
</div> </div>
</div> </div>
</Transition> </Transition>
@ -494,8 +485,6 @@ export default function KvmIdRoute() {
<Outlet context={{ connectWebRTC }} /> <Outlet context={{ connectWebRTC }} />
</Modal> </Modal>
<UpdateDialog open={isUpdateDialogOpen} setOpen={setIsUpdateDialogOpen} />
{kvmTerminal && ( {kvmTerminal && (
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" /> <Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
)} )}

View File

@ -0,0 +1,31 @@
import { useNavigate } from "react-router-dom";
import { GridCard } from "@/components/Card";
import { useUpdateStore } from "@/hooks/stores";
import { Dialog } from "@/components/UpdateDialog";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useCallback } from "react";
export default function UpdateRoute() {
const navigate = useNavigate();
const { setModalView } = useUpdateStore();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
send("tryUpdate", {});
setModalView("updating");
}, [send, setModalView]);
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
setOpen={open => {
if (!open) {
navigate("..");
}
}}
onConfirmUpdate={onConfirmUpdate}
/>
</GridCard>
);
}