refactor-ota-dialog (#768)
Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
co-authored by
philon-
parent
27ed4e58bd
commit
1dbf0b07b6
@@ -626,7 +626,7 @@ export class MeshDevice {
|
|||||||
public async reboot(time: number): Promise<number> {
|
public async reboot(time: number): Promise<number> {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
Emitter[Emitter.Reboot],
|
Emitter[Emitter.Reboot],
|
||||||
`🔌 Rebooting node ${time > 0 ? "now" : `in ${time} seconds`}`,
|
`🔌 Rebooting node ${time === 0 ? "now" : `in ${time} seconds`}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const reboot = create(Protobuf.Admin.AdminMessageSchema, {
|
const reboot = create(Protobuf.Admin.AdminMessageSchema, {
|
||||||
@@ -649,7 +649,7 @@ export class MeshDevice {
|
|||||||
public async rebootOta(time: number): Promise<number> {
|
public async rebootOta(time: number): Promise<number> {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
Emitter[Emitter.RebootOta],
|
Emitter[Emitter.RebootOta],
|
||||||
`🔌 Rebooting into OTA mode ${time > 0 ? "now" : `in ${time} seconds`}`,
|
`🔌 Rebooting into OTA mode ${time === 0 ? "now" : `in ${time} seconds`}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rebootOta = create(Protobuf.Admin.AdminMessageSchema, {
|
const rebootOta = create(Protobuf.Admin.AdminMessageSchema, {
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
"qrGenerator": "Generator",
|
"qrGenerator": "Generator",
|
||||||
"qrImport": "Import",
|
"qrImport": "Import",
|
||||||
"scheduleShutdown": "Schedule Shutdown",
|
"scheduleShutdown": "Schedule Shutdown",
|
||||||
"scheduleReboot": "Schedule Reboot",
|
"scheduleReboot": "Reboot Device",
|
||||||
"rebootToOtaMode": "Reboot To OTA Mode",
|
|
||||||
"resetNodeDb": "Reset Node DB",
|
"resetNodeDb": "Reset Node DB",
|
||||||
"factoryResetDevice": "Factory Reset Device",
|
"factoryResetDevice": "Factory Reset Device",
|
||||||
"factoryResetConfig": "Factory Reset Config",
|
"factoryResetConfig": "Factory Reset Config",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"now": "Now",
|
"now": "Now",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
"rebootOtaNow": "Reboot to OTA Mode Now",
|
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"requestNewKeys": "Request New Keys",
|
"requestNewKeys": "Request New Keys",
|
||||||
"requestPosition": "Request Position",
|
"requestPosition": "Request Position",
|
||||||
|
|||||||
@@ -132,15 +132,15 @@
|
|||||||
"sharableUrl": "Sharable URL",
|
"sharableUrl": "Sharable URL",
|
||||||
"title": "Generate QR Code"
|
"title": "Generate QR Code"
|
||||||
},
|
},
|
||||||
"rebootOta": {
|
|
||||||
"title": "Schedule Reboot",
|
|
||||||
"description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.",
|
|
||||||
"enterDelay": "Enter delay (sec)",
|
|
||||||
"scheduled": "Reboot has been scheduled"
|
|
||||||
},
|
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"title": "Schedule Reboot",
|
"title": "Reboot device",
|
||||||
"description": "Reboot the connected node after x minutes."
|
"description": "Reboot now or schedule a reboot of the connected node. Optionally, you can choose to reboot into OTA (Over-the-Air) mode.",
|
||||||
|
"ota": "Reboot into OTA mode",
|
||||||
|
"enterDelay": "Enter delay",
|
||||||
|
"scheduled": "Reboot has been scheduled",
|
||||||
|
"schedule": "Schedule reboot",
|
||||||
|
"now": "Reboot now",
|
||||||
|
"cancel": "Cancel scheduled reboot"
|
||||||
},
|
},
|
||||||
"refreshKeys": {
|
"refreshKeys": {
|
||||||
"description": {
|
"description": {
|
||||||
|
|||||||
@@ -189,13 +189,6 @@ export const CommandPalette = () => {
|
|||||||
setDialogOpen("reboot", true);
|
setDialogOpen("reboot", true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t("contextual.command.rebootToOtaMode"),
|
|
||||||
icon: RefreshCwIcon,
|
|
||||||
action() {
|
|
||||||
setDialogOpen("rebootOTA", true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t("contextual.command.resetNodeDb"),
|
label: t("contextual.command.resetNodeDb"),
|
||||||
icon: TrashIcon,
|
icon: TrashIcon,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDeta
|
|||||||
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||||
import { RebootOTADialog } from "@components/Dialog/RebootOTADialog.tsx";
|
|
||||||
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||||
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
@@ -79,12 +78,6 @@ export const DialogManager = () => {
|
|||||||
setDialogOpen("refreshKeys", open);
|
setDialogOpen("refreshKeys", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<RebootOTADialog
|
|
||||||
open={dialog.rebootOTA}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setDialogOpen("rebootOTA", open);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DeleteMessagesDialog
|
<DeleteMessagesDialog
|
||||||
open={dialog.deleteMessages}
|
open={dialog.deleteMessages}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
+66
-23
@@ -7,10 +7,15 @@ import type {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import type { JSX } from "react/jsx-runtime";
|
import type { JSX } from "react/jsx-runtime";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { RebootOTADialog } from "./RebootOTADialog.tsx";
|
import { RebootDialog } from "./RebootDialog.tsx";
|
||||||
|
|
||||||
|
const rebootMock = vi.fn();
|
||||||
const rebootOtaMock = vi.fn();
|
const rebootOtaMock = vi.fn();
|
||||||
let mockConnection: { rebootOta: (delay: number) => void } | undefined = {
|
let mockConnection: {
|
||||||
|
rebootOta: (delay: number) => void,
|
||||||
|
reboot: (delay: number) => void
|
||||||
|
} | undefined = {
|
||||||
|
reboot: rebootMock,
|
||||||
rebootOta: rebootOtaMock,
|
rebootOta: rebootOtaMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ vi.mock("@components/UI/Dialog.tsx", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("RebootOTADialog", () => {
|
describe("RebootDialog", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
rebootOtaMock.mockClear();
|
rebootOtaMock.mockClear();
|
||||||
@@ -72,17 +77,43 @@ describe("RebootOTADialog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders dialog with default input value", () => {
|
it("renders dialog with default input value", () => {
|
||||||
render(<RebootOTADialog open onOpenChange={() => {}} />);
|
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }),
|
screen.getByRole("heading", { name: /reboot device/i, level: 1 }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText(/reboot to ota mode now/i)).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("calls correct reboot function based on OTA checkbox state", () => {
|
||||||
|
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||||
|
|
||||||
|
// Schedule non-OTA reboot
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(5);
|
||||||
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rebootMock.mockClear();
|
||||||
|
rebootOtaMock.mockClear();
|
||||||
|
|
||||||
|
// Cancel scheduled
|
||||||
|
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||||
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rebootMock.mockClear();
|
||||||
|
rebootOtaMock.mockClear();
|
||||||
|
|
||||||
|
// Schedule OTA reboot
|
||||||
|
fireEvent.click(screen.getByText(/reboot into ota mode/i));
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootOtaMock).toHaveBeenCalledWith(5);
|
||||||
|
expect(rebootMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("schedules a reboot with delay and calls rebootOta", async () => {
|
it("schedules a reboot with delay and calls rebootOta", async () => {
|
||||||
const onOpenChangeMock = vi.fn();
|
const onOpenChangeMock = vi.fn();
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||||
target: { value: "3" },
|
target: { value: "3" },
|
||||||
@@ -90,26 +121,24 @@ describe("RebootOTADialog", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(3);
|
||||||
|
|
||||||
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
|
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
|
||||||
|
|
||||||
vi.advanceTimersByTime(3000);
|
vi.advanceTimersByTime(3000);
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||||
expect(rebootOtaMock).toHaveBeenCalledWith(0);
|
|
||||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("triggers an instant reboot", async () => {
|
it("triggers an instant reboot", async () => {
|
||||||
const onOpenChangeMock = vi.fn();
|
const onOpenChangeMock = vi.fn();
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/reboot to ota mode now/i));
|
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(rebootMock).toHaveBeenCalledWith(0);
|
||||||
expect(rebootOtaMock).toHaveBeenCalledWith(5);
|
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not call reboot if connection is undefined", async () => {
|
it("does not call reboot if connection is undefined", async () => {
|
||||||
@@ -117,16 +146,30 @@ describe("RebootOTADialog", () => {
|
|||||||
|
|
||||||
mockConnection = undefined;
|
mockConnection = undefined;
|
||||||
|
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
|
||||||
vi.advanceTimersByTime(5000);
|
vi.advanceTimersByTime(5000);
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(rebootMock).not.toHaveBeenCalled();
|
||||||
expect(rebootOtaMock).not.toHaveBeenCalled();
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
expect(onOpenChangeMock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
mockConnection = { rebootOta: rebootOtaMock };
|
mockConnection = { reboot: rebootMock, rebootOta: rebootOtaMock };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels a scheduled reboot and calls rebootOta with -1", async () => {
|
||||||
|
const onOpenChangeMock = vi.fn();
|
||||||
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||||
|
target: { value: "4" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(4);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||||
|
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -8,21 +8,82 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
|
import { Separator } from "@components/UI/Seperator.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { RefreshCwIcon } from "lucide-react";
|
import { ClockIcon, OctagonXIcon, RefreshCwIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Checkbox } from "../UI/Checkbox/index.tsx";
|
||||||
|
|
||||||
export interface RebootDialogProps {
|
export interface RebootDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
||||||
|
|
||||||
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
||||||
|
const [isScheduled, setIsScheduled] = useState(false);
|
||||||
|
const [isOTA, setIsOTA] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState(DEFAULT_REBOOT_DELAY.toString());
|
||||||
|
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>();
|
||||||
|
|
||||||
const [time, setTime] = useState<number>(5);
|
const handleReboot = (delay: number) => {
|
||||||
|
if (!connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOTA) {
|
||||||
|
connection.rebootOta(delay);
|
||||||
|
} else {
|
||||||
|
connection.reboot(delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetTime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.validity.valid) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = e.target.value;
|
||||||
|
setInputValue(val);
|
||||||
|
|
||||||
|
const parsed = Number(val);
|
||||||
|
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||||
|
setTime(parsed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRebootWithTimeout = async () => {
|
||||||
|
setIsScheduled(true);
|
||||||
|
|
||||||
|
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
|
||||||
|
|
||||||
|
handleReboot(delay);
|
||||||
|
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
setIsScheduled(false);
|
||||||
|
onOpenChange(false);
|
||||||
|
setInputValue(DEFAULT_REBOOT_DELAY.toString());
|
||||||
|
}, delay * 1000);
|
||||||
|
setTimeoutId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setIsScheduled(false);
|
||||||
|
handleReboot(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstantReboot = async () => {
|
||||||
|
handleReboot(0);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -32,24 +93,66 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
|||||||
<DialogTitle>{t("reboot.title")}</DialogTitle>
|
<DialogTitle>{t("reboot.title")}</DialogTitle>
|
||||||
<DialogDescription>{t("reboot.description")}</DialogDescription>
|
<DialogDescription>{t("reboot.description")}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex gap-2 p-4">
|
<Separator />
|
||||||
<Input
|
{!isScheduled ? (
|
||||||
type="number"
|
<>
|
||||||
className="dark:text-slate-900"
|
<Checkbox
|
||||||
value={time}
|
checked={isOTA}
|
||||||
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
onChange={(checked) => setIsOTA(checked)}
|
||||||
/>
|
className="px-2"
|
||||||
<Button
|
>
|
||||||
className="w-24"
|
{t("reboot.ota")}
|
||||||
name="now"
|
</Checkbox>
|
||||||
onClick={() => {
|
<div className="flex gap-2 px-2 items-center relative">
|
||||||
connection?.reboot(2).then(() => onOpenChange(false));
|
<Input
|
||||||
}}
|
type="number"
|
||||||
>
|
min={1}
|
||||||
<RefreshCwIcon className="mr-2" size={16} />
|
max={86400}
|
||||||
{t("button.now")}
|
value={inputValue}
|
||||||
</Button>
|
onChange={handleSetTime}
|
||||||
</div>
|
placeholder={t("reboot.enterDelay")}
|
||||||
|
suffix={t("unit.second.plural")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRebootWithTimeout()}
|
||||||
|
data-testid="scheduleRebootBtn"
|
||||||
|
className="w-9/12"
|
||||||
|
>
|
||||||
|
<ClockIcon className="mr-2" size={18} />
|
||||||
|
{t("reboot.schedule")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="px-2">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
name="rebootNow"
|
||||||
|
onClick={() => handleInstantReboot()}
|
||||||
|
className=" w-full"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon className="mr-2" size={16} />
|
||||||
|
{t("reboot.now")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="px-2">
|
||||||
|
<div className="pb-6 pt-2 text-center">
|
||||||
|
<Label className=" text-gray-700 dark:text-gray-300 ">
|
||||||
|
{t("reboot.scheduled")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
name="cancelReboot"
|
||||||
|
onClick={() => handleCancel()}
|
||||||
|
className=" w-full"
|
||||||
|
data-testid="cancelRebootBtn"
|
||||||
|
>
|
||||||
|
<OctagonXIcon className="mr-2" size={16} />
|
||||||
|
{t("reboot.cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@components/UI/Dialog.tsx";
|
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export interface RebootOTADialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
|
||||||
|
|
||||||
export const RebootOTADialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: RebootOTADialogProps) => {
|
|
||||||
const { t } = useTranslation("dialog");
|
|
||||||
const { connection } = useDevice();
|
|
||||||
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
|
||||||
const [isScheduled, setIsScheduled] = useState(false);
|
|
||||||
const [inputValue, setInputValue] = useState(DEFAULT_REBOOT_DELAY.toString());
|
|
||||||
|
|
||||||
const handleSetTime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.validity.valid) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const val = e.target.value;
|
|
||||||
setInputValue(val);
|
|
||||||
|
|
||||||
const parsed = Number(val);
|
|
||||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
||||||
setTime(parsed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRebootWithTimeout = async () => {
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsScheduled(true);
|
|
||||||
|
|
||||||
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, delay * 1000);
|
|
||||||
}).finally(() => {
|
|
||||||
setIsScheduled(false);
|
|
||||||
onOpenChange(false);
|
|
||||||
setInputValue(DEFAULT_REBOOT_DELAY.toString());
|
|
||||||
});
|
|
||||||
connection.rebootOta(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInstantReboot = async () => {
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection.rebootOta(DEFAULT_REBOOT_DELAY);
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogClose />
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("rebootOta.title")}</DialogTitle>
|
|
||||||
<DialogDescription>{t("rebootOta.description")}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex gap-2 p-2 items-center relative">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={86400}
|
|
||||||
className="dark:text-slate-900 appearance-none"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={handleSetTime}
|
|
||||||
placeholder={t("rebootOta.enterDelay")}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRebootWithTimeout()}
|
|
||||||
data-testid="scheduleRebootBtn"
|
|
||||||
className="w-9/12"
|
|
||||||
>
|
|
||||||
<ClockIcon className="mr-2" size={18} />
|
|
||||||
{isScheduled ? t("rebootOta.scheduled") : t("rebootOta.title")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
name="rebootNow"
|
|
||||||
onClick={() => handleInstantReboot()}
|
|
||||||
>
|
|
||||||
<RefreshCwIcon className="mr-2" size={16} />
|
|
||||||
{t("button.rebootOtaNow")}
|
|
||||||
</Button>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -36,7 +36,6 @@ export const mockDeviceStore: Device = {
|
|||||||
QR: false,
|
QR: false,
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
reboot: false,
|
reboot: false,
|
||||||
rebootOTA: false,
|
|
||||||
deviceName: false,
|
deviceName: false,
|
||||||
nodeRemoval: false,
|
nodeRemoval: false,
|
||||||
pkiBackup: false,
|
pkiBackup: false,
|
||||||
@@ -86,4 +85,6 @@ export const mockDeviceStore: Device = {
|
|||||||
sendAdminMessage: vi.fn(),
|
sendAdminMessage: vi.fn(),
|
||||||
updateFavorite: vi.fn(),
|
updateFavorite: vi.fn(),
|
||||||
updateIgnored: vi.fn(),
|
updateIgnored: vi.fn(),
|
||||||
|
getAllUnreadCount: vi.fn().mockReturnValue(0),
|
||||||
|
getUnreadCount: vi.fn().mockReturnValue(0),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export interface Device {
|
|||||||
QR: boolean;
|
QR: boolean;
|
||||||
shutdown: boolean;
|
shutdown: boolean;
|
||||||
reboot: boolean;
|
reboot: boolean;
|
||||||
rebootOTA: boolean;
|
|
||||||
deviceName: boolean;
|
deviceName: boolean;
|
||||||
nodeRemoval: boolean;
|
nodeRemoval: boolean;
|
||||||
pkiBackup: boolean;
|
pkiBackup: boolean;
|
||||||
@@ -172,7 +171,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
|
|||||||
nodeDetails: false,
|
nodeDetails: false,
|
||||||
unsafeRoles: false,
|
unsafeRoles: false,
|
||||||
refreshKeys: false,
|
refreshKeys: false,
|
||||||
rebootOTA: false,
|
|
||||||
deleteMessages: false,
|
deleteMessages: false,
|
||||||
managedMode: false,
|
managedMode: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user