Browse Source

Add new node info panel

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
e2417edf13
  1. 9
      package.json
  2. 87
      pnpm-lock.yaml
  3. 2
      src/components/Channel.tsx
  4. 11
      src/components/Map/Marker.tsx
  5. 27
      src/components/TabButton.tsx
  6. 23
      src/index.css
  7. 1
      src/index.tsx
  8. 66
      src/pages/Nodes/Index.tsx
  9. 255
      src/pages/Nodes/NodeCard.tsx
  10. 98
      src/pages/Nodes/Sidebar.tsx

9
package.json

@ -26,13 +26,14 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-file-icon": "^1.1.0", "react-file-icon": "^1.1.0",
"react-hook-form": "^7.22.5", "react-hook-form": "^7.23.0",
"react-i18next": "^11.15.3", "react-i18next": "^11.15.3",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-qr-code": "^2.0.3", "react-qr-code": "^2.0.3",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-select": "^5.2.1", "react-select": "^5.2.2",
"react-use-clipboard": "^1.0.7",
"rfc4648": "^1.5.1", "rfc4648": "^1.5.1",
"swr": "^1.1.2", "swr": "^1.1.2",
"timeago-react": "^3.0.4", "timeago-react": "^3.0.4",
@ -66,9 +67,9 @@
"tailwindcss": "^3.0.13", "tailwindcss": "^3.0.13",
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"vite": "^2.7.10", "vite": "^2.7.12",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.12", "vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.4.2" "workbox-window": "^6.4.2"
} }
} }

87
pnpm-lock.yaml

@ -38,13 +38,14 @@ specifiers:
react-dom: ^17.0.2 react-dom: ^17.0.2
react-error-boundary: ^3.1.4 react-error-boundary: ^3.1.4
react-file-icon: ^1.1.0 react-file-icon: ^1.1.0
react-hook-form: ^7.22.5 react-hook-form: ^7.23.0
react-i18next: ^11.15.3 react-i18next: ^11.15.3
react-icons: ^4.3.1 react-icons: ^4.3.1
react-json-pretty: ^2.2.0 react-json-pretty: ^2.2.0
react-qr-code: ^2.0.3 react-qr-code: ^2.0.3
react-redux: ^7.2.6 react-redux: ^7.2.6
react-select: ^5.2.1 react-select: ^5.2.2
react-use-clipboard: ^1.0.7
rfc4648: ^1.5.1 rfc4648: ^1.5.1
swr: ^1.1.2 swr: ^1.1.2
tailwindcss: ^3.0.13 tailwindcss: ^3.0.13
@ -53,9 +54,9 @@ specifiers:
type-route: ^0.6.0 type-route: ^0.6.0
typescript: ^4.5.4 typescript: ^4.5.4
use-breakpoint: ^3.0.1 use-breakpoint: ^3.0.1
vite: ^2.7.10 vite: ^2.7.12
vite-plugin-cdn-import: ^0.3.5 vite-plugin-cdn-import: ^0.3.5
vite-plugin-pwa: ^0.11.12 vite-plugin-pwa: ^0.11.13
workbox-window: ^6.4.2 workbox-window: ^6.4.2
dependencies: dependencies:
@ -73,13 +74,14 @@ dependencies:
react-dom: 17.0[email protected] react-dom: 17.0[email protected]
react-error-boundary: 3.1[email protected] react-error-boundary: 3.1[email protected]
react-file-icon: 1.1[email protected][email protected] react-file-icon: 1.1[email protected][email protected]
react-hook-form: 7.22.5[email protected] react-hook-form: 7.23.0[email protected]
react-i18next: 11.15.3_bc514be083f1f06b28df24d5713fc600 react-i18next: 11.15.3_bc514be083f1f06b28df24d5713fc600
react-icons: 4.3[email protected] react-icons: 4.3[email protected]
react-json-pretty: 2.2[email protected][email protected] react-json-pretty: 2.2[email protected][email protected]
react-qr-code: 2.0[email protected] react-qr-code: 2.0[email protected]
react-redux: 7.2[email protected][email protected] react-redux: 7.2[email protected][email protected]
react-select: 5.2.1_b3482aaf5744fc7c2aeb7941b0e0a78f react-select: 5.2.2_b3482aaf5744fc7c2aeb7941b0e0a78f
react-use-clipboard: 1.0[email protected][email protected]
rfc4648: 1.5.1 rfc4648: 1.5.1
swr: 1.1[email protected] swr: 1.1[email protected]
timeago-react: 3.0[email protected] timeago-react: 3.0[email protected]
@ -113,9 +115,9 @@ devDependencies:
tailwindcss: 3.0.13_ef48b3b8837f8a23677bffe8f9cd866d tailwindcss: 3.0.13_ef48b3b8837f8a23677bffe8f9cd866d
tar: 6.1.11 tar: 6.1.11
typescript: 4.5.4 typescript: 4.5.4
vite: 2.7.10 vite: 2.7.12
vite-plugin-cdn-import: 0.3.5 vite-plugin-cdn-import: 0.3.5
vite-plugin-pwa: 0.11.1[email protected] vite-plugin-pwa: 0.11.1[email protected]
workbox-window: 6.4.2 workbox-window: 6.4.2
packages: packages:
@ -1529,7 +1531,7 @@ packages:
react: 17.0.2 react: 17.0.2
react-dom: 17.0[email protected] react-dom: 17.0[email protected]
react-icons: 4.3[email protected] react-icons: 4.3[email protected]
react-select: 5.2.1_b3482aaf5744fc7c2aeb7941b0e0a78f react-select: 5.2.2_b3482aaf5744fc7c2aeb7941b0e0a78f
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- '@types/react' - '@types/react'
@ -2201,7 +2203,7 @@ packages:
postcss: ^8.1.0 postcss: ^8.1.0
dependencies: dependencies:
browserslist: 4.19.1 browserslist: 4.19.1
caniuse-lite: 1.0.30001298 caniuse-lite: 1.0.30001299
fraction.js: 4.1.2 fraction.js: 4.1.2
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
@ -2310,8 +2312,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001298 caniuse-lite: 1.0.30001299
electron-to-chromium: 1.4.42 electron-to-chromium: 1.4.45
escalade: 3.1.1 escalade: 3.1.1
node-releases: 2.0.1 node-releases: 2.0.1
picocolors: 1.0.0 picocolors: 1.0.0
@ -2350,8 +2352,8 @@ packages:
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: true dev: true
/caniuse-lite/1.0.30001298: /caniuse-lite/1.0.30001299:
resolution: {integrity: sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==} resolution: {integrity: sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==}
dev: true dev: true
/chalk/2.4.2: /chalk/2.4.2:
@ -2458,6 +2460,12 @@ packages:
safe-buffer: 5.1.2 safe-buffer: 5.1.2
dev: true dev: true
/copy-to-clipboard/3.3.1:
resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==}
dependencies:
toggle-selection: 1.0.6
dev: false
/core-js-compat/3.20.2: /core-js-compat/3.20.2:
resolution: {integrity: sha512-qZEzVQ+5Qh6cROaTPFLNS4lkvQ6mBzE3R6A6EEpssj7Zr2egMHgsy4XapdifqJDGC9CBiNv7s+ejI96rLNQFdg==} resolution: {integrity: sha512-qZEzVQ+5Qh6cROaTPFLNS4lkvQ6mBzE3R6A6EEpssj7Zr2egMHgsy4XapdifqJDGC9CBiNv7s+ejI96rLNQFdg==}
dependencies: dependencies:
@ -2551,7 +2559,7 @@ packages:
object-is: 1.1.5 object-is: 1.1.5
object-keys: 1.1.1 object-keys: 1.1.1
object.assign: 4.1.2 object.assign: 4.1.2
regexp.prototype.flags: 1.3.1 regexp.prototype.flags: 1.3.2
side-channel: 1.0.4 side-channel: 1.0.4
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
which-collection: 1.0.1 which-collection: 1.0.1
@ -2670,8 +2678,8 @@ packages:
jake: 10.8.2 jake: 10.8.2
dev: true dev: true
/electron-to-chromium/1.4.42: /electron-to-chromium/1.4.45:
resolution: {integrity: sha512-JJLT8bjdswJzk8sNRnQjee0MGtO4zTn1t7eWwYPr8gPTadQgNRR/wFRKLGD6HZVZby39yHERkvuCVKNm10r7Dg==} resolution: {integrity: sha512-czF9eYVuOmlY/vxyMQz2rGlNSjZpxNQYBe1gmQv7al171qOIhgyO9k7D5AKlgeTCSPKk+LHhj5ZyIdmEub9oNg==}
dev: true dev: true
/emoji-regex/8.0.0: /emoji-regex/8.0.0:
@ -4220,8 +4228,8 @@ packages:
resolution: {integrity: sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=} resolution: {integrity: sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=}
dev: false dev: false
/nanoid/3.1.31: /nanoid/3.1.32:
resolution: {integrity: sha512-ZivnJm0o9bb13p2Ot5CpgC2rQdzB9Uxm/mFZweqm5eMViqOJe3PV6LU2E30SiLgheesmcPrjquqraoolONSA0A==} resolution: {integrity: sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
dev: true dev: true
@ -4541,7 +4549,7 @@ packages:
resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==} resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
dependencies: dependencies:
nanoid: 3.1.31 nanoid: 3.1.32
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.1 source-map-js: 1.0.1
dev: true dev: true
@ -4699,8 +4707,8 @@ packages:
tinycolor2: 1.4.2 tinycolor2: 1.4.2
dev: false dev: false
/react-hook-form/7.22.5[email protected]: /react-hook-form/7.23.0[email protected]:
resolution: {integrity: sha512-Q2zaeQFXdVQ8l3hcywhltH+Nzj4vo50wMVujHDVN/1Xy9IOaSZJwYBXA2CYTpK6rq41fnXviw3jTLb04c7Gu9Q==} resolution: {integrity: sha512-bO1JCkPAjmpuKhfUpFhsjWn2RIPgWUpep8qpMAKCoc8NM8ytBA5nDx5p99wNhZWrblYQFvU+dVy9g1oYo/JKoQ==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 react: ^16.8.0 || ^17
@ -4793,8 +4801,8 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/react-select/5.2.1_b3482aaf5744fc7c2aeb7941b0e0a78f: /react-select/5.2.2_b3482aaf5744fc7c2aeb7941b0e0a78f:
resolution: {integrity: sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==} resolution: {integrity: sha512-miGS2rT1XbFNjduMZT+V73xbJEeMzVkJOz727F6MeAr2hKE0uUSA8Ff7vD44H32x2PD3SRB6OXTY/L+fTV3z9w==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0
@ -4827,6 +4835,17 @@ packages:
react-dom: 17.0[email protected] react-dom: 17.0[email protected]
dev: false dev: false
/react-use-clipboard/[email protected][email protected]:
resolution: {integrity: sha512-blIprqARyITp0uVw/2Rh87mcujqXdH6vZ5NrcuXEhI5EmjBGxcGnwt/79+vdN7rwM6OliGj481lOj6ZCcsiYEQ==}
peerDependencies:
react: ^16.8.0 || ^17
react-dom: ^16.8.0 || ^17
dependencies:
copy-to-clipboard: 3.3.1
react: 17.0.2
react-dom: 17.0[email protected]
dev: false
/react/17.0.2: /react/17.0.2:
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4897,8 +4916,8 @@ packages:
'@babel/runtime': 7.16.7 '@babel/runtime': 7.16.7
dev: true dev: true
/regexp.prototype.flags/1.3.1: /regexp.prototype.flags/1.3.2:
resolution: {integrity: sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==} resolution: {integrity: sha512-uaro52GSI5be7+ssxjxxnLlleDBN3VHIWQHvBhfeeSXRQkuV/0Jo/hBU+omYH6NUkM+LYpTHnRRf2W/v+x7LzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
@ -5212,7 +5231,7 @@ packages:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
has-symbols: 1.0.2 has-symbols: 1.0.2
internal-slot: 1.0.3 internal-slot: 1.0.3
regexp.prototype.flags: 1.3.1 regexp.prototype.flags: 1.3.2
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
@ -5492,6 +5511,10 @@ packages:
is-number: 7.0.0 is-number: 7.0.0
dev: true dev: true
/toggle-selection/1.0.6:
resolution: {integrity: sha1-bkWxJj8gF/oKzH2J14sVuL932jI=}
dev: false
/tr46/1.0.1: /tr46/1.0.1:
resolution: {integrity: sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=} resolution: {integrity: sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=}
dependencies: dependencies:
@ -5661,8 +5684,8 @@ packages:
- rollup - rollup
dev: true dev: true
/vite-plugin-pwa/0.11.1[email protected]: /vite-plugin-pwa/0.11.1[email protected]:
resolution: {integrity: sha512-XqFmA4y9C4RBb5osSsa26GVwOSwbzf2GNVcT5+06KYYdguqLpuI9FW7iV/akZqg0OUNUpH4tHfme8SnHA4PIXA==} resolution: {integrity: sha512-Ssj14m3TRVLfkFEAWSMcFE2d1cSdEZyrVTzfY2lSL+umHYvcIFHVDAY143sygtBCb44OPczsAOmWwBTxwOvh7g==}
peerDependencies: peerDependencies:
vite: ^2.0.0 vite: ^2.0.0
dependencies: dependencies:
@ -5670,7 +5693,7 @@ packages:
fast-glob: 3.2.10 fast-glob: 3.2.10
pretty-bytes: 5.6.0 pretty-bytes: 5.6.0
rollup: 2.63.0 rollup: 2.63.0
vite: 2.7.10 vite: 2.7.12
workbox-build: 6.4.2 workbox-build: 6.4.2
workbox-window: 6.4.2 workbox-window: 6.4.2
transitivePeerDependencies: transitivePeerDependencies:
@ -5679,8 +5702,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/vite/2.7.10: /vite/2.7.12:
resolution: {integrity: sha512-KEY96ntXUid1/xJihJbgmLZx7QSC2D4Tui0FdS0Old5OokYzFclcofhtxtjDdGOk/fFpPbHv9yw88+rB93Tb8w==} resolution: {integrity: sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==}
engines: {node: '>=12.2.0'} engines: {node: '>=12.2.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:

2
src/components/Channel.tsx

@ -181,7 +181,7 @@ export const Channel = ({ channel }: ChannelProps): JSX.Element => {
</div> </div>
</> </>
</Disclosure.Button> </Disclosure.Button>
<Disclosure.Panel className="p-2 border-t"> <Disclosure.Panel className="p-2 border-t border-gray-300 dark:border-gray-600">
{loading && <Loading />} {loading && <Loading />}
<div className="flex px-2 my-auto"> <div className="flex px-2 my-auto">
<form className="w-full gap-3"> <form className="w-full gap-3">

11
src/components/Map/Marker.tsx

@ -2,20 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import mapbox from 'mapbox-gl'; import mapbox from 'mapbox-gl';
import ReactDOMServer from 'react-dom/server';
import { useMapbox } from '@hooks/useMapbox'; import { useMapbox } from '@hooks/useMapbox';
export interface MarkerProps extends Omit<mapbox.MarkerOptions, 'element'> { export interface MarkerProps extends Omit<mapbox.MarkerOptions, 'element'> {
children?: React.ReactNode; children?: React.ReactNode;
center: mapbox.LngLatLike; center: mapbox.LngLatLike;
popup: JSX.Element;
} }
export const Marker = ({ export const Marker = ({
children, children,
center, center,
popup,
...props ...props
}: MarkerProps): JSX.Element => { }: MarkerProps): JSX.Element => {
const { map } = useMapbox(); const { map } = useMapbox();
@ -23,14 +20,10 @@ export const Marker = ({
const addMarker = React.useCallback((): void => { const addMarker = React.useCallback((): void => {
if (map) { if (map) {
const marker = new mapbox.Marker(ref.current, props) const marker = new mapbox.Marker(ref.current, props).setLngLat(center);
.setLngLat(center)
.setPopup(
new mapbox.Popup().setHTML(ReactDOMServer.renderToString(popup)),
);
marker.addTo(map); marker.addTo(map);
} }
}, [map, center, props, popup]); }, [map, center, props]);
React.useEffect(() => { React.useEffect(() => {
map?.on('load', () => { map?.on('load', () => {

27
src/components/TabButton.tsx

@ -0,0 +1,27 @@
import type React from 'react';
import { Tab } from '@headlessui/react';
export interface TabButtonProps {
children: React.ReactNode;
}
export const TabButton = ({ children }: TabButtonProps): JSX.Element => {
return (
<Tab
className={({ selected }): string =>
`border-gray-300 hover:border-b-2 dark:border-gray-600 w-full ${
selected ? 'border-b-2' : 'border-b-0'
} `
}
>
<div className="my-auto text-gray-500 group dark:text-gray-400">
<div className="flex p-2 rounded-t-md hover:bg-gray-200 dark:hover:bg-gray-600">
<div className="m-auto transition duration-200 ease-in-out group-active:scale-90">
{children}
</div>
</div>
</div>
</Tab>
);
};

23
src/index.css

@ -33,3 +33,26 @@
box-sizing: unset !important; box-sizing: unset !important;
padding: 0 !important; padding: 0 !important;
} }
.__json-pretty__ {
line-height: 1.3;
color: #748096;
overflow: auto;
}
.__json-pretty__ .__json-key__ {
color: #b553bf;
}
.__json-pretty__ .__json-value__ {
color: #93a3bf;
}
.__json-pretty__ .__json-string__ {
color: #fba856;
}
.__json-pretty__ .__json-boolean__ {
color: #448aa9;
}
.__json-pretty-error__ {
line-height: 1.3;
color: #748096;
overflow: auto;
}

1
src/index.tsx

@ -1,6 +1,5 @@
import '@meshtastic/components/dist/style.css'; import '@meshtastic/components/dist/style.css';
import '@app/index.css'; import '@app/index.css';
import 'react-json-pretty/themes/monikai.css';
import '@core/translation'; import '@core/translation';
import React from 'react'; import React from 'react';

66
src/pages/Nodes/Index.tsx

@ -1,7 +1,10 @@
import React from 'react'; 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 { Drawer } from '@components/generic/Drawer';
import { Map } from '@components/Map'; import { Map } from '@components/Map';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -9,6 +12,7 @@ import { useBreakpoint } from '@hooks/useBreakpoint';
import { IconButton } from '@meshtastic/components'; import { IconButton } from '@meshtastic/components';
import { NodeCard } from './NodeCard'; import { NodeCard } from './NodeCard';
import { Sidebar } from './Sidebar';
export const Nodes = (): JSX.Element => { export const Nodes = (): JSX.Element => {
const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware); const myNodeInfo = useAppSelector((state) => state.meshtastic.radio.hardware);
@ -25,6 +29,8 @@ export const Nodes = (): JSX.Element => {
const [navOpen, setNavOpen] = React.useState(false); const [navOpen, setNavOpen] = React.useState(false);
const { breakpoint } = useBreakpoint(); const { breakpoint } = useBreakpoint();
const [sidebarOpen, setSidebarOpen] = React.useState(false);
const [selectedNode, setSelectedNode] = React.useState<Node | undefined>();
return ( return (
<div className="relative flex w-full dark:text-white"> <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="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 Nodes
</div> </div>
<div className="md:hidden"> <div className="md:hidden">
@ -53,15 +59,67 @@ export const Nodes = (): JSX.Element => {
No nodes found. No nodes found.
</span> </span>
)} )}
{myNode && <NodeCard node={myNode} myNodeInfo={myNodeInfo} />} {myNode && (
<NodeCard
node={myNode}
isMyNode={true}
setSelected={(): void => {
setSelectedNode(myNode);
setSidebarOpen(true);
}}
/>
)}
{nodes {nodes
.filter((node) => node.number !== myNodeInfo.myNodeNum) .filter((node) => node.number !== myNodeInfo.myNodeNum)
.map((node) => ( .map((node) => (
<NodeCard key={node.number} node={node} /> <NodeCard
key={node.number}
node={node}
setSelected={(): void => {
setSelectedNode(node);
setSidebarOpen(true);
}}
/>
))} ))}
</Drawer> </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 /> <Map />
{sidebarOpen && selectedNode && (
<Sidebar
closeSidebar={(): void => {
setSidebarOpen(false);
}}
node={selectedNode}
/>
)}
</div> </div>
); );
}; };

255
src/pages/Nodes/NodeCard.tsx

@ -1,51 +1,42 @@
import React from 'react'; import React from 'react';
import mapbox from 'mapbox-gl'; import mapbox from 'mapbox-gl';
import { FaSatellite } from 'react-icons/fa'; import { FiAlignLeft } from 'react-icons/fi';
import { FiCode, FiMapPin } from 'react-icons/fi';
import { GiLightningFrequency } from 'react-icons/gi';
import { import {
MdAccountCircle, MdAccountCircle,
MdArrowDropDown,
MdArrowDropUp,
MdGpsFixed, MdGpsFixed,
MdGpsNotFixed, MdGpsNotFixed,
MdGpsOff, MdGpsOff,
MdSdStorage,
MdSignalCellularAlt,
} from 'react-icons/md'; } from 'react-icons/md';
import JSONPretty from 'react-json-pretty';
import TimeAgo from 'timeago-react'; 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 type { Node } from '@core/slices/meshtasticSlice';
import { Disclosure } from '@headlessui/react';
import { useMapbox } from '@hooks/useMapbox'; import { useMapbox } from '@hooks/useMapbox';
import { Card, IconButton } from '@meshtastic/components'; import { IconButton } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
type PositionConfidence = 'high' | 'low' | 'none'; type PositionConfidence = 'high' | 'low' | 'none';
type NodeAge = 'young' | 'aging' | 'old' | 'dead'; type NodeAge = 'young' | 'aging' | 'old' | 'dead';
export interface NodeCardProps { export interface NodeCardProps {
node: Node; node: Node;
myNodeInfo?: Protobuf.MyNodeInfo; isMyNode?: boolean;
setSelected: () => void;
} }
export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => { export const NodeCard = ({
const [snrAverage, setSnrAverage] = React.useState(0); node,
const [satsAverage, setSatsAverage] = React.useState(0); isMyNode,
const [showDebug, setShowDebug] = React.useState(false); setSelected,
}: NodeCardProps): JSX.Element => {
const { map } = useMapbox(); const { map } = useMapbox();
React.useEffect(() => { // React.useEffect(() => {
setSnrAverage( // setSnrAverage(
node.snr // node.snr
.slice(node.snr.length - 3, node.snr.length) // .slice(node.snr.length - 3, node.snr.length)
.reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length), // .reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length),
); // );
}, [node.snr]); // }, [node.snr]);
const [PositionConfidence, setPositionConfidence] = const [PositionConfidence, setPositionConfidence] =
React.useState<PositionConfidence>('none'); React.useState<PositionConfidence>('none');
const [age, setAge] = React.useState<NodeAge>('young'); const [age, setAge] = React.useState<NodeAge>('young');
@ -72,165 +63,69 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
: 'none', : 'none',
); );
}, [node.currentPosition]); }, [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 ( return (
<> <div className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700">
<Modal <div className="flex w-full gap-1 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
open={showDebug} {isMyNode ? (
onClose={(): void => { <MdAccountCircle className="my-auto" />
setShowDebug(false); ) : (
}} <div
> className={`my-auto w-3 h-3 rounded-full ${
<Card> age === 'young'
<div className="p-10 overflow-y-auto text-left max-h-96"> ? 'bg-green-500'
<JSONPretty data={node} /> : age === 'aging'
</div> ? 'bg-yellow-500'
</Card> : age === 'old'
</Modal> ? 'bg-red-500'
{node.currentPosition && ( : 'bg-gray-500'
<Marker }`}
center={ />
new mapbox.LngLat( )}
node.currentPosition.longitudeI / 1e7, <div className="my-auto">{node.user?.longName}</div>
node.currentPosition.latitudeI / 1e7,
)
}
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"> <div className="my-auto ml-auto text-xs font-semibold">
{!myNodeInfo && ( {!isMyNode && (
<span> <span>
{node.lastHeard.getTime() ? ( {node.lastHeard.getTime() ? (
<TimeAgo datetime={node.lastHeard} /> <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 /> 'Never'
) )}
} </span>
/>
</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>
<div className="my-auto"> <IconButton
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]} disabled={PositionConfidence === 'none'}
</div> onClick={(e): void => {
<div className="ml-auto"> e.stopPropagation();
<IconButton if (PositionConfidence !== 'none' && node.currentPosition) {
onClick={(): void => { map?.flyTo({
setShowDebug(true); center: new mapbox.LngLat(
}} node.currentPosition.longitudeI / 1e7,
icon={<FiCode className="w-5 h-5" />} node.currentPosition.latitudeI / 1e7,
/> ),
</div> zoom: 16,
</div> });
<div className="flex"> }
<MdSignalCellularAlt className="my-auto" /> }}
SNR: icon={
{node.snr[node.snr.length - 1] < snrAverage ? ( PositionConfidence === 'high' ? (
<MdArrowDropDown className="text-red-500" /> <MdGpsFixed />
) : ( ) : PositionConfidence === 'low' ? (
<MdArrowDropUp className="text-green-500" /> <MdGpsNotFixed />
)}
{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" /> <MdGpsOff />
)} )
{node.currentPosition?.satsInView ?? 0}, Average: {satsAverage} }
</div> />
</Disclosure.Panel> <IconButton
</Disclosure> onClick={(): void => {
</> setSelected();
}}
icon={<FiAlignLeft />}
/>
{/* <FiBatteryCharging /> */}
</div>
</div>
); );
}; };

98
src/pages/Nodes/Sidebar.tsx

@ -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>
);
};
Loading…
Cancel
Save