Channel cleanup & sidebar unification
This commit is contained in:
@@ -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,131 @@
|
||||
import React from 'react';
|
||||
|
||||
import mapbox from 'mapbox-gl';
|
||||
import { FiAlignLeft } from 'react-icons/fi';
|
||||
import {
|
||||
MdAccountCircle,
|
||||
MdGpsFixed,
|
||||
MdGpsNotFixed,
|
||||
MdGpsOff,
|
||||
} from 'react-icons/md';
|
||||
import TimeAgo from 'timeago-react';
|
||||
|
||||
import type { Node } from '@core/slices/meshtasticSlice';
|
||||
import { useMapbox } from '@hooks/useMapbox';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
type PositionConfidence = 'high' | 'low' | 'none';
|
||||
type NodeAge = 'young' | 'aging' | 'old' | 'dead';
|
||||
|
||||
export interface NodeCardProps {
|
||||
node: Node;
|
||||
isMyNode?: boolean;
|
||||
setSelected: () => void;
|
||||
}
|
||||
|
||||
export const NodeCard = ({
|
||||
node,
|
||||
isMyNode,
|
||||
setSelected,
|
||||
}: NodeCardProps): JSX.Element => {
|
||||
const { map } = useMapbox();
|
||||
|
||||
// React.useEffect(() => {
|
||||
// setSnrAverage(
|
||||
// node.snr
|
||||
// .slice(node.snr.length - 3, node.snr.length)
|
||||
// .reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length),
|
||||
// );
|
||||
// }, [node.snr]);
|
||||
const [PositionConfidence, setPositionConfidence] =
|
||||
React.useState<PositionConfidence>('none');
|
||||
const [age, setAge] = React.useState<NodeAge>('young');
|
||||
|
||||
React.useEffect(() => {
|
||||
setAge(
|
||||
node.lastHeard > new Date(Date.now() - 1000 * 60 * 15)
|
||||
? 'young'
|
||||
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 30)
|
||||
? 'aging'
|
||||
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 60)
|
||||
? 'old'
|
||||
: 'dead',
|
||||
);
|
||||
}, [node.lastHeard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setPositionConfidence(
|
||||
node.currentPosition
|
||||
? new Date(node.currentPosition.posTimestamp * 1000) >
|
||||
new Date(new Date().getTime() - 1000 * 60 * 30)
|
||||
? 'high'
|
||||
: 'low'
|
||||
: 'none',
|
||||
);
|
||||
}, [node.currentPosition]);
|
||||
return (
|
||||
<div className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700">
|
||||
<div className="flex w-full gap-1 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
|
||||
{isMyNode ? (
|
||||
<MdAccountCircle className="my-auto" />
|
||||
) : (
|
||||
<div
|
||||
className={`my-auto w-3 h-3 rounded-full ${
|
||||
age === 'young'
|
||||
? 'bg-green-500'
|
||||
: age === 'aging'
|
||||
? 'bg-yellow-500'
|
||||
: age === 'old'
|
||||
? 'bg-red-500'
|
||||
: 'bg-gray-500'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<div className="my-auto">{node.user?.longName}</div>
|
||||
|
||||
<div className="my-auto ml-auto text-xs font-semibold">
|
||||
{!isMyNode && (
|
||||
<span>
|
||||
{node.lastHeard.getTime() ? (
|
||||
<TimeAgo datetime={node.lastHeard} />
|
||||
) : (
|
||||
'Never'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
disabled={PositionConfidence === 'none'}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
if (PositionConfidence !== 'none' && node.currentPosition) {
|
||||
map?.flyTo({
|
||||
center: new mapbox.LngLat(
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
),
|
||||
zoom: 16,
|
||||
});
|
||||
}
|
||||
}}
|
||||
icon={
|
||||
PositionConfidence === 'high' ? (
|
||||
<MdGpsFixed />
|
||||
) : PositionConfidence === 'low' ? (
|
||||
<MdGpsNotFixed />
|
||||
) : (
|
||||
<MdGpsOff />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
setSelected();
|
||||
}}
|
||||
icon={<FiAlignLeft />}
|
||||
/>
|
||||
{/* <FiBatteryCharging /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 => {
|
||||
return (
|
||||
<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">
|
||||
<a className="font-medium whitespace-nowrap text-primary">
|
||||
{tagline}
|
||||
|
||||
Reference in New Issue
Block a user