2.0 Overhaul start
This commit is contained in:
+4
-1
@@ -1 +1,4 @@
|
|||||||
{}
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
|
|||||||
+16
-10
@@ -20,22 +20,23 @@
|
|||||||
"homepage": "https://meshtastic.org",
|
"homepage": "https://meshtastic.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@hookform/resolvers": "^2.9.7",
|
"@headlessui/react": "^1.7.2",
|
||||||
|
"@heroicons/react": "^2.0.11",
|
||||||
|
"@hookform/resolvers": "^2.9.8",
|
||||||
"@meshtastic/eslint-config": "^1.0.8",
|
"@meshtastic/eslint-config": "^1.0.8",
|
||||||
"@meshtastic/meshtasticjs": "^0.6.98",
|
"@meshtastic/meshtasticjs": "^0.6.99",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.7",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"evergreen-ui": "^6.10.3",
|
|
||||||
"geodesy": "^2.4.0",
|
"geodesy": "^2.4.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
||||||
"maplibre-gl": "^2.4.0",
|
"maplibre-gl": "^2.4.0",
|
||||||
"modern-css-reset": "^1.4.0",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.34.2",
|
"react-hook-form": "^7.35.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"react-json-pretty": "^2.2.0",
|
"react-json-pretty": "^2.2.0",
|
||||||
"react-map-gl": "^7.0.19",
|
"react-map-gl": "^7.0.19",
|
||||||
@@ -44,20 +45,25 @@
|
|||||||
"zustand": "4.1.1"
|
"zustand": "4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.196",
|
"@types/chrome": "^0.0.197",
|
||||||
"@types/geodesy": "^2.2.3",
|
"@types/geodesy": "^2.2.3",
|
||||||
"@types/node": "^18.7.16",
|
"@types/node": "^18.7.18",
|
||||||
"@types/react": "^18.0.18",
|
"@types/react": "^18.0.20",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/w3c-web-serial": "^1.0.2",
|
"@types/w3c-web-serial": "^1.0.2",
|
||||||
"@types/web-bluetooth": "^0.0.15",
|
"@types/web-bluetooth": "^0.0.15",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
|
"autoprefixer": "^10.4.11",
|
||||||
"gzipper": "^7.1.0",
|
"gzipper": "^7.1.0",
|
||||||
|
"postcss": "^8.4.16",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
"rollup-plugin-visualizer": "^5.8.1",
|
||||||
|
"tailwindcss": "^3.1.8",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"vite": "^3.1.0",
|
"vite": "^3.1.3",
|
||||||
"vite-plugin-environment": "^1.1.2"
|
"vite-plugin-environment": "^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+560
-723
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
+26
-9
@@ -1,20 +1,37 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
import { MapProvider } from "react-map-gl";
|
import { MapProvider } from "react-map-gl";
|
||||||
|
|
||||||
import { AppLayout } from "@components/layout/AppLayout.js";
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
|
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
||||||
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
|
|
||||||
|
import { DeviceSelector } from "./components/DeviceSelector.js";
|
||||||
|
import { NewDevice } from "./components/NewDevice.js";
|
||||||
|
import { PageNav } from "./components/PageNav.js";
|
||||||
|
import { Sidebar } from "./components/Sidebar.js";
|
||||||
import { PageRouter } from "./PageRouter.js";
|
import { PageRouter } from "./PageRouter.js";
|
||||||
|
|
||||||
export const App = (): JSX.Element => {
|
export const App = (): JSX.Element => {
|
||||||
|
const { getDevice } = useDeviceStore();
|
||||||
|
const { selectedDevice } = useAppStore();
|
||||||
|
|
||||||
|
const device = getDevice(selectedDevice);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane display="flex">
|
<div className="h-full flex w-full">
|
||||||
<AppLayout>
|
<DeviceSelector />
|
||||||
<MapProvider>
|
|
||||||
<PageRouter />
|
{device && (
|
||||||
</MapProvider>
|
<DeviceWrapper device={device}>
|
||||||
</AppLayout>
|
<Sidebar />
|
||||||
</Pane>
|
<PageNav />
|
||||||
|
<MapProvider>
|
||||||
|
<PageRouter />
|
||||||
|
</MapProvider>
|
||||||
|
</DeviceWrapper>
|
||||||
|
)}
|
||||||
|
{selectedDevice === 0 && <NewDevice />}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type React from "react";
|
import type React from 'react';
|
||||||
|
|
||||||
import { DeviceContext } from "@core/providers/useDevice.js";
|
import { DeviceContext } from '@core/providers/useDevice.js';
|
||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from '@core/stores/deviceStore.js';
|
||||||
|
|
||||||
export interface DeviceProps {
|
export interface DeviceProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { ChannelsPage } from "@pages/Channels/index.js";
|
import { ChannelsPage } from "@pages/Channels.js";
|
||||||
import { ConfigPage } from "@pages/Config/index.js";
|
import { ConfigPage } from "@pages/Config/index.js";
|
||||||
import { ExtensionsPage } from "@pages/Extensions/Index.js";
|
import { ExtensionsPage } from "@pages/Extensions/Index.js";
|
||||||
import { InfoPage } from "@pages/Info/index.js";
|
import { InfoPage } from "@pages/Info.js";
|
||||||
import { MapPage } from "@pages/Map/index.js";
|
import { MapPage } from "@pages/Map.js";
|
||||||
import { MessagesPage } from "@pages/Messages/index.js";
|
import { MessagesPage } from "@pages/Messages.js";
|
||||||
|
|
||||||
export const PageRouter = (): JSX.Element => {
|
export const PageRouter = (): JSX.Element => {
|
||||||
const { activePage } = useDevice();
|
const { activePage } = useDevice();
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import type { ButtonHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
iconBefore?: JSX.Element;
|
||||||
|
iconAfter?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = ({
|
||||||
|
size = "md",
|
||||||
|
variant = "primary",
|
||||||
|
iconBefore,
|
||||||
|
iconAfter,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: ButtonProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`px-3 w-full rounded-md flex border border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 ${
|
||||||
|
variant === "primary"
|
||||||
|
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||||
|
: "bg-orange-100 text-orange-700 hover:bg-orange-200"
|
||||||
|
} ${
|
||||||
|
size === "sm"
|
||||||
|
? "h-8 text-sm"
|
||||||
|
: size === "md"
|
||||||
|
? "h-10 text-sm"
|
||||||
|
: "h-10 text-base"
|
||||||
|
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""}`}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className="flex items-center m-auto gap-2 font-medium">
|
||||||
|
{iconBefore}
|
||||||
|
{children}
|
||||||
|
{iconAfter}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
|
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
||||||
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export const DeviceSelector = (): JSX.Element => {
|
||||||
|
const { getDevices } = useDeviceStore();
|
||||||
|
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex bg-slate-50 w-16 items-center whitespace-nowrap py-12 text-sm [writing-mode:vertical-rl] h-full">
|
||||||
|
<span className="font-mono text-slate-500">Connected Devices</span>
|
||||||
|
<span className="mt-6 flex gap-4 font-bold text-slate-900">
|
||||||
|
{getDevices().map((device) => (
|
||||||
|
<div
|
||||||
|
key={device.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDevice(device.id);
|
||||||
|
}}
|
||||||
|
className="group flex w-8 h-8 p-0.5 cursor-pointer drop-shadow-md"
|
||||||
|
>
|
||||||
|
<Hashicon size={32} value={device.hardware.myNodeNum.toString()} />
|
||||||
|
<div
|
||||||
|
className={`absolute -left-1.5 w-0.5 h-7 rounded-full group-hover:bg-orange-300 ${
|
||||||
|
device.id === selectedDevice
|
||||||
|
? "bg-orange-400"
|
||||||
|
: "bg-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDevice(0);
|
||||||
|
}}
|
||||||
|
className={`w-8 h-8 p-2 border-dashed border-2 rounded-md hover:border-orange-300 cursor-pointer ${
|
||||||
|
selectedDevice === 0 ? "border-orange-400" : "border-slate-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<img src="Logo_Black.svg" className="px-3 mt-auto" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { CogIcon, CrossIcon, IconButton, Text } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { TabbedContent, TabType } from "../layout/page/TabbedContent.js";
|
|
||||||
import { Dialog } from "./index.js";
|
|
||||||
|
|
||||||
export interface HelpDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HelpDialog = ({ isOpen, close }: HelpDialogProps): JSX.Element => {
|
|
||||||
const tabs: TabType[] = [
|
|
||||||
{
|
|
||||||
name: "Device Config",
|
|
||||||
icon: CogIcon,
|
|
||||||
element: () => (
|
|
||||||
<div>
|
|
||||||
<Text>Title</Text>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Device Config",
|
|
||||||
icon: CogIcon,
|
|
||||||
element: () => (
|
|
||||||
<div>
|
|
||||||
<Text>Title 2</Text>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog isOpen={isOpen} close={close} title="Help">
|
|
||||||
<TabbedContent
|
|
||||||
tabs={tabs}
|
|
||||||
actions={[() => <IconButton icon={CrossIcon} onClick={close} />]}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
HelperManagementIcon,
|
|
||||||
IconButton,
|
|
||||||
majorScale,
|
|
||||||
MoreIcon,
|
|
||||||
Table,
|
|
||||||
TagIcon,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { toMGRS } from "@app/core/utils/toMGRS.js";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
import { Dialog } from "./index.js";
|
|
||||||
|
|
||||||
export interface PeersDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PeersDialog = ({
|
|
||||||
isOpen,
|
|
||||||
close,
|
|
||||||
}: PeersDialogProps): JSX.Element => {
|
|
||||||
const { hardware, nodes, connection, setPeerInfoOpen, setActivePeer } =
|
|
||||||
useDevice();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog isOpen={isOpen} close={close} width={majorScale(120)}>
|
|
||||||
<Table>
|
|
||||||
<Table.Head>
|
|
||||||
<Table.HeaderCell flexBasis={48} flexShrink={0} flexGrow={0} />
|
|
||||||
<Table.TextHeaderCell flexBasis={96} flexShrink={0} flexGrow={0}>
|
|
||||||
Number
|
|
||||||
</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell flexBasis={116} flexShrink={0} flexGrow={0}>
|
|
||||||
Name
|
|
||||||
</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell flexBasis={48} flexShrink={0} flexGrow={0}>
|
|
||||||
SNR
|
|
||||||
</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell>Location</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell>Telemetry</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell>Last Heard</Table.TextHeaderCell>
|
|
||||||
<Table.TextHeaderCell>Actions</Table.TextHeaderCell>
|
|
||||||
</Table.Head>
|
|
||||||
<Table.Body height={240}>
|
|
||||||
{nodes
|
|
||||||
.filter((n) => n.data.num !== hardware.myNodeNum)
|
|
||||||
.map((node) => (
|
|
||||||
<Table.Row
|
|
||||||
key={node.data.num}
|
|
||||||
isSelectable
|
|
||||||
onSelect={() => {
|
|
||||||
setActivePeer(node.data.num);
|
|
||||||
setPeerInfoOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table.Cell flexBasis={48} flexShrink={0} flexGrow={0}>
|
|
||||||
<Hashicon
|
|
||||||
value={node.data.num.toString()}
|
|
||||||
size={majorScale(3)}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.TextCell flexBasis={96} flexShrink={0} flexGrow={0}>
|
|
||||||
{node.data.num}
|
|
||||||
</Table.TextCell>
|
|
||||||
<Table.TextCell flexBasis={116} flexShrink={0} flexGrow={0}>
|
|
||||||
{node.data.user?.longName}
|
|
||||||
</Table.TextCell>
|
|
||||||
<Table.TextCell flexBasis={48} flexShrink={0} flexGrow={0}>
|
|
||||||
{node.data.snr}
|
|
||||||
</Table.TextCell>
|
|
||||||
<Table.TextCell>
|
|
||||||
{toMGRS(
|
|
||||||
node.data.position?.latitudeI,
|
|
||||||
node.data.position?.longitudeI
|
|
||||||
)}
|
|
||||||
</Table.TextCell>
|
|
||||||
<Table.TextCell>Tmp</Table.TextCell>
|
|
||||||
<Table.TextCell>
|
|
||||||
{new Date(node.data.lastHeard * 1000).toLocaleString()}
|
|
||||||
</Table.TextCell>
|
|
||||||
<Table.Cell gap={majorScale(1)}>
|
|
||||||
<Tooltip content="Manage">
|
|
||||||
<IconButton icon={HelperManagementIcon} />
|
|
||||||
</Tooltip>
|
|
||||||
<IconButton
|
|
||||||
icon={TagIcon}
|
|
||||||
onClick={() => {
|
|
||||||
void connection?.sendPacket(
|
|
||||||
Protobuf.AdminMessage.toBinary({
|
|
||||||
payloadVariant: {
|
|
||||||
oneofKind: "getConfigRequest",
|
|
||||||
getConfigRequest:
|
|
||||||
Protobuf.AdminMessage_ConfigType.LORA_CONFIG,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Protobuf.PortNum.ADMIN_APP,
|
|
||||||
node.data.num,
|
|
||||||
true,
|
|
||||||
7,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
async (test) => {
|
|
||||||
console.log(test);
|
|
||||||
|
|
||||||
console.log("got response");
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton icon={MoreIcon} />
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,21 +2,14 @@ import type React from "react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { fromByteArray } from "base64-js";
|
import { fromByteArray } from "base64-js";
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
ClipboardIcon,
|
|
||||||
FormField,
|
|
||||||
IconButton,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
TextInputField,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { QRCode } from "react-qrcode-logo";
|
import { QRCode } from "react-qrcode-logo";
|
||||||
|
|
||||||
|
import { Dialog } from "@headlessui/react";
|
||||||
|
import { ClipboardIcon } from "@heroicons/react/24/outline";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
import { Dialog } from "./index.js";
|
import { Checkbox } from "../form/Checkbox.js";
|
||||||
|
import { Input } from "../form/Input.js";
|
||||||
|
|
||||||
export interface QRDialogProps {
|
export interface QRDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -54,64 +47,65 @@ export const QRDialog = ({
|
|||||||
}, [channels, selectedChannels, loraConfig]);
|
}, [channels, selectedChannels, loraConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <Dialog
|
<Dialog open={isOpen} onClose={close}>
|
||||||
// isShown={isOpen}
|
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||||
//
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
// onCloseComplete={close}
|
<Dialog.Panel className="mx-auto max-w-sm rounded bg-white p-3">
|
||||||
// hasFooter={false}
|
<Dialog.Title>Generate QR Code</Dialog.Title>
|
||||||
// >
|
|
||||||
<Dialog isOpen={isOpen} close={close} title="Generate QR Code" background>
|
<Dialog.Description>
|
||||||
<Pane display="flex">
|
This will permanently deactivate your account
|
||||||
<FormField
|
</Dialog.Description>
|
||||||
width="12rem"
|
<div className="flex">
|
||||||
label="Channels to include"
|
<div className="flex flex-col">
|
||||||
description="The current LoRa configuration will also be shared."
|
<span className="font-medium text-lg">Channels to include</span>
|
||||||
>
|
<span className="text-sm text-slate-600">
|
||||||
{channels.map((channel) => (
|
The current LoRa configuration will also be shared.
|
||||||
<Checkbox
|
</span>
|
||||||
key={channel.index}
|
{channels.map((channel) => (
|
||||||
disabled={channel.role === Protobuf.Channel_Role.DISABLED}
|
<Checkbox
|
||||||
label={
|
key={channel.index}
|
||||||
channel.settings?.name.length
|
disabled={channel.role === Protobuf.Channel_Role.DISABLED}
|
||||||
? channel.settings.name
|
label={
|
||||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
channel.settings?.name.length
|
||||||
? "Primary"
|
? channel.settings.name
|
||||||
: `Channel: ${channel.index}`
|
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
}
|
? "Primary"
|
||||||
checked={selectedChannels.includes(channel.index)}
|
: `Channel: ${channel.index}`
|
||||||
onChange={() => {
|
}
|
||||||
if (selectedChannels.includes(channel.index)) {
|
checked={selectedChannels.includes(channel.index)}
|
||||||
setSelectedChannels(
|
onChange={() => {
|
||||||
selectedChannels.filter((c) => c !== channel.index)
|
if (selectedChannels.includes(channel.index)) {
|
||||||
);
|
setSelectedChannels(
|
||||||
} else {
|
selectedChannels.filter((c) => c !== channel.index)
|
||||||
setSelectedChannels([...selectedChannels, channel.index]);
|
);
|
||||||
}
|
} else {
|
||||||
}}
|
setSelectedChannels([...selectedChannels, channel.index]);
|
||||||
/>
|
}
|
||||||
))}
|
}}
|
||||||
</FormField>
|
/>
|
||||||
<Pane
|
))}
|
||||||
display="flex"
|
</div>
|
||||||
flexDirection="column"
|
<div className="flex flex-col flex-grow m-2">
|
||||||
flexGrow={1}
|
<div className="flex m-auto">
|
||||||
margin={majorScale(1)}
|
<QRCode value={QRCodeURL} size={250} qrStyle="dots" />
|
||||||
>
|
</div>
|
||||||
<Pane display="flex" margin="auto">
|
<div className="flex gap-2">
|
||||||
<QRCode value={QRCodeURL} size={250} qrStyle="dots" />
|
<Input
|
||||||
</Pane>
|
label="Sharable URL"
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
value={QRCodeURL}
|
||||||
<TextInputField
|
action={{
|
||||||
label="Sharable URL"
|
icon: <ClipboardIcon className="h-4" />,
|
||||||
value={QRCodeURL}
|
action: () => {
|
||||||
width="100%"
|
console.log("");
|
||||||
/>
|
},
|
||||||
<Tooltip content="Copy to Clipboard">
|
}}
|
||||||
<IconButton icon={ClipboardIcon} marginTop="1.6rem" />
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
</Pane>
|
</div>
|
||||||
</Pane>
|
</div>
|
||||||
</Pane>
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { SelectField } from "evergreen-ui";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { LoRaValidation } from "@app/validation/config/lora.js";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
|
||||||
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
import { Form } from "../form/Form.js";
|
|
||||||
import { Dialog } from "./index.js";
|
|
||||||
|
|
||||||
export interface RegionDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RegionDialog = ({ isOpen }: RegionDialogProps): JSX.Element => {
|
|
||||||
const { config, connection } = useDevice();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isDirty },
|
|
||||||
reset,
|
|
||||||
} = useForm<LoRaValidation>({
|
|
||||||
defaultValues: config.lora,
|
|
||||||
resolver: classValidatorResolver(LoRaValidation),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset(config.lora);
|
|
||||||
}, [reset, config.lora]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
|
||||||
setLoading(true);
|
|
||||||
void connection?.setConfig(
|
|
||||||
{
|
|
||||||
payloadVariant: {
|
|
||||||
oneofKind: "lora",
|
|
||||||
lora: data,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
await Promise.resolve();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog isOpen={isOpen} close={close} title="Set Device Region" background>
|
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
|
||||||
<SelectField
|
|
||||||
label="Region"
|
|
||||||
description="This is a description."
|
|
||||||
isInvalid={!!errors.region?.message}
|
|
||||||
validationMessage={errors.region?.message}
|
|
||||||
{...register("region", { valueAsNumber: true })}
|
|
||||||
>
|
|
||||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
|
||||||
</SelectField>
|
|
||||||
</Form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CrossIcon,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
majorScale,
|
|
||||||
Overlay,
|
|
||||||
Pane,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
export interface DialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
close: () => void;
|
|
||||||
title?: string;
|
|
||||||
background?: boolean;
|
|
||||||
width?: number;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Dialog = ({
|
|
||||||
isOpen,
|
|
||||||
close,
|
|
||||||
title,
|
|
||||||
background,
|
|
||||||
width,
|
|
||||||
children,
|
|
||||||
}: DialogProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Overlay
|
|
||||||
isShown={isOpen}
|
|
||||||
onExit={close}
|
|
||||||
containerProps={{
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pane
|
|
||||||
role="dialog"
|
|
||||||
width={width ?? majorScale(80)}
|
|
||||||
margin="auto"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
zIndex={1}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
padding={majorScale(3)}
|
|
||||||
background={background ? "white" : undefined}
|
|
||||||
>
|
|
||||||
{background && (
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
justifyContent="space-between"
|
|
||||||
marginBottom={majorScale(2)}
|
|
||||||
>
|
|
||||||
<Heading size={600}>{title}</Heading>
|
|
||||||
<IconButton icon={CrossIcon} onClick={close} />
|
|
||||||
</Pane>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</Pane>
|
|
||||||
</Overlay>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import type { ButtonHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export interface IconButtonProps
|
||||||
|
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
icon?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconButton = ({
|
||||||
|
size = "md",
|
||||||
|
variant = "primary",
|
||||||
|
icon,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: IconButtonProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`flex border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 ${
|
||||||
|
variant === "primary"
|
||||||
|
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||||
|
: "bg-orange-100 text-orange-700 hover:bg-orange-200"
|
||||||
|
} ${
|
||||||
|
size === "sm" ? "h-8 w-8" : size === "md" ? "h-10 w-10" : "h-12 w-12"
|
||||||
|
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""}`}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className="m-auto">{icon}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { FiBluetooth, FiTerminal, FiWifi } from "react-icons/fi";
|
||||||
|
|
||||||
|
import { TabbedContent, TabType } from "./layout/page/TabbedContent.js";
|
||||||
|
import { BLE } from "./PageComponents/Connect/BLE.js";
|
||||||
|
import { HTTP } from "./PageComponents/Connect/HTTP.js";
|
||||||
|
import { Serial } from "./PageComponents/Connect/Serial.js";
|
||||||
|
|
||||||
|
export const NewDevice = () => {
|
||||||
|
const tabs: TabType[] = [
|
||||||
|
{
|
||||||
|
name: "BLE",
|
||||||
|
icon: <FiBluetooth className="h-4" />,
|
||||||
|
element: BLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP",
|
||||||
|
icon: <FiWifi className="h-4" />,
|
||||||
|
element: HTTP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Serial",
|
||||||
|
icon: <FiTerminal className="h-4" />,
|
||||||
|
element: Serial,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-96 h-96 m-auto">
|
||||||
|
<TabbedContent tabs={tabs} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Form } from "@components/form/Form";
|
||||||
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import {
|
||||||
|
ArrowPathIcon,
|
||||||
|
EyeIcon,
|
||||||
|
EyeSlashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
import { Select } from "../form/Select.js";
|
||||||
|
import { Toggle } from "../form/Toggle.js";
|
||||||
|
|
||||||
|
export interface SettingsPanelProps {
|
||||||
|
channel: Protobuf.Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||||
|
const { connection } = useDevice();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||||
|
const [pskHidden, setPskHidden] = useState(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
} = useForm<
|
||||||
|
Omit<Protobuf.ChannelSettings, "psk"> & { psk: string; enabled: boolean }
|
||||||
|
>({
|
||||||
|
defaultValues: {
|
||||||
|
enabled: [
|
||||||
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
|
Protobuf.Channel_Role.PRIMARY,
|
||||||
|
].find((role) => role === channel?.role)
|
||||||
|
? true
|
||||||
|
: false,
|
||||||
|
...channel?.settings,
|
||||||
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
enabled: [
|
||||||
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
|
Protobuf.Channel_Role.PRIMARY,
|
||||||
|
].find((role) => role === channel?.role)
|
||||||
|
? true
|
||||||
|
: false,
|
||||||
|
...channel?.settings,
|
||||||
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
|
});
|
||||||
|
}, [channel, reset]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
const channelData = Protobuf.Channel.create({
|
||||||
|
role:
|
||||||
|
channel?.role === Protobuf.Channel_Role.PRIMARY
|
||||||
|
? Protobuf.Channel_Role.PRIMARY
|
||||||
|
: data.enabled
|
||||||
|
? Protobuf.Channel_Role.SECONDARY
|
||||||
|
: Protobuf.Channel_Role.DISABLED,
|
||||||
|
index: channel?.index,
|
||||||
|
settings: {
|
||||||
|
...data,
|
||||||
|
psk: toByteArray(data.psk ?? ""),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection?.setChannel(channelData, (): Promise<void> => {
|
||||||
|
reset({ ...data });
|
||||||
|
setLoading(false);
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
title="Channel Editor"
|
||||||
|
breadcrumbs={[
|
||||||
|
"Channels",
|
||||||
|
channel.settings?.name.length
|
||||||
|
? channel.settings.name
|
||||||
|
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
|
? "Primary"
|
||||||
|
: `Channel: ${channel.index}`,
|
||||||
|
]}
|
||||||
|
reset={() =>
|
||||||
|
reset({
|
||||||
|
enabled: [
|
||||||
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
|
Protobuf.Channel_Role.PRIMARY,
|
||||||
|
].find((role) => role === channel?.role)
|
||||||
|
? true
|
||||||
|
: false,
|
||||||
|
...channel?.settings,
|
||||||
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
{channel?.index !== 0 && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="enabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle
|
||||||
|
label="Enabled"
|
||||||
|
description="Description"
|
||||||
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Name"
|
||||||
|
description="Max transmit power in dBm"
|
||||||
|
{...register("name")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
label="Key Size"
|
||||||
|
description="Desired size of generated key."
|
||||||
|
value={keySize}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||||
|
}}
|
||||||
|
action={{
|
||||||
|
icon: <ArrowPathIcon className="h-4" />,
|
||||||
|
action: () => {
|
||||||
|
const key = new Uint8Array(keySize / 8);
|
||||||
|
crypto.getRandomValues(key);
|
||||||
|
setValue("psk", fromByteArray(key));
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={128}>128 Bit</option>
|
||||||
|
<option value={256}>256 Bit</option>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
width="100%"
|
||||||
|
label="Pre-Shared Key"
|
||||||
|
description="Max transmit power in dBm"
|
||||||
|
type={pskHidden ? "password" : "text"}
|
||||||
|
action={{
|
||||||
|
icon: pskHidden ? (
|
||||||
|
<EyeIcon className="w-4" />
|
||||||
|
) : (
|
||||||
|
<EyeSlashIcon className="w-4" />
|
||||||
|
),
|
||||||
|
action: () => {
|
||||||
|
setPskHidden(!pskHidden);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...register("psk")}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="uplinkEnabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle
|
||||||
|
label="Uplink Enabled"
|
||||||
|
description="Description"
|
||||||
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="downlinkEnabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle
|
||||||
|
label="Downlink Enabled"
|
||||||
|
description="Description"
|
||||||
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { BluetoothValidation } from "@app/validation/config/bluetooth.js";
|
import { BluetoothValidation } from "@app/validation/config/bluetooth.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -54,43 +56,41 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Bluetooth Config"
|
||||||
label="Bluetooth Enabled"
|
breadcrumbs={["Config", "Bluetooth"]}
|
||||||
description="Description"
|
reset={() => reset(config.bluetooth)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
|
checked={value}
|
||||||
<SelectField
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
label="Pairing mode"
|
label="Pairing mode"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.mode?.message}
|
|
||||||
validationMessage={errors.mode?.message}
|
|
||||||
{...register("mode", { valueAsNumber: true })}
|
{...register("mode", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_BluetoothConfig_PairingMode)}
|
{renderOptions(Protobuf.Config_BluetoothConfig_PairingMode)}
|
||||||
</SelectField>
|
</Select>
|
||||||
|
|
||||||
<TextInputField
|
<Input
|
||||||
display={
|
disabled={
|
||||||
pairingMode !== Protobuf.Config_BluetoothConfig_PairingMode.FIXED_PIN
|
pairingMode !== Protobuf.Config_BluetoothConfig_PairingMode.FIXED_PIN
|
||||||
? "none"
|
|
||||||
: "block"
|
|
||||||
}
|
}
|
||||||
label="Pin"
|
label="Pin"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.fixedPin?.message}
|
|
||||||
validationMessage={errors.fixedPin?.message}
|
|
||||||
{...register("fixedPin", {
|
{...register("fixedPin", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, toaster } from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { DeviceValidation } from "@app/validation/config/device.js";
|
import { DeviceValidation } from "@app/validation/config/device.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -39,7 +40,7 @@ export const Device = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
toaster.success("Successfully updated device config");
|
// toaster.success("Successfully updated device config");
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -47,58 +48,45 @@ export const Device = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<SelectField
|
title="Device Config"
|
||||||
|
breadcrumbs={["Config", "Device"]}
|
||||||
|
reset={() => reset(config.device)}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
label="Role"
|
label="Role"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.role?.message}
|
|
||||||
validationMessage={errors.role?.message}
|
|
||||||
{...register("role", { valueAsNumber: true })}
|
{...register("role", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_DeviceConfig_Role)}
|
{renderOptions(Protobuf.Config_DeviceConfig_Role)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<FormField
|
<Controller
|
||||||
label="Serial Console Disabled"
|
name="serialEnabled"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.serialDisabled?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.serialDisabled?.message}
|
<Toggle
|
||||||
>
|
label="Serial Output Enabled"
|
||||||
<Controller
|
description="Description"
|
||||||
name="serialDisabled"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Controller
|
||||||
</FormField>
|
name="debugLogEnabled"
|
||||||
<FormField
|
control={control}
|
||||||
label="Factory Reset Device"
|
render={({ field: { value, ...rest } }) => (
|
||||||
description="Description"
|
<Toggle
|
||||||
isInvalid={!!errors.factoryReset?.message}
|
label="Enabled Debug Log"
|
||||||
validationMessage={errors.factoryReset?.message}
|
description="Description"
|
||||||
>
|
checked={value}
|
||||||
<Controller
|
{...rest}
|
||||||
name="factoryReset"
|
/>
|
||||||
control={control}
|
)}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField
|
|
||||||
label="Enabled Debug Log"
|
|
||||||
description="Description"
|
|
||||||
isInvalid={!!errors.debugLogEnabled?.message}
|
|
||||||
validationMessage={errors.debugLogEnabled?.message}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="debugLogEnabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { DisplayValidation } from "@app/validation/config/display.js";
|
import { DisplayValidation } from "@app/validation/config/display.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -46,42 +48,47 @@ export const Display = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<TextInputField
|
title="Display Config"
|
||||||
|
breadcrumbs={["Config", "Display"]}
|
||||||
|
reset={() => reset(config.display)}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
label="Screen Timeout"
|
label="Screen Timeout"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
{...register("screenOnSecs", { valueAsNumber: true })}
|
{...register("screenOnSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Carousel Delay"
|
label="Carousel Delay"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
{...register("autoScreenCarouselSecs", { valueAsNumber: true })}
|
{...register("autoScreenCarouselSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<Select
|
||||||
label="GPS Display Units"
|
label="GPS Display Units"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
{...register("gpsFormat", { valueAsNumber: true })}
|
{...register("gpsFormat", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
|
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<FormField
|
<Controller
|
||||||
label="Compass North Top"
|
name="compassNorthTop"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.compassNorthTop?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.compassNorthTop?.message}
|
<Toggle
|
||||||
>
|
label="Compass North Top"
|
||||||
<Controller
|
description="Description"
|
||||||
name="compassNorthTop"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { LoRaValidation } from "@app/validation/config/lora.js";
|
import { LoRaValidation } from "@app/validation/config/lora.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -14,7 +16,6 @@ import { Protobuf } from "@meshtastic/meshtasticjs";
|
|||||||
export const LoRa = (): JSX.Element => {
|
export const LoRa = (): JSX.Element => {
|
||||||
const { config, connection } = useDevice();
|
const { config, connection } = useDevice();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [usePreset, setUsePreset] = useState(true);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -27,6 +28,12 @@ export const LoRa = (): JSX.Element => {
|
|||||||
resolver: classValidatorResolver(LoRaValidation),
|
resolver: classValidatorResolver(LoRaValidation),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const usePreset = useWatch({
|
||||||
|
control,
|
||||||
|
name: "usePreset",
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(config.lora);
|
reset(config.lora);
|
||||||
}, [reset, config.lora]);
|
}, [reset, config.lora]);
|
||||||
@@ -49,115 +56,109 @@ export const LoRa = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="LoRa Config"
|
||||||
label="Use Preset"
|
breadcrumbs={["Config", "LoRa"]}
|
||||||
description="Description"
|
reset={() => reset(config.lora)}
|
||||||
isInvalid={!!errors.txDisabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.txDisabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Switch
|
>
|
||||||
height={24}
|
<Controller
|
||||||
marginLeft="auto"
|
name="usePreset"
|
||||||
checked={usePreset}
|
control={control}
|
||||||
onChange={(e) => setUsePreset(e.target.checked)}
|
render={({ field: { value, ...rest } }) => (
|
||||||
/>
|
<Toggle
|
||||||
</FormField>
|
label="Use Preset"
|
||||||
<SelectField
|
description="Description"
|
||||||
display={usePreset ? "block" : "none"}
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
label="Preset"
|
label="Preset"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.modemPreset?.message}
|
disabled={!usePreset}
|
||||||
validationMessage={errors.modemPreset?.message}
|
|
||||||
{...register("modemPreset", { valueAsNumber: true })}
|
{...register("modemPreset", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||||
</SelectField>
|
</Select>
|
||||||
|
|
||||||
<TextInputField
|
<Input
|
||||||
display={usePreset ? "none" : "block"}
|
|
||||||
label="Bandwidth"
|
label="Bandwidth"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
hint="MHz"
|
suffix="MHz"
|
||||||
isInvalid={!!errors.bandwidth?.message}
|
error={errors.bandwidth?.message}
|
||||||
validationMessage={errors.bandwidth?.message}
|
|
||||||
{...register("bandwidth", {
|
{...register("bandwidth", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
display={usePreset ? "none" : "block"}
|
|
||||||
label="Spread Factor"
|
label="Spread Factor"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
hint="CPS"
|
suffix="CPS"
|
||||||
isInvalid={!!errors.spreadFactor?.message}
|
error={errors.spreadFactor?.message}
|
||||||
validationMessage={errors.spreadFactor?.message}
|
|
||||||
{...register("spreadFactor", {
|
{...register("spreadFactor", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
display={usePreset ? "none" : "block"}
|
|
||||||
label="Coding Rate"
|
label="Coding Rate"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.codingRate?.message}
|
error={errors.codingRate?.message}
|
||||||
validationMessage={errors.codingRate?.message}
|
|
||||||
{...register("codingRate", {
|
{...register("codingRate", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Transmit Power"
|
|
||||||
description="Max transmit power in dBm"
|
|
||||||
type="number"
|
|
||||||
isInvalid={!!errors.txPower?.message}
|
|
||||||
validationMessage={errors.txPower?.message}
|
|
||||||
{...register("txPower", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<TextInputField
|
|
||||||
label="Hop Limit"
|
|
||||||
description="This is a description."
|
|
||||||
hint="Hops"
|
|
||||||
type="number"
|
|
||||||
isInvalid={!!errors.hopLimit?.message}
|
|
||||||
validationMessage={errors.hopLimit?.message}
|
|
||||||
{...register("hopLimit", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
label="Transmit Disabled"
|
|
||||||
description="Description"
|
|
||||||
isInvalid={!!errors.txDisabled?.message}
|
|
||||||
validationMessage={errors.txDisabled?.message}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="txDisabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<TextInputField
|
|
||||||
label="Frequency Offset"
|
label="Frequency Offset"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Hz"
|
suffix="Hz"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.frequencyOffset?.message}
|
error={errors.frequencyOffset?.message}
|
||||||
validationMessage={errors.frequencyOffset?.message}
|
|
||||||
{...register("frequencyOffset", { valueAsNumber: true })}
|
{...register("frequencyOffset", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<Select
|
||||||
label="Region"
|
label="Region"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.region?.message}
|
|
||||||
validationMessage={errors.region?.message}
|
|
||||||
{...register("region", { valueAsNumber: true })}
|
{...register("region", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||||
</SelectField>
|
</Select>
|
||||||
|
<Input
|
||||||
|
label="Hop Limit"
|
||||||
|
description="This is a description."
|
||||||
|
suffix="Hops"
|
||||||
|
type="number"
|
||||||
|
error={errors.hopLimit?.message}
|
||||||
|
{...register("hopLimit", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="txEnabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle
|
||||||
|
label="Transmit Enabled"
|
||||||
|
description="Description"
|
||||||
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Transmit Power"
|
||||||
|
description="Max transmit power in dBm"
|
||||||
|
type="number"
|
||||||
|
error={errors.txPower?.message}
|
||||||
|
{...register("txPower", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
|
||||||
FormField,
|
|
||||||
SelectField,
|
|
||||||
Switch,
|
|
||||||
TextInputField,
|
|
||||||
toaster,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
|
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
|
||||||
import { NetworkValidation } from "@app/validation/config/network.js";
|
import { NetworkValidation } from "@app/validation/config/network.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
@@ -51,7 +47,7 @@ export const Network = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
toaster.success("Successfully updated Network config");
|
// toaster.success("Successfully updated Network config");
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -59,48 +55,53 @@ export const Network = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Network Config"
|
||||||
label="WiFi Enabled"
|
breadcrumbs={["Config", "Network"]}
|
||||||
description="Description"
|
reset={() => reset(config.network)}
|
||||||
isInvalid={!!errors.wifiEnabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.wifiEnabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="wifiEnabled"
|
<Controller
|
||||||
control={control}
|
name="wifiEnabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="WiFi Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<SelectField
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
label="WiFi Mode"
|
label="WiFi Mode"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
|
disabled={!wifiEnabled}
|
||||||
{...register("wifiMode", { valueAsNumber: true })}
|
{...register("wifiMode", { valueAsNumber: true })}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_NetworkConfig_WiFiMode)}
|
{renderOptions(Protobuf.Config_NetworkConfig_WiFiMode)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<TextInputField
|
<Input
|
||||||
label="SSID"
|
label="SSID"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.wifiSsid?.message}
|
error={errors.wifiSsid?.message}
|
||||||
validationMessage={errors.wifiSsid?.message}
|
disabled={!wifiEnabled}
|
||||||
{...register("wifiSsid")}
|
{...register("wifiSsid")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="PSK"
|
label="PSK"
|
||||||
type="password"
|
type="password"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.wifiPsk?.message}
|
error={errors.wifiPsk?.message}
|
||||||
validationMessage={errors.wifiPsk?.message}
|
disabled={!wifiEnabled}
|
||||||
{...register("wifiPsk")}
|
{...register("wifiPsk")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="NTP Server"
|
label="NTP Server"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
isInvalid={!!errors.ntpServer?.message}
|
error={errors.ntpServer?.message}
|
||||||
validationMessage={errors.ntpServer?.message}
|
|
||||||
{...register("ntpServer")}
|
{...register("ntpServer")}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormField,
|
|
||||||
SelectMenu,
|
|
||||||
Switch,
|
|
||||||
TextInputField,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { PositionValidation } from "@app/validation/config/position.js";
|
import { PositionValidation } from "@app/validation/config/position.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { bitwiseDecode } from "@core/utils/bitwise";
|
|
||||||
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export const Position = (): JSX.Element => {
|
export const Position = (): JSX.Element => {
|
||||||
const { config, connection } = useDevice();
|
const { config, connection } = useDevice();
|
||||||
@@ -29,15 +22,6 @@ export const Position = (): JSX.Element => {
|
|||||||
} = useForm<PositionValidation>({
|
} = useForm<PositionValidation>({
|
||||||
defaultValues: config.position,
|
defaultValues: config.position,
|
||||||
resolver: classValidatorResolver(PositionValidation),
|
resolver: classValidatorResolver(PositionValidation),
|
||||||
// defaultValues: {
|
|
||||||
// ...preferences,
|
|
||||||
// positionBroadcastSecs:
|
|
||||||
// preferences.positionBroadcastSecs === 0
|
|
||||||
// ? preferences.role === Protobuf.Role.Router
|
|
||||||
// ? 43200
|
|
||||||
// : 900
|
|
||||||
// : preferences.positionBroadcastSecs,
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -62,76 +46,74 @@ export const Position = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<TextInputField
|
title="Position Config"
|
||||||
hint="Seconds"
|
breadcrumbs={["Config", "Position"]}
|
||||||
|
reset={() => reset(config.position)}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
suffix="Seconds"
|
||||||
label="Broadcast Interval"
|
label="Broadcast Interval"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.positionBroadcastSecs?.message}
|
error={errors.positionBroadcastSecs?.message}
|
||||||
validationMessage={errors.positionBroadcastSecs?.message}
|
|
||||||
{...register("positionBroadcastSecs", { valueAsNumber: true })}
|
{...register("positionBroadcastSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Disable Smart Position"
|
name="positionBroadcastSmartEnabled"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.positionBroadcastSmartDisabled?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.positionBroadcastSmartDisabled?.message}
|
<Toggle
|
||||||
>
|
label="Enable Smart Position"
|
||||||
<Controller
|
description="Description"
|
||||||
name="positionBroadcastSmartDisabled"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Controller
|
||||||
</FormField>
|
name="fixedPosition"
|
||||||
<FormField
|
control={control}
|
||||||
label="Use Fixed Position"
|
render={({ field: { value, ...rest } }) => (
|
||||||
description="Description"
|
<Toggle
|
||||||
isInvalid={!!errors.fixedPosition?.message}
|
label="Use Fixed Position"
|
||||||
validationMessage={errors.fixedPosition?.message}
|
description="Description"
|
||||||
>
|
checked={value}
|
||||||
<Controller
|
{...rest}
|
||||||
name="fixedPosition"
|
/>
|
||||||
control={control}
|
)}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
<Controller
|
||||||
)}
|
name="gpsEnabled"
|
||||||
/>
|
control={control}
|
||||||
</FormField>
|
render={({ field: { value, ...rest } }) => (
|
||||||
<FormField
|
<Toggle
|
||||||
label="Disable GPS"
|
label="GPS Enabled"
|
||||||
description="Description"
|
description="Description"
|
||||||
isInvalid={!!errors.gpsDisabled?.message}
|
checked={value}
|
||||||
validationMessage={errors.gpsDisabled?.message}
|
{...rest}
|
||||||
>
|
/>
|
||||||
<Controller
|
)}
|
||||||
name="gpsDisabled"
|
/>
|
||||||
control={control}
|
<Input
|
||||||
render={({ field: { value, ...field } }) => (
|
suffix="Seconds"
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<TextInputField
|
|
||||||
hint="Seconds"
|
|
||||||
label="GPS Update Interval"
|
label="GPS Update Interval"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.gpsUpdateInterval?.message}
|
error={errors.gpsUpdateInterval?.message}
|
||||||
validationMessage={errors.gpsUpdateInterval?.message}
|
|
||||||
{...register("gpsUpdateInterval", { valueAsNumber: true })}
|
{...register("gpsUpdateInterval", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Last GPS Attempt"
|
label="Last GPS Attempt"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.gpsAttemptTime?.message}
|
error={errors.gpsAttemptTime?.message}
|
||||||
validationMessage={errors.gpsAttemptTime?.message}
|
|
||||||
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Controller
|
{/* <Controller
|
||||||
name="positionFlags"
|
name="positionFlags"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }): JSX.Element => {
|
render={({ field, fieldState }): JSX.Element => {
|
||||||
@@ -218,7 +200,7 @@ export const Position = (): JSX.Element => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { PowerValidation } from "@app/validation/config/power.js";
|
import { PowerValidation } from "@app/validation/config/power.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -44,81 +45,79 @@ export const Power = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<TextInputField
|
title="Power Config"
|
||||||
|
breadcrumbs={["Config", "Power"]}
|
||||||
|
reset={() => reset(config.power)}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
label="Shutdown on battery delay"
|
label="Shutdown on battery delay"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.onBatteryShutdownAfterSecs?.message}
|
error={errors.onBatteryShutdownAfterSecs?.message}
|
||||||
validationMessage={errors.onBatteryShutdownAfterSecs?.message}
|
|
||||||
{...register("onBatteryShutdownAfterSecs", { valueAsNumber: true })}
|
{...register("onBatteryShutdownAfterSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Power Saving"
|
name="isPowerSaving"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.isPowerSaving?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.isPowerSaving?.message}
|
<Toggle
|
||||||
>
|
label="Power Saving"
|
||||||
<Controller
|
description="Description"
|
||||||
name="isPowerSaving"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Input
|
||||||
</FormField>
|
|
||||||
<TextInputField
|
|
||||||
label="ADC Multiplier Override ratio"
|
label="ADC Multiplier Override ratio"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.adcMultiplierOverride?.message}
|
error={errors.adcMultiplierOverride?.message}
|
||||||
validationMessage={errors.adcMultiplierOverride?.message}
|
|
||||||
{...register("adcMultiplierOverride", { valueAsNumber: true })}
|
{...register("adcMultiplierOverride", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Minimum Wake Time"
|
label="Minimum Wake Time"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.minWakeSecs?.message}
|
error={errors.minWakeSecs?.message}
|
||||||
validationMessage={errors.minWakeSecs?.message}
|
|
||||||
{...register("minWakeSecs", { valueAsNumber: true })}
|
{...register("minWakeSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Mesh SDS Timeout"
|
label="Mesh SDS Timeout"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.meshSdsTimeoutSecs?.message}
|
error={errors.meshSdsTimeoutSecs?.message}
|
||||||
validationMessage={errors.meshSdsTimeoutSecs?.message}
|
|
||||||
{...register("meshSdsTimeoutSecs", { valueAsNumber: true })}
|
{...register("meshSdsTimeoutSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="SDS"
|
label="SDS"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.sdsSecs?.message}
|
error={errors.sdsSecs?.message}
|
||||||
validationMessage={errors.sdsSecs?.message}
|
|
||||||
{...register("sdsSecs", { valueAsNumber: true })}
|
{...register("sdsSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="LS"
|
label="LS"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.lsSecs?.message}
|
error={errors.lsSecs?.message}
|
||||||
validationMessage={errors.lsSecs?.message}
|
|
||||||
{...register("lsSecs", { valueAsNumber: true })}
|
{...register("lsSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Wait Bluetooth"
|
label="Wait Bluetooth"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
isInvalid={!!errors.waitBluetoothSecs?.message}
|
error={errors.waitBluetoothSecs?.message}
|
||||||
validationMessage={errors.waitBluetoothSecs?.message}
|
|
||||||
{...register("waitBluetoothSecs", { valueAsNumber: true })}
|
{...register("waitBluetoothSecs", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { UserValidation } from "@app/validation/config/user.js";
|
import { UserValidation } from "@app/validation/config/user.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -50,27 +52,39 @@ export const User = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<TextInputField
|
title="User Config"
|
||||||
|
breadcrumbs={["Config", "User"]}
|
||||||
|
reset={() => {
|
||||||
|
reset({
|
||||||
|
longName: myNode?.data.user?.longName,
|
||||||
|
shortName: myNode?.data.user?.shortName,
|
||||||
|
isLicensed: myNode?.data.user?.isLicensed,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
loading={loading}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
label="Device ID"
|
label="Device ID"
|
||||||
description="Preset unique identifier for this device."
|
description="Preset unique identifier for this device."
|
||||||
isInvalid={!!errors.id?.message}
|
error={errors.id?.message}
|
||||||
validationMessage={errors.id?.message}
|
|
||||||
{...register("id")}
|
{...register("id")}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Device Name"
|
label="Device Name"
|
||||||
description="Personalised name for this device."
|
description="Personalised name for this device."
|
||||||
{...register("longName")}
|
{...register("longName")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Short Name"
|
label="Short Name"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
maxLength={3}
|
maxLength={3}
|
||||||
{...register("shortName")}
|
{...register("shortName")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Mac Address"
|
label="Mac Address"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
disabled
|
disabled
|
||||||
@@ -81,28 +95,26 @@ export const User = (): JSX.Element => {
|
|||||||
?.join(":") ?? ""
|
?.join(":") ?? ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<Select
|
||||||
label="Hardware"
|
label="Hardware"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
disabled
|
disabled
|
||||||
value={myNode?.data.user?.hwModel}
|
value={myNode?.data.user?.hwModel}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.HardwareModel)}
|
{renderOptions(Protobuf.HardwareModel)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<FormField
|
<Controller
|
||||||
label="Licenced Operator?"
|
name="isLicensed"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.isLicensed?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.isLicensed?.message}
|
<Toggle
|
||||||
>
|
label="Licenced Operator?"
|
||||||
<Controller
|
description="Description"
|
||||||
name="isLicensed"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-27
@@ -1,17 +1,15 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Button, majorScale, Pane } from "evergreen-ui";
|
import { Button } from "@components/Button.js";
|
||||||
import { FiPlusCircle } from "react-icons/fi";
|
|
||||||
|
|
||||||
import type { CloseProps } from "@components/SlideSheets/NewDevice.js";
|
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.js";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.js";
|
||||||
|
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { Constants, IBLEConnection } from "@meshtastic/meshtasticjs";
|
import { Constants, IBLEConnection } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export const BLE = ({ close }: CloseProps): JSX.Element => {
|
export const BLE = (): JSX.Element => {
|
||||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
@@ -34,30 +32,25 @@ export const BLE = ({ close }: CloseProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<div className="flex flex-col p-4 gap-2 w-full">
|
||||||
display="flex"
|
<div className="flex gap-2 flex-col h-48 overflow-y-auto">
|
||||||
flexDirection="column"
|
{bleDevices.map((device, index) => (
|
||||||
padding={majorScale(2)}
|
<Button
|
||||||
gap={majorScale(2)}
|
key={index}
|
||||||
>
|
variant="secondary"
|
||||||
{bleDevices.map((device, index) => (
|
onClick={() => {
|
||||||
<Button
|
void onConnect(device);
|
||||||
key={index}
|
}}
|
||||||
onClick={() => {
|
>
|
||||||
void onConnect(device);
|
{device.name}
|
||||||
}}
|
</Button>
|
||||||
>
|
))}
|
||||||
{device.name}
|
</div>
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
iconBefore={<PlusCircleIcon className="w-4" />}
|
||||||
gap={majorScale(1)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void navigator.bluetooth
|
void navigator.bluetooth
|
||||||
.requestDevice({
|
.requestDevice({
|
||||||
@@ -72,8 +65,7 @@ export const BLE = ({ close }: CloseProps): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New device
|
New device
|
||||||
<FiPlusCircle />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+32
-43
@@ -1,27 +1,18 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import {
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
Button,
|
|
||||||
FormField,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
Switch,
|
|
||||||
TextInputField,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { FiPlusCircle } from "react-icons/fi";
|
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
|
import { Button } from "@components/Button.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.js";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.js";
|
||||||
|
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { IHTTPConnection } from "@meshtastic/meshtasticjs";
|
import { IHTTPConnection } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export interface HTTPProps {
|
export const HTTP = (): JSX.Element => {
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HTTP = ({ close }: HTTPProps): JSX.Element => {
|
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
const { register, handleSubmit, control } = useForm<{
|
const { register, handleSubmit, control } = useForm<{
|
||||||
@@ -34,6 +25,12 @@ export const HTTP = ({ close }: HTTPProps): JSX.Element => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const TLSEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: "tls",
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
const id = randId();
|
const id = randId();
|
||||||
const device = addDevice(id);
|
const device = addDevice(id);
|
||||||
@@ -47,42 +44,34 @@ export const HTTP = ({ close }: HTTPProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
<form onSubmit={onSubmit}>
|
<form className="w-full p-4 gap-2 flex flex-col" onSubmit={onSubmit}>
|
||||||
<Pane
|
<div className="h-48 flex flex-col gap-2">
|
||||||
display="flex"
|
<Input
|
||||||
flexDirection="column"
|
|
||||||
padding={majorScale(2)}
|
|
||||||
gap={majorScale(2)}
|
|
||||||
>
|
|
||||||
<TextInputField
|
|
||||||
label="IP Address/Hostname"
|
label="IP Address/Hostname"
|
||||||
|
prefix={TLSEnabled ? "https://" : "http://"}
|
||||||
placeholder="000.000.000.000 / meshtastic.local"
|
placeholder="000.000.000.000 / meshtastic.local"
|
||||||
{...register("ip")}
|
{...register("ip")}
|
||||||
/>
|
/>
|
||||||
<FormField label="Use TLS">
|
<Controller
|
||||||
<Controller
|
name="tls"
|
||||||
name="tls"
|
control={control}
|
||||||
control={control}
|
render={({ field: { value, ...rest } }) => (
|
||||||
render={({ field: { value, ...field } }) => (
|
<Toggle
|
||||||
<Switch
|
label="Use TLS"
|
||||||
height={24}
|
description="Description"
|
||||||
marginLeft="auto"
|
checked={value}
|
||||||
checked={value}
|
{...rest}
|
||||||
{...field}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</div>
|
||||||
</FormField>
|
<Button iconBefore={<PlusCircleIcon className="w-4" />} type="submit">
|
||||||
<Button appearance="primary" gap={majorScale(1)} type="submit">
|
Connect
|
||||||
Connect
|
</Button>
|
||||||
<FiPlusCircle />
|
|
||||||
</Button>
|
|
||||||
</Pane>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+19
-47
@@ -1,14 +1,12 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Button, majorScale, Pane } from "evergreen-ui";
|
import { Button } from "@components/Button.js";
|
||||||
import { FiPlusCircle } from "react-icons/fi";
|
|
||||||
|
|
||||||
import type { CloseProps } from "@components/SlideSheets/NewDevice.js";
|
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.js";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.js";
|
||||||
|
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { ISerialConnection } from "@meshtastic/meshtasticjs";
|
import { ISerialConnection } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
interface USBID {
|
interface USBID {
|
||||||
@@ -16,7 +14,7 @@ interface USBID {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Serial = ({ close }: CloseProps): JSX.Element => {
|
export const Serial = (): JSX.Element => {
|
||||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const { setSelectedDevice } = useAppStore();
|
const { setSelectedDevice } = useAppStore();
|
||||||
@@ -45,50 +43,25 @@ export const Serial = ({ close }: CloseProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VID: USBID[] = [
|
|
||||||
{
|
|
||||||
id: 9114,
|
|
||||||
name: "TBA",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const PID: USBID[] = [
|
|
||||||
{
|
|
||||||
id: 32809,
|
|
||||||
name: "TBA",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<div className="flex flex-col p-4 gap-2 w-full">
|
||||||
display="flex"
|
<div className="flex gap-2 flex-col h-48 overflow-y-auto">
|
||||||
flexDirection="column"
|
{serialPorts.map((port, index) => (
|
||||||
padding={majorScale(2)}
|
<Button
|
||||||
gap={majorScale(2)}
|
key={index}
|
||||||
>
|
variant="secondary"
|
||||||
{serialPorts.map((port, index) => (
|
onClick={() => {
|
||||||
<Button
|
void onConnect(port);
|
||||||
key={index}
|
}}
|
||||||
gap={5}
|
>
|
||||||
onClick={() => {
|
{port.getInfo().usbVendorId} - {port.getInfo().usbProductId}
|
||||||
void onConnect(port);
|
</Button>
|
||||||
}}
|
))}
|
||||||
>
|
</div>
|
||||||
{VID.find((id) => id.id === port.getInfo().usbVendorId ?? 0)?.name ??
|
|
||||||
"Unknown"}{" "}
|
|
||||||
-{" "}
|
|
||||||
{PID.find((id) => id.id === port.getInfo().usbProductId ?? 0)?.name ??
|
|
||||||
"Unknown"}
|
|
||||||
<FiPlusCircle />
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
iconBefore={<PlusCircleIcon className="w-4" />}
|
||||||
gap={majorScale(1)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void navigator.serial.requestPort().then((port) => {
|
void navigator.serial.requestPort().then((port) => {
|
||||||
setSerialPorts(serialPorts.concat(port));
|
setSerialPorts(serialPorts.concat(port));
|
||||||
@@ -96,8 +69,7 @@ export const Serial = ({ close }: CloseProps): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New device
|
New device
|
||||||
<FiPlusCircle />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
|
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||||
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import type { Channel } from "@core/stores/deviceStore.js";
|
||||||
|
import { MapPinIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface ChannelChatProps {
|
||||||
|
channel: Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||||
|
const { nodes, connection, ackMessage } = useDevice();
|
||||||
|
const [currentMessage, setCurrentMessage] = useState("");
|
||||||
|
|
||||||
|
const sendMessage = (): void => {
|
||||||
|
void connection?.sendText(
|
||||||
|
currentMessage,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
channel.config.index,
|
||||||
|
(id) => {
|
||||||
|
ackMessage(channel.config.index, id);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setCurrentMessage("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-grow">
|
||||||
|
<div className="flex flex-col flex-grow">
|
||||||
|
{channel.messages.map((message, index) => (
|
||||||
|
<Message
|
||||||
|
key={index}
|
||||||
|
message={message}
|
||||||
|
lastMsgSameUser={
|
||||||
|
index === 0
|
||||||
|
? false
|
||||||
|
: channel.messages[index - 1].packet.from ===
|
||||||
|
message.packet.from
|
||||||
|
}
|
||||||
|
sender={
|
||||||
|
nodes.find((node) => node.data.num === message.packet.from)?.data
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<form
|
||||||
|
className="w-full"
|
||||||
|
onSubmit={(e): void => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-grow gap-2">
|
||||||
|
<span className="w-full">
|
||||||
|
<Input
|
||||||
|
minLength={2}
|
||||||
|
label=""
|
||||||
|
placeholder="Enter Message"
|
||||||
|
value={currentMessage}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setCurrentMessage(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<IconButton
|
||||||
|
variant="secondary"
|
||||||
|
icon={<PaperAirplaneIcon className="h-4 text-slate-500" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<IconButton
|
||||||
|
variant="secondary"
|
||||||
|
icon={<MapPinIcon className="h-4 text-slate-500" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { WaypointMessage } from "@components/PageComponents/Messages/WaypointMessage.js";
|
||||||
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import type { AllMessageTypes } from "@core/stores/deviceStore.js";
|
||||||
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
EllipsisHorizontalCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
export interface MessageProps {
|
||||||
|
lastMsgSameUser: boolean;
|
||||||
|
message: AllMessageTypes;
|
||||||
|
sender?: Protobuf.NodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Message = ({
|
||||||
|
lastMsgSameUser,
|
||||||
|
message,
|
||||||
|
sender,
|
||||||
|
}: MessageProps): JSX.Element => {
|
||||||
|
const { setPeerInfoOpen, setActivePeer } = useDevice();
|
||||||
|
|
||||||
|
const openPeer = (): void => {
|
||||||
|
setActivePeer(message.packet.from);
|
||||||
|
setPeerInfoOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return lastMsgSameUser ? (
|
||||||
|
<div className="flex ml-4">
|
||||||
|
{message.ack ? (
|
||||||
|
<CheckCircleIcon className="my-auto text-slate-200 h-4" />
|
||||||
|
) : (
|
||||||
|
<EllipsisHorizontalCircleIcon className="my-auto text-slate-200 h-4" />
|
||||||
|
)}
|
||||||
|
{"waypointID" in message ? (
|
||||||
|
<WaypointMessage waypointID={message.waypointID} />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`ml-4 pl-2 border-l-2 border-l-slate-200 ${
|
||||||
|
message.ack ? "text-black" : "text-slate-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mx-4 gap-2 mt-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="cursor-pointer w-6" onClick={openPeer}>
|
||||||
|
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer font-medium text-slate-700"
|
||||||
|
onClick={openPeer}
|
||||||
|
>
|
||||||
|
{sender?.user?.longName ?? "UNK"}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm">
|
||||||
|
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{message.ack ? (
|
||||||
|
<CheckCircleIcon className="my-auto text-slate-200 h-4" />
|
||||||
|
) : (
|
||||||
|
<EllipsisHorizontalCircleIcon className="my-auto text-slate-200 h-4" />
|
||||||
|
)}
|
||||||
|
{"waypointID" in message ? (
|
||||||
|
<WaypointMessage waypointID={message.waypointID} />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`ml-4 pl-2 border-l-2 border-l-slate-200 ${
|
||||||
|
message.ack ? "text-black" : "text-slate-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+10
-16
@@ -1,13 +1,8 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import {
|
import { Input } from "@app/components/form/Input.js";
|
||||||
Button,
|
import { Select } from "@app/components/form/Select.js";
|
||||||
majorScale,
|
import { Button } from "@components/Button.js";
|
||||||
Pane,
|
|
||||||
SelectField,
|
|
||||||
TextInputField,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
@@ -22,20 +17,19 @@ export const NewLocationMessage = (): JSX.Element => {
|
|||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane width={240} margin={majorScale(2)}>
|
<div className="w-96 m-4">
|
||||||
<form
|
<form
|
||||||
onSubmit={(e): void => {
|
onSubmit={(e): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextInputField label="Name" />
|
<Input label="Name" />
|
||||||
<TextInputField label="Description" />
|
<Input label="Description" />
|
||||||
<SelectField label="Type" value={LocationType.MGRS}>
|
<Select label="Type" value={LocationType.MGRS}>
|
||||||
{renderOptions(LocationType)}
|
{renderOptions(LocationType)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<TextInputField label="Coordinates" />
|
<Input label="Coordinates" />
|
||||||
<Button
|
<Button
|
||||||
width="100%"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void connection?.sendWaypoint(
|
void connection?.sendWaypoint(
|
||||||
Protobuf.Waypoint.create({
|
Protobuf.Waypoint.create({
|
||||||
@@ -50,6 +44,6 @@ export const NewLocationMessage = (): JSX.Element => {
|
|||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
import { toMGRS } from "@core/utils/toMGRS.js";
|
||||||
|
import { MapPinIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface WaypointMessageProps {
|
||||||
|
waypointID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WaypointMessage = ({
|
||||||
|
waypointID,
|
||||||
|
}: WaypointMessageProps): JSX.Element => {
|
||||||
|
const { waypoints } = useDevice();
|
||||||
|
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ml-4 pl-2 border-l-slate-200 border-l-2">
|
||||||
|
<div className="gap-2 flex rounded-md p-2 shadow-md shadow-orange-300">
|
||||||
|
<MapPinIcon className="m-auto w-6 text-slate-600" />
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="font-bold">{waypoint?.name}</div>
|
||||||
|
<span className="text-sm font-mono text-slate-500">
|
||||||
|
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">{waypoint?.description}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Select } from "@app/components/form/Select.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
|
import { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -52,57 +54,60 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Canned Message Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "Canned Message"]}
|
||||||
description="This is a description."
|
reset={() => reset(moduleConfig.cannedMessage)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<FormField
|
checked={value}
|
||||||
label="Rotary Encoder #1 Enabled"
|
{...rest}
|
||||||
description="This is a description."
|
/>
|
||||||
isInvalid={!!errors.rotary1Enabled?.message}
|
)}
|
||||||
validationMessage={errors.rotary1Enabled?.message}
|
/>
|
||||||
>
|
<Controller
|
||||||
<Controller
|
name="rotary1Enabled"
|
||||||
name="rotary1Enabled"
|
control={control}
|
||||||
control={control}
|
render={({ field: { value, ...rest } }) => (
|
||||||
render={({ field: { value, ...field } }) => (
|
<Toggle
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
label="Rotary Encoder #1 Enabled"
|
||||||
)}
|
description="Description"
|
||||||
/>
|
checked={value}
|
||||||
</FormField>
|
{...rest}
|
||||||
<TextInputField
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
label="Encoder Pin A"
|
label="Encoder Pin A"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
{...register("inputbrokerPinA", { valueAsNumber: true })}
|
{...register("inputbrokerPinA", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Encoder Pin B"
|
label="Encoder Pin B"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
{...register("inputbrokerPinB", { valueAsNumber: true })}
|
{...register("inputbrokerPinB", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="Endoer Pin Press"
|
label="Endoer Pin Press"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
type="number"
|
type="number"
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
{...register("inputbrokerPinPress", { valueAsNumber: true })}
|
{...register("inputbrokerPinPress", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<Select
|
||||||
label="Clockwise event"
|
label="Clockwise event"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
@@ -111,8 +116,8 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
{renderOptions(
|
{renderOptions(
|
||||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||||
)}
|
)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<SelectField
|
<Select
|
||||||
label="Counter Clockwise event"
|
label="Counter Clockwise event"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
@@ -121,8 +126,8 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
{renderOptions(
|
{renderOptions(
|
||||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||||
)}
|
)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<SelectField
|
<Select
|
||||||
label="Press event"
|
label="Press event"
|
||||||
description="This is a description."
|
description="This is a description."
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
@@ -131,41 +136,37 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
{renderOptions(
|
{renderOptions(
|
||||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||||
)}
|
)}
|
||||||
</SelectField>
|
</Select>
|
||||||
<FormField
|
<Controller
|
||||||
label="Up Down enabled"
|
name="updown1Enabled"
|
||||||
description="This is a description."
|
control={control}
|
||||||
isInvalid={!!errors.updown1Enabled?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.updown1Enabled?.message}
|
<Toggle
|
||||||
>
|
label="Up Down enabled"
|
||||||
<Controller
|
description="Description"
|
||||||
name="updown1Enabled"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Input
|
||||||
</FormField>
|
|
||||||
<TextInputField
|
|
||||||
label="Allow Input Source"
|
label="Allow Input Source"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={moduleEnabled}
|
disabled={moduleEnabled}
|
||||||
{...register("allowInputSource")}
|
{...register("allowInputSource")}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Send Bell"
|
name="sendBell"
|
||||||
description="This is a description."
|
control={control}
|
||||||
isInvalid={!!errors.sendBell?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.sendBell?.message}
|
<Toggle
|
||||||
>
|
label="Send Bell"
|
||||||
<Controller
|
description="Description"
|
||||||
name="sendBell"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
|
import { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -50,32 +51,37 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="External Notification Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "External Notification"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.externalNotification)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<TextInputField
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Output MS"
|
label="Output MS"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
hint="ms"
|
suffix="ms"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("outputMs", {
|
{...register("outputMs", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Output"
|
label="Output"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -84,51 +90,42 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Active"
|
name="active"
|
||||||
description="Description"
|
control={control}
|
||||||
disabled={!moduleEnabled}
|
render={({ field: { value, ...rest } }) => (
|
||||||
isInvalid={!!errors.active?.message}
|
<Toggle
|
||||||
validationMessage={errors.active?.message}
|
label="Active"
|
||||||
>
|
description="Description"
|
||||||
<Controller
|
checked={value}
|
||||||
name="active"
|
{...rest}
|
||||||
control={control}
|
/>
|
||||||
render={({ field: { value, ...field } }) => (
|
)}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
/>
|
||||||
)}
|
<Controller
|
||||||
/>
|
name="alertMessage"
|
||||||
</FormField>
|
control={control}
|
||||||
<FormField
|
render={({ field: { value, ...rest } }) => (
|
||||||
label="Message"
|
<Toggle
|
||||||
description="Description"
|
label="Message"
|
||||||
disabled={!moduleEnabled}
|
description="Description"
|
||||||
isInvalid={!!errors.alertMessage?.message}
|
checked={value}
|
||||||
validationMessage={errors.alertMessage?.message}
|
{...rest}
|
||||||
>
|
/>
|
||||||
<Controller
|
)}
|
||||||
name="alertMessage"
|
/>
|
||||||
control={control}
|
<Controller
|
||||||
render={({ field: { value, ...field } }) => (
|
name="alertBell"
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
control={control}
|
||||||
)}
|
render={({ field: { value, ...rest } }) => (
|
||||||
/>
|
<Toggle
|
||||||
</FormField>
|
label="Bell"
|
||||||
<FormField
|
description="Description"
|
||||||
label="Bell"
|
checked={value}
|
||||||
description="Description"
|
{...rest}
|
||||||
disabled={!moduleEnabled}
|
/>
|
||||||
isInvalid={!!errors.alertBell?.message}
|
)}
|
||||||
validationMessage={errors.alertBell?.message}
|
/>
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="alertBell"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { MQTTValidation } from "@app/validation/moduleConfig/mqtt.js";
|
import { MQTTValidation } from "@app/validation/moduleConfig/mqtt.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -50,34 +51,39 @@ export const MQTT = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="MQTT Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "MQTT"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.mqtt)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<TextInputField
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
label="MQTT Server Address"
|
label="MQTT Server Address"
|
||||||
description="Description"
|
description="Description"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("address")}
|
{...register("address")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="MQTT Username"
|
label="MQTT Username"
|
||||||
description="Description"
|
description="Description"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("username")}
|
{...register("username")}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
label="MQTT Password"
|
label="MQTT Password"
|
||||||
description="Description"
|
description="Description"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -85,36 +91,30 @@ export const MQTT = (): JSX.Element => {
|
|||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Encryption Enabled"
|
name="encryptionEnabled"
|
||||||
description="Description"
|
control={control}
|
||||||
disabled={!moduleEnabled}
|
render={({ field: { value, ...rest } }) => (
|
||||||
isInvalid={!!errors.encryptionEnabled?.message}
|
<Toggle
|
||||||
validationMessage={errors.encryptionEnabled?.message}
|
label="Encryption Enabled"
|
||||||
>
|
description="Description"
|
||||||
<Controller
|
checked={value}
|
||||||
name="encryptionEnabled"
|
{...rest}
|
||||||
control={control}
|
/>
|
||||||
render={({ field: { value, ...field } }) => (
|
)}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
/>
|
||||||
)}
|
<Controller
|
||||||
/>
|
name="jsonEnabled"
|
||||||
</FormField>
|
control={control}
|
||||||
<FormField
|
render={({ field: { value, ...rest } }) => (
|
||||||
label="JSON Output Enabled"
|
<Toggle
|
||||||
description="Description"
|
label="JSON Output Enabled"
|
||||||
disabled={!moduleEnabled}
|
description="Description"
|
||||||
isInvalid={!!errors.jsonEnabled?.message}
|
checked={value}
|
||||||
validationMessage={errors.jsonEnabled?.message}
|
{...rest}
|
||||||
>
|
/>
|
||||||
<Controller
|
)}
|
||||||
name="jsonEnabled"
|
/>
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
|
import { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -51,46 +52,48 @@ export const RangeTest = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Range Test Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "Range Test"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.rangeTest)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<TextInputField
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Message Interval"
|
label="Message Interval"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
{...register("sender", {
|
{...register("sender", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Save CSV to storage"
|
name="save"
|
||||||
description="Description"
|
control={control}
|
||||||
disabled={!moduleEnabled}
|
render={({ field: { value, ...rest } }) => (
|
||||||
isInvalid={!!errors.save?.message}
|
<Toggle
|
||||||
validationMessage={errors.save?.message}
|
label="Save CSV to storage"
|
||||||
>
|
description="Description"
|
||||||
<Controller
|
checked={value}
|
||||||
name="save"
|
{...rest}
|
||||||
control={control}
|
/>
|
||||||
render={({ field: { value, ...field } }) => (
|
)}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { SerialValidation } from "@app/validation/moduleConfig/serial.js";
|
import { SerialValidation } from "@app/validation/moduleConfig/serial.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -51,37 +52,39 @@ export const Serial = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Serial Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "Serial"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.serial)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<FormField
|
checked={value}
|
||||||
label="Echo"
|
{...rest}
|
||||||
description="Description"
|
/>
|
||||||
disabled={!moduleEnabled}
|
)}
|
||||||
isInvalid={!!errors.echo?.message}
|
/>
|
||||||
validationMessage={errors.echo?.message}
|
<Controller
|
||||||
>
|
name="echo"
|
||||||
<Controller
|
control={control}
|
||||||
name="echo"
|
render={({ field: { value, ...rest } }) => (
|
||||||
control={control}
|
<Toggle
|
||||||
render={({ field: { value, ...field } }) => (
|
label="Echo"
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
description="Description"
|
||||||
)}
|
checked={value}
|
||||||
/>
|
{...rest}
|
||||||
</FormField>
|
/>
|
||||||
<TextInputField
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="RX"
|
label="RX"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -90,7 +93,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="TX Pin"
|
label="TX Pin"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -99,7 +102,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Baud Rate"
|
label="Baud Rate"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -108,7 +111,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Timeout"
|
label="Timeout"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -117,7 +120,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Mode"
|
label="Mode"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
|
import { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -51,47 +52,49 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Store & Forward Config"
|
||||||
label="Module Enabled"
|
breadcrumbs={["Module Config", "Store & Forward"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.storeForward)}
|
||||||
isInvalid={!!errors.enabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.enabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="enabled"
|
<Controller
|
||||||
control={control}
|
name="enabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<FormField
|
checked={value}
|
||||||
label="Heartbeat Enabled"
|
{...rest}
|
||||||
description="Description"
|
/>
|
||||||
disabled={!moduleEnabled}
|
)}
|
||||||
isInvalid={!!errors.heartbeat?.message}
|
/>
|
||||||
validationMessage={errors.heartbeat?.message}
|
<Controller
|
||||||
>
|
name="heartbeat"
|
||||||
<Controller
|
control={control}
|
||||||
name="heartbeat"
|
render={({ field: { value, ...rest } }) => (
|
||||||
control={control}
|
<Toggle
|
||||||
render={({ field: { value, ...field } }) => (
|
label="Heartbeat Enabled"
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
description="Description"
|
||||||
)}
|
checked={value}
|
||||||
/>
|
{...rest}
|
||||||
</FormField>
|
/>
|
||||||
<TextInputField
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="Number of records"
|
label="Number of records"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
hint="Records"
|
suffix="Records"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("records", {
|
{...register("records", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="History return max"
|
label="History return max"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
@@ -100,7 +103,7 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextInputField
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
label="History return window"
|
label="History return window"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
|
import { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -44,58 +45,59 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
<Form
|
||||||
<FormField
|
title="Telemetry Config"
|
||||||
label="Measurement Enabled"
|
breadcrumbs={["Module Config", "Telemetry"]}
|
||||||
description="Description"
|
reset={() => reset(moduleConfig.telemetry)}
|
||||||
isInvalid={!!errors.environmentMeasurementEnabled?.message}
|
loading={loading}
|
||||||
validationMessage={errors.environmentMeasurementEnabled?.message}
|
dirty={isDirty}
|
||||||
>
|
onSubmit={onSubmit}
|
||||||
<Controller
|
>
|
||||||
name="environmentMeasurementEnabled"
|
<Controller
|
||||||
control={control}
|
name="environmentMeasurementEnabled"
|
||||||
render={({ field: { value, ...field } }) => (
|
control={control}
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
render={({ field: { value, ...rest } }) => (
|
||||||
)}
|
<Toggle
|
||||||
/>
|
label="Module Enabled"
|
||||||
</FormField>
|
description="Description"
|
||||||
<FormField
|
checked={value}
|
||||||
label="Displayed on Screen"
|
{...rest}
|
||||||
description="Description"
|
/>
|
||||||
isInvalid={!!errors.environmentScreenEnabled?.message}
|
)}
|
||||||
validationMessage={errors.environmentScreenEnabled?.message}
|
/>
|
||||||
>
|
<Controller
|
||||||
<Controller
|
name="environmentScreenEnabled"
|
||||||
name="environmentScreenEnabled"
|
control={control}
|
||||||
control={control}
|
render={({ field: { value, ...rest } }) => (
|
||||||
render={({ field: { value, ...field } }) => (
|
<Toggle
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
label="Displayed on Screen"
|
||||||
)}
|
description="Description"
|
||||||
/>
|
checked={value}
|
||||||
</FormField>
|
{...rest}
|
||||||
<TextInputField
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
label="Update Interval"
|
label="Update Interval"
|
||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
hint="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
{...register("environmentUpdateInterval", {
|
{...register("environmentUpdateInterval", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<Controller
|
||||||
label="Display Farenheit"
|
name="environmentDisplayFahrenheit"
|
||||||
description="Description"
|
control={control}
|
||||||
isInvalid={!!errors.environmentDisplayFahrenheit?.message}
|
render={({ field: { value, ...rest } }) => (
|
||||||
validationMessage={errors.environmentDisplayFahrenheit?.message}
|
<Toggle
|
||||||
>
|
label="Display Farenheit"
|
||||||
<Controller
|
description="Description"
|
||||||
name="environmentDisplayFahrenheit"
|
checked={value}
|
||||||
control={control}
|
{...rest}
|
||||||
render={({ field: { value, ...field } }) => (
|
/>
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
import type { Page } from "@app/core/stores/deviceStore.js";
|
||||||
|
import {
|
||||||
|
BeakerIcon,
|
||||||
|
Cog8ToothIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
InboxIcon,
|
||||||
|
MapIcon,
|
||||||
|
Square3Stack3DIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export const PageNav = (): JSX.Element => {
|
||||||
|
const { activePage, setActivePage } = useDevice();
|
||||||
|
|
||||||
|
interface NavLink {
|
||||||
|
name: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
page: Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages: NavLink[] = [
|
||||||
|
{
|
||||||
|
name: "Messages",
|
||||||
|
icon: <InboxIcon />,
|
||||||
|
page: "messages",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map",
|
||||||
|
icon: <MapIcon />,
|
||||||
|
page: "map",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Extensions",
|
||||||
|
icon: <BeakerIcon />,
|
||||||
|
page: "extensions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config",
|
||||||
|
icon: <Cog8ToothIcon />,
|
||||||
|
page: "config",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Channels",
|
||||||
|
icon: <Square3Stack3DIcon />,
|
||||||
|
page: "channels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Info",
|
||||||
|
icon: <IdentificationIcon />,
|
||||||
|
page: "info",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex bg-slate-50 w-12 items-center whitespace-nowrap py-4 text-sm [writing-mode:vertical-rl] h-full border-r border-slate-200 flex-shrink-0">
|
||||||
|
<span className="mt-6 flex gap-4 font-bold text-slate-500">
|
||||||
|
{pages.map((Link) => (
|
||||||
|
<div
|
||||||
|
key={Link.name}
|
||||||
|
onClick={() => {
|
||||||
|
setActivePage(Link.page);
|
||||||
|
}}
|
||||||
|
className={`w-8 h-8 p-1 border-2 rounded-md hover:border-orange-300 cursor-pointer ${
|
||||||
|
Link.page === activePage
|
||||||
|
? "border-orange-400"
|
||||||
|
: "border-slate-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Link.icon}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
ResetIcon,
|
|
||||||
Spinner,
|
|
||||||
StatusIndicator,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
|
|
||||||
export const Progress = (): JSX.Element => {
|
|
||||||
const {
|
|
||||||
hardware,
|
|
||||||
channels,
|
|
||||||
config,
|
|
||||||
moduleConfig,
|
|
||||||
setReady,
|
|
||||||
nodes,
|
|
||||||
connection,
|
|
||||||
} = useDevice();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
hardware.myNodeNum !== 0 &&
|
|
||||||
Object.keys(config).length === 7 &&
|
|
||||||
Object.keys(moduleConfig).length === 7 &&
|
|
||||||
channels.length === hardware.maxChannels
|
|
||||||
) {
|
|
||||||
setReady(true);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
config,
|
|
||||||
moduleConfig,
|
|
||||||
channels,
|
|
||||||
hardware.maxChannels,
|
|
||||||
hardware.myNodeNum,
|
|
||||||
setReady,
|
|
||||||
]);
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
flexGrow={1}
|
|
||||||
margin={majorScale(3)}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
elevation={1}
|
|
||||||
background="white"
|
|
||||||
>
|
|
||||||
<Pane display="flex" margin="auto" gap={majorScale(6)}>
|
|
||||||
<Pane
|
|
||||||
marginY="auto"
|
|
||||||
display="flex"
|
|
||||||
height="72px"
|
|
||||||
width="72px"
|
|
||||||
minWidth="72px"
|
|
||||||
backgroundColor="#F8E3DA"
|
|
||||||
borderRadius="50%"
|
|
||||||
>
|
|
||||||
<Spinner height="32px" width="32px" margin="auto" />
|
|
||||||
</Pane>
|
|
||||||
<Pane>
|
|
||||||
<Pane display="flex" flexDirection="column">
|
|
||||||
<StatusIndicator
|
|
||||||
color={hardware.myNodeNum !== 0 ? "success" : "disabled"}
|
|
||||||
>
|
|
||||||
Device Info
|
|
||||||
</StatusIndicator>
|
|
||||||
<StatusIndicator color={nodes.length ? "success" : "disabled"}>
|
|
||||||
Peers ({nodes.length})
|
|
||||||
</StatusIndicator>
|
|
||||||
<StatusIndicator
|
|
||||||
color={Object.keys(config).length === 7 ? "success" : "disabled"}
|
|
||||||
>
|
|
||||||
Device Config {`(${Object.keys(config).length - 1} / 6)`}
|
|
||||||
</StatusIndicator>
|
|
||||||
<StatusIndicator
|
|
||||||
color={
|
|
||||||
Object.keys(moduleConfig).length === 7 ? "success" : "disabled"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Module Config {`(${Object.keys(moduleConfig).length - 1} / 6)`}
|
|
||||||
</StatusIndicator>
|
|
||||||
<StatusIndicator
|
|
||||||
color={
|
|
||||||
channels.length > 0 && channels.length === hardware.maxChannels
|
|
||||||
? "success"
|
|
||||||
: "disabled"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Channels{" "}
|
|
||||||
{hardware.myNodeNum !== 0 &&
|
|
||||||
`(${channels.length} / ${hardware.maxChannels})`}
|
|
||||||
</StatusIndicator>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
void connection?.configure();
|
|
||||||
}}
|
|
||||||
iconBefore={ResetIcon}
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
|
import { Types } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
import { ConfiguringWidget } from "./Widgets/ConfiguringWidget.js";
|
||||||
|
import { DeviceWidget } from "./Widgets/DeviceWidget.js";
|
||||||
|
import { NodeInfoWidget } from "./Widgets/NodeInfoWidget.js";
|
||||||
|
import { PeersWidget } from "./Widgets/PeersWidget.js";
|
||||||
|
import { PositionWidget } from "./Widgets/PositionWidget.js";
|
||||||
|
|
||||||
|
export const Sidebar = (): JSX.Element => {
|
||||||
|
const { removeDevice } = useDeviceStore();
|
||||||
|
const { connection, hardware, nodes, status } = useDevice();
|
||||||
|
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col relative bg-slate-50 w-80 p-2 border-x border-slate-200 gap-2 flex-shrink-0">
|
||||||
|
<DeviceWidget
|
||||||
|
name={
|
||||||
|
nodes.find((n) => n.data.num === hardware.myNodeNum)?.data.user
|
||||||
|
?.longName ?? "UNK"
|
||||||
|
}
|
||||||
|
nodeNum={hardware.myNodeNum.toString()}
|
||||||
|
disconnected={status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED}
|
||||||
|
disconnect={() => {
|
||||||
|
void connection?.disconnect();
|
||||||
|
setSelectedDevice(0);
|
||||||
|
removeDevice(selectedDevice ?? 0);
|
||||||
|
}}
|
||||||
|
reconnect={() => {
|
||||||
|
console.log("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div className="text-left">
|
||||||
|
<p className="text-xl font-bold text-slate-900">
|
||||||
|
<a href="/">Their Side</a>
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-font-medium leading-8 text-slate-700">
|
||||||
|
Conversations with the most tragically misunderstood people of our
|
||||||
|
time.
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* */}
|
||||||
|
{/* */}
|
||||||
|
{/* */}
|
||||||
|
{/* */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900">Information</h3>
|
||||||
|
<dl className="mt-2 divide-y divide-gray-200 border-t border-b border-gray-200">
|
||||||
|
<div className="flex justify-between py-3 text-sm font-medium">
|
||||||
|
<dt className="text-gray-500">Firmware version</dt>
|
||||||
|
<dd className="whitespace-nowrap text-gray-900 hover:underline hover:text-orange-400 cursor-pointer">
|
||||||
|
{hardware.firmwareVersion}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div className="flex justify-between py-3 text-sm font-medium">
|
||||||
|
<dt className="text-gray-500">Bitrate</dt>
|
||||||
|
<dd className="whitespace-nowrap text-gray-900">
|
||||||
|
{hardware.bitrate.toFixed(2)}
|
||||||
|
<span className="font-mono text-slate-500 text-sm ">bps</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeInfoWidget />
|
||||||
|
{/* <BatteryWidget /> */}
|
||||||
|
<PeersWidget />
|
||||||
|
<PositionWidget />
|
||||||
|
|
||||||
|
<ConfiguringWidget />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Heading,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
Paragraph,
|
|
||||||
SideSheet,
|
|
||||||
Tab,
|
|
||||||
Tablist,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { FiBluetooth, FiTerminal, FiWifi } from "react-icons/fi";
|
|
||||||
|
|
||||||
import type { TabType } from "@components/layout/page/TabbedContent.js";
|
|
||||||
import { BLE } from "@components/SlideSheets/tabs/connect/BLE.js";
|
|
||||||
import { HTTP } from "@components/SlideSheets/tabs/connect/HTTP.js";
|
|
||||||
import { Serial } from "@components/SlideSheets/tabs/connect/Serial.js";
|
|
||||||
|
|
||||||
export interface NewDeviceProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CloseProps {
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type connType = "http" | "ble" | "serial";
|
|
||||||
|
|
||||||
export interface ConnTab extends Omit<TabType, "element"> {
|
|
||||||
connType: connType;
|
|
||||||
element: ({ close }: CloseProps) => JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NewDevice = ({ open, onClose }: NewDeviceProps) => {
|
|
||||||
const [selectedConnType, setSelectedConnType] = useState<connType>("ble");
|
|
||||||
|
|
||||||
const tabs: ConnTab[] = [
|
|
||||||
{
|
|
||||||
connType: "ble",
|
|
||||||
icon: FiBluetooth,
|
|
||||||
name: "BLE",
|
|
||||||
element: BLE,
|
|
||||||
disabled: !navigator.bluetooth,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
connType: "http",
|
|
||||||
icon: FiWifi,
|
|
||||||
name: "HTTP",
|
|
||||||
element: HTTP,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
connType: "serial",
|
|
||||||
icon: FiTerminal,
|
|
||||||
name: "Serial",
|
|
||||||
element: Serial,
|
|
||||||
disabled: !navigator.serial,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SideSheet
|
|
||||||
isShown={open}
|
|
||||||
onCloseComplete={onClose}
|
|
||||||
containerProps={{
|
|
||||||
display: "flex",
|
|
||||||
flex: "1",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pane zIndex={1} flexShrink={0} elevation={1} backgroundColor="white">
|
|
||||||
<Pane padding={16} borderBottom="muted">
|
|
||||||
<Heading size={600}>Connect new device</Heading>
|
|
||||||
<Paragraph size={400} color="muted">
|
|
||||||
Optional description or sub title
|
|
||||||
</Paragraph>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" padding={8}>
|
|
||||||
<Tablist>
|
|
||||||
{tabs.map((TabData, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
gap={5}
|
|
||||||
isSelected={selectedConnType === TabData.connType}
|
|
||||||
onSelect={() => setSelectedConnType(TabData.connType)}
|
|
||||||
disabled={TabData.disabled}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<TabData.icon />
|
|
||||||
{TabData.name}
|
|
||||||
</>
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" overflowY="scroll" background="tint1" padding={16}>
|
|
||||||
{tabs.map((TabData, index) => (
|
|
||||||
<Pane
|
|
||||||
key={index}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
backgroundColor="white"
|
|
||||||
elevation={1}
|
|
||||||
flexGrow={1}
|
|
||||||
display={selectedConnType === TabData.connType ? "block" : "none"}
|
|
||||||
>
|
|
||||||
{!TabData.disabled && <TabData.element close={onClose} />}
|
|
||||||
</Pane>
|
|
||||||
))}
|
|
||||||
</Pane>
|
|
||||||
</SideSheet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { GeolocationIcon, Pane, PropertyIcon, SideSheet } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { SlideSheetTabbedContent } from "@components/layout/page/SlideSheetTabbedContent.js";
|
|
||||||
import type { TabType } from "@components/layout/page/TabbedContent.js";
|
|
||||||
import { Location } from "@components/SlideSheets/tabs/nodes/Location.js";
|
|
||||||
import { Overview } from "@components/SlideSheets/tabs/nodes/Overview.js";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import type { Node } from "@core/stores/deviceStore.js";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export const PeerInfo = () => {
|
|
||||||
const { peerInfoOpen, activePeer, setPeerInfoOpen, nodes } = useDevice();
|
|
||||||
const [node, setNode] = useState<Node | undefined>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNode(nodes.find((n) => n.data.num === activePeer));
|
|
||||||
}, [nodes, activePeer]);
|
|
||||||
|
|
||||||
const tabs: TabType[] = [
|
|
||||||
{
|
|
||||||
name: "Info",
|
|
||||||
icon: PropertyIcon,
|
|
||||||
element: () => <Overview node={node} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Location",
|
|
||||||
icon: GeolocationIcon,
|
|
||||||
element: () => <Location node={node} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SideSheet
|
|
||||||
isShown={peerInfoOpen}
|
|
||||||
onCloseComplete={() => {
|
|
||||||
setPeerInfoOpen(false);
|
|
||||||
}}
|
|
||||||
containerProps={{
|
|
||||||
display: "flex",
|
|
||||||
flex: "1",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SlideSheetTabbedContent
|
|
||||||
heading={node?.data.user?.longName ?? "UNK"}
|
|
||||||
description={Protobuf.HardwareModel[node?.data.user?.hwModel ?? 0]}
|
|
||||||
tabs={tabs}
|
|
||||||
tabIcon={
|
|
||||||
<Pane marginY="auto">
|
|
||||||
<Hashicon size={32} value={(node?.data.num ?? 0).toString()} />
|
|
||||||
</Pane>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SideSheet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
import JSONPretty from "react-json-pretty";
|
|
||||||
|
|
||||||
import type { Node } from "@core/stores/deviceStore.js";
|
|
||||||
|
|
||||||
export interface LocationProps {
|
|
||||||
node?: Node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Location = ({ node }: LocationProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Pane>
|
|
||||||
<JSONPretty data={node?.data.position} />
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
import JSONPretty from "react-json-pretty";
|
|
||||||
|
|
||||||
import type { Node } from "@core/stores/deviceStore.js";
|
|
||||||
|
|
||||||
export interface OverviewProps {
|
|
||||||
node?: Node;
|
|
||||||
}
|
|
||||||
export const Overview = ({ node }: OverviewProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Pane>
|
|
||||||
<JSONPretty data={node?.data.user} />
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
|
||||||
|
export const ConfiguringWidget = (): JSX.Element => {
|
||||||
|
const {
|
||||||
|
hardware,
|
||||||
|
channels,
|
||||||
|
config,
|
||||||
|
moduleConfig,
|
||||||
|
setReady,
|
||||||
|
nodes,
|
||||||
|
connection,
|
||||||
|
} = useDevice();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
hardware.myNodeNum !== 0 &&
|
||||||
|
Object.keys(config).length === 7 &&
|
||||||
|
Object.keys(moduleConfig).length === 7 &&
|
||||||
|
channels.length === hardware.maxChannels
|
||||||
|
) {
|
||||||
|
setReady(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
config,
|
||||||
|
moduleConfig,
|
||||||
|
channels,
|
||||||
|
hardware.maxChannels,
|
||||||
|
hardware.myNodeNum,
|
||||||
|
setReady,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col rounded-2xl mb-4 text-sm space-y-3 bg-[#f9e3aa] text-black">
|
||||||
|
<p className="text-xl font-bold">Connecting to device</p>
|
||||||
|
<ol className="flex flex-col overflow-hidden gap-3">
|
||||||
|
<StatusIndicator
|
||||||
|
title="Device Info"
|
||||||
|
current={hardware.myNodeNum ? 1 : 0}
|
||||||
|
total={0}
|
||||||
|
/>
|
||||||
|
<StatusIndicator title="Peers" current={nodes.length} total={0} />
|
||||||
|
<StatusIndicator
|
||||||
|
title="Device Config"
|
||||||
|
current={Object.keys(config).length - 1}
|
||||||
|
total={6}
|
||||||
|
/>
|
||||||
|
<StatusIndicator
|
||||||
|
title="Module Config"
|
||||||
|
current={Object.keys(moduleConfig).length - 1}
|
||||||
|
total={6}
|
||||||
|
/>
|
||||||
|
<StatusIndicator
|
||||||
|
title="Channels"
|
||||||
|
current={channels.length}
|
||||||
|
total={hardware.maxChannels ?? 0}
|
||||||
|
/>
|
||||||
|
</ol>
|
||||||
|
<div
|
||||||
|
className="mt-2 rounded-md bg-[#dabb6b] p-1 ring-[#f9e3aa] cursor-pointer text-center"
|
||||||
|
onClick={() => {
|
||||||
|
void connection?.configure();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StatusIndicatorProps {
|
||||||
|
title: string;
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusIndicator = ({
|
||||||
|
title,
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
}: StatusIndicatorProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<li className="relative">
|
||||||
|
<div
|
||||||
|
className={`absolute top-4 left-2.5 -ml-px h-full w-0.5 ${
|
||||||
|
current >= total ? "bg-green-500" : "bg-[#f9e3aa]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<div
|
||||||
|
className={`flex relative z-10 h-5 w-5 rounded-full border-2 ${
|
||||||
|
current === 0
|
||||||
|
? "border-[#dabb6b] bg-[#f9e3aa]"
|
||||||
|
: current >= total
|
||||||
|
? "bg-green-500 border-green-500"
|
||||||
|
: "bg-[#f9e3aa] border-green-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`m-auto h-1.5 w-1.5 rounded-full ${
|
||||||
|
current > 0 ? "bg-green-500" : "bg-[#f9e3aa]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="flex text-sm ml-4 gap-1">
|
||||||
|
<span className="font-medium">{title}</span>
|
||||||
|
<span className="font-mono text-slate-500">
|
||||||
|
({current}
|
||||||
|
{total !== 0 && `/${total}`})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
|
import { XCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import { Button } from "../Button.js";
|
||||||
|
|
||||||
|
export interface DeviceWidgetProps {
|
||||||
|
name: string;
|
||||||
|
nodeNum: string;
|
||||||
|
disconnected: boolean;
|
||||||
|
disconnect: () => void;
|
||||||
|
reconnect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeviceWidget = ({
|
||||||
|
name,
|
||||||
|
nodeNum,
|
||||||
|
disconnected,
|
||||||
|
disconnect,
|
||||||
|
reconnect,
|
||||||
|
}: DeviceWidgetProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="relative rounded-xl bg-emerald-400 overflow-hidden">
|
||||||
|
<div className="absolute w-full h-full bottom-20">
|
||||||
|
<Hashicon size={350} value={nodeNum} />
|
||||||
|
</div>
|
||||||
|
<div className="flex backdrop-blur-md backdrop-brightness-50 backdrop-hue-rotate-30 p-3">
|
||||||
|
<div className="drop-shadow-md">
|
||||||
|
<Hashicon size={96} value={nodeNum} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col">
|
||||||
|
<span className="font-bold text-slate-200 ml-auto text-xl whitespace-nowrap">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<div className="ml-auto my-auto">
|
||||||
|
<Button
|
||||||
|
onClick={disconnected ? reconnect : disconnect}
|
||||||
|
variant={disconnected ? "secondary" : "primary"}
|
||||||
|
size="sm"
|
||||||
|
iconAfter={<XCircleIcon className="h-4" />}
|
||||||
|
>
|
||||||
|
{disconnected ? "Reconnect" : "Disconnect"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export interface NodeInfoWidgetProps {}
|
||||||
|
|
||||||
|
export const NodeInfoWidget = ({}: NodeInfoWidgetProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col rounded-2xl mb-4 text-sm space-y-3 bg-[#f9e3aa] text-black">
|
||||||
|
node info
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export interface PeersWidgetProps {}
|
||||||
|
|
||||||
|
export const PeersWidget = ({}: PeersWidgetProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col rounded-2xl mb-4 text-sm space-y-3 bg-[#f9e3aa] text-black">
|
||||||
|
Peers
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export interface PositionWidgetProps {}
|
||||||
|
|
||||||
|
export const PositionWidget = ({}: PositionWidgetProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col rounded-2xl mb-4 text-sm space-y-3 bg-[#f9e3aa] text-black">
|
||||||
|
position
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { forwardRef, InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
options?: string[];
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
action?: {
|
||||||
|
icon: JSX.Element;
|
||||||
|
action: () => void;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
function Input(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
options,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
action,
|
||||||
|
error,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: CheckboxProps,
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-start">
|
||||||
|
<div className="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="comments" className="font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<p id="comments-description" className="text-gray-500">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { HTMLProps } from "react";
|
import type { HTMLProps } from "react";
|
||||||
|
|
||||||
import { Button, majorScale, Pane, SavedIcon, Spinner } from "evergreen-ui";
|
import { FiSave } from "react-icons/fi";
|
||||||
|
|
||||||
|
import { Button } from "@components/Button.js";
|
||||||
|
import {
|
||||||
|
ArrowPathIcon,
|
||||||
|
ArrowUturnLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
HomeIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export interface FormProps extends HTMLProps<HTMLFormElement> {
|
export interface FormProps extends HTMLProps<HTMLFormElement> {
|
||||||
|
title: string;
|
||||||
|
breadcrumbs: string[];
|
||||||
|
reset: () => void;
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Form = ({
|
export const Form = ({
|
||||||
|
title,
|
||||||
|
breadcrumbs,
|
||||||
|
reset,
|
||||||
loading,
|
loading,
|
||||||
dirty,
|
dirty,
|
||||||
children,
|
children,
|
||||||
@@ -18,31 +32,48 @@ export const Form = ({
|
|||||||
}: FormProps): JSX.Element => {
|
}: FormProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
<form onSubmit={onSubmit} style={{ position: "relative" }} {...props}>
|
<form className="w-full" onSubmit={onSubmit} {...props}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<Pane
|
<div className="absolute flex w-full h-full bg-slate-600 rounded-md z-10">
|
||||||
position="absolute"
|
<ArrowPathIcon className="h-8 animate-spin m-auto" />
|
||||||
display="flex"
|
</div>
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
backgroundColor="rgba(67, 90, 111, 0.2)"
|
|
||||||
zIndex={10}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
>
|
|
||||||
<Spinner margin="auto" />
|
|
||||||
</Pane>
|
|
||||||
)}
|
)}
|
||||||
{children}
|
<div className="select-none rounded-md p-4 bg-gray-700">
|
||||||
<Pane display="flex" marginTop={majorScale(2)}>
|
<ol className="flex gap-4">
|
||||||
<Button
|
<li className="text-gray-400 hover:text-gray-200 cursor-pointer">
|
||||||
type="submit"
|
<HomeIcon className="h-5 w-5 flex-shrink-0 text-gray-400" />
|
||||||
marginLeft="auto"
|
</li>
|
||||||
disabled={!dirty}
|
{breadcrumbs.map((breadcrumb, index) => (
|
||||||
iconBefore={<SavedIcon />}
|
<li key={index} className="flex gap-4">
|
||||||
>
|
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 text-gray-500" />
|
||||||
Save
|
<span className="text-sm font-medium text-gray-400 hover:text-gray-200 cursor-pointer">
|
||||||
</Button>
|
{breadcrumb}
|
||||||
</Pane>
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<div className="mt-2 flex items-center">
|
||||||
|
<h2 className="font-bold text-white truncate text-3xl tracking-tight">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-2 ml-auto">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
iconBefore={<ArrowUturnLeftIcon className="w-4" />}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button disabled={!dirty} iconBefore={<FiSave className="w-4" />}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-2 gap-3">{children}</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { forwardRef, InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
action?: {
|
||||||
|
icon: JSX.Element;
|
||||||
|
action: () => void;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||||
|
{ label, description, prefix, suffix, action, error, ...rest }: InputProps,
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Label */}
|
||||||
|
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||||
|
{/* */}
|
||||||
|
<div className="relative flex rounded-md shadow-sm">
|
||||||
|
{prefix && (
|
||||||
|
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm">
|
||||||
|
{prefix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className={`block w-full min-w-0 flex-1 rounded-md border border-gray-300 px-3 h-10 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
|
||||||
|
prefix ? "rounded-l-none" : ""
|
||||||
|
} ${action ? "rounded-r-none" : ""}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{suffix && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
|
<span className="text-gray-500 sm:text-sm" id="price-currency">
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{action && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={action.action}
|
||||||
|
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
{/* <span>Sort</span> */}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
|
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { forwardRef, SelectHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
options?: string[];
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
action?: {
|
||||||
|
icon: JSX.Element;
|
||||||
|
action: () => void;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
options,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
action,
|
||||||
|
error,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: SelectProps,
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="location"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="flex">
|
||||||
|
<select
|
||||||
|
ref={ref}
|
||||||
|
className={`block w-full min-w-0 flex-1 rounded-md border border-gray-300 px-3 py-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
|
||||||
|
prefix ? "rounded-l-none" : ""
|
||||||
|
} ${action ? "rounded-r-none" : ""}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{options &&
|
||||||
|
options.map((option, index) => (
|
||||||
|
<option key={index}>{option}</option>
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
{action && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={action.action}
|
||||||
|
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
export interface ToggleProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
}: ToggleProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Switch.Group as="div" className="flex items-center justify-between">
|
||||||
|
<span className="flex flex-grow flex-col">
|
||||||
|
<Switch.Label
|
||||||
|
as="span"
|
||||||
|
className="text-sm font-medium text-gray-900"
|
||||||
|
passive
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Switch.Label>
|
||||||
|
<Switch.Description as="span" className="text-sm text-gray-500">
|
||||||
|
{description}
|
||||||
|
</Switch.Description>
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
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-indigo-500 focus:ring-offset-2 ${
|
||||||
|
checked ? "bg-indigo-600" : "bg-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
checked ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { majorScale, Pane } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
|
||||||
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
|
||||||
|
|
||||||
import { NoDevice } from "../misc/NoDevice.js";
|
|
||||||
import { Progress } from "../Progress.js";
|
|
||||||
import { PeerInfo } from "../SlideSheets/PeerInfo.js";
|
|
||||||
import { Header } from "./Header.js";
|
|
||||||
import { Sidebar } from "./Sidebar/index.js";
|
|
||||||
|
|
||||||
export interface AppLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppLayout = ({ children }: AppLayoutProps): JSX.Element => {
|
|
||||||
const { getDevices } = useDeviceStore();
|
|
||||||
const { selectedDevice } = useAppStore();
|
|
||||||
|
|
||||||
const devices = getDevices();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
width="100vw"
|
|
||||||
display="flex"
|
|
||||||
background="tint1"
|
|
||||||
flexDirection="column"
|
|
||||||
minHeight="100vh"
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
<Pane display="flex" height="100%" width="100%">
|
|
||||||
{devices.length ? (
|
|
||||||
devices.map((device) => (
|
|
||||||
<Pane
|
|
||||||
key={device.id}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
display={device.id === selectedDevice ? "grid" : "none"}
|
|
||||||
gap={majorScale(3)}
|
|
||||||
gridTemplateColumns="16rem 1fr"
|
|
||||||
>
|
|
||||||
<DeviceWrapper device={device}>
|
|
||||||
{device && device.ready ? (
|
|
||||||
<>
|
|
||||||
<Sidebar />
|
|
||||||
<PeerInfo />
|
|
||||||
<Pane height="100%" display="flex">
|
|
||||||
{children}
|
|
||||||
</Pane>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Pane
|
|
||||||
width="100%"
|
|
||||||
flexGrow={1}
|
|
||||||
margin={majorScale(3)}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
background="white"
|
|
||||||
elevation={1}
|
|
||||||
/>
|
|
||||||
<Progress />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DeviceWrapper>
|
|
||||||
</Pane>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<NoDevice />
|
|
||||||
)}
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
CrossIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
HelpIcon,
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
PlusIcon,
|
|
||||||
StatusIndicator,
|
|
||||||
Tab,
|
|
||||||
Tablist,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { FiGithub } from "react-icons/fi";
|
|
||||||
|
|
||||||
import { HelpDialog } from "@components/Dialog/HelpDialog.js";
|
|
||||||
import { NewDevice } from "@components/SlideSheets/NewDevice.js";
|
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Types } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export const Header = (): JSX.Element => {
|
|
||||||
const { getDevices, removeDevice } = useDeviceStore();
|
|
||||||
const [newConnectionOpen, setNewConnectionOpen] = useState(false);
|
|
||||||
const [helpDialogOpen, setHelpDialogOpen] = useState(false);
|
|
||||||
const { selectedDevice, setSelectedDevice } = useAppStore();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
is="nav"
|
|
||||||
width="100%"
|
|
||||||
position="sticky"
|
|
||||||
top={0}
|
|
||||||
backgroundColor="white"
|
|
||||||
zIndex={10}
|
|
||||||
height={majorScale(8)}
|
|
||||||
flexShrink={0}
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
borderBottom="muted"
|
|
||||||
>
|
|
||||||
<NewDevice
|
|
||||||
open={newConnectionOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setNewConnectionOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
width={majorScale(12)}
|
|
||||||
marginRight={majorScale(22)}
|
|
||||||
>
|
|
||||||
<Link href=".">
|
|
||||||
<Pane
|
|
||||||
is="img"
|
|
||||||
width={100}
|
|
||||||
height={28}
|
|
||||||
src="Logo_Black.svg"
|
|
||||||
cursor="pointer"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</Pane>
|
|
||||||
<Tablist display="flex" marginX={majorScale(4)}>
|
|
||||||
{getDevices().map((device) => (
|
|
||||||
<Tab
|
|
||||||
key={device.id}
|
|
||||||
gap={majorScale(1)}
|
|
||||||
isSelected={device.id === selectedDevice}
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedDevice(device.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Hashicon value={device.hardware.myNodeNum.toString()} size={20} />
|
|
||||||
{device.nodes.find((n) => n.data.num === device.hardware.myNodeNum)
|
|
||||||
?.data.user?.shortName ?? "UNK"}
|
|
||||||
<StatusIndicator
|
|
||||||
color={
|
|
||||||
[
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
|
||||||
].includes(device.status)
|
|
||||||
? "success"
|
|
||||||
: [
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
].includes(device.status)
|
|
||||||
? "warning"
|
|
||||||
: "danger"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
marginLeft="auto"
|
|
||||||
gap={majorScale(1)}
|
|
||||||
marginRight={majorScale(2)}
|
|
||||||
>
|
|
||||||
<Tooltip content="Connect new device">
|
|
||||||
<Button
|
|
||||||
display="inline-flex"
|
|
||||||
marginY="auto"
|
|
||||||
appearance="primary"
|
|
||||||
iconBefore={<PlusIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
setNewConnectionOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{getDevices().length !== 0 && (
|
|
||||||
<Tooltip content="Disconnect active device">
|
|
||||||
<Button
|
|
||||||
iconBefore={CrossIcon}
|
|
||||||
onClick={() => {
|
|
||||||
void getDevices()
|
|
||||||
.find((d) => d.id === selectedDevice)
|
|
||||||
?.connection?.disconnect();
|
|
||||||
removeDevice(selectedDevice ?? 0);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip content="Visit GitHub">
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/meshtastic/meshtastic-web"
|
|
||||||
>
|
|
||||||
<Button iconBefore={FiGithub}>
|
|
||||||
{process.env.COMMIT_HASH ?? "DEVELOPMENT"}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
<IconButton
|
|
||||||
icon={HelpIcon}
|
|
||||||
onClick={() => {
|
|
||||||
setHelpDialogOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<HelpDialog
|
|
||||||
isOpen={helpDialogOpen}
|
|
||||||
close={() => {
|
|
||||||
setHelpDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip content="Visit Meshtastic.org">
|
|
||||||
<Link target="_blank" href="https://meshtastic.org/">
|
|
||||||
<IconButton icon={GlobeIcon} />
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Heading,
|
|
||||||
Link,
|
|
||||||
majorScale,
|
|
||||||
MapMarkerIcon,
|
|
||||||
Pane,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { FiBluetooth, FiTerminal, FiWifi } from "react-icons/fi";
|
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import { toMGRS } from "@core/utils/toMGRS.js";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Types } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export const DeviceCard = (): JSX.Element => {
|
|
||||||
const { hardware, nodes, status, connection } = useDevice();
|
|
||||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
flexGrow={1}
|
|
||||||
flexDirection="column"
|
|
||||||
marginTop="auto"
|
|
||||||
gap={majorScale(1)}
|
|
||||||
>
|
|
||||||
<Pane display="flex" gap={majorScale(2)}>
|
|
||||||
<Hashicon value={hardware.myNodeNum.toString()} size={42} />
|
|
||||||
<Pane flexGrow={1}>
|
|
||||||
<Heading>{myNode?.data.user?.longName}</Heading>
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/meshtastic/meshtastic-device/releases/"
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
color="green"
|
|
||||||
width="100%"
|
|
||||||
marginRight={8}
|
|
||||||
display="flex"
|
|
||||||
marginTop={4}
|
|
||||||
>
|
|
||||||
{hardware.firmwareVersion}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<MapMarkerIcon />
|
|
||||||
<Badge
|
|
||||||
color={myNode?.data.position?.latitudeI ? "green" : "red"}
|
|
||||||
display="flex"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{toMGRS(
|
|
||||||
myNode?.data.position?.latitudeI,
|
|
||||||
myNode?.data.position?.longitudeI
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
{connection?.connType === "ble" && <FiBluetooth />}
|
|
||||||
{connection?.connType === "http" && <FiWifi />}
|
|
||||||
{connection?.connType === "serial" && <FiTerminal />}
|
|
||||||
<Badge
|
|
||||||
color={
|
|
||||||
[
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
|
||||||
].includes(status)
|
|
||||||
? "green"
|
|
||||||
: [
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
].includes(status)
|
|
||||||
? "orange"
|
|
||||||
: "red"
|
|
||||||
}
|
|
||||||
display="flex"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
|
||||||
].includes(status)
|
|
||||||
? "Connected"
|
|
||||||
: [
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
|
||||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
|
||||||
].includes(status)
|
|
||||||
? "Connecting"
|
|
||||||
: "Disconnected"}
|
|
||||||
</Badge>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ArrayIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
IconComponent,
|
|
||||||
InboxIcon,
|
|
||||||
InfoSignIcon,
|
|
||||||
LabTestIcon,
|
|
||||||
LayersIcon,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
SettingsIcon,
|
|
||||||
Tab,
|
|
||||||
Tablist,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { PeersDialog } from "@components/Dialog/PeersDialog.js";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import type { Page } from "@core/stores/deviceStore.js";
|
|
||||||
|
|
||||||
import { DeviceCard } from "./DeviceCard.js";
|
|
||||||
|
|
||||||
interface NavLink {
|
|
||||||
name: string;
|
|
||||||
icon: IconComponent;
|
|
||||||
page: Page;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Sidebar = (): JSX.Element => {
|
|
||||||
const { activePage, setActivePage } = useDevice();
|
|
||||||
const [PeersDialogOpen, setPeersDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const navLinks: NavLink[] = [
|
|
||||||
{
|
|
||||||
name: "Messages",
|
|
||||||
icon: InboxIcon,
|
|
||||||
page: "messages",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Map",
|
|
||||||
icon: GlobeIcon,
|
|
||||||
page: "map",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Extensions",
|
|
||||||
icon: LabTestIcon,
|
|
||||||
page: "extensions",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Config",
|
|
||||||
icon: SettingsIcon,
|
|
||||||
page: "config",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Channels",
|
|
||||||
icon: LayersIcon,
|
|
||||||
page: "channels",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Info",
|
|
||||||
icon: InfoSignIcon,
|
|
||||||
page: "info",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
width="100%"
|
|
||||||
flexGrow={1}
|
|
||||||
margin={majorScale(3)}
|
|
||||||
padding={majorScale(2)}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
background="white"
|
|
||||||
elevation={1}
|
|
||||||
>
|
|
||||||
<Tablist>
|
|
||||||
{navLinks.map((Link) => (
|
|
||||||
<Tab
|
|
||||||
key={Link.name}
|
|
||||||
userSelect="none"
|
|
||||||
gap={majorScale(2)}
|
|
||||||
disabled={Link.disabled}
|
|
||||||
direction="vertical"
|
|
||||||
isSelected={Link.page === activePage}
|
|
||||||
onSelect={() => {
|
|
||||||
setActivePage(Link.page);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link.icon />
|
|
||||||
{Link.name}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
<Tab
|
|
||||||
userSelect="none"
|
|
||||||
gap={5}
|
|
||||||
direction="vertical"
|
|
||||||
isSelected={PeersDialogOpen}
|
|
||||||
onSelect={() => {
|
|
||||||
setPeersDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrayIcon />
|
|
||||||
Peers
|
|
||||||
</Tab>
|
|
||||||
</Tablist>
|
|
||||||
<PeersDialog
|
|
||||||
isOpen={PeersDialogOpen}
|
|
||||||
close={() => {
|
|
||||||
setPeersDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Pane display="flex" flexGrow={1}>
|
|
||||||
<DeviceCard />
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Heading,
|
|
||||||
IconComponent,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
Paragraph,
|
|
||||||
Tab,
|
|
||||||
Tablist,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import type { IconType } from "react-icons";
|
|
||||||
|
|
||||||
export interface TabType {
|
|
||||||
name: string;
|
|
||||||
icon: IconComponent | IconType;
|
|
||||||
element: () => JSX.Element;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlideSheetTabbedContentProps {
|
|
||||||
heading: string;
|
|
||||||
description: string;
|
|
||||||
tabs: TabType[];
|
|
||||||
tabIcon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SlideSheetTabbedContent = ({
|
|
||||||
heading,
|
|
||||||
description,
|
|
||||||
tabs,
|
|
||||||
tabIcon,
|
|
||||||
}: SlideSheetTabbedContentProps): JSX.Element => {
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Pane zIndex={1} flexShrink={0} elevation={1} backgroundColor="white">
|
|
||||||
<Pane
|
|
||||||
display="flex"
|
|
||||||
padding={16}
|
|
||||||
borderBottom="muted"
|
|
||||||
gap={majorScale(1)}
|
|
||||||
>
|
|
||||||
{tabIcon}
|
|
||||||
<Pane>
|
|
||||||
<Heading size={600}>{heading}</Heading>
|
|
||||||
<Paragraph size={400} color="muted">
|
|
||||||
{description}
|
|
||||||
</Paragraph>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" padding={8}>
|
|
||||||
<Tablist>
|
|
||||||
{tabs.map((Entry, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
userSelect="none"
|
|
||||||
disabled={Entry.disabled}
|
|
||||||
gap={5}
|
|
||||||
onSelect={() => setSelectedTab(index)}
|
|
||||||
isSelected={selectedTab === index}
|
|
||||||
>
|
|
||||||
<Entry.icon />
|
|
||||||
{Entry.name}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" overflowY="scroll" background="tint1" padding={16}>
|
|
||||||
{tabs.map((Entry, index) => (
|
|
||||||
<Pane
|
|
||||||
key={index}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
backgroundColor="white"
|
|
||||||
elevation={1}
|
|
||||||
flexGrow={1}
|
|
||||||
display={selectedTab === index ? "block" : "none"}
|
|
||||||
>
|
|
||||||
{!Entry.disabled && <Entry.element />}
|
|
||||||
</Pane>
|
|
||||||
))}
|
|
||||||
</Pane>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { IconComponent, majorScale, Pane, Tab, Tablist } from "evergreen-ui";
|
import { Tab } from "@headlessui/react";
|
||||||
import type { IconType } from "react-icons";
|
|
||||||
|
|
||||||
export interface TabType {
|
export interface TabType {
|
||||||
name: string;
|
name: string;
|
||||||
icon: IconComponent | IconType;
|
icon?: JSX.Element;
|
||||||
element: () => JSX.Element;
|
element: () => JSX.Element;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -20,55 +19,41 @@ export const TabbedContent = ({
|
|||||||
tabs,
|
tabs,
|
||||||
actions,
|
actions,
|
||||||
}: TabbedContentProps): JSX.Element => {
|
}: TabbedContentProps): JSX.Element => {
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<Tab.Group as="div" className="flex flex-col gap-2 p-4 flex-grow">
|
||||||
margin={majorScale(3)}
|
<Tab.List className="flex gap-4 border-b pb-3">
|
||||||
borderRadius={majorScale(1)}
|
{tabs.map((entry, index) => (
|
||||||
background="white"
|
<Tab key={index}>
|
||||||
elevation={1}
|
{({ selected }) => (
|
||||||
display="flex"
|
<div
|
||||||
flexGrow={1}
|
className={`flex gap-3 h-10 font-medium text-sm rounded-md cursor-pointer px-3 ${
|
||||||
flexDirection="column"
|
selected
|
||||||
padding={majorScale(2)}
|
? "bg-gray-100 text-gray-700"
|
||||||
gap={majorScale(2)}
|
: "text-gray-500 hover:text-gray-700"
|
||||||
>
|
}
|
||||||
<Pane borderBottom="muted" paddingBottom={majorScale(2)}>
|
`}
|
||||||
<Pane display="flex">
|
|
||||||
<Tablist>
|
|
||||||
{tabs.map((Entry, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
userSelect="none"
|
|
||||||
disabled={Entry.disabled}
|
|
||||||
gap={5}
|
|
||||||
onSelect={() => setSelectedTab(index)}
|
|
||||||
isSelected={selectedTab === index}
|
|
||||||
>
|
>
|
||||||
<Entry.icon />
|
{entry.icon && (
|
||||||
{Entry.name}
|
<div className="m-auto text-slate-500">{entry.icon}</div>
|
||||||
</Tab>
|
)}
|
||||||
))}
|
<span className="m-auto">{entry.name}</span>
|
||||||
</Tablist>
|
</div>
|
||||||
|
)}
|
||||||
<Pane marginLeft="auto">
|
</Tab>
|
||||||
{actions?.map((Action, index) => (
|
))}
|
||||||
<Action key={index} />
|
<div className="ml-auto">
|
||||||
))}
|
{actions?.map((Action, index) => (
|
||||||
</Pane>
|
<Action key={index} />
|
||||||
</Pane>
|
))}
|
||||||
</Pane>
|
</div>
|
||||||
{tabs.map((Entry, index) => (
|
</Tab.List>
|
||||||
<Pane
|
<Tab.Panels as={Fragment}>
|
||||||
key={index}
|
{tabs.map((entry, index) => (
|
||||||
display={selectedTab === index ? "flex" : "none"}
|
<Tab.Panel key={index} className="flex flex-grow">
|
||||||
flexDirection="column"
|
<entry.element />
|
||||||
flexGrow={1}
|
</Tab.Panel>
|
||||||
>
|
))}
|
||||||
{!Entry.disabled && <Entry.element />}
|
</Tab.Panels>
|
||||||
</Pane>
|
</Tab.Group>
|
||||||
))}
|
|
||||||
</Pane>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { DisableIcon, EmptyState, Pane } from "evergreen-ui";
|
|
||||||
|
|
||||||
export const NoDevice = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Pane elevation={1} margin="auto">
|
|
||||||
<EmptyState
|
|
||||||
title="No Device Connected"
|
|
||||||
orientation="horizontal"
|
|
||||||
background="light"
|
|
||||||
icon={<DisableIcon color="#EBAC91" />}
|
|
||||||
iconBgColor="#F8E3DA"
|
|
||||||
description="You must connect a Meshtastic device to continue."
|
|
||||||
/>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from '@core/stores/deviceStore.js';
|
||||||
|
|
||||||
export const DeviceContext = createContext<Device | undefined>(undefined);
|
export const DeviceContext = createContext<Device | undefined>(undefined);
|
||||||
|
|
||||||
export const useDevice = (): Device => {
|
export const useDevice = (): Device => {
|
||||||
const context = useContext(DeviceContext);
|
const context = useContext(DeviceContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useDevice must be used within a ConnectionProvider");
|
throw new Error("useDevice must be used within a DeviceProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import create from "zustand";
|
import create from 'zustand';
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
selectedDevice?: number;
|
selectedDevice: number;
|
||||||
devices: {
|
devices: {
|
||||||
id: number;
|
id: number;
|
||||||
num: number;
|
num: number;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export interface DeviceState {
|
|||||||
addDevice: (id: number) => Device;
|
addDevice: (id: number) => Device;
|
||||||
removeDevice: (id: number) => void;
|
removeDevice: (id: number) => void;
|
||||||
getDevices: () => Device[];
|
getDevices: () => Device[];
|
||||||
|
getDevice: (id: number) => Device | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDeviceStore = create<DeviceState>((set, get) => ({
|
export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||||
@@ -470,6 +471,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getDevices: () => Array.from(get().devices.values()),
|
getDevices: () => Array.from(get().devices.values()),
|
||||||
|
|
||||||
|
getDevice: (id) => get().devices.get(id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DeviceContext = createContext<Device | undefined>(undefined);
|
export const DeviceContext = createContext<Device | undefined>(undefined);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import "modern-css-reset/dist/reset.min.css";
|
import "./index.css";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Button, LayerIcon, LayerOutlineIcon, Tooltip } from "evergreen-ui";
|
import { Channel } from "@app/components/PageComponents/Channel.js";
|
||||||
import { IoQrCodeOutline } from "react-icons/io5";
|
import { Button } from "@components/Button.js";
|
||||||
|
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import { QrCodeIcon } from "@heroicons/react/24/outline";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
import { Channel } from "@pages/Channels/Channel.js";
|
|
||||||
|
|
||||||
export const ChannelsPage = (): JSX.Element => {
|
export const ChannelsPage = (): JSX.Element => {
|
||||||
const { channels, config } = useDevice();
|
const { channels, config } = useDevice();
|
||||||
@@ -21,10 +20,6 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
: channel.config.role === Protobuf.Channel_Role.PRIMARY
|
: channel.config.role === Protobuf.Channel_Role.PRIMARY
|
||||||
? "Primary"
|
? "Primary"
|
||||||
: `Channel: ${channel.config.index}`,
|
: `Channel: ${channel.config.index}`,
|
||||||
icon:
|
|
||||||
channel.config.role !== Protobuf.Channel_Role.DISABLED
|
|
||||||
? LayerIcon
|
|
||||||
: LayerOutlineIcon,
|
|
||||||
element: () => <Channel channel={channel.config} />,
|
element: () => <Channel channel={channel.config} />,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -43,16 +38,15 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
actions={[
|
actions={[
|
||||||
() => (
|
() => (
|
||||||
<Tooltip content="Open QR code generator">
|
<Button
|
||||||
<Button
|
variant="secondary"
|
||||||
onClick={() => {
|
iconBefore={<QrCodeIcon className="w-4" />}
|
||||||
setQRDialogOpen(true);
|
onClick={() => {
|
||||||
}}
|
setQRDialogOpen(true);
|
||||||
iconBefore={IoQrCodeOutline}
|
}}
|
||||||
>
|
>
|
||||||
QR Code
|
QR Code
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
),
|
),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
EyeOffIcon,
|
|
||||||
EyeOpenIcon,
|
|
||||||
FormField,
|
|
||||||
IconButton,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
RefreshIcon,
|
|
||||||
SelectField,
|
|
||||||
Switch,
|
|
||||||
TextInputField,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Form } from "@components/form/Form";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export interface SettingsPanelProps {
|
|
||||||
channel: Protobuf.Channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|
||||||
const { connection } = useDevice();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
|
||||||
const [pskHidden, setPskHidden] = useState(true);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isDirty },
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
setValue,
|
|
||||||
} = useForm<
|
|
||||||
Omit<Protobuf.ChannelSettings, "psk"> & { psk: string; enabled: boolean }
|
|
||||||
>({
|
|
||||||
defaultValues: {
|
|
||||||
enabled: [
|
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
|
||||||
].find((role) => role === channel?.role)
|
|
||||||
? true
|
|
||||||
: false,
|
|
||||||
...channel?.settings,
|
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reset({
|
|
||||||
enabled: [
|
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
|
||||||
].find((role) => role === channel?.role)
|
|
||||||
? true
|
|
||||||
: false,
|
|
||||||
...channel?.settings,
|
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
|
||||||
});
|
|
||||||
}, [channel, reset]);
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
|
||||||
setLoading(true);
|
|
||||||
const channelData = Protobuf.Channel.create({
|
|
||||||
role:
|
|
||||||
channel?.role === Protobuf.Channel_Role.PRIMARY
|
|
||||||
? Protobuf.Channel_Role.PRIMARY
|
|
||||||
: data.enabled
|
|
||||||
? Protobuf.Channel_Role.SECONDARY
|
|
||||||
: Protobuf.Channel_Role.DISABLED,
|
|
||||||
index: channel?.index,
|
|
||||||
settings: {
|
|
||||||
...data,
|
|
||||||
psk: toByteArray(data.psk ?? ""),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await connection?.setChannel(channelData, (): Promise<void> => {
|
|
||||||
reset({ ...data });
|
|
||||||
setLoading(false);
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
|
||||||
{channel?.index !== 0 && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
label="Enabled"
|
|
||||||
description="Description"
|
|
||||||
isInvalid={!!errors.enabled?.message}
|
|
||||||
validationMessage={errors.enabled?.message}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="enabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch
|
|
||||||
height={24}
|
|
||||||
marginLeft="auto"
|
|
||||||
checked={value}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<TextInputField
|
|
||||||
label="Name"
|
|
||||||
description="Max transmit power in dBm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<SelectField
|
|
||||||
width="100%"
|
|
||||||
label="Key Size"
|
|
||||||
description="Desired size of generated key."
|
|
||||||
value={keySize}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value={128}>128 Bit</option>
|
|
||||||
<option value={256}>256 Bit</option>
|
|
||||||
</SelectField>
|
|
||||||
<Tooltip content="Generate new key">
|
|
||||||
<IconButton
|
|
||||||
marginTop={majorScale(6)}
|
|
||||||
onClick={(
|
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
const key = new Uint8Array(keySize / 8);
|
|
||||||
crypto.getRandomValues(key);
|
|
||||||
setValue("psk", fromByteArray(key));
|
|
||||||
}}
|
|
||||||
icon={<RefreshIcon />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<TextInputField
|
|
||||||
width="100%"
|
|
||||||
label="Pre-Shared Key"
|
|
||||||
description="Max transmit power in dBm"
|
|
||||||
type={pskHidden ? "password" : "text"}
|
|
||||||
{...register("psk")}
|
|
||||||
/>
|
|
||||||
<Tooltip content={pskHidden ? "Show key" : "Hide key"}>
|
|
||||||
<Button
|
|
||||||
marginTop={majorScale(6)}
|
|
||||||
width={majorScale(12)}
|
|
||||||
onClick={(
|
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
setPskHidden(!pskHidden);
|
|
||||||
}}
|
|
||||||
iconBefore={pskHidden ? <EyeOpenIcon /> : <EyeOffIcon />}
|
|
||||||
>
|
|
||||||
{pskHidden ? "Show" : "Hide"}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Pane>
|
|
||||||
<FormField
|
|
||||||
label="Uplink Enabled"
|
|
||||||
description="Description"
|
|
||||||
isInvalid={!!errors.uplinkEnabled?.message}
|
|
||||||
validationMessage={errors.uplinkEnabled?.message}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="uplinkEnabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField
|
|
||||||
label="Downlink Enabled"
|
|
||||||
description="Description"
|
|
||||||
isInvalid={!!errors.downlinkEnabled?.message}
|
|
||||||
validationMessage={errors.downlinkEnabled?.message}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="downlinkEnabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, ...field } }) => (
|
|
||||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
export const AppConfig = (): JSX.Element => {
|
export const AppConfig = (): JSX.Element => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
|
|
||||||
const configSections = [
|
const configSections = [
|
||||||
{
|
{
|
||||||
label: "Interface",
|
label: "Interface",
|
||||||
@@ -26,31 +23,31 @@ export const AppConfig = (): JSX.Element => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane display="flex">
|
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||||
<Pane flexBasis={150} marginRight={24}>
|
<Tab.List className="flex flex-col w-44 gap-1">
|
||||||
<Tablist>
|
|
||||||
{configSections.map((Config, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
direction="vertical"
|
|
||||||
isSelected={index === selectedIndex}
|
|
||||||
onSelect={() => setSelectedIndex(index)}
|
|
||||||
>
|
|
||||||
{Config.label}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
</Pane>
|
|
||||||
<Pane flex="1">
|
|
||||||
{configSections.map((Config, index) => (
|
{configSections.map((Config, index) => (
|
||||||
<Pane
|
<Tab key={index} as={Fragment}>
|
||||||
key={index}
|
{({ selected }) => (
|
||||||
display={index === selectedIndex ? "block" : "none"}
|
<div
|
||||||
>
|
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||||
<Config.element />
|
selected
|
||||||
</Pane>
|
? "bg-gray-100 text-gray-900"
|
||||||
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Config.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</Tab.List>
|
||||||
</Pane>
|
<Tab.Panels as={Fragment}>
|
||||||
|
{configSections.map((Config, index) => (
|
||||||
|
<Tab.Panel key={index} as={Fragment}>
|
||||||
|
<Config.element />
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { Network } from "@app/components/PageComponents/Config/Network.js";
|
import { Network } from "@app/components/PageComponents/Config/Network.js";
|
||||||
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.js";
|
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.js";
|
||||||
@@ -12,9 +10,9 @@ import { Position } from "@components/PageComponents/Config/Position.js";
|
|||||||
import { Power } from "@components/PageComponents/Config/Power.js";
|
import { Power } from "@components/PageComponents/Config/Power.js";
|
||||||
import { User } from "@components/PageComponents/Config/User.js";
|
import { User } from "@components/PageComponents/Config/User.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
export const DeviceConfig = (): JSX.Element => {
|
export const DeviceConfig = (): JSX.Element => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
const { hardware } = useDevice();
|
const { hardware } = useDevice();
|
||||||
|
|
||||||
const configSections = [
|
const configSections = [
|
||||||
@@ -54,32 +52,31 @@ export const DeviceConfig = (): JSX.Element => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane display="flex">
|
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||||
<Pane flexBasis={150} marginRight={24}>
|
<Tab.List className="flex flex-col w-44 gap-1">
|
||||||
<Tablist>
|
|
||||||
{configSections.map((Config, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
direction="vertical"
|
|
||||||
isSelected={index === selectedIndex}
|
|
||||||
onSelect={() => setSelectedIndex(index)}
|
|
||||||
disabled={Config.disabled}
|
|
||||||
>
|
|
||||||
{Config.label}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
</Pane>
|
|
||||||
<Pane flex="1">
|
|
||||||
{configSections.map((Config, index) => (
|
{configSections.map((Config, index) => (
|
||||||
<Pane
|
<Tab key={index} as={Fragment}>
|
||||||
key={index}
|
{({ selected }) => (
|
||||||
display={index === selectedIndex ? "block" : "none"}
|
<div
|
||||||
>
|
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||||
<Config.element />
|
selected
|
||||||
</Pane>
|
? "bg-gray-100 text-gray-900"
|
||||||
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Config.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</Tab.List>
|
||||||
</Pane>
|
<Tab.Panels as={Fragment}>
|
||||||
|
{configSections.map((Config, index) => (
|
||||||
|
<Tab.Panel key={index} as={Fragment}>
|
||||||
|
<Config.element />
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage";
|
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage";
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||||
@@ -10,6 +8,7 @@ import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js"
|
|||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
||||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
export const ModuleConfig = (): JSX.Element => {
|
export const ModuleConfig = (): JSX.Element => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
@@ -46,31 +45,31 @@ export const ModuleConfig = (): JSX.Element => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane display="flex">
|
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||||
<Pane flexBasis={150} marginRight={24}>
|
<Tab.List className="flex flex-col gap-1 w-44">
|
||||||
<Tablist>
|
|
||||||
{configSections.map((Config, index) => (
|
|
||||||
<Tab
|
|
||||||
key={index}
|
|
||||||
direction="vertical"
|
|
||||||
isSelected={index === selectedIndex}
|
|
||||||
onSelect={() => setSelectedIndex(index)}
|
|
||||||
>
|
|
||||||
{Config.label}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tablist>
|
|
||||||
</Pane>
|
|
||||||
<Pane flex="1">
|
|
||||||
{configSections.map((Config, index) => (
|
{configSections.map((Config, index) => (
|
||||||
<Pane
|
<Tab key={index} as={Fragment}>
|
||||||
key={index}
|
{({ selected }) => (
|
||||||
display={index === selectedIndex ? "block" : "none"}
|
<div
|
||||||
>
|
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||||
<Config.element />
|
selected
|
||||||
</Pane>
|
? "bg-gray-100 text-gray-900"
|
||||||
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Config.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</Tab.List>
|
||||||
</Pane>
|
<Tab.Panels as={Fragment}>
|
||||||
|
{configSections.map((Config, index) => (
|
||||||
|
<Tab.Panel key={index} as={Fragment}>
|
||||||
|
<Config.element />
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { ApplicationsIcon, CogIcon, CubeIcon } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||||
|
import {
|
||||||
|
Cog8ToothIcon,
|
||||||
|
CubeTransparentIcon,
|
||||||
|
WindowIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { AppConfig } from "@pages/Config/AppConfig.js";
|
import { AppConfig } from "@pages/Config/AppConfig.js";
|
||||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
||||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.js";
|
import { ModuleConfig } from "@pages/Config/ModuleConfig.js";
|
||||||
@@ -11,17 +14,17 @@ export const ConfigPage = (): JSX.Element => {
|
|||||||
const tabs: TabType[] = [
|
const tabs: TabType[] = [
|
||||||
{
|
{
|
||||||
name: "Device Config",
|
name: "Device Config",
|
||||||
icon: CogIcon,
|
icon: <Cog8ToothIcon className="h-4" />,
|
||||||
element: DeviceConfig,
|
element: DeviceConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Module Config",
|
name: "Module Config",
|
||||||
icon: CubeIcon,
|
icon: <CubeTransparentIcon className="h-4" />,
|
||||||
element: ModuleConfig,
|
element: ModuleConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "App Config",
|
name: "App Config",
|
||||||
icon: ApplicationsIcon,
|
icon: <WindowIcon className="h-4" />,
|
||||||
element: AppConfig,
|
element: AppConfig,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
|
||||||
export const Environment = (): JSX.Element => {
|
export const Environment = (): JSX.Element => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane>
|
<div>
|
||||||
{nodes.map((node, index) => (
|
{nodes.map((node, index) => (
|
||||||
<Pane key={index}>{JSON.stringify(node.environmentMetrics)}</Pane>
|
<div key={index}>{JSON.stringify(node.environmentMetrics)}</div>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
|
|
||||||
export interface File {
|
export interface File {
|
||||||
nameModified: string;
|
nameModified: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -33,9 +31,9 @@ export const FileBrowser = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane>
|
<div>
|
||||||
{data?.data.files.map((file) => (
|
{data?.data.files.map((file) => (
|
||||||
<Pane key={file.name}>
|
<div key={file.name}>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -43,8 +41,8 @@ export const FileBrowser = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
{file.name.replace("static/", "").replace(".gz", "")}
|
{file.name.replace("static/", "").replace(".gz", "")}
|
||||||
</a>
|
</a>
|
||||||
</Pane>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { DocumentIcon, GanttChartIcon, RainIcon } from "evergreen-ui";
|
|
||||||
|
|
||||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import {
|
||||||
|
CloudIcon,
|
||||||
|
DocumentIcon,
|
||||||
|
SignalIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { Environment } from "@pages/Extensions/Environment.js";
|
import { Environment } from "@pages/Extensions/Environment.js";
|
||||||
import { FileBrowser } from "@pages/Extensions/FileBrowser";
|
import { FileBrowser } from "@pages/Extensions/FileBrowser";
|
||||||
|
|
||||||
@@ -13,19 +16,19 @@ export const ExtensionsPage = (): JSX.Element => {
|
|||||||
const tabs: TabType[] = [
|
const tabs: TabType[] = [
|
||||||
{
|
{
|
||||||
name: "File Browser",
|
name: "File Browser",
|
||||||
icon: DocumentIcon,
|
icon: <DocumentIcon className="h-4" />,
|
||||||
element: FileBrowser,
|
element: FileBrowser,
|
||||||
disabled: !hardware.hasWifi,
|
disabled: !hardware.hasWifi,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Range Test",
|
name: "Range Test",
|
||||||
icon: GanttChartIcon,
|
icon: <SignalIcon className="h-4" />,
|
||||||
element: FileBrowser,
|
element: FileBrowser,
|
||||||
disabled: !hardware.hasWifi,
|
disabled: !hardware.hasWifi,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Environment",
|
name: "Environment",
|
||||||
icon: RainIcon,
|
icon: <CloudIcon className="h-4" />,
|
||||||
element: Environment,
|
element: Environment,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import { Pane } from "evergreen-ui";
|
|
||||||
import JSONPretty from "react-json-pretty";
|
import JSONPretty from "react-json-pretty";
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -11,9 +10,9 @@ export const InfoPage = (): JSX.Element => {
|
|||||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane>
|
<div>
|
||||||
<JSONPretty data={myNode} />
|
<JSONPretty data={myNode} />
|
||||||
<JSONPretty data={hardware} />
|
<JSONPretty data={hardware} />
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,61 +1,29 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import {
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
LocateIcon,
|
|
||||||
majorScale,
|
|
||||||
MapMarkerIcon,
|
|
||||||
Pane,
|
|
||||||
Text,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import { Map, Marker, useMap } from "react-map-gl";
|
import { Map, Marker, useMap } from "react-map-gl";
|
||||||
|
|
||||||
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
|
import { MapPinIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export const MapPage = (): JSX.Element => {
|
export const MapPage = (): JSX.Element => {
|
||||||
const { nodes, waypoints } = useDevice();
|
const { nodes, waypoints } = useDevice();
|
||||||
const { current: map } = useMap();
|
const { current: map } = useMap();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<div className="flex-grow">
|
||||||
margin={majorScale(3)}
|
<div className="absolute z-10 right-0 top-0 m-2 rounded-md p-2 shadow-md bg-white">
|
||||||
borderRadius={majorScale(1)}
|
<div className="font-medium text-lg p-1">Title</div>
|
||||||
elevation={1}
|
<div className="flex flex-col gap-2">
|
||||||
display="flex"
|
|
||||||
flexGrow={1}
|
|
||||||
flexDirection="column"
|
|
||||||
gap={majorScale(2)}
|
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
|
||||||
>
|
|
||||||
<Pane
|
|
||||||
position="absolute"
|
|
||||||
zIndex={10}
|
|
||||||
right={0}
|
|
||||||
top={0}
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
padding={majorScale(1)}
|
|
||||||
margin={majorScale(1)}
|
|
||||||
background="tint1"
|
|
||||||
width={majorScale(28)}
|
|
||||||
elevation={1}
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<Pane padding={majorScale(1)} background="tint2">
|
|
||||||
<Heading>Title</Heading>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" flexDirection="column" gap={majorScale(1)}>
|
|
||||||
{nodes.map((n) => (
|
{nodes.map((n) => (
|
||||||
<Pane key={n.data.num} display="flex" gap={majorScale(1)}>
|
<div key={n.data.num} className="flex gap-2">
|
||||||
<Hashicon value={n.data.num.toString()} size={24} />
|
<Hashicon value={n.data.num.toString()} size={24} />
|
||||||
<Text>{n.data.user?.longName}</Text>
|
<div>{n.data.user?.longName}</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={LocateIcon}
|
icon={<MapPinIcon className="h-4" />}
|
||||||
marginLeft="auto"
|
size="sm"
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (n.data.position?.latitudeI) {
|
if (n.data.position?.latitudeI) {
|
||||||
map?.flyTo({
|
map?.flyTo({
|
||||||
@@ -68,10 +36,10 @@ export const MapPage = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Pane>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Pane>
|
</div>
|
||||||
</Pane>
|
</div>
|
||||||
<Map
|
<Map
|
||||||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
||||||
mapLib={maplibregl}
|
mapLib={maplibregl}
|
||||||
@@ -84,9 +52,9 @@ export const MapPage = (): JSX.Element => {
|
|||||||
latitude={wp.latitudeI / 1e7}
|
latitude={wp.latitudeI / 1e7}
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
>
|
>
|
||||||
<Pane>
|
<div>
|
||||||
<MapMarkerIcon />
|
<MapPinIcon className="h-4" />
|
||||||
</Pane>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
))}
|
||||||
{nodes.map((n) => {
|
{nodes.map((n) => {
|
||||||
@@ -104,6 +72,6 @@ export const MapPage = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Map>
|
</Map>
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import {
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
CircleIcon,
|
|
||||||
EditIcon,
|
|
||||||
IconButton,
|
|
||||||
Pane,
|
|
||||||
RingIcon,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TabbedContent,
|
TabbedContent,
|
||||||
TabType,
|
TabType,
|
||||||
} from "@components/layout/page/TabbedContent.js";
|
} from "@components/layout/page/TabbedContent.js";
|
||||||
|
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import {
|
||||||
|
EllipsisHorizontalCircleIcon,
|
||||||
|
PencilIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
import { ChannelChat } from "./ChannelChat.js";
|
|
||||||
|
|
||||||
export const MessagesPage = (): JSX.Element => {
|
export const MessagesPage = (): JSX.Element => {
|
||||||
const { channels, setActivePage } = useDevice();
|
const { channels, setActivePage } = useDevice();
|
||||||
|
|
||||||
@@ -28,29 +24,32 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
: channel.config.index === 0
|
: channel.config.index === 0
|
||||||
? "Primary"
|
? "Primary"
|
||||||
: `Ch ${channel.config.index}`,
|
: `Ch ${channel.config.index}`,
|
||||||
icon: channel.messages.length ? RingIcon : CircleIcon,
|
icon: channel.messages.length ? (
|
||||||
|
<EllipsisHorizontalCircleIcon className="h-4" />
|
||||||
|
) : (
|
||||||
|
<XCircleIcon className="h-4" />
|
||||||
|
),
|
||||||
element: () => <ChannelChat channel={channel} />,
|
element: () => <ChannelChat channel={channel} />,
|
||||||
disabled: channel.config.role === Protobuf.Channel_Role.DISABLED,
|
disabled: channel.config.role === Protobuf.Channel_Role.DISABLED,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane display="flex" flexDirection="column" width="100%">
|
<div className="flex flex-col w-full">
|
||||||
<TabbedContent
|
<TabbedContent
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
actions={[
|
actions={[
|
||||||
() => (
|
() => (
|
||||||
<Tooltip content="Edit Channels">
|
<IconButton
|
||||||
<IconButton
|
variant="secondary"
|
||||||
icon={EditIcon}
|
icon={<PencilIcon className="h-4" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActivePage("channels");
|
setActivePage("channels");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
),
|
),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Pane>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { ChangeEvent, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
AddLocationIcon,
|
|
||||||
IconButton,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
Popover,
|
|
||||||
SendMessageIcon,
|
|
||||||
TextInputField,
|
|
||||||
Tooltip,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import type { Channel } from "@core/stores/deviceStore.js";
|
|
||||||
import { Message } from "@pages/Messages/Message.js";
|
|
||||||
import { NewLocationMessage } from "@pages/Messages/NewLocationMessage.js";
|
|
||||||
|
|
||||||
export interface ChannelChatProps {
|
|
||||||
channel: Channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
|
||||||
const { nodes, connection, ackMessage } = useDevice();
|
|
||||||
const [currentMessage, setCurrentMessage] = useState("");
|
|
||||||
|
|
||||||
const sendMessage = (): void => {
|
|
||||||
void connection?.sendText(
|
|
||||||
currentMessage,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
channel.config.index,
|
|
||||||
(id) => {
|
|
||||||
ackMessage(channel.config.index, id);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setCurrentMessage("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane display="flex" flexDirection="column" flexGrow={1}>
|
|
||||||
<Pane display="flex" flexDirection="column" flexGrow={1}>
|
|
||||||
{channel.messages.map((message, index) => (
|
|
||||||
<Message
|
|
||||||
key={index}
|
|
||||||
message={message}
|
|
||||||
lastMsgSameUser={
|
|
||||||
index === 0
|
|
||||||
? false
|
|
||||||
: channel.messages[index - 1].packet.from ===
|
|
||||||
message.packet.from
|
|
||||||
}
|
|
||||||
sender={
|
|
||||||
nodes.find((node) => node.data.num === message.packet.from)?.data
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<form
|
|
||||||
style={{ display: "flex", flexGrow: 1 }}
|
|
||||||
onSubmit={(e): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessage();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pane display="flex" flexGrow={1} gap={majorScale(1)}>
|
|
||||||
<TextInputField
|
|
||||||
marginTop="auto"
|
|
||||||
minLength={2}
|
|
||||||
width="100%"
|
|
||||||
label=""
|
|
||||||
placeholder="Enter Message"
|
|
||||||
marginBottom={0}
|
|
||||||
value={currentMessage}
|
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
setCurrentMessage(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip content="Send">
|
|
||||||
<IconButton
|
|
||||||
icon={SendMessageIcon}
|
|
||||||
marginTop={majorScale(2)}
|
|
||||||
width={majorScale(8)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Pane>
|
|
||||||
</form>
|
|
||||||
<Tooltip content="Send Location">
|
|
||||||
<Popover content={<NewLocationMessage />}>
|
|
||||||
<IconButton
|
|
||||||
icon={AddLocationIcon}
|
|
||||||
marginTop={majorScale(2)}
|
|
||||||
width={majorScale(8)}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</Tooltip>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CircleIcon,
|
|
||||||
FullCircleIcon,
|
|
||||||
majorScale,
|
|
||||||
Pane,
|
|
||||||
Small,
|
|
||||||
Strong,
|
|
||||||
Text,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import type { AllMessageTypes } from "@app/core/stores/deviceStore.js";
|
|
||||||
import { WaypointMessage } from "@app/pages/Messages/WaypointMessage.js";
|
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
|
||||||
|
|
||||||
export interface MessageProps {
|
|
||||||
lastMsgSameUser: boolean;
|
|
||||||
message: AllMessageTypes;
|
|
||||||
sender?: Protobuf.NodeInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Message = ({
|
|
||||||
lastMsgSameUser,
|
|
||||||
message,
|
|
||||||
sender,
|
|
||||||
}: MessageProps): JSX.Element => {
|
|
||||||
const { setPeerInfoOpen, setActivePeer } = useDevice();
|
|
||||||
|
|
||||||
const openPeer = (): void => {
|
|
||||||
setActivePeer(message.packet.from);
|
|
||||||
setPeerInfoOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return lastMsgSameUser ? (
|
|
||||||
<Pane display="flex" marginLeft={majorScale(3)}>
|
|
||||||
{message.ack ? (
|
|
||||||
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
|
|
||||||
) : (
|
|
||||||
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
|
|
||||||
)}
|
|
||||||
{"waypointID" in message ? (
|
|
||||||
<WaypointMessage waypointID={message.waypointID} />
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
color={message.ack ? "#474d66" : "#9c9fab"}
|
|
||||||
marginLeft={majorScale(2)}
|
|
||||||
paddingLeft={majorScale(1)}
|
|
||||||
borderLeft="3px solid #e6e6e6"
|
|
||||||
>
|
|
||||||
{message.text}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Pane>
|
|
||||||
) : (
|
|
||||||
<Pane marginX={majorScale(2)} gap={majorScale(1)} marginTop={majorScale(1)}>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<Pane onClick={openPeer} cursor="pointer" width={majorScale(3)}>
|
|
||||||
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
|
||||||
</Pane>
|
|
||||||
<Strong onClick={openPeer} cursor="pointer" size={500}>
|
|
||||||
{sender?.user?.longName ?? "UNK"}
|
|
||||||
</Strong>
|
|
||||||
<Small>
|
|
||||||
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</Small>
|
|
||||||
</Pane>
|
|
||||||
<Pane display="flex" marginLeft={majorScale(1)}>
|
|
||||||
{message.ack ? (
|
|
||||||
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
|
|
||||||
) : (
|
|
||||||
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
|
|
||||||
)}
|
|
||||||
{"waypointID" in message ? (
|
|
||||||
<WaypointMessage waypointID={message.waypointID} />
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
color={message.ack ? "#474d66" : "#9c9fab"}
|
|
||||||
marginLeft={majorScale(2)}
|
|
||||||
paddingLeft={majorScale(1)}
|
|
||||||
borderLeft="3px solid #e6e6e6"
|
|
||||||
>
|
|
||||||
{message.text}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Heading,
|
|
||||||
LocateIcon,
|
|
||||||
majorScale,
|
|
||||||
minorScale,
|
|
||||||
Pane,
|
|
||||||
Small,
|
|
||||||
Text,
|
|
||||||
} from "evergreen-ui";
|
|
||||||
|
|
||||||
import { useDevice } from "@app/core/providers/useDevice.js";
|
|
||||||
import { toMGRS } from "@core/utils/toMGRS.js";
|
|
||||||
|
|
||||||
export interface WaypointMessageProps {
|
|
||||||
waypointID: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WaypointMessage = ({
|
|
||||||
waypointID,
|
|
||||||
}: WaypointMessageProps): JSX.Element => {
|
|
||||||
const { waypoints } = useDevice();
|
|
||||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pane
|
|
||||||
marginLeft={majorScale(2)}
|
|
||||||
paddingLeft={majorScale(1)}
|
|
||||||
borderLeft="3px solid #e6e6e6"
|
|
||||||
>
|
|
||||||
<Pane
|
|
||||||
gap={majorScale(1)}
|
|
||||||
display="flex"
|
|
||||||
borderRadius={majorScale(1)}
|
|
||||||
elevation={1}
|
|
||||||
padding={minorScale(1)}
|
|
||||||
>
|
|
||||||
<LocateIcon color="#474d66" marginY="auto" />
|
|
||||||
<Pane>
|
|
||||||
<Pane display="flex" gap={majorScale(1)}>
|
|
||||||
<Heading>{waypoint?.name}</Heading>
|
|
||||||
<Text color="orange">
|
|
||||||
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
|
||||||
</Text>
|
|
||||||
</Pane>
|
|
||||||
<Small>{waypoint?.description}</Small>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
</Pane>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
import { IsBoolean, IsEnum } from "class-validator";
|
import { IsBoolean, IsEnum } from 'class-validator';
|
||||||
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export class DeviceValidation implements Protobuf.Config_DeviceConfig {
|
export class DeviceValidation implements Protobuf.Config_DeviceConfig {
|
||||||
@IsEnum(Protobuf.Config_DeviceConfig_Role)
|
@IsEnum(Protobuf.Config_DeviceConfig_Role)
|
||||||
role: Protobuf.Config_DeviceConfig_Role;
|
role: Protobuf.Config_DeviceConfig_Role;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
serialDisabled: boolean;
|
serialEnabled: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
factoryReset: boolean;
|
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
debugLogEnabled: boolean;
|
debugLogEnabled: boolean;
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import { IsArray, IsBoolean, IsEnum, IsInt, Max, Min } from "class-validator";
|
|||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export class LoRaValidation implements Protobuf.Config_LoRaConfig {
|
export class LoRaValidation implements Protobuf.Config_LoRaConfig {
|
||||||
@IsInt()
|
@IsBoolean()
|
||||||
@Min(0)
|
usePreset: boolean;
|
||||||
@Max(10)
|
|
||||||
txPower: number;
|
|
||||||
|
|
||||||
@IsEnum(Protobuf.Config_LoRaConfig_ModemPreset)
|
@IsEnum(Protobuf.Config_LoRaConfig_ModemPreset)
|
||||||
modemPreset: Protobuf.Config_LoRaConfig_ModemPreset;
|
modemPreset: Protobuf.Config_LoRaConfig_ModemPreset;
|
||||||
@@ -36,7 +34,11 @@ export class LoRaValidation implements Protobuf.Config_LoRaConfig {
|
|||||||
hopLimit: number;
|
hopLimit: number;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
txDisabled: boolean;
|
txEnabled: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
txPower: number;
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
ignoreIncoming: number[];
|
ignoreIncoming: number[];
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { IsBoolean, IsInt } from "class-validator";
|
import { IsBoolean, IsInt } from 'class-validator';
|
||||||
|
|
||||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export class PositionValidation implements Protobuf.Config_PositionConfig {
|
export class PositionValidation implements Protobuf.Config_PositionConfig {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
positionBroadcastSecs: number;
|
positionBroadcastSecs: number;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
positionBroadcastSmartDisabled: boolean;
|
positionBroadcastSmartEnabled: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
fixedPosition: boolean;
|
fixedPosition: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
gpsDisabled: boolean;
|
gpsEnabled: boolean;
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
gpsUpdateInterval: number;
|
gpsUpdateInterval: number;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user