Overhaul (#83)
* WIP Layout Refactor * New Connection Dialog * WIP form overhaul * Fix remaining config pages
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
import { Label } from "../UI/Label.js";
|
||||
|
||||
export interface User {
|
||||
longName: string;
|
||||
shortName: string;
|
||||
}
|
||||
|
||||
export interface DeviceNameDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const DeviceNameDialog = ({
|
||||
open,
|
||||
onOpenChange
|
||||
}: DeviceNameDialogProps): JSX.Element => {
|
||||
const { hardware, nodes, connection, setDialogOpen } = useDevice();
|
||||
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
|
||||
const { register, handleSubmit } = useForm<User>({
|
||||
values: {
|
||||
longName: myNode?.data.user?.longName ?? "Unknown",
|
||||
shortName: myNode?.data.user?.shortName ?? "Unknown"
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
connection?.setOwner(
|
||||
new Protobuf.User({
|
||||
...myNode?.data.user,
|
||||
...data
|
||||
})
|
||||
);
|
||||
setDialogOpen("deviceName", false);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change Device Name</DialogTitle>
|
||||
<DialogDescription>
|
||||
The Device will restart once the config is saved.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="gap-4">
|
||||
<form onSubmit={onSubmit}>
|
||||
<Label>Long Name</Label>
|
||||
<Input {...register("longName")} />
|
||||
<Label>Short Name</Label>
|
||||
<Input maxLength={4} {...register("shortName")} />
|
||||
</form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => onSubmit()}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +1,48 @@
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
||||
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
||||
import { DeviceNameDialog } from "./DeviceNameDialog.js";
|
||||
|
||||
export const DialogManager = (): JSX.Element => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
return (
|
||||
<>
|
||||
<QRDialog
|
||||
isOpen={dialog.QR}
|
||||
close={() => {
|
||||
setDialogOpen("QR", false);
|
||||
open={dialog.QR}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("QR", open);
|
||||
}}
|
||||
channels={channels.map((ch) => ch.config)}
|
||||
loraConfig={config.lora}
|
||||
/>
|
||||
<ImportDialog
|
||||
isOpen={dialog.import}
|
||||
close={() => {
|
||||
setDialogOpen("import", false);
|
||||
open={dialog.import}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("import", open);
|
||||
}}
|
||||
channels={channels.map((ch) => ch.config)}
|
||||
loraConfig={config.lora}
|
||||
/>
|
||||
<ShutdownDialog
|
||||
isOpen={dialog.shutdown}
|
||||
close={() => {
|
||||
open={dialog.shutdown}
|
||||
onOpenChange={() => {
|
||||
setDialogOpen("shutdown", false);
|
||||
}}
|
||||
/>
|
||||
<RebootDialog
|
||||
isOpen={dialog.reboot}
|
||||
close={() => {
|
||||
open={dialog.reboot}
|
||||
onOpenChange={() => {
|
||||
setDialogOpen("reboot", false);
|
||||
}}
|
||||
/>
|
||||
<DeviceNameDialog
|
||||
open={dialog.deviceName}
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("deviceName", open);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,24 +2,29 @@ import { useEffect, useState } from "react";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { Checkbox } from "@components/form/Checkbox.js";
|
||||
import { Input } from "@components/form/Input.js";
|
||||
import { Dialog } from "@components/generic/Dialog.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
import { Select } from "@components/form/Select.js";
|
||||
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
||||
import { Toggle } from "@components/form/Toggle.js";
|
||||
import { Button } from "@components/form/Button.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
|
||||
export interface ImportDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
loraConfig?: Protobuf.Config_LoRaConfig;
|
||||
channels: Protobuf.Channel[];
|
||||
}
|
||||
|
||||
export const ImportDialog = ({
|
||||
isOpen,
|
||||
close
|
||||
open,
|
||||
onOpenChange
|
||||
}: ImportDialogProps): JSX.Element => {
|
||||
const [QRCodeURL, setQRCodeURL] = useState<string>("");
|
||||
const [channelSet, setChannelSet] = useState<Protobuf.ChannelSet>();
|
||||
@@ -68,69 +73,73 @@ export const ImportDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={"Import Channel Set"}
|
||||
description={"The current LoRa configuration will be overridden."}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Input
|
||||
label="Channel Set/QR Code URL"
|
||||
value={QRCodeURL}
|
||||
suffix={validURL ? "✅" : "❌"}
|
||||
onChange={(e) => {
|
||||
setQRCodeURL(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{validURL && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="w-36">
|
||||
<Toggle
|
||||
className="flex-col gap-2"
|
||||
label="Use Preset?"
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Channel Set</DialogTitle>
|
||||
<DialogDescription>
|
||||
The current LoRa configuration will be overridden.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Input
|
||||
label="Channel Set/QR Code URL"
|
||||
value={QRCodeURL}
|
||||
suffix={validURL ? "✅" : "❌"}
|
||||
onChange={(e) => {
|
||||
setQRCodeURL(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{validURL && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="w-36">
|
||||
<Toggle
|
||||
label="Use Preset?"
|
||||
disabled
|
||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||
/>
|
||||
</div>
|
||||
{/* <Select
|
||||
label="Modem Preset"
|
||||
disabled
|
||||
checked={channelSet?.loraConfig?.usePreset ?? true}
|
||||
/>
|
||||
value={channelSet?.loraConfig?.modemPreset}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select> */}
|
||||
</div>
|
||||
<Select
|
||||
label="Modem Preset"
|
||||
{/* <Select
|
||||
label="Region"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.modemPreset}
|
||||
value={channelSet?.loraConfig?.region}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
label="Region"
|
||||
disabled
|
||||
value={channelSet?.loraConfig?.region}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</Select> */}
|
||||
|
||||
<span className="text-md block font-medium text-textPrimary">
|
||||
Channels:
|
||||
</span>
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
{channelSet?.settings.map((channel, index) => (
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={
|
||||
channel.name.length
|
||||
? channel.name
|
||||
: `Channel: ${channel.id}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<span className="text-md block font-medium text-textPrimary">
|
||||
Channels:
|
||||
</span>
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
{channelSet?.settings.map((channel, index) => (
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={
|
||||
channel.name.length
|
||||
? channel.name
|
||||
: `Channel: ${channel.id}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={() => apply()} disabled={!validURL}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={apply} disabled={!validURL}>
|
||||
Apply
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Input } from "@components/form/Input.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../UI/Tabs.js";
|
||||
import { Subtle } from "../UI/Typography/Subtle.js";
|
||||
import { Link } from "../UI/Typography/Link.js";
|
||||
import { HTTP } from "../PageComponents/Connect/HTTP.js";
|
||||
import { BLE } from "../PageComponents/Connect/BLE.js";
|
||||
import { Serial } from "../PageComponents/Connect/Serial.js";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "HTTP",
|
||||
element: HTTP,
|
||||
disabled: false,
|
||||
disabledMessage: "Unsuported connection method"
|
||||
},
|
||||
{
|
||||
label: "Bluetooth",
|
||||
element: BLE,
|
||||
disabled: !navigator.bluetooth,
|
||||
disabledMessage:
|
||||
"Web Bluetooth is currently only supported by Chromium-based browsers",
|
||||
disabledLink:
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
|
||||
},
|
||||
{
|
||||
label: "Serial",
|
||||
element: Serial,
|
||||
disabled: !navigator.serial,
|
||||
disabledMessage:
|
||||
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility"
|
||||
}
|
||||
];
|
||||
export interface NewDeviceProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const NewDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange
|
||||
}: NewDeviceProps): JSX.Element => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect New Device</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="HTTP">
|
||||
<TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger value={tab.label} disabled={tab.disabled}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent value={tab.label}>
|
||||
{tab.disabled ? (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{tab.disabledMessage}
|
||||
</p>
|
||||
) : (
|
||||
<tab.element />
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{(!navigator.bluetooth || !navigator.serial) && (
|
||||
<>
|
||||
<Subtle>
|
||||
Web Bluetooth and Web Serial are currently only supported by
|
||||
Chromium-based browsers.
|
||||
</Subtle>
|
||||
<Subtle>
|
||||
Read more:
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
|
||||
Web Bluetooth
|
||||
</Link>
|
||||
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
|
||||
Web Serial
|
||||
</Link>
|
||||
</Subtle>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { QRCode } from "react-qrcode-logo";
|
||||
import { Checkbox } from "@components/form/Checkbox.js";
|
||||
import { Input } from "@components/form/Input.js";
|
||||
import { Dialog } from "@components/generic/Dialog.js";
|
||||
import { ClipboardIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { ClipboardIcon } from "lucide-react";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export interface QRDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
loraConfig?: Protobuf.Config_LoRaConfig;
|
||||
channels: Protobuf.Channel[];
|
||||
}
|
||||
|
||||
export const QRDialog = ({
|
||||
isOpen,
|
||||
close,
|
||||
open,
|
||||
onOpenChange,
|
||||
loraConfig,
|
||||
channels
|
||||
}: QRDialogProps): JSX.Element => {
|
||||
@@ -44,58 +50,57 @@ export const QRDialog = ({
|
||||
}, [channels, selectedChannels, loraConfig]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={"Generate QR Code"}
|
||||
description={"The current LoRa configuration will also be shared."}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
>
|
||||
<div className="flex gap-3 px-4 py-5 sm:p-6">
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
{channels.map((channel) => (
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
disabled={
|
||||
channel.index === 0 ||
|
||||
channel.role === Protobuf.Channel_Role.DISABLED
|
||||
}
|
||||
label={
|
||||
channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`
|
||||
}
|
||||
checked={selectedChannels.includes(channel.index)}
|
||||
onChange={() => {
|
||||
if (selectedChannels.includes(channel.index)) {
|
||||
setSelectedChannels(
|
||||
selectedChannels.filter((c) => c !== channel.index)
|
||||
);
|
||||
} else {
|
||||
setSelectedChannels([...selectedChannels, channel.index]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Generate QR Code</DialogTitle>
|
||||
<DialogDescription>
|
||||
The current LoRa configuration will also be shared.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex gap-3 px-4 py-5 sm:p-6">
|
||||
<div className="flex w-40 flex-col gap-1">
|
||||
{channels.map((channel) => (
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
label={
|
||||
channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`
|
||||
}
|
||||
checked={selectedChannels.includes(channel.index)}
|
||||
onChange={() => {
|
||||
if (selectedChannels.includes(channel.index)) {
|
||||
setSelectedChannels(
|
||||
selectedChannels.filter((c) => c !== channel.index)
|
||||
);
|
||||
} else {
|
||||
setSelectedChannels([...selectedChannels, channel.index]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<QRCode value={QRCodeURL} size={200} qrStyle="dots" />
|
||||
</div>
|
||||
</div>
|
||||
<QRCode value={QRCodeURL} size={200} qrStyle="dots" />
|
||||
</div>
|
||||
|
||||
<div className="sm:px-6">
|
||||
<Input
|
||||
label="Sharable URL"
|
||||
value={QRCodeURL}
|
||||
disabled
|
||||
action={{
|
||||
icon: <ClipboardIcon className="h-4" />,
|
||||
action() {
|
||||
void navigator.clipboard.writeText(QRCodeURL);
|
||||
toast.success("Copied URL to Clipboard");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Input
|
||||
label="Sharable URL"
|
||||
value={QRCodeURL}
|
||||
disabled
|
||||
action={{
|
||||
icon: <ClipboardIcon size={16} />,
|
||||
action() {
|
||||
void navigator.clipboard.writeText(QRCodeURL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,56 +1,64 @@
|
||||
import { useState } from "react";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Dialog } from "@components/generic/Dialog.js";
|
||||
import { ArrowPathIcon, ClockIcon } from "@heroicons/react/24/outline";
|
||||
import { Button } from "@components/form/Button.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Input } from "@components/form/Input.js";
|
||||
|
||||
export interface RebootDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RebootDialog = ({
|
||||
isOpen,
|
||||
close
|
||||
open,
|
||||
onOpenChange
|
||||
}: RebootDialogProps): JSX.Element => {
|
||||
const { connection, setDialogOpen } = useDevice();
|
||||
|
||||
const [time, setTime] = useState<number>(5);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={"Schedule Reboot"}
|
||||
description={"Reboot the connected node after x minutes."}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Input
|
||||
type="number"
|
||||
value={time}
|
||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
||||
action={{
|
||||
icon: <ClockIcon className="w-4" />,
|
||||
action() {
|
||||
connection
|
||||
?.reboot(time * 60)
|
||||
.then(() => setDialogOpen("reboot", false));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
onClick={() => {
|
||||
connection?.reboot(2).then(() => setDialogOpen("reboot", false));
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<ArrowPathIcon className="w-4" />
|
||||
</span>
|
||||
Now
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Schedule Reboot</DialogTitle>
|
||||
<DialogDescription>
|
||||
Reboot the connected node after x minutes.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Input
|
||||
type="number"
|
||||
value={time}
|
||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
||||
action={{
|
||||
icon: <ClockIcon size={16} />,
|
||||
action() {
|
||||
connection
|
||||
?.reboot(time * 60)
|
||||
.then(() => setDialogOpen("reboot", false));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
onClick={() => {
|
||||
connection?.reboot(2).then(() => setDialogOpen("reboot", false));
|
||||
}}
|
||||
>
|
||||
<RefreshCwIcon size={16} />
|
||||
Now
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,56 +1,66 @@
|
||||
import { useState } from "react";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Dialog } from "@components/generic/Dialog.js";
|
||||
import { ClockIcon, PowerIcon } from "@heroicons/react/24/outline";
|
||||
import { Button } from "@components/form/Button.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@components/UI/Dialog.js";
|
||||
import { ClockIcon, PowerIcon } from "lucide-react";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { Input } from "@components/form/Input.js";
|
||||
|
||||
export interface ShutdownDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const ShutdownDialog = ({
|
||||
isOpen,
|
||||
close
|
||||
open,
|
||||
onOpenChange
|
||||
}: ShutdownDialogProps): JSX.Element => {
|
||||
const { connection, setDialogOpen } = useDevice();
|
||||
|
||||
const [time, setTime] = useState<number>(5);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={"Schedule Shutdown"}
|
||||
description={"Turn off the connected node after x minutes."}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Input
|
||||
type="number"
|
||||
value={time}
|
||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
||||
action={{
|
||||
icon: <ClockIcon className="w-4" />,
|
||||
action() {
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Schedule Shutdown</DialogTitle>
|
||||
<DialogDescription>
|
||||
Turn off the connected node after x minutes.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-2 p-4">
|
||||
<Input
|
||||
type="number"
|
||||
value={time}
|
||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
||||
action={{
|
||||
icon: <ClockIcon size={16} />,
|
||||
action() {
|
||||
connection
|
||||
?.shutdown(time * 60)
|
||||
.then(() => setDialogOpen("shutdown", false));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
onClick={() => {
|
||||
connection
|
||||
?.shutdown(time * 60)
|
||||
?.shutdown(2)
|
||||
.then(() => setDialogOpen("shutdown", false));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="w-24"
|
||||
onClick={() => {
|
||||
connection
|
||||
?.shutdown(2)
|
||||
.then(() => setDialogOpen("shutdown", false));
|
||||
}}
|
||||
>
|
||||
<PowerIcon className="w-4" />
|
||||
<span>Now</span>
|
||||
</Button>
|
||||
</div>
|
||||
}}
|
||||
>
|
||||
<PowerIcon size={16} />
|
||||
<span>Now</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user