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