17 changed files with 608 additions and 477 deletions
@ -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> |
||||
|
); |
||||
|
}; |
||||
@ -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> |
|
||||
); |
|
||||
}; |
|
||||
Loading…
Reference in new issue