mirror of https://github.com/jetkvm/kvm.git
feat(ui): Add dedicated update route and refactor update dialog state management
This commit is contained in:
parent
a2652c5265
commit
bb4ee2a6c7
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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 />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue