Channel cleanup & sidebar unification
This commit is contained in:
+1
-2
@@ -11,7 +11,7 @@ import { requestNotificationPermission } from '@core/utils/notifications';
|
|||||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { Messages } from '@pages/Messages';
|
import { Messages } from '@pages/Messages';
|
||||||
import { Nodes } from '@pages/Nodes/Index';
|
import { Nodes } from '@pages/Nodes';
|
||||||
import { NotFound } from '@pages/NotFound';
|
import { NotFound } from '@pages/NotFound';
|
||||||
import { Plugins } from '@pages/Plugins/Index';
|
import { Plugins } from '@pages/Plugins/Index';
|
||||||
import { Settings } from '@pages/settings/Index';
|
import { Settings } from '@pages/settings/Index';
|
||||||
@@ -46,7 +46,6 @@ export const App = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification.permission === ''
|
|
||||||
requestNotificationPermission().catch((e) => {
|
requestNotificationPermission().catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { fromByteArray, toByteArray } from 'base64-js';
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
import { FaQrcode } from 'react-icons/fa';
|
|
||||||
import {
|
|
||||||
FiChevronDown,
|
|
||||||
FiChevronUp,
|
|
||||||
FiCode,
|
|
||||||
FiRotateCcw,
|
|
||||||
FiSave,
|
|
||||||
} from 'react-icons/fi';
|
|
||||||
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
|
||||||
import JSONPretty from 'react-json-pretty';
|
|
||||||
import QRCode from 'react-qr-code';
|
|
||||||
|
|
||||||
import { Loading } from '@components/generic/Loading';
|
|
||||||
import { Modal } from '@components/generic/Modal';
|
|
||||||
import { connection } from '@core/connection';
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from '@meshtastic/components';
|
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
export interface ChannelProps {
|
|
||||||
channel: Protobuf.Channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Channel = ({ channel }: ChannelProps): JSX.Element => {
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
const [showQr, setShowQr] = React.useState(false);
|
|
||||||
const [keySize, setKeySize] = React.useState<128 | 256>(256);
|
|
||||||
const [pskHidden, setPskHidden] = React.useState(true);
|
|
||||||
const [showDebug, setShowDebug] = React.useState(false);
|
|
||||||
|
|
||||||
const { register, handleSubmit, setValue, control, formState, reset } =
|
|
||||||
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)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const watchPsk = useWatch({
|
|
||||||
control,
|
|
||||||
name: 'psk',
|
|
||||||
defaultValue: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
open={showDebug}
|
|
||||||
onClose={(): void => {
|
|
||||||
setShowDebug(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<div className="p-10 overflow-y-auto text-left max-h-96">
|
|
||||||
<JSONPretty data={channel} />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
open={showQr}
|
|
||||||
onClose={(): void => {
|
|
||||||
setShowQr(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<QRCode
|
|
||||||
className="rounded-md"
|
|
||||||
value={`https://www.meshtastic.org/d/#${watchPsk}`}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Modal>
|
|
||||||
<Disclosure
|
|
||||||
as="div"
|
|
||||||
className="bg-gray-100 rounded-md dark:bg-secondaryDark"
|
|
||||||
>
|
|
||||||
{({ open }): JSX.Element => (
|
|
||||||
<>
|
|
||||||
<Disclosure.Button
|
|
||||||
as="div"
|
|
||||||
className="relative flex justify-between p-3"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div className="flex my-auto space-x-2">
|
|
||||||
<div
|
|
||||||
className={`h-3 my-auto w-3 rounded-full ${
|
|
||||||
[
|
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
|
||||||
].find((role) => role === channel.role)
|
|
||||||
? 'bg-green-500'
|
|
||||||
: 'bg-gray-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{channel.settings?.name.length
|
|
||||||
? channel.settings.name
|
|
||||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
|
||||||
? 'Primary'
|
|
||||||
: `Channel: ${channel.index}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{open && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
disabled={loading || !formState.isDirty}
|
|
||||||
icon={<FiRotateCcw />}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={async (e): Promise<void> => {
|
|
||||||
e.stopPropagation();
|
|
||||||
await onSubmit();
|
|
||||||
}}
|
|
||||||
disabled={loading || !formState.isDirty}
|
|
||||||
icon={<FiSave />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowDebug(true);
|
|
||||||
}}
|
|
||||||
icon={<FiCode className="w-5 h-5" />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowQr(true);
|
|
||||||
}}
|
|
||||||
icon={<FaQrcode />}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={open ? <FiChevronUp /> : <FiChevronDown />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel className="p-2 border-t border-gray-300 dark:border-gray-600">
|
|
||||||
{loading && <Loading />}
|
|
||||||
<div className="flex px-2 my-auto">
|
|
||||||
<form className="w-full gap-3">
|
|
||||||
{channel.index !== 0 && (
|
|
||||||
<>
|
|
||||||
<Checkbox
|
|
||||||
label="Enabled"
|
|
||||||
{...register('enabled', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<Input label="Name" {...register('name')} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Key Size"
|
|
||||||
options={[
|
|
||||||
{ name: '128 Bit', value: 128 },
|
|
||||||
{ name: '256 Bit', value: 256 },
|
|
||||||
]}
|
|
||||||
value={keySize}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Pre-Shared Key"
|
|
||||||
type={pskHidden ? 'password' : 'text'}
|
|
||||||
disabled
|
|
||||||
action={
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
setPskHidden(!pskHidden);
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
pskHidden ? <MdVisibility /> : <MdVisibilityOff />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
const key = new Uint8Array(keySize);
|
|
||||||
crypto.getRandomValues(key);
|
|
||||||
setValue('psk', fromByteArray(key));
|
|
||||||
}}
|
|
||||||
icon={<MdRefresh />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{...register('psk')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Uplink Enabled"
|
|
||||||
{...register('uplinkEnabled')}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Downlink Enabled"
|
|
||||||
{...register('downlinkEnabled')}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { FiX } from 'react-icons/fi';
|
||||||
|
|
||||||
|
import { IconButton } from '@meshtastic/components';
|
||||||
|
|
||||||
|
export interface SidebarProps {
|
||||||
|
title: string;
|
||||||
|
tagline: string;
|
||||||
|
footer?: JSX.Element;
|
||||||
|
closeSidebar: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = ({
|
||||||
|
title,
|
||||||
|
tagline,
|
||||||
|
closeSidebar,
|
||||||
|
children,
|
||||||
|
}: SidebarProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="absolute z-50 flex flex-col w-full h-full bg-white border-l border-gray-300 md:z-10 md:max-w-sm md:static min-w-max dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-medium text-gray-400">{title}</h3>
|
||||||
|
<h1 className="text-lg font-medium truncate">{tagline}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mb-auto">
|
||||||
|
<IconButton
|
||||||
|
onClick={(): void => {
|
||||||
|
closeSidebar();
|
||||||
|
}}
|
||||||
|
icon={<FiX />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children ?? (
|
||||||
|
<div className="flex flex-grow bg-gray-100 dark:bg-primaryDark">
|
||||||
|
<div className="m-auto text-lg font-medium">Please select item</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { FiCheck, FiClipboard } from 'react-icons/fi';
|
||||||
|
import useCopyClipboard from 'react-use-clipboard';
|
||||||
|
|
||||||
|
import type { ButtonProps } from '@meshtastic/components';
|
||||||
|
import { IconButton } from '@meshtastic/components';
|
||||||
|
|
||||||
|
export interface CopyButtonProps extends ButtonProps {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyButton = ({
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: CopyButtonProps): JSX.Element => {
|
||||||
|
const [isCopied, setCopied] = useCopyClipboard(data, {
|
||||||
|
successDuration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
placeholder={``}
|
||||||
|
onClick={(): void => {
|
||||||
|
setCopied();
|
||||||
|
}}
|
||||||
|
icon={isCopied ? <FiCheck /> : <FiClipboard />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FiCode, FiMapPin, FiSliders, FiUser } from 'react-icons/fi';
|
||||||
|
import { IoTelescope } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { DebugPanel } from '@app/components/pages/nodes/panels/DebugPanel';
|
||||||
|
import { InfoPanel } from '@app/components/pages/nodes/panels/InfoPanel';
|
||||||
|
import { PositionPanel } from '@app/components/pages/nodes/panels/PositionPanel';
|
||||||
|
import { Sidebar } from '@components/generic/Sidebar';
|
||||||
|
import { TabButton } from '@components/TabButton';
|
||||||
|
import type { Node } from '@core/slices/meshtasticSlice';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
|
||||||
|
export interface NodeSidebarProps {
|
||||||
|
node: Node;
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeSidebar = ({
|
||||||
|
node,
|
||||||
|
closeSidebar,
|
||||||
|
}: NodeSidebarProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
title={node.number.toString()}
|
||||||
|
tagline={`${node.user?.longName}(${node.user?.shortName})`}
|
||||||
|
closeSidebar={closeSidebar}
|
||||||
|
>
|
||||||
|
<Tab.Group>
|
||||||
|
<div className="shadow-md">
|
||||||
|
<Tab.List className="flex justify-between border-b border-gray-300 dark:border-gray-600">
|
||||||
|
<TabButton>
|
||||||
|
<FiUser />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<FiMapPin />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<IoTelescope />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<FiSliders />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<FiCode />
|
||||||
|
</TabButton>
|
||||||
|
</Tab.List>
|
||||||
|
</div>
|
||||||
|
<Tab.Panels className="flex-grow overflow-y-auto bg-gray-100 dark:bg-primaryDark">
|
||||||
|
<InfoPanel />
|
||||||
|
<PositionPanel node={node} />
|
||||||
|
<Tab.Panel className="p-2">Content 3</Tab.Panel>
|
||||||
|
<Tab.Panel className="p-2">Remote Administration</Tab.Panel>
|
||||||
|
<DebugPanel node={node} />
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
|
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||||
|
import type { Node } from '@core/slices/meshtasticSlice';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
|
||||||
|
export interface DebugPanelProps {
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DebugPanel = ({ node }: DebugPanelProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Tab.Panel className="relative">
|
||||||
|
<div className="fixed right-0 m-2">
|
||||||
|
<CopyButton data={JSON.stringify(node)} />
|
||||||
|
</div>
|
||||||
|
<JSONPretty className="max-w-sm" data={node} />
|
||||||
|
</Tab.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
|
||||||
|
export const InfoPanel = (): JSX.Element => {
|
||||||
|
return <Tab.Panel className="p-2">Info</Tab.Panel>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||||
|
import type { Node } from '@core/slices/meshtasticSlice';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
|
||||||
|
export interface PositionPanelProps {
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PositionPanel = ({ node }: PositionPanelProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Tab.Panel className="p-2">
|
||||||
|
{node.currentPosition && (
|
||||||
|
<div className="flex justify-between h-10 px-1 text-gray-500 bg-transparent bg-gray-200 border border-gray-300 rounded-md select-none dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 ">
|
||||||
|
<div className="px-1 my-auto">
|
||||||
|
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)},
|
||||||
|
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)}
|
||||||
|
</div>
|
||||||
|
<CopyButton
|
||||||
|
data={
|
||||||
|
node.currentPosition
|
||||||
|
? `${node.currentPosition.latitudeI / 1e7},${
|
||||||
|
node.currentPosition.longitudeI / 1e7
|
||||||
|
}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { FaQrcode } from 'react-icons/fa';
|
||||||
|
import { FiCode, FiSliders } from 'react-icons/fi';
|
||||||
|
|
||||||
|
import { TabButton } from '@app/components/TabButton';
|
||||||
|
import { Sidebar } from '@components/generic/Sidebar';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import { DebugPanel } from './panels/DebugPanel';
|
||||||
|
import { QRCodePanel } from './panels/QRCodePanel';
|
||||||
|
import { SettingsPanel } from './panels/SettingsPanel';
|
||||||
|
|
||||||
|
export interface ChannelsSidebarProps {
|
||||||
|
channel?: Protobuf.Channel;
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChannelsSidebar = ({
|
||||||
|
channel,
|
||||||
|
closeSidebar,
|
||||||
|
}: ChannelsSidebarProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
title={
|
||||||
|
channel
|
||||||
|
? channel.settings?.name.length
|
||||||
|
? channel.settings.name
|
||||||
|
: `Channel: ${channel.index}`
|
||||||
|
: 'Please select channel'
|
||||||
|
}
|
||||||
|
tagline={channel ? Protobuf.Channel_Role[channel.role] : '...'}
|
||||||
|
closeSidebar={closeSidebar}
|
||||||
|
>
|
||||||
|
{channel && (
|
||||||
|
<Tab.Group>
|
||||||
|
<div className="shadow-md">
|
||||||
|
<Tab.List className="flex justify-between border-b border-gray-300 dark:border-gray-600">
|
||||||
|
<TabButton>
|
||||||
|
<FiSliders />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<FaQrcode />
|
||||||
|
</TabButton>
|
||||||
|
<TabButton>
|
||||||
|
<FiCode />
|
||||||
|
</TabButton>
|
||||||
|
</Tab.List>
|
||||||
|
</div>
|
||||||
|
<Tab.Panels className="flex flex-grow overflow-y-auto bg-gray-100 dark:bg-primaryDark">
|
||||||
|
<SettingsPanel channel={channel} />
|
||||||
|
<QRCodePanel channel={channel} />
|
||||||
|
<DebugPanel channel={channel} />
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
)}
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import JSONPretty from 'react-json-pretty';
|
||||||
|
|
||||||
|
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface DebugPanelProps {
|
||||||
|
channel: Protobuf.Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Tab.Panel className="relative">
|
||||||
|
<div className="fixed right-0 m-2">
|
||||||
|
<CopyButton data={JSON.stringify(channel)} />
|
||||||
|
</div>
|
||||||
|
<JSONPretty className="max-w-sm" data={channel} />
|
||||||
|
</Tab.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import QRCode from 'react-qr-code';
|
||||||
|
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface QRCodePanelProps {
|
||||||
|
channel: Protobuf.Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Tab.Panel className="flex flex-grow p-2">
|
||||||
|
<div className="m-auto">
|
||||||
|
<QRCode
|
||||||
|
className="rounded-md"
|
||||||
|
value={`https://www.meshtastic.org/d/#${channel.index}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { fromByteArray, toByteArray } from 'base64-js';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { FiSave } from 'react-icons/fi';
|
||||||
|
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
||||||
|
|
||||||
|
import { Loading } from '@app/components/generic/Loading';
|
||||||
|
import { connection } from '@app/core/connection';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface SettingsPanelProps {
|
||||||
|
channel: Protobuf.Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [keySize, setKeySize] = React.useState<128 | 256>(256);
|
||||||
|
const [pskHidden, setPskHidden] = React.useState(true);
|
||||||
|
|
||||||
|
const { register, handleSubmit, setValue, formState, reset } = 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)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
React.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 (
|
||||||
|
<Tab.Panel className="flex flex-col w-full">
|
||||||
|
{loading && <Loading />}
|
||||||
|
<form className="flex-grow gap-3 p-2">
|
||||||
|
{channel?.index !== 0 && (
|
||||||
|
<>
|
||||||
|
<Checkbox
|
||||||
|
label="Enabled"
|
||||||
|
{...register('enabled', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input label="Name" {...register('name')} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Key Size"
|
||||||
|
options={[
|
||||||
|
{ name: '128 Bit', value: 128 },
|
||||||
|
{ name: '256 Bit', value: 256 },
|
||||||
|
]}
|
||||||
|
value={keySize}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Pre-Shared Key"
|
||||||
|
type={pskHidden ? 'password' : 'text'}
|
||||||
|
disabled
|
||||||
|
action={
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
onClick={(): void => {
|
||||||
|
setPskHidden(!pskHidden);
|
||||||
|
}}
|
||||||
|
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={(): void => {
|
||||||
|
const key = new Uint8Array(keySize);
|
||||||
|
crypto.getRandomValues(key);
|
||||||
|
setValue('psk', fromByteArray(key));
|
||||||
|
}}
|
||||||
|
icon={<MdRefresh />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{...register('psk')}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
|
||||||
|
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
|
||||||
|
</form>
|
||||||
|
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
||||||
|
<div className="p-2 ml-auto">
|
||||||
|
<IconButton
|
||||||
|
disabled={!formState.isDirty}
|
||||||
|
onClick={async (): Promise<void> => {
|
||||||
|
await onSubmit();
|
||||||
|
}}
|
||||||
|
icon={<FiSave />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ export const PrimaryTemplate = ({
|
|||||||
}: PrimaryTemplateProps): JSX.Element => {
|
}: PrimaryTemplateProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-auto h-full min-w-0">
|
<div className="flex flex-col flex-auto h-full min-w-0">
|
||||||
<div className="flex p-4 bg-white border-b border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
<div className="flex p-2 bg-white border-b border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<a className="font-medium whitespace-nowrap text-primary">
|
<a className="font-medium whitespace-nowrap text-primary">
|
||||||
{tagline}
|
{tagline}
|
||||||
|
|||||||
@@ -4,17 +4,15 @@ import mapbox from 'mapbox-gl';
|
|||||||
import { FiMapPin, FiXCircle } from 'react-icons/fi';
|
import { FiMapPin, FiXCircle } from 'react-icons/fi';
|
||||||
|
|
||||||
import { Marker } from '@app/components/Map/Marker';
|
import { Marker } from '@app/components/Map/Marker';
|
||||||
import type { Node } from '@app/core/slices/meshtasticSlice.js';
|
import type { Node } from '@app/core/slices/meshtasticSlice';
|
||||||
import { Drawer } from '@components/generic/Drawer';
|
import { Drawer } from '@components/generic/Drawer';
|
||||||
import { Map } from '@components/Map';
|
import { Map } from '@components/Map';
|
||||||
|
import { NodeSidebar } from '@components/pages/nodes/NodeSidebar';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
import { useBreakpoint } from '@hooks/useBreakpoint';
|
import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||||
import { IconButton } from '@meshtastic/components';
|
import { IconButton } from '@meshtastic/components';
|
||||||
|
|
||||||
import { NodeCard } from './NodeCard';
|
import { NodeCard } from '../components/pages/nodes/NodeCard';
|
||||||
import { Sidebar } from './Sidebar';
|
|
||||||
|
|
||||||
// const getMarkerRadius = ():number => {}
|
|
||||||
|
|
||||||
export const Nodes = (): JSX.Element => {
|
export const Nodes = (): JSX.Element => {
|
||||||
const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware);
|
const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware);
|
||||||
@@ -28,9 +26,9 @@ export const Nodes = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const myNode = nodes.find((node) => node.number === myNodeInfo.myNodeNum);
|
const myNode = nodes.find((node) => node.number === myNodeInfo.myNodeNum);
|
||||||
const [navOpen, setNavOpen] = React.useState(false);
|
|
||||||
|
|
||||||
const { breakpoint } = useBreakpoint();
|
const { breakpoint } = useBreakpoint();
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
||||||
const [selectedNode, setSelectedNode] = React.useState<Node | undefined>();
|
const [selectedNode, setSelectedNode] = React.useState<Node | undefined>();
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ export const Nodes = (): JSX.Element => {
|
|||||||
<Map />
|
<Map />
|
||||||
|
|
||||||
{sidebarOpen && selectedNode && (
|
{sidebarOpen && selectedNode && (
|
||||||
<Sidebar
|
<NodeSidebar
|
||||||
closeSidebar={(): void => {
|
closeSidebar={(): void => {
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FiCheck,
|
|
||||||
FiClipboard,
|
|
||||||
FiCode,
|
|
||||||
FiMapPin,
|
|
||||||
FiSliders,
|
|
||||||
FiUser,
|
|
||||||
FiX,
|
|
||||||
} from 'react-icons/fi';
|
|
||||||
import { IoTelescope } from 'react-icons/io5';
|
|
||||||
import JSONPretty from 'react-json-pretty';
|
|
||||||
import useCopyClipboard from 'react-use-clipboard';
|
|
||||||
|
|
||||||
import { TabButton } from '@app/components/TabButton';
|
|
||||||
import type { Node } from '@app/core/slices/meshtasticSlice';
|
|
||||||
import { Tab } from '@headlessui/react';
|
|
||||||
import { IconButton } from '@meshtastic/components';
|
|
||||||
|
|
||||||
export interface SidebarProps {
|
|
||||||
node: Node;
|
|
||||||
closeSidebar: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Sidebar = ({ node, closeSidebar }: SidebarProps): JSX.Element => {
|
|
||||||
const [toCopy, setToCopy] = React.useState<string>('');
|
|
||||||
const [isCopied, setCopied] = useCopyClipboard(toCopy, {
|
|
||||||
successDuration: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute z-50 flex flex-col w-full h-full bg-white border-l border-gray-300 md:z-10 md:max-w-sm md:static min-w-max dark:border-gray-600 dark:bg-secondaryDark">
|
|
||||||
<Tab.Group>
|
|
||||||
<div className="shadow-md">
|
|
||||||
<div className="p-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-medium text-gray-400">
|
|
||||||
{node.number}
|
|
||||||
</h3>
|
|
||||||
<h1 className="text-lg font-medium truncate">
|
|
||||||
{node.user?.longName}({node.user?.shortName})
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="mb-auto">
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
closeSidebar();
|
|
||||||
}}
|
|
||||||
icon={<FiX />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tab.List className="flex justify-between border-b border-gray-300 dark:border-gray-600">
|
|
||||||
<TabButton>
|
|
||||||
<FiUser />
|
|
||||||
</TabButton>
|
|
||||||
<TabButton>
|
|
||||||
<FiMapPin />
|
|
||||||
</TabButton>
|
|
||||||
<TabButton>
|
|
||||||
<IoTelescope />
|
|
||||||
</TabButton>
|
|
||||||
<TabButton>
|
|
||||||
<FiSliders />
|
|
||||||
</TabButton>
|
|
||||||
<TabButton>
|
|
||||||
<FiCode />
|
|
||||||
</TabButton>
|
|
||||||
</Tab.List>
|
|
||||||
</div>
|
|
||||||
<Tab.Panels className="flex-grow overflow-y-auto bg-gray-100 dark:bg-primaryDark">
|
|
||||||
<Tab.Panel className="p-2">Content 1</Tab.Panel>
|
|
||||||
<Tab.Panel className="p-2">
|
|
||||||
{node.currentPosition && (
|
|
||||||
<div className="flex justify-between h-10 px-1 text-gray-500 bg-transparent bg-gray-200 border border-gray-300 rounded-md select-none dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 ">
|
|
||||||
<div className="px-1 my-auto">
|
|
||||||
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)},
|
|
||||||
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)}
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
placeholder={``}
|
|
||||||
onClick={(): void => {
|
|
||||||
setToCopy(
|
|
||||||
node.currentPosition
|
|
||||||
? `${node.currentPosition.latitudeI / 1e7},${
|
|
||||||
node.currentPosition.longitudeI / 1e7
|
|
||||||
}`
|
|
||||||
: '',
|
|
||||||
);
|
|
||||||
setCopied();
|
|
||||||
}}
|
|
||||||
icon={isCopied ? <FiCheck /> : <FiClipboard />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="p-2">Content 3</Tab.Panel>
|
|
||||||
<Tab.Panel className="p-2">Remote Administration</Tab.Panel>
|
|
||||||
<Tab.Panel className="relative">
|
|
||||||
<div className="fixed right-0 m-2">
|
|
||||||
<IconButton
|
|
||||||
onClick={(): void => {
|
|
||||||
setToCopy(JSON.stringify(node));
|
|
||||||
setCopied();
|
|
||||||
}}
|
|
||||||
icon={isCopied ? <FiCheck /> : <FiClipboard />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<JSONPretty className="max-w-sm" data={node} />
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+163
-100
@@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FiCode, FiMenu } from 'react-icons/fi';
|
import { FiExternalLink, FiMenu, FiX } from 'react-icons/fi';
|
||||||
import JSONPretty from 'react-json-pretty';
|
import {
|
||||||
|
RiArrowDownLine,
|
||||||
|
RiArrowUpDownLine,
|
||||||
|
RiArrowUpLine,
|
||||||
|
} from 'react-icons/ri';
|
||||||
|
|
||||||
import { Channel } from '@components/Channel';
|
import { Tooltip } from '@app/components/generic/Tooltip';
|
||||||
|
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
|
||||||
import { FormFooter } from '@components/FormFooter';
|
import { FormFooter } from '@components/FormFooter';
|
||||||
import { Cover } from '@components/generic/Cover';
|
|
||||||
import { Loading } from '@components/generic/Loading';
|
import { Loading } from '@components/generic/Loading';
|
||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
import { connection } from '@core/connection';
|
import { connection } from '@core/connection';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -20,6 +25,8 @@ import {
|
|||||||
} from '@meshtastic/components';
|
} from '@meshtastic/components';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import { ChannelsSidebar } from '../../components/pages/settings/channels/ChannelsSidebar';
|
||||||
|
|
||||||
export interface ChannelsProps {
|
export interface ChannelsProps {
|
||||||
navOpen?: boolean;
|
navOpen?: boolean;
|
||||||
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@@ -34,10 +41,13 @@ export const Channels = ({
|
|||||||
channels.find(
|
channels.find(
|
||||||
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY,
|
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY,
|
||||||
) ?? channels[0];
|
) ?? channels[0];
|
||||||
|
const { breakpoint } = useBreakpoint();
|
||||||
const [usePreset, setUsePreset] = React.useState(true);
|
const [usePreset, setUsePreset] = React.useState(true);
|
||||||
const [debug, setDebug] = React.useState(false);
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [sidebarOpen, setSidebarOpen] = React.useState(breakpoint !== 'sm');
|
||||||
|
const [selectedChannel, setSelectedChannel] = React.useState<
|
||||||
|
ChannelData | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState } = useForm<
|
const { register, handleSubmit, reset, formState } = useForm<
|
||||||
DeepOmit<Protobuf.Channel, 'psk'>
|
DeepOmit<Protobuf.Channel, 'psk'>
|
||||||
@@ -66,102 +76,155 @@ export const Channels = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrimaryTemplate
|
<>
|
||||||
title="Channels"
|
<PrimaryTemplate
|
||||||
tagline="Settings"
|
title="Channels"
|
||||||
leftButton={
|
tagline="Settings"
|
||||||
<IconButton
|
leftButton={
|
||||||
icon={<FiMenu className="w-5 h-5" />}
|
<IconButton
|
||||||
onClick={(): void => {
|
icon={<FiMenu className="w-5 h-5" />}
|
||||||
setNavOpen && setNavOpen(!navOpen);
|
onClick={(): void => {
|
||||||
}}
|
setNavOpen && setNavOpen(!navOpen);
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
rightButton={
|
}
|
||||||
<IconButton
|
footer={
|
||||||
icon={<FiCode className="w-5 h-5" />}
|
<FormFooter
|
||||||
active={debug}
|
dirty={formState.isDirty}
|
||||||
onClick={(): void => {
|
saveAction={onSubmit}
|
||||||
setDebug(!debug);
|
clearAction={reset}
|
||||||
}}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
>
|
||||||
footer={
|
<div className="space-y-4">
|
||||||
<FormFooter
|
{adminChannel && (
|
||||||
dirty={formState.isDirty}
|
<Card>
|
||||||
saveAction={onSubmit}
|
{loading && <Loading />}
|
||||||
clearAction={reset}
|
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
||||||
/>
|
{/* TODO: get gap working */}
|
||||||
}
|
<Checkbox
|
||||||
>
|
checked={usePreset}
|
||||||
<div className="space-y-4">
|
label="Use Presets"
|
||||||
{adminChannel && (
|
onChange={(e): void => setUsePreset(e.target.checked)}
|
||||||
<Card>
|
|
||||||
{loading && <Loading />}
|
|
||||||
<div className="w-full max-w-3xl p-10 md:max-w-xl">
|
|
||||||
{/* TODO: get gap working */}
|
|
||||||
<Checkbox
|
|
||||||
checked={usePreset}
|
|
||||||
label="Use Presets"
|
|
||||||
onChange={(e): void => setUsePreset(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
{usePreset ? (
|
|
||||||
<Select
|
|
||||||
label="Preset"
|
|
||||||
optionsEnum={Protobuf.ChannelSettings_ModemConfig}
|
|
||||||
{...register('settings.modemConfig', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
label="Bandwidth"
|
|
||||||
type="number"
|
|
||||||
suffix="MHz"
|
|
||||||
{...register('settings.bandwidth', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Spread Factor"
|
|
||||||
type="number"
|
|
||||||
suffix="CPS"
|
|
||||||
min={7}
|
|
||||||
max={12}
|
|
||||||
{...register('settings.spreadFactor', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Coding Rate"
|
|
||||||
type="number"
|
|
||||||
{...register('settings.codingRate', {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
label="Transmit Power"
|
|
||||||
type="number"
|
|
||||||
suffix="dBm"
|
|
||||||
{...register('settings.txPower', { valueAsNumber: true })}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
<form onSubmit={onSubmit}>
|
||||||
|
{usePreset ? (
|
||||||
|
<Select
|
||||||
|
label="Preset"
|
||||||
|
optionsEnum={Protobuf.ChannelSettings_ModemConfig}
|
||||||
|
{...register('settings.modemConfig', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
label="Bandwidth"
|
||||||
|
type="number"
|
||||||
|
suffix="MHz"
|
||||||
|
{...register('settings.bandwidth', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Spread Factor"
|
||||||
|
type="number"
|
||||||
|
suffix="CPS"
|
||||||
|
min={7}
|
||||||
|
max={12}
|
||||||
|
{...register('settings.spreadFactor', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Coding Rate"
|
||||||
|
type="number"
|
||||||
|
{...register('settings.codingRate', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
label="Transmit Power"
|
||||||
|
type="number"
|
||||||
|
suffix="dBm"
|
||||||
|
{...register('settings.txPower', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
<Card>
|
||||||
|
<div className="w-full p-4 space-y-2 md:p-10">
|
||||||
|
{channels.map((channel) => (
|
||||||
|
<div
|
||||||
|
key={channel.channel.index}
|
||||||
|
onClick={(): void => {
|
||||||
|
setSelectedChannel(channel);
|
||||||
|
setSidebarOpen(true);
|
||||||
|
}}
|
||||||
|
className={`flex justify-between p-2 border border-gray-300 dark:border-gray-600 bg-gray-100 rounded-md dark:bg-secondaryDark shadow-md ${
|
||||||
|
selectedChannel?.channel.index === channel.channel.index
|
||||||
|
? 'border-primary dark:border-primary'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex my-auto space-x-2">
|
||||||
|
<div
|
||||||
|
className={`h-3 my-auto w-3 rounded-full ${
|
||||||
|
[
|
||||||
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
|
Protobuf.Channel_Role.PRIMARY,
|
||||||
|
].find((role) => role === channel.channel.role)
|
||||||
|
? 'bg-green-500'
|
||||||
|
: 'bg-gray-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{channel.channel.settings?.name.length
|
||||||
|
? channel.channel.settings.name
|
||||||
|
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
|
? 'Primary'
|
||||||
|
: `Channel: ${channel.channel.index}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Tooltip contents={`MQTT Status`}>
|
||||||
|
<div className="p-2 rounded-md">
|
||||||
|
{channel.channel.settings?.uplinkEnabled &&
|
||||||
|
channel.channel.settings?.downlinkEnabled ? (
|
||||||
|
<RiArrowUpDownLine className="p-0.5 group-active:scale-90" />
|
||||||
|
) : channel.channel.settings?.uplinkEnabled ? (
|
||||||
|
<RiArrowUpLine className="p-0.5 group-active:scale-90" />
|
||||||
|
) : channel.channel.settings?.downlinkEnabled ? (
|
||||||
|
<RiArrowDownLine className="p-0.5 group-active:scale-90" />
|
||||||
|
) : (
|
||||||
|
<FiX className="p-0.5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton
|
||||||
|
active={
|
||||||
|
selectedChannel?.channel.index === channel.channel.index
|
||||||
|
}
|
||||||
|
icon={<FiExternalLink />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
</div>
|
||||||
<Card>
|
</PrimaryTemplate>
|
||||||
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
|
{sidebarOpen && (
|
||||||
<div className="w-full p-4 space-y-2 md:p-10">
|
<ChannelsSidebar
|
||||||
{channels.map((channel) => (
|
closeSidebar={(): void => {
|
||||||
<Channel key={channel.channel.index} channel={channel.channel} />
|
setSidebarOpen(false);
|
||||||
))}
|
}}
|
||||||
</div>
|
channel={selectedChannel?.channel}
|
||||||
</Card>
|
/>
|
||||||
</div>
|
)}
|
||||||
</PrimaryTemplate>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user