Browse Source

Add Import dialog, fix device icons & toggle styling

pull/66/head
Sacha Weatherstone 3 years ago
parent
commit
56aeba9140
  1. 26
      package.json
  2. 951
      pnpm-lock.yaml
  3. 26
      src/components/CommandPalette/Index.tsx
  4. 1
      src/components/CommandPalette/SearchResult.tsx
  5. 11
      src/components/Dialog/DialogManager.tsx
  6. 134
      src/components/Dialog/ImportDialog.tsx
  7. 11
      src/components/form/Toggle.tsx
  8. 13
      src/core/stores/deviceStore.ts
  9. 4
      src/pages/Channels.tsx

26
package.json

@ -23,7 +23,7 @@
"homepage": "https://meshtastic.org",
"dependencies": {
"@emeraldpay/hashicon-react": "^0.5.2",
"@headlessui/react": "^1.7.5",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@hookform/error-message": "^2.0.1",
"@hookform/resolvers": "^2.9.10",
@ -31,7 +31,7 @@
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.8",
"base64-js": "^1.5.1",
"chart.js": "^4.0.1",
"chart.js": "^4.1.1",
"chartjs-adapter-date-fns": "^3.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@ -42,9 +42,9 @@
"maplibre-gl": "2.4.0",
"pretty-ms": "^8.0.0",
"react": "^18.2.0",
"react-chartjs-2": "^5.0.1",
"react-chartjs-2": "^5.1.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.40.0",
"react-hook-form": "^7.41.1",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.7.1",
"react-json-pretty": "^2.2.0",
@ -56,25 +56,25 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/chrome": "^0.0.204",
"@types/chrome": "^0.0.206",
"@types/geodesy": "^2.2.3",
"@types/node": "^18.11.13",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-dom": "^18.0.10",
"@types/w3c-web-serial": "^1.0.3",
"@types/web-bluetooth": "^0.0.16",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.29.0",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"gzipper": "^7.2.0",
"postcss": "^8.4.19",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.1",
"rollup-plugin-visualizer": "^5.8.3",
@ -82,8 +82,8 @@
"tar": "^6.1.13",
"tslib": "^2.4.1",
"typescript": "^4.9.4",
"unimported": "^1.23.0",
"vite": "^4.0.0",
"unimported": "^1.24.0",
"vite": "^4.0.3",
"vite-plugin-environment": "^1.1.3"
}
}

951
pnpm-lock.yaml

File diff suppressed because it is too large

26
src/components/CommandPalette/Index.tsx

@ -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",

1
src/components/CommandPalette/SearchResult.tsx

@ -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
src/components/Dialog/DialogManager.tsx

@ -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
src/components/Dialog/ImportDialog.tsx

@ -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>
);
};

11
src/components/form/Toggle.tsx

@ -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
src/core/stores/deviceStore.ts

@ -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) => {

4
src/pages/Channels.tsx

@ -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

Loading…
Cancel
Save