Overhaul (#83)

* WIP Layout Refactor

* New Connection Dialog

* WIP form overhaul

* Fix remaining config pages
This commit is contained in:
Sacha Weatherstone
2023-02-12 15:31:25 +11:00
committed by GitHub
parent c184c5add6
commit e59fe138f1
121 changed files with 5358 additions and 4701 deletions
@@ -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>
);
};
+18 -11
View File
@@ -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);
}}
/>
</>
);
};
+75 -66
View File
@@ -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>
);
};
+97
View File
@@ -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:&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
Web Bluetooth
</Link>
&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
Web Serial
</Link>
</Subtle>
</>
)}
</DialogContent>
</Dialog>
);
};
+63 -58
View File
@@ -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>
);
};
+48 -40
View File
@@ -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>
);
};
+48 -38
View File
@@ -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>
);
};