mirror of https://github.com/jetkvm/kvm.git
refactor(ui): Replace react-router-dom navigation with custom navigation hook
This commit introduces a new custom navigation hook `useDeviceUiNavigation` to replace direct usage of `useNavigate` across multiple components: - Removed direct `useNavigate` imports in various components - Added `navigateTo` method from new navigation hook - Updated navigation calls in ActionBar, MountPopover, UpdateInProgressStatusCard, and other routes - Simplified navigation logic and prepared for potential future navigation enhancements - Removed console logs and unnecessary comments
This commit is contained in:
parent
078e719133
commit
e51667e4cb
|
@ -17,13 +17,14 @@ import MountPopopover from "./popovers/MountPopover";
|
|||
import { Fragment, useCallback, useRef } from "react";
|
||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||
import ExtensionPopover from "./popovers/ExtensionPopover";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
export default function Actionbar({
|
||||
requestFullscreen,
|
||||
}: {
|
||||
requestFullscreen: () => Promise<void>;
|
||||
}) {
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||
|
||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||
|
@ -54,8 +55,6 @@ export default function Actionbar({
|
|||
[setDisableFocusTrap],
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||
<div
|
||||
|
@ -269,7 +268,7 @@ export default function Actionbar({
|
|||
theme="light"
|
||||
text="Settings"
|
||||
LeadingIcon={LuSettings}
|
||||
onClick={() => navigate("settings")}
|
||||
onClick={() => navigateTo("/settings")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ const Modal = React.memo(function Modal({
|
|||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
console.log("Modal", open);
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} className="relative z-10">
|
||||
<DialogBackdrop
|
||||
|
|
|
@ -2,10 +2,10 @@ import { cx } from "@/cva.config";
|
|||
import { Button } from "./Button";
|
||||
import { GridCard } from "./Card";
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
export default function UpdateInProgressStatusCard() {
|
||||
const navigate = useNavigate();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
return (
|
||||
<div className="w-full select-none opacity-100 transition-all duration-300 ease-in-out">
|
||||
|
@ -31,10 +31,7 @@ export default function UpdateInProgressStatusCard() {
|
|||
className="pointer-events-auto"
|
||||
theme="light"
|
||||
text="View Details"
|
||||
onClick={() => {
|
||||
// TODO: this wont work in cloud mode
|
||||
navigate("/settings/general/update");
|
||||
}}
|
||||
onClick={() => navigateTo("/settings/general/update")}
|
||||
/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "../../notifications";
|
||||
import { useClose } from "@headlessui/react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
|
||||
|
@ -187,7 +188,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
syncRemoteVirtualMediaState();
|
||||
}, [syncRemoteVirtualMediaState, location.pathname]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
return (
|
||||
<GridCard>
|
||||
|
@ -307,7 +308,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
text="Add New Media"
|
||||
onClick={() => {
|
||||
setModalView("mode");
|
||||
navigate("mount");
|
||||
navigateTo("/mount");
|
||||
}}
|
||||
LeadingIcon={LuPlus}
|
||||
/>
|
||||
|
|
|
@ -517,9 +517,7 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
|||
}));
|
||||
|
||||
interface UsbConfigModalState {
|
||||
modalView:
|
||||
| "updateUsbConfig"
|
||||
| "updateUsbConfigSuccess";
|
||||
modalView: "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||
errorMessage: string | null;
|
||||
setModalView: (view: UsbConfigModalState["modalView"]) => void;
|
||||
setErrorMessage: (message: string | null) => void;
|
||||
|
@ -548,14 +546,10 @@ interface LocalAuthModalState {
|
|||
| "creationSuccess"
|
||||
| "deleteSuccess"
|
||||
| "updateSuccess";
|
||||
errorMessage: string | null;
|
||||
setModalView: (view: LocalAuthModalState["modalView"]) => void;
|
||||
setErrorMessage: (message: string | null) => void;
|
||||
}
|
||||
|
||||
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
|
||||
modalView: "createPassword",
|
||||
errorMessage: null,
|
||||
setModalView: view => set({ modalView: view }),
|
||||
setErrorMessage: message => set({ errorMessage: message }),
|
||||
}));
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { useNavigate, useParams, NavigateOptions } from "react-router-dom";
|
||||
import { isOnDevice } from "../main";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
/**
|
||||
* Hook that provides context-aware navigation and path generation
|
||||
* that works in both cloud and device modes.
|
||||
*
|
||||
* In cloud mode, paths are prefixed with /devices/:id
|
||||
* In device mode, paths start from the root
|
||||
* Relative paths (starting with . or ..) are preserved in both modes
|
||||
* Supports all React Router navigation options
|
||||
*/
|
||||
export function useDeviceUiNavigation() {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
// Get the device ID from params
|
||||
const deviceId = useMemo(() => params.id, [params.id]);
|
||||
|
||||
// Function to generate the correct path
|
||||
const getPath = useCallback(
|
||||
(path: string): string => {
|
||||
// Check if it's a relative path (starts with . or ..)
|
||||
const isRelativePath = path.startsWith(".") || path === "";
|
||||
|
||||
// If it's a relative path, don't modify it
|
||||
if (isRelativePath) return path;
|
||||
|
||||
// Ensure absolute path starts with a slash
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
|
||||
if (isOnDevice) {
|
||||
return normalizedPath;
|
||||
} else {
|
||||
if (!deviceId) {
|
||||
console.error("No device ID found in params when generating path");
|
||||
throw new Error("No device ID found in params when generating path");
|
||||
}
|
||||
return `/devices/${deviceId}${normalizedPath}`;
|
||||
}
|
||||
},
|
||||
[deviceId],
|
||||
);
|
||||
|
||||
// Function to navigate to the correct path with all options
|
||||
const navigateTo = useCallback(
|
||||
(path: string, options?: NavigateOptions) => {
|
||||
navigate(getPath(path), options);
|
||||
},
|
||||
[getPath, navigate],
|
||||
);
|
||||
|
||||
return {
|
||||
navigateTo,
|
||||
getPath,
|
||||
};
|
||||
}
|
|
@ -93,9 +93,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
clearMountMediaState();
|
||||
syncRemoteVirtualMediaState()
|
||||
.then(() => {
|
||||
navigate("..");
|
||||
})
|
||||
.then(() => navigate(".."))
|
||||
.catch(err => {
|
||||
triggerError(err instanceof Error ? err.message : String(err));
|
||||
})
|
||||
|
|
|
@ -13,10 +13,11 @@ import { CLOUD_APP } from "../ui.config";
|
|||
import notifications from "../notifications";
|
||||
import { isOnDevice } from "../main";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const navigate = useNavigate();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
const [devChannel, setDevChannel] = useState(false);
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
|
@ -135,10 +136,7 @@ export default function SettingsGeneralRoute() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
text="Check for Updates"
|
||||
onClick={() => {
|
||||
// TODO: this wont work in cloud mode
|
||||
navigate("./update");
|
||||
}}
|
||||
onClick={() => navigateTo("./update")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
|||
import notifications from "@/notifications";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
|
||||
export default function SettingsGeneralUpdateRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -36,15 +37,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
// TODO: This wont work in cloud mode
|
||||
navigate("..");
|
||||
}}
|
||||
onConfirmUpdate={onConfirmUpdate}
|
||||
/>
|
||||
);
|
||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export interface SystemVersionInfo {
|
||||
|
@ -61,7 +54,7 @@ export function Dialog({
|
|||
onClose: () => void;
|
||||
onConfirmUpdate: () => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
||||
const { modalView, setModalView, otaState } = useUpdateStore();
|
||||
|
@ -113,10 +106,7 @@ export function Dialog({
|
|||
{modalView === "updating" && (
|
||||
<UpdatingDeviceState
|
||||
otaState={otaState}
|
||||
onMinimizeUpgradeDialog={() => {
|
||||
// TODO: This wont work in cloud mode
|
||||
navigate("/");
|
||||
}}
|
||||
onMinimizeUpgradeDialog={() => navigateTo("/")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { SectionHeader } from "@/components/SectionHeader";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import { useNavigate, useLoaderData } from "react-router-dom";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { Button } from "../components/Button";
|
||||
import { DEVICE_API } from "../ui.config";
|
||||
import api from "../api";
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
export const loader = async () => {
|
||||
const status = await api
|
||||
|
@ -15,7 +16,7 @@ export const loader = async () => {
|
|||
|
||||
export default function SettingsSecurityIndexRoute() {
|
||||
const { authMode } = useLoaderData() as LocalDevice;
|
||||
const navigate = useNavigate();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
@ -35,7 +36,7 @@ export default function SettingsSecurityIndexRoute() {
|
|||
theme="light"
|
||||
text="Disable Protection"
|
||||
onClick={() => {
|
||||
navigate("local-auth", { state: { init: "deletePassword" } });
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -44,7 +45,7 @@ export default function SettingsSecurityIndexRoute() {
|
|||
theme="light"
|
||||
text="Enable Password"
|
||||
onClick={() => {
|
||||
navigate("local-auth", { state: { init: "createPassword" } });
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -60,7 +61,7 @@ export default function SettingsSecurityIndexRoute() {
|
|||
theme="light"
|
||||
text="Change Password"
|
||||
onClick={() => {
|
||||
navigate("local-auth", { state: { init: "updatePassword" } });
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
|
@ -3,32 +3,33 @@ import { Button } from "@components/Button";
|
|||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import api from "@/api";
|
||||
import { useLocalAuthModalStore } from "@/hooks/stores";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useLocation, useRevalidator } from "react-router-dom";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
|
||||
export default function LocalAuthRoute() {
|
||||
const navigate = useNavigate();
|
||||
const { setModalView } = useLocalAuthModalStore();
|
||||
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const location = useLocation();
|
||||
const init = location.state?.init;
|
||||
|
||||
useEffect(() => {
|
||||
if (!init) {
|
||||
navigate("..");
|
||||
navigateTo("..");
|
||||
} else {
|
||||
setModalView(init);
|
||||
}
|
||||
}, [init, navigate, setModalView]);
|
||||
}, [init, navigateTo, setModalView]);
|
||||
|
||||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog onClose={() => navigate("..")} />;
|
||||
return <Dialog onClose={() => navigateTo("..")} />;
|
||||
}
|
||||
|
||||
export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
const { modalView, setModalView } = useLocalAuthModalStore();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const revalidator = useRevalidator();
|
||||
|
||||
const handleCreatePassword = async (password: string, confirmPassword: string) => {
|
||||
if (password === "") {
|
||||
|
@ -45,6 +46,8 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
const res = await api.POST("/auth/password-local", { password });
|
||||
if (res.ok) {
|
||||
setModalView("creationSuccess");
|
||||
// The rest of the app needs to revalidate the device authMode
|
||||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while setting the password");
|
||||
|
@ -82,6 +85,8 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
if (res.ok) {
|
||||
setModalView("updateSuccess");
|
||||
// The rest of the app needs to revalidate the device authMode
|
||||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while changing the password");
|
||||
|
@ -101,6 +106,8 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
const res = await api.DELETE("/auth/local-password", { password });
|
||||
if (res.ok) {
|
||||
setModalView("deleteSuccess");
|
||||
// The rest of the app needs to revalidate the device authMode
|
||||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while disabling the password");
|
||||
|
|
|
@ -38,6 +38,7 @@ import Terminal from "@components/Terminal";
|
|||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import Modal from "../components/Modal";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
interface LocalLoaderResp {
|
||||
authMode: "password" | "noPassword" | null;
|
||||
|
@ -322,11 +323,11 @@ export default function KvmIdRoute() {
|
|||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||
|
||||
const [hasUpdated, setHasUpdated] = useState(false);
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||
if (resp.method === "otherSessionConnected") {
|
||||
console.log("otherSessionConnected", resp.params);
|
||||
navigate("other-session");
|
||||
navigateTo("/other-session");
|
||||
}
|
||||
|
||||
if (resp.method === "usbState") {
|
||||
|
@ -350,8 +351,7 @@ export default function KvmIdRoute() {
|
|||
|
||||
if (otaState.error) {
|
||||
setModalView("error");
|
||||
// TODO: this wont work in cloud mode
|
||||
navigate("update");
|
||||
navigateTo("/settings/general/update");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -381,12 +381,9 @@ export default function KvmIdRoute() {
|
|||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||
useEffect(() => {
|
||||
if (queryParams.get("updateSuccess")) {
|
||||
// TODO: this wont work in cloud mode
|
||||
navigate("./settings/general/update", {
|
||||
state: { updateSuccess: true },
|
||||
});
|
||||
navigateTo("/settings/general/update", { state: { updateSuccess: true } });
|
||||
}
|
||||
}, [navigate, queryParams, setModalView, setQueryParams]);
|
||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||
|
||||
const diskChannel = useRTCStore(state => state.diskChannel)!;
|
||||
const file = useMountMediaStore(state => state.localFile)!;
|
||||
|
@ -442,10 +439,8 @@ export default function KvmIdRoute() {
|
|||
const outlet = useOutlet();
|
||||
const location = useLocation();
|
||||
const onModalClose = useCallback(() => {
|
||||
if (location.pathname !== "/other-session") {
|
||||
navigate("..");
|
||||
}
|
||||
}, [navigate, location.pathname]);
|
||||
if (location.pathname !== "/other-session") navigateTo("..");
|
||||
}, [navigateTo, location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -497,7 +492,7 @@ export default function KvmIdRoute() {
|
|||
onKeyUp={e => e.stopPropagation()}
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Escape") navigate("./");
|
||||
if (e.key === "Escape") navigateTo("/");
|
||||
}}
|
||||
>
|
||||
<Modal open={outlet !== null} onClose={onModalClose}>
|
||||
|
|
Loading…
Reference in New Issue