Channel cleanup & sidebar unification
This commit is contained in:
@@ -4,17 +4,15 @@ import mapbox from 'mapbox-gl';
|
||||
import { FiMapPin, FiXCircle } from 'react-icons/fi';
|
||||
|
||||
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 { Map } from '@components/Map';
|
||||
import { NodeSidebar } from '@components/pages/nodes/NodeSidebar';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
import { NodeCard } from './NodeCard';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
// const getMarkerRadius = ():number => {}
|
||||
import { NodeCard } from '../components/pages/nodes/NodeCard';
|
||||
|
||||
export const Nodes = (): JSX.Element => {
|
||||
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 [navOpen, setNavOpen] = React.useState(false);
|
||||
|
||||
const { breakpoint } = useBreakpoint();
|
||||
const [navOpen, setNavOpen] = React.useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
||||
const [selectedNode, setSelectedNode] = React.useState<Node | undefined>();
|
||||
|
||||
@@ -119,7 +117,7 @@ export const Nodes = (): JSX.Element => {
|
||||
<Map />
|
||||
|
||||
{sidebarOpen && selectedNode && (
|
||||
<Sidebar
|
||||
<NodeSidebar
|
||||
closeSidebar={(): void => {
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
@@ -1,131 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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 { useForm } from 'react-hook-form';
|
||||
import { FiCode, FiMenu } from 'react-icons/fi';
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
import { FiExternalLink, FiMenu, FiX } from 'react-icons/fi';
|
||||
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 { Cover } from '@components/generic/Cover';
|
||||
import { Loading } from '@components/generic/Loading';
|
||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||
import {
|
||||
Card,
|
||||
Checkbox,
|
||||
@@ -20,6 +25,8 @@ import {
|
||||
} from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { ChannelsSidebar } from '../../components/pages/settings/channels/ChannelsSidebar';
|
||||
|
||||
export interface ChannelsProps {
|
||||
navOpen?: boolean;
|
||||
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -34,10 +41,13 @@ export const Channels = ({
|
||||
channels.find(
|
||||
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY,
|
||||
) ?? channels[0];
|
||||
|
||||
const { breakpoint } = useBreakpoint();
|
||||
const [usePreset, setUsePreset] = React.useState(true);
|
||||
const [debug, setDebug] = 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<
|
||||
DeepOmit<Protobuf.Channel, 'psk'>
|
||||
@@ -66,102 +76,155 @@ export const Channels = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<PrimaryTemplate
|
||||
title="Channels"
|
||||
tagline="Settings"
|
||||
leftButton={
|
||||
<IconButton
|
||||
icon={<FiMenu className="w-5 h-5" />}
|
||||
onClick={(): void => {
|
||||
setNavOpen && setNavOpen(!navOpen);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
rightButton={
|
||||
<IconButton
|
||||
icon={<FiCode className="w-5 h-5" />}
|
||||
active={debug}
|
||||
onClick={(): void => {
|
||||
setDebug(!debug);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<FormFooter
|
||||
dirty={formState.isDirty}
|
||||
saveAction={onSubmit}
|
||||
clearAction={reset}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{adminChannel && (
|
||||
<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 })}
|
||||
<>
|
||||
<PrimaryTemplate
|
||||
title="Channels"
|
||||
tagline="Settings"
|
||||
leftButton={
|
||||
<IconButton
|
||||
icon={<FiMenu className="w-5 h-5" />}
|
||||
onClick={(): void => {
|
||||
setNavOpen && setNavOpen(!navOpen);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<FormFooter
|
||||
dirty={formState.isDirty}
|
||||
saveAction={onSubmit}
|
||||
clearAction={reset}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{adminChannel && (
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
|
||||
<div className="w-full p-4 space-y-2 md:p-10">
|
||||
{channels.map((channel) => (
|
||||
<Channel key={channel.channel.index} channel={channel.channel} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</PrimaryTemplate>
|
||||
</div>
|
||||
</PrimaryTemplate>
|
||||
{sidebarOpen && (
|
||||
<ChannelsSidebar
|
||||
closeSidebar={(): void => {
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
channel={selectedChannel?.channel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user