Add Import dialog, fix device icons & toggle styling

This commit is contained in:
Sacha Weatherstone
2022-12-28 10:53:52 +11:00
parent 7ce8d82aa4
commit 56aeba9140
9 changed files with 550 additions and 627 deletions
+21 -5
View File
@@ -28,6 +28,7 @@ import { Hashicon } from "@emeraldpay/hashicon-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
import {
ArchiveBoxXMarkIcon,
ArrowDownOnSquareStackIcon,
ArrowPathIcon,
ArrowsRightLeftIcon,
BeakerIcon,
@@ -44,6 +45,7 @@ import {
PlusIcon,
PowerIcon,
QrCodeIcon,
QueueListIcon,
Square3Stack3DIcon,
TrashIcon,
UsersIcon,
@@ -83,6 +85,7 @@ export const CommandPalette = (): JSX.Element => {
const {
setQRDialogOpen,
setImportDialogOpen,
setShutdownDialogOpen,
setRebootDialogOpen,
setActivePage,
@@ -167,7 +170,7 @@ export const CommandPalette = (): JSX.Element => {
)?.data.user?.longName ?? device.hardware.myNodeNum.toString(),
icon: (
<Hashicon
size={24}
size={18}
value={device.hardware.myNodeNum.toString()}
/>
),
@@ -191,11 +194,24 @@ export const CommandPalette = (): JSX.Element => {
icon: CubeTransparentIcon,
commands: [
{
name: "QR Code Generator",
name: "QR Code",
icon: QrCodeIcon,
action() {
setQRDialogOpen(true);
}
subItems: [
{
name: "Generator",
icon: <QueueListIcon className="w-4" />,
action() {
setQRDialogOpen(true);
}
},
{
name: "Import",
icon: <ArrowDownOnSquareStackIcon className="w-4" />,
action() {
setImportDialogOpen(true);
}
}
]
},
{
name: "Reset Peers",
@@ -53,6 +53,7 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
>
{({ active }) => (
<>
{item.icon}
<span className="ml-3">{item.name}</span>
{active && (
<ChevronRightIcon className="ml-auto h-4 text-gray-400" />
+11
View File
@@ -5,11 +5,14 @@ import { QRDialog } from "@components/Dialog/QRDialog.js";
import { RebootDialog } from "./RebootDialog.js";
import { ShutdownDialog } from "./ShutdownDialog.js";
import { ImportDialog } from "./ImportDialog.js";
export const DialogManager = (): JSX.Element => {
const {
channels,
config,
importDialogOpen,
setImportDialogOpen,
QRDialogOpen,
setQRDialogOpen,
shutdownDialogOpen,
@@ -27,6 +30,14 @@ export const DialogManager = (): JSX.Element => {
channels={channels.map((ch) => ch.config)}
loraConfig={config.lora}
/>
<ImportDialog
isOpen={importDialogOpen}
close={() => {
setImportDialogOpen(false);
}}
channels={channels.map((ch) => ch.config)}
loraConfig={config.lora}
/>
<ShutdownDialog
isOpen={shutdownDialogOpen}
close={() => {
+134
View File
@@ -0,0 +1,134 @@
import type React from "react";
import { useEffect, useState } from "react";
import { fromByteArray, toByteArray } 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 { Protobuf } from "@meshtastic/meshtasticjs";
import { Select } from "../form/Select.js";
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
import { Toggle } from "../form/Toggle.js";
import { Button } from "../form/Button.js";
import { useDevice } from "@app/core/providers/useDevice.js";
export interface ImportDialogProps {
isOpen: boolean;
close: () => void;
loraConfig?: Protobuf.Config_LoRaConfig;
channels: Protobuf.Channel[];
}
export const ImportDialog = ({
isOpen,
close,
}: ImportDialogProps): JSX.Element => {
const [QRCodeURL, setQRCodeURL] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.ChannelSet>();
const [validURL, setValidURL] = useState<boolean>(false);
const {connection} = useDevice()
useEffect(() => {
const base64String = QRCodeURL.split("e/#")[1]
?.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(QRCodeURL.length + ((4 - (QRCodeURL.length % 4)) % 4), "=");
try {
setChannelSet(Protobuf.ChannelSet.fromBinary(toByteArray(base64String)));
setValidURL(true);
} catch (error) {
setValidURL(false);
setChannelSet(undefined);
}
}, [QRCodeURL]);
const apply = () => {
channelSet?.settings.map(((ch, index) => {
connection?.setChannel({
channel: {
index,
role: index === 0 ? Protobuf.Channel_Role.PRIMARY: Protobuf.Channel_Role.SECONDARY,
settings: ch
}
})
}))
if (channelSet?.loraConfig) {
connection?.setConfig({
config: {
payloadVariant: {
oneofKind: 'lora',
lora: channelSet.loraConfig
}
}
})
}
}
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?"
disabled
checked={channelSet?.loraConfig?.usePreset ?? true}
/>
</div>
<Select
label="Modem Preset"
disabled
value={channelSet?.loraConfig?.modemPreset}
>
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
</Select>
</div>
<Select
label="Region"
disabled
value={channelSet?.loraConfig?.region}
>
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
</Select>
<span className="block text-md font-medium text-gray-700">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>)}
<Button onClick={() => apply()} disabled={!validURL}>Apply</Button></div>
</Dialog>
);
};
+8 -3
View File
@@ -7,6 +7,7 @@ export interface ToggleProps {
label?: string;
description?: string;
disabled?: boolean;
className?: string;
onChange?: (checked: boolean) => void;
}
@@ -15,15 +16,19 @@ export const Toggle = ({
label,
description,
disabled,
className,
onChange
}: ToggleProps): JSX.Element => {
return (
<Switch.Group as="div" className="flex items-center justify-between">
<Switch.Group
as="div"
className={`flex items-center justify-between ${className}`}
>
<span className="flex flex-grow flex-col">
{label && (
<Switch.Label
as="span"
className="text-sm font-medium text-gray-900"
className="block text-sm font-medium text-gray-700"
passive
>
{label}
@@ -41,7 +46,7 @@ export const Toggle = ({
onChange={onChange}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 ${
checked ? "bg-orange-600" : "bg-orange-100"
} ${disabled ? "cursor-not-allowed bg-orange-400" : ""}`}
} ${disabled ? "cursor-not-allowed bg-orange-200" : ""}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
+13
View File
@@ -55,6 +55,7 @@ export interface Device {
waypoints: Protobuf.Waypoint[];
regionUnset: boolean;
currentMetrics: Protobuf.DeviceMetrics;
importDialogOpen: boolean;
QRDialogOpen: boolean;
shutdownDialogOpen: boolean;
rebootDialogOpen: boolean;
@@ -80,6 +81,7 @@ export interface Device {
addWaypointMessage: (message: WaypointIDWithAck) => void;
addDeviceMetadataMessage: (metadata: Types.DeviceMetadataPacket) => void;
ackMessage: (channelIndex: number, messageId: number) => void;
setImportDialogOpen: (open: boolean) => void;
setQRDialogOpen: (open: boolean) => void;
setShutdownDialogOpen: (open: boolean) => void;
setRebootDialogOpen: (open: boolean) => void;
@@ -118,6 +120,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
waypoints: [],
regionUnset: false,
currentMetrics: Protobuf.DeviceMetrics.create(),
importDialogOpen: false,
QRDialogOpen: false,
shutdownDialogOpen: false,
rebootDialogOpen: false,
@@ -536,6 +539,16 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
})
);
},
setImportDialogOpen: (open: boolean) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.importDialogOpen = open;
}
})
);
},
setQRDialogOpen: (open: boolean) => {
set(
produce<DeviceState>((draft) => {
+2 -2
View File
@@ -11,7 +11,7 @@ import {
import { Protobuf } from "@meshtastic/meshtasticjs";
export const ChannelsPage = (): JSX.Element => {
const { channels, setQRDialogOpen } = useDevice();
const { channels, setQRDialogOpen, setImportDialogOpen } = useDevice();
const tabs: TabType[] = channels.map((channel) => {
return {
@@ -33,7 +33,7 @@ export const ChannelsPage = (): JSX.Element => {
variant="secondary"
iconBefore={<ArrowDownOnSquareStackIcon className="w-4" />}
onClick={() => {
setQRDialogOpen(true);
setImportDialogOpen(true);
}}
>
Import