Add new node info panel
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FiXCircle } from 'react-icons/fi';
|
||||
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 { Drawer } from '@components/generic/Drawer';
|
||||
import { Map } from '@components/Map';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
@@ -9,6 +12,7 @@ import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
import { NodeCard } from './NodeCard';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
export const Nodes = (): JSX.Element => {
|
||||
const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware);
|
||||
@@ -25,6 +29,8 @@ export const Nodes = (): JSX.Element => {
|
||||
const [navOpen, setNavOpen] = React.useState(false);
|
||||
|
||||
const { breakpoint } = useBreakpoint();
|
||||
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
||||
const [selectedNode, setSelectedNode] = React.useState<Node | undefined>();
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full dark:text-white">
|
||||
@@ -36,7 +42,7 @@ export const Nodes = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between m-6 mr-6">
|
||||
<div className="text-4xl font-extrabold leading-none tracking-tight">
|
||||
<div className="text-3xl font-extrabold leading-none tracking-tight">
|
||||
Nodes
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
@@ -53,15 +59,67 @@ export const Nodes = (): JSX.Element => {
|
||||
No nodes found.
|
||||
</span>
|
||||
)}
|
||||
{myNode && <NodeCard node={myNode} myNodeInfo={myNodeInfo} />}
|
||||
{myNode && (
|
||||
<NodeCard
|
||||
node={myNode}
|
||||
isMyNode={true}
|
||||
setSelected={(): void => {
|
||||
setSelectedNode(myNode);
|
||||
setSidebarOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{nodes
|
||||
.filter((node) => node.number !== myNodeInfo.myNodeNum)
|
||||
.map((node) => (
|
||||
<NodeCard key={node.number} node={node} />
|
||||
<NodeCard
|
||||
key={node.number}
|
||||
node={node}
|
||||
setSelected={(): void => {
|
||||
setSelectedNode(node);
|
||||
setSidebarOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Drawer>
|
||||
|
||||
{nodes.map((node) => {
|
||||
return (
|
||||
node.currentPosition && (
|
||||
<Marker
|
||||
center={
|
||||
new mapbox.LngLat(
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={(): void => {
|
||||
setSelectedNode(node);
|
||||
setSidebarOpen(true);
|
||||
}}
|
||||
className="z-50 bg-blue-500 border-2 border-blue-500 rounded-full bg-opacity-30"
|
||||
>
|
||||
<div className="m-4 ">
|
||||
<FiMapPin className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</Marker>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
<Map />
|
||||
|
||||
{sidebarOpen && selectedNode && (
|
||||
<Sidebar
|
||||
closeSidebar={(): void => {
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
node={selectedNode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+77
-182
@@ -1,51 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import mapbox from 'mapbox-gl';
|
||||
import { FaSatellite } from 'react-icons/fa';
|
||||
import { FiCode, FiMapPin } from 'react-icons/fi';
|
||||
import { GiLightningFrequency } from 'react-icons/gi';
|
||||
import { FiAlignLeft } from 'react-icons/fi';
|
||||
import {
|
||||
MdAccountCircle,
|
||||
MdArrowDropDown,
|
||||
MdArrowDropUp,
|
||||
MdGpsFixed,
|
||||
MdGpsNotFixed,
|
||||
MdGpsOff,
|
||||
MdSdStorage,
|
||||
MdSignalCellularAlt,
|
||||
} from 'react-icons/md';
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
import TimeAgo from 'timeago-react';
|
||||
|
||||
import { Modal } from '@components/generic/Modal';
|
||||
import { Marker } from '@components/Map/Marker';
|
||||
import type { Node } from '@core/slices/meshtasticSlice';
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { useMapbox } from '@hooks/useMapbox';
|
||||
import { Card, IconButton } from '@meshtastic/components';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
type PositionConfidence = 'high' | 'low' | 'none';
|
||||
type NodeAge = 'young' | 'aging' | 'old' | 'dead';
|
||||
|
||||
export interface NodeCardProps {
|
||||
node: Node;
|
||||
myNodeInfo?: Protobuf.MyNodeInfo;
|
||||
isMyNode?: boolean;
|
||||
setSelected: () => void;
|
||||
}
|
||||
|
||||
export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
||||
const [snrAverage, setSnrAverage] = React.useState(0);
|
||||
const [satsAverage, setSatsAverage] = React.useState(0);
|
||||
const [showDebug, setShowDebug] = React.useState(false);
|
||||
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]);
|
||||
// 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');
|
||||
@@ -72,165 +63,69 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
||||
: 'none',
|
||||
);
|
||||
}, [node.currentPosition]);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// setSatsAverage(
|
||||
// node.positions
|
||||
// .filter((pos) => pos.satsInView !== 0)
|
||||
// .slice(node.positions.length - 3, node.positions.length)
|
||||
// .reduce((a, b) => {
|
||||
// return a.satsInView + b.satsInView;
|
||||
// }).satsInView / (node.positions.length > 3 ? 3 : node.positions.length),
|
||||
// );
|
||||
// }, [node.positions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={showDebug}
|
||||
onClose={(): void => {
|
||||
setShowDebug(false);
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<div className="p-10 overflow-y-auto text-left max-h-96">
|
||||
<JSONPretty data={node} />
|
||||
</div>
|
||||
</Card>
|
||||
</Modal>
|
||||
{node.currentPosition && (
|
||||
<Marker
|
||||
center={
|
||||
new mapbox.LngLat(
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
<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 />
|
||||
)
|
||||
}
|
||||
popup={<div>Popup</div>}
|
||||
>
|
||||
<div className="z-50 bg-blue-500 border-2 border-blue-500 rounded-full bg-opacity-30">
|
||||
<div className="m-4 ">
|
||||
<FiMapPin className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</Marker>
|
||||
)}
|
||||
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark"
|
||||
>
|
||||
{myNodeInfo ? (
|
||||
<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">
|
||||
{!myNodeInfo && (
|
||||
<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 />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="p-2">
|
||||
{myNodeInfo && (
|
||||
<div>
|
||||
<div className="flex justify-between">
|
||||
<span className="flex">
|
||||
<MdSdStorage className="my-auto" />
|
||||
Firmware Ver:
|
||||
</span>
|
||||
<span>{myNodeInfo.firmwareVersion}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="flex">
|
||||
<GiLightningFrequency className="my-auto" />
|
||||
Freq Bands:
|
||||
</span>
|
||||
<span>{myNodeInfo.numBands}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="my-auto">
|
||||
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
setShowDebug(true);
|
||||
}}
|
||||
icon={<FiCode className="w-5 h-5" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<MdSignalCellularAlt className="my-auto" />
|
||||
SNR:
|
||||
{node.snr[node.snr.length - 1] < snrAverage ? (
|
||||
<MdArrowDropDown className="text-red-500" />
|
||||
) : (
|
||||
<MdArrowDropUp className="text-green-500" />
|
||||
)}
|
||||
{node.snr[node.snr.length - 1]}, Average: {snrAverage}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<FaSatellite className="my-auto" />
|
||||
Sats:
|
||||
{(node.currentPosition?.satsInView ?? 0) < satsAverage ? (
|
||||
<MdArrowDropDown className="text-red-500" />
|
||||
) : (
|
||||
<MdArrowDropUp className="text-green-500" />
|
||||
)}
|
||||
{node.currentPosition?.satsInView ?? 0}, Average: {satsAverage}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
</>
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
setSelected();
|
||||
}}
|
||||
icon={<FiAlignLeft />}
|
||||
/>
|
||||
{/* <FiBatteryCharging /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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="h-full bg-white border-l border-gray-300 dark:border-gray-600 w-96 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="h-full bg-gray-100 dark:bg-primaryDark">
|
||||
<Tab.Panel>Content 1</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<div></div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Content 3</Tab.Panel>
|
||||
<Tab.Panel>Remote Administration</Tab.Panel>
|
||||
<Tab.Panel className="relative">
|
||||
<div className="absolute right-0 m-2">
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
setToCopy(JSON.stringify(node));
|
||||
setCopied();
|
||||
}}
|
||||
icon={isCopied ? <FiCheck /> : <FiClipboard />}
|
||||
/>
|
||||
</div>
|
||||
<JSONPretty data={node} />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user