Browse Source

Move plugins to settings page

pull/21/head
Sacha Weatherstone 5 years ago
parent
commit
161d432330
  1. 4
      package.json
  2. 344
      pnpm-lock.yaml
  3. 4
      src/App.tsx
  4. 9
      src/components/generic/Loading.tsx
  5. 2
      src/components/generic/Sidebar.tsx
  6. 22
      src/components/generic/form/Form.tsx
  7. 7
      src/components/menu/BottomNav.tsx
  8. 19
      src/components/menu/Navigation.tsx
  9. 56
      src/components/modals/VersionInfo.tsx
  10. 76
      src/components/pages/settings/plugins/PluginsSidebar.tsx
  11. 32
      src/components/pages/settings/plugins/panels/ExternalNotifications/DebugPanel.tsx
  12. 97
      src/components/pages/settings/plugins/panels/ExternalNotifications/SettingsPlanel.tsx
  13. 28
      src/components/pages/settings/plugins/panels/RangeTest/DebugPanel.tsx
  14. 79
      src/components/pages/settings/plugins/panels/RangeTest/SettingsPanel.tsx
  15. 31
      src/components/pages/settings/plugins/panels/Serial/DebugPanel.tsx
  16. 100
      src/components/pages/settings/plugins/panels/Serial/SettingsPanel.tsx
  17. 32
      src/components/pages/settings/plugins/panels/StoreForward/DebugPanel.tsx
  18. 95
      src/components/pages/settings/plugins/panels/StoreForward/SettingsPanel.tsx
  19. 0
      src/components/pages/settings/radio/channels/ChannelsSidebar.tsx
  20. 0
      src/components/pages/settings/radio/channels/panels/DebugPanel.tsx
  21. 0
      src/components/pages/settings/radio/channels/panels/QRCodePanel.tsx
  22. 7
      src/components/pages/settings/radio/channels/panels/SettingsPanel.tsx
  23. 1
      src/core/router.ts
  24. 135
      src/pages/Plugins/ExternalNotification.tsx
  25. 128
      src/pages/Plugins/Files.tsx
  26. 59
      src/pages/Plugins/Index.tsx
  27. 106
      src/pages/Plugins/RangeTest.tsx
  28. 133
      src/pages/Plugins/Serial.tsx
  29. 98
      src/pages/Plugins/StoreAndForward.tsx
  30. 4
      src/pages/settings/Channels.tsx
  31. 12
      src/pages/settings/Index.tsx
  32. 130
      src/pages/settings/Plugins.tsx

4
package.json

@ -14,7 +14,7 @@
"dependencies": {
"@floating-ui/react-dom": "^0.4.3",
"@headlessui/react": "^1.4.3",
"@meshtastic/components": "^1.0.17",
"@meshtastic/components": "^1.0.19",
"@meshtastic/meshtasticjs": "^0.6.38",
"@reduxjs/toolkit": "^1.7.1",
"@tippyjs/react": "^4.2.6",
@ -65,7 +65,7 @@
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"gzipper": "^6.2.1",
"gzipper": "^7.0.0",
"postcss": "^8.4.5",
"prettier": "^2.5.1",
"tailwindcss": "^3.0.15",

344
pnpm-lock.yaml

@ -3,7 +3,7 @@ lockfileVersion: 5.3
specifiers:
'@floating-ui/react-dom': ^0.4.3
'@headlessui/react': ^1.4.3
'@meshtastic/components': ^1.0.17
'@meshtastic/components': ^1.0.19
'@meshtastic/meshtasticjs': ^0.6.38
'@reduxjs/toolkit': ^1.7.1
'@tippyjs/react': ^4.2.6
@ -30,7 +30,7 @@ specifiers:
eslint-plugin-react: ^7.28.0
eslint-plugin-react-hooks: ^4.3.0
graphql-request: ^3.7.0
gzipper: ^6.2.1
gzipper: ^7.0.0
i18next: ^21.6.6
i18next-browser-languagedetector: ^6.1.2
mapbox-gl: ^2.6.1
@ -66,7 +66,7 @@ specifiers:
dependencies:
'@floating-ui/react-dom': 0.4.3_b3482aaf5744fc7c2aeb7941b0e0a78f
'@headlessui/react': 1.4[email protected][email protected]
'@meshtastic/components': 1.0.17_@[email protected]
'@meshtastic/components': 1.0.19_@[email protected]
'@meshtastic/meshtasticjs': 0.6.38
'@reduxjs/toolkit': 1.7[email protected][email protected]
'@tippyjs/react': 4.2[email protected][email protected]
@ -117,7 +117,7 @@ devDependencies:
eslint-plugin-import: 2.25[email protected]
eslint-plugin-react: 7.28[email protected]
eslint-plugin-react-hooks: 4.3[email protected]
gzipper: 6.2.1
gzipper: 7.0.0
postcss: 8.4.5
prettier: 2.5.1
tailwindcss: 3.0.15_ef48b3b8837f8a23677bffe8f9cd866d
@ -1451,6 +1451,13 @@ packages:
- '@types/react'
dev: false
/@gfx/zopfli/1.0.15:
resolution: {integrity: sha512-7mBgpi7UD82fsff5ThQKet0uBTl4BYerQuc+/qA1ELTwWEiIedRTcD3JgiUu9wwZ2kytW8JOb165rSdAt8PfcQ==}
engines: {node: '>= 8'}
dependencies:
base64-js: 1.5.1
dev: true
/@headlessui/react/[email protected][email protected]:
resolution: {integrity: sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==}
engines: {node: '>=10'}
@ -1532,8 +1539,8 @@ packages:
engines: {node: '>=6.0.0'}
dev: false
/@meshtastic/components/1.0.17_@[email protected]:
resolution: {integrity: sha512-e9ETFrzQUtpxVHc6NiiYMCkr7Ml4v7xlbILWK3LV48ALMDojBAFKLHdxuClpt0Rszo91ORACSSVpUkJNfWnp1A==}
/@meshtastic/components/1.0.19_@[email protected]:
resolution: {integrity: sha512-Uls4JpJFrxi86MlxRNTPRHFBW5m/3VVFaz1anvam1vHXpJZz/ikcEmW5Xgu+f7lkBO6JGtEgvdXSbJFCkr5gqQ==}
dependencies:
inter-ui: 3.19.3
react: 17.0.2
@ -1715,8 +1722,8 @@ packages:
'@types/geojson': 7946.0.8
dev: true
/@types/node/17.0.8:
resolution: {integrity: sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==}
/@types/node/17.0.9:
resolution: {integrity: sha512-5dNBXu/FOER+EXnyah7rn8xlNrfMOQb/qXnw4NQgLkCygKBKhdmF/CA5oXVOKZLBEahw8s2WP9LxIcN/oDDRgQ==}
dev: true
/@types/parse-json/4.0.0:
@ -1763,7 +1770,7 @@ packages:
/@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 17.0.8
'@types/node': 17.0.9
dev: true
/@types/scheduler/0.16.2:
@ -2113,11 +2120,6 @@ packages:
engines: {node: '>=6'}
dev: true
/ansi-regex/2.1.1:
resolution: {integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=}
engines: {node: '>=0.10.0'}
dev: true
/ansi-regex/5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@ -2145,17 +2147,6 @@ packages:
picomatch: 2.3.1
dev: true
/aproba/1.2.0:
resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==}
dev: true
/are-we-there-yet/1.1.7:
resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==}
dependencies:
delegates: 1.0.0
readable-stream: 2.3.7
dev: true
/arg/5.0.1:
resolution: {integrity: sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==}
dev: true
@ -2230,7 +2221,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.19.1
caniuse-lite: 1.0.30001299
caniuse-lite: 1.0.30001300
fraction.js: 4.1.2
normalize-range: 0.1.2
picocolors: 1.0.0
@ -2308,14 +2299,6 @@ packages:
engines: {node: '>=8'}
dev: true
/bl/4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.0
dev: true
/boring-avatars/1.6.1:
resolution: {integrity: sha512-P7BZRz1DEdx61iW7q8EoB10rDLGGH4ec8uwQsMKOCag8TznC5A/efd5OBuL9Su3HjMEDHN3X8/JRRQNsVZMl4Q==}
dev: false
@ -2339,7 +2322,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001299
caniuse-lite: 1.0.30001300
electron-to-chromium: 1.4.46
escalade: 3.1.1
node-releases: 2.0.1
@ -2350,13 +2333,6 @@ packages:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
/buffer/5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: true
/builtin-modules/3.2.0:
resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==}
engines: {node: '>=6'}
@ -2379,8 +2355,8 @@ packages:
engines: {node: '>= 6'}
dev: true
/caniuse-lite/1.0.30001299:
resolution: {integrity: sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==}
/caniuse-lite/1.0.30001300:
resolution: {integrity: sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==}
dev: true
/chalk/2.4.2:
@ -2415,25 +2391,11 @@ packages:
fsevents: 2.3.2
dev: true
/chownr/1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: true
/chownr/2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
dev: true
/clone/1.0.4:
resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=}
engines: {node: '>=0.8'}
dev: true
/code-point-at/1.1.0:
resolution: {integrity: sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=}
engines: {node: '>=0.10.0'}
dev: true
/color-convert/1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@ -2466,10 +2428,6 @@ packages:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
/commander/3.0.2:
resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==}
dev: true
/commander/7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
@ -2484,10 +2442,6 @@ packages:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
dev: true
/console-control-strings/1.1.0:
resolution: {integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=}
dev: true
/convert-source-map/1.8.0:
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
dependencies:
@ -2579,13 +2533,6 @@ packages:
ms: 2.1.2
dev: true
/decompress-response/4.2.1:
resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==}
engines: {node: '>=8'}
dependencies:
mimic-response: 2.1.0
dev: true
/deep-equal/2.0.5:
resolution: {integrity: sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==}
dependencies:
@ -2606,11 +2553,6 @@ packages:
which-typed-array: 1.1.7
dev: true
/deep-extend/0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
dev: true
/deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@ -2620,12 +2562,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/defaults/1.0.3:
resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=}
dependencies:
clone: 1.0.4
dev: true
/define-properties/1.1.3:
resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==}
engines: {node: '>= 0.4'}
@ -2642,16 +2578,6 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/delegates/1.0.0:
resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=}
dev: true
/detect-libc/1.0.3:
resolution: {integrity: sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=}
engines: {node: '>=0.10'}
hasBin: true
dev: true
/detective/5.2.0:
resolution: {integrity: sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==}
engines: {node: '>=0.8.0'}
@ -3383,11 +3309,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/expand-template/2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
dev: true
/extract-files/9.0.0:
resolution: {integrity: sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==}
engines: {node: ^10.17.0 || ^12.0.0 || >= 13.7.0}
@ -3493,10 +3414,6 @@ packages:
resolution: {integrity: sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==}
dev: true
/fs-constants/1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
dev: true
/fs-extra/9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@ -3534,19 +3451,6 @@ packages:
resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=}
dev: true
/gauge/2.7.4:
resolution: {integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=}
dependencies:
aproba: 1.2.0
console-control-strings: 1.1.0
has-unicode: 2.0.1
object-assign: 4.1.1
signal-exit: 3.0.6
string-width: 1.0.2
strip-ansi: 3.0.1
wide-align: 1.1.5
dev: true
/gensync/1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@ -3581,10 +3485,6 @@ packages:
get-intrinsic: 1.1.1
dev: true
/github-from-package/0.0.0:
resolution: {integrity: sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=}
dev: true
/gl-matrix/3.4.3:
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
dev: false
@ -3656,14 +3556,14 @@ packages:
resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==}
dev: false
/gzipper/6.2.1:
resolution: {integrity: sha512-HGCFv3hZy87B+AHRuDpbyRbvOdaJllyGB+OTqDqKy54e8M1RDgF64QN/buyqpqhvSXWePNIE0jqbHpcjCWG6YQ==}
/gzipper/7.0.0:
resolution: {integrity: sha512-Pfr8FXg4JOQ9hwWWS+d5KDOokRlfjkuNuK7HVJsiXwQhYTugKMHXAnAeEuKr5ILtxX0cAzJBtR6g3Wclze8+LA==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@gfx/zopfli': 1.0.15
commander: 7.2.0
deep-equal: 2.0.5
node-zopfli: 2.1.4
simple-zstd: 1.4.0
uuid: 8.3.2
dev: true
@ -3694,10 +3594,6 @@ packages:
has-symbols: 1.0.2
dev: true
/has-unicode/2.0.1:
resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=}
dev: true
/has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
@ -3745,6 +3641,7 @@ packages:
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
/ignore/4.0.6:
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
@ -3784,10 +3681,6 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/ini/1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
dev: true
/inter-ui/3.19.3:
resolution: {integrity: sha512-5FG9fjuYOXocIfjzcCBhICL5cpvwEetseL3FU6tP3d6Bn7g8wODhB+I9RNGRTizCT7CUG4GOK54OPxqq3msQgg==}
dev: false
@ -3857,13 +3750,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/is-fullwidth-code-point/1.0.0:
resolution: {integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs=}
engines: {node: '>=0.10.0'}
dependencies:
number-is-nan: 1.0.1
dev: true
/is-fullwidth-code-point/3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
@ -4010,7 +3896,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 17.0.8
'@types/node': 17.0.9
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@ -4251,11 +4137,6 @@ packages:
mime-db: 1.51.0
dev: false
/mimic-response/2.1.0:
resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
engines: {node: '>=8'}
dev: true
/minimatch/3.0.4:
resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
dependencies:
@ -4280,10 +4161,6 @@ packages:
yallist: 4.0.0
dev: true
/mkdirp-classic/0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
dev: true
/mkdirp/1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@ -4306,30 +4183,16 @@ packages:
resolution: {integrity: sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=}
dev: false
/nanoid/3.1.32:
resolution: {integrity: sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==}
/nanoid/3.2.0:
resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
/napi-build-utils/1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: true
/natural-compare/1.4.0:
resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=}
dev: true
/node-abi/2.30.1:
resolution: {integrity: sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==}
dependencies:
semver: 5.7.1
dev: true
/node-addon-api/1.7.2:
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
dev: true
/node-fetch/2.6.1:
resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==}
engines: {node: 4.x || >=6.0.0}
@ -4339,22 +4202,6 @@ packages:
resolution: {integrity: sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==}
dev: true
/node-zopfli/2.1.4:
resolution: {integrity: sha512-5kxbPxNQHbORFBNNf083V7h0dTFy6+C1W4ZA4qTXjpCQqEMzxAQFAOfi6BlCmXAkC/+Vr8d6h0XOmdjvp+9FNw==}
engines: {node: '>=8'}
hasBin: true
requiresBuild: true
dependencies:
commander: 3.0.2
defaults: 1.0.3
node-addon-api: 1.7.2
prebuild-install: 5.3.6
dev: true
/noop-logger/0.1.1:
resolution: {integrity: sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=}
dev: true
/normalize-path/3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@ -4365,20 +4212,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/npmlog/4.1.2:
resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==}
dependencies:
are-we-there-yet: 1.1.7
console-control-strings: 1.1.0
gauge: 2.7.4
set-blocking: 2.0.0
dev: true
/number-is-nan/1.0.1:
resolution: {integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=}
engines: {node: '>=0.10.0'}
dev: true
/object-assign/4.1.1:
resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=}
engines: {node: '>=0.10.0'}
@ -4632,7 +4465,7 @@ packages:
resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.1.32
nanoid: 3.2.0
picocolors: 1.0.0
source-map-js: 1.0.1
dev: true
@ -4641,28 +4474,6 @@ packages:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
dev: false
/prebuild-install/5.3.6:
resolution: {integrity: sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==}
engines: {node: '>=6'}
hasBin: true
dependencies:
detect-libc: 1.0.3
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.5
mkdirp-classic: 0.5.3
napi-build-utils: 1.0.2
node-abi: 2.30.1
noop-logger: 0.1.1
npmlog: 4.1.2
pump: 3.0.0
rc: 1.2.8
simple-get: 3.1.0
tar-fs: 2.1.1
tunnel-agent: 0.6.0
which-pm-runs: 1.0.0
dev: true
/prelude-ls/1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -4707,13 +4518,6 @@ packages:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
dev: false
/pump/3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
dev: true
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
@ -4746,16 +4550,6 @@ packages:
safe-buffer: 5.2.1
dev: true
/rc/1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.5
strip-json-comments: 2.0.1
dev: true
/react-dom/[email protected]:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
peerDependencies:
@ -5144,11 +4938,6 @@ packages:
object-assign: 4.1.1
dev: false
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
dev: true
/semver/6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true
@ -5173,10 +4962,6 @@ packages:
randombytes: 2.1.0
dev: true
/set-blocking/2.0.0:
resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=}
dev: true
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -5197,22 +4982,6 @@ packages:
object-inspect: 1.12.0
dev: true
/signal-exit/3.0.6:
resolution: {integrity: sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==}
dev: true
/simple-concat/1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
dev: true
/simple-get/3.1.0:
resolution: {integrity: sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==}
dependencies:
decompress-response: 4.2.1
once: 1.4.0
simple-concat: 1.0.1
dev: true
/simple-zstd/1.4.0:
resolution: {integrity: sha512-9zBNnu7MkwRiZm7voFUX7ehCcLO2d1FmJ2RWEVsN8Exw2tVYK9k/0/8WjPUmSmtoHOyoFTkHHaOLuPSwkgFmrA==}
dependencies:
@ -5287,15 +5056,6 @@ packages:
resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
dev: true
/string-width/1.0.2:
resolution: {integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=}
engines: {node: '>=0.10.0'}
dependencies:
code-point-at: 1.1.0
is-fullwidth-code-point: 1.0.0
strip-ansi: 3.0.1
dev: true
/string-width/4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@ -5353,13 +5113,6 @@ packages:
is-regexp: 1.0.0
dev: true
/strip-ansi/3.0.1:
resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=}
engines: {node: '>=0.10.0'}
dependencies:
ansi-regex: 2.1.1
dev: true
/strip-ansi/6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -5377,11 +5130,6 @@ packages:
engines: {node: '>=10'}
dev: true
/strip-json-comments/2.0.1:
resolution: {integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=}
engines: {node: '>=0.10.0'}
dev: true
/strip-json-comments/3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@ -5474,26 +5222,6 @@ packages:
- ts-node
dev: true
/tar-fs/2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.0
tar-stream: 2.2.0
dev: true
/tar-stream/2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
dependencies:
bl: 4.1.0
end-of-stream: 1.4.4
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.0
dev: true
/tar/6.1.11:
resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==}
engines: {node: '>= 10'}
@ -5633,12 +5361,6 @@ packages:
typescript: 4.5.4
dev: true
/tunnel-agent/0.6.0:
resolution: {integrity: sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=}
dependencies:
safe-buffer: 5.2.1
dev: true
/type-check/0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -5867,10 +5589,6 @@ packages:
is-weakset: 2.0.2
dev: true
/which-pm-runs/1.0.0:
resolution: {integrity: sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=}
dev: true
/which-typed-array/1.1.7:
resolution: {integrity: sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==}
engines: {node: '>= 0.4'}
@ -5891,12 +5609,6 @@ packages:
isexe: 2.0.0
dev: true
/wide-align/1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
dependencies:
string-width: 1.0.2
dev: true
/word-wrap/1.2.3:
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
engines: {node: '>=0.10.0'}

4
src/App.tsx

@ -13,7 +13,6 @@ import { useAppSelector } from '@hooks/useAppSelector';
import { Messages } from '@pages/Messages';
import { Nodes } from '@pages/Nodes';
import { NotFound } from '@pages/NotFound';
import { Plugins } from '@pages/Plugins/Index';
import { Settings } from '@pages/settings/Index';
import { ErrorFallback } from './components/ErrorFallback';
@ -67,7 +66,7 @@ export const App = (): JSX.Element => {
<div className="flex">
<Logo />
</div>
<Navigation className="flex" />
<Navigation />
</div>
</div>
</div>
@ -81,7 +80,6 @@ export const App = (): JSX.Element => {
<Nodes />
</MapboxProvider>
)}
{route.name === 'plugins' && <Plugins />}
{route.name === 'settings' && <Settings />}
{route.name === false && <NotFound />}
</ErrorBoundary>

9
src/components/generic/Loading.tsx

@ -1,9 +0,0 @@
import type React from 'react';
export const Loading = (): JSX.Element => {
return (
<div className="absolute top-0 bottom-0 left-0 right-0 z-10 flex rounded-md backdrop-filter backdrop-blur-sm">
<div className="m-auto text-lg font-medium text-gray-400">Loading</div>
</div>
);
};

2
src/components/generic/Sidebar.tsx

@ -37,7 +37,7 @@ export const Sidebar = ({
</div>
</div>
{children ?? (
<div className="flex flex-grow bg-gray-100 dark:bg-primaryDark">
<div className="flex flex-grow bg-gray-50 dark:bg-primaryDark">
<div className="m-auto text-lg font-medium">Please select item</div>
</div>
)}

22
src/components/generic/form/Form.tsx

@ -0,0 +1,22 @@
import type React from 'react';
import { Loading } from '@meshtastic/components';
export interface FormProps {
loading?: boolean;
children: React.ReactNode;
}
export const Form = ({ loading, children }: FormProps): JSX.Element => {
return (
<form
onSubmit={(e): void => {
e.preventDefault();
}}
className="relative flex-grow gap-3 p-2"
>
{loading && <Loading />}
{children}
</form>
);
};

7
src/components/menu/BottomNav.tsx

@ -26,6 +26,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import { Tooltip } from '../generic/Tooltip';
import { VersionInfo } from '../modals/VersionInfo';
// export interface BottomNavProps {
@ -99,6 +100,12 @@ export const BottomNav = (): JSX.Element => {
</div>
<div className="flex">
<VersionInfo
visible={showVersionInfo}
onclose={(): void => {
setShowVersionInfo(false);
}}
/>
<Tooltip contents={`Current Commit`}>
<div
onClick={(): void => {

19
src/components/menu/Navigation.tsx

@ -1,19 +1,11 @@
import type React from 'react';
import { FiGrid, FiMessageSquare, FiPackage, FiSettings } from 'react-icons/fi';
import { FiGrid, FiMessageSquare, FiSettings } from 'react-icons/fi';
import type { Link } from 'type-route';
import { routes, useRoute } from '@core/router';
type DefaultDivProps = JSX.IntrinsicElements['div'];
export type NavigationProps = DefaultDivProps;
export const Navigation = ({
onClick,
className,
...props
}: NavigationProps): JSX.Element => {
export const Navigation = (): JSX.Element => {
const route = useRoute();
return (
@ -34,13 +26,6 @@ export const Navigation = ({
link={routes.nodes().link}
/>
<NavLink
name="Plugins"
icon={<FiPackage className="w-5 h-5 my-auto group-active:scale-90" />}
active={route.name === 'plugins'}
link={routes.plugins().link}
/>
<NavLink
name="Settings"
icon={<FiSettings className="w-5 h-5 my-auto group-active:scale-90" />}

56
src/components/modals/VersionInfo.tsx

@ -0,0 +1,56 @@
import React from 'react';
import { Modal } from '@components/generic/Modal';
import { Card } from '@meshtastic/components';
export interface VersionInfoProps {
visible: boolean;
onclose: () => void;
}
export const VersionInfo = ({
visible,
onclose,
}: VersionInfoProps): JSX.Element => {
// const { data } = useSWR<CommitHistory>(
// `query {
// repository(owner: "meshtastic", name: "meshtastic-web") {
// ref(qualifiedName: "master") {
// name
// target {
// ... on Commit {
// history(first: 4) {
// edges {
// node {
// abbreviatedOid
// message
// author {
// avatarUrl
// name
// }
// }
// }
// }
// }
// }
// }
// }
// }`,
// fetcher,
// );
// console.log(data);
return (
<Modal
open={visible}
onClose={(): void => {
onclose();
}}
>
<Card>
<div className="w-full max-w-3xl p-10">Version Info</div>
{/* {data?.sha} */}
</Card>
</Modal>
);
};

76
src/components/pages/settings/plugins/PluginsSidebar.tsx

@ -0,0 +1,76 @@
import type React from 'react';
import { FiCode, FiSliders } from 'react-icons/fi';
import { TabButton } from '@app/components/TabButton';
import type { Plugin } from '@app/pages/settings/Plugins';
import { Sidebar } from '@components/generic/Sidebar';
import { Tab } from '@headlessui/react';
import { ExternalNotificationsDebugPanel } from './panels/ExternalNotifications/DebugPanel';
import { ExternalNotificationsSettingsPlanel } from './panels/ExternalNotifications/SettingsPlanel';
import { RangeTestDebugPanel } from './panels/RangeTest/DebugPanel';
import { RangeTestSettingsPanel } from './panels/RangeTest/SettingsPanel';
import { SerialDebugPanel } from './panels/Serial/DebugPanel';
import { SerialSettingsPanel } from './panels/Serial/SettingsPanel';
import { StoreForwardDebugPanel } from './panels/StoreForward/DebugPanel';
import { StoreForwardSettingsPanel } from './panels/StoreForward/SettingsPanel';
export interface PluginsSidebarProps {
plugin?: Plugin;
closeSidebar: () => void;
}
export const PluginsSidebar = ({
plugin,
closeSidebar,
}: PluginsSidebarProps): JSX.Element => {
return (
<Sidebar
title={plugin ?? 'Please select plugin'}
tagline={plugin ? 'settings' : '...'}
closeSidebar={closeSidebar}
>
{plugin && (
<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>
<FiCode />
</TabButton>
</Tab.List>
</div>
<Tab.Panels className="flex flex-grow overflow-y-auto bg-gray-100 dark:bg-primaryDark">
{plugin === 'Range Test' && (
<>
<RangeTestSettingsPanel />
<RangeTestDebugPanel />
</>
)}
{plugin === 'External Notifications' && (
<>
<ExternalNotificationsSettingsPlanel />
<ExternalNotificationsDebugPanel />
</>
)}
{plugin === 'Serial' && (
<>
<SerialSettingsPanel />
<SerialDebugPanel />
</>
)}
{plugin === 'Store & Forward' && (
<>
<StoreForwardSettingsPanel />
<StoreForwardDebugPanel />
</>
)}
</Tab.Panels>
</Tab.Group>
)}
</Sidebar>
);
};

32
src/components/pages/settings/plugins/panels/ExternalNotifications/DebugPanel.tsx

@ -0,0 +1,32 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import { Tab } from '@headlessui/react';
export const ExternalNotificationsDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
};
return (
<Tab.Panel className="relative">
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</Tab.Panel>
);
};

97
src/components/pages/settings/plugins/panels/ExternalNotifications/SettingsPlanel.tsx

@ -0,0 +1,97 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Tab } from '@headlessui/react';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<Tab.Panel className="flex flex-col w-full">
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!pluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
suffix="ms"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</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>
);
};

28
src/components/pages/settings/plugins/panels/RangeTest/DebugPanel.tsx

@ -0,0 +1,28 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import { Tab } from '@headlessui/react';
export const RangeTestDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
};
return (
<Tab.Panel className="relative">
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</Tab.Panel>
);
};

79
src/components/pages/settings/plugins/panels/RangeTest/SettingsPanel.tsx

@ -0,0 +1,79 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Tab } from '@headlessui/react';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const RangeTestSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return (
<Tab.Panel className="flex flex-col w-full">
<Form loading={loading}>
<Checkbox
label="Range Test Plugin Enabled?"
{...register('rangeTestPluginEnabled')}
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!pluginEnabled}
{...register('rangeTestPluginSave')}
/>
<Input
type="number"
label="Message Interval"
disabled={!pluginEnabled}
suffix="Seconds"
{...register('rangeTestPluginSender', {
valueAsNumber: true,
})}
/>
</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>
);
};

31
src/components/pages/settings/plugins/panels/Serial/DebugPanel.tsx

@ -0,0 +1,31 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import { Tab } from '@headlessui/react';
export const SerialDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTxd: preferences.serialpluginTxd,
serialpluginTimeout: preferences.serialpluginTimeout,
};
return (
<Tab.Panel className="relative">
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</Tab.Panel>
);
};

100
src/components/pages/settings/plugins/panels/Serial/SettingsPanel.tsx

@ -0,0 +1,100 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Tab } from '@headlessui/react';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const SerialSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<Tab.Panel className="flex flex-col w-full">
<Form loading={loading}>
<Checkbox label="Plugin Enabled" {...register('serialpluginEnabled')} />
<Checkbox
label="Echo"
disabled={!pluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!pluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!pluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!pluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!pluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</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>
);
};

32
src/components/pages/settings/plugins/panels/StoreForward/DebugPanel.tsx

@ -0,0 +1,32 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import { Tab } from '@headlessui/react';
export const StoreForwardDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
storeForwardPluginHistoryReturnMax:
preferences.storeForwardPluginHistoryReturnMax,
storeForwardPluginHistoryReturnWindow:
preferences.storeForwardPluginHistoryReturnWindow,
};
return (
<Tab.Panel className="relative">
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</Tab.Panel>
);
};

95
src/components/pages/settings/plugins/panels/StoreForward/SettingsPanel.tsx

@ -0,0 +1,95 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Tab } from '@headlessui/react';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const StoreForwardSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'storeForwardPluginEnabled',
defaultValue: false,
});
return (
<Tab.Panel className="flex flex-col w-full">
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('storeForwardPluginEnabled')}
/>
<Checkbox
label="Heartbeat Enabled"
disabled={!pluginEnabled}
{...register('storeForwardPluginHeartbeat')}
/>
<Input
type="number"
label="Number of records"
suffix="Records"
disabled={!pluginEnabled}
{...register('storeForwardPluginRecords', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return max"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnMax', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return window"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnWindow', {
valueAsNumber: true,
})}
/>
</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>
);
};

0
src/components/pages/settings/channels/ChannelsSidebar.tsx → src/components/pages/settings/radio/channels/ChannelsSidebar.tsx

0
src/components/pages/settings/channels/panels/DebugPanel.tsx → src/components/pages/settings/radio/channels/panels/DebugPanel.tsx

0
src/components/pages/settings/channels/panels/QRCodePanel.tsx → src/components/pages/settings/radio/channels/panels/QRCodePanel.tsx

7
src/components/pages/settings/channels/panels/SettingsPanel.tsx → src/components/pages/settings/radio/channels/panels/SettingsPanel.tsx

@ -5,7 +5,7 @@ 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 { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { Tab } from '@headlessui/react';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
@ -73,8 +73,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
return (
<Tab.Panel className="flex flex-col w-full">
{loading && <Loading />}
<form className="flex-grow gap-3 p-2">
<Form loading={loading}>
{channel?.index !== 0 && (
<>
<Checkbox
@ -122,7 +121,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
/>
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
</form>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="p-2 ml-auto">
<IconButton

1
src/core/router.ts

@ -3,6 +3,5 @@ import { createRouter, defineRoute } from 'type-route';
export const { RouteProvider, useRoute, routes } = createRouter({
messages: defineRoute('/'),
nodes: defineRoute('/nodes'),
plugins: defineRoute('/plugins'),
settings: defineRoute('/settings'),
});

135
src/pages/Plugins/ExternalNotification.tsx

@ -1,135 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface ExternalNotificationProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const ExternalNotification = ({
navOpen,
setNavOpen,
}: ExternalNotificationProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs:
preferences.extNotificationPluginOutputMs,
},
});
React.useEffect(() => {
reset({
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchExternalNotificationPluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="External Notification"
tagline="Plugin"
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="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

128
src/pages/Plugins/Files.tsx

@ -1,128 +0,0 @@
import type React from 'react';
// import { DefaultExtensionType, defaultStyles, FileIcon } from 'react-file-icon';
import { FiMenu, FiTrash, FiUploadCloud } from 'react-icons/fi';
import useSWR from 'swr';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connectionUrl } from '@core/connection';
import fetcher from '@core/utils/fetcher';
import { Card, IconButton } from '@meshtastic/components';
export interface RangeTestProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
interface IFile {
name: string;
nameModified: string;
size: number;
}
interface IFiles {
data: {
files: IFile[];
filesystem: {
free: number;
total: number;
used: number;
};
};
status: boolean;
}
export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
const { data } = useSWR<IFiles>(
`http://${connectionUrl}/json/spiffs/browse/static`,
fetcher,
);
return (
<PrimaryTemplate
title="File Browser"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
>
<div className="flex flex-col justify-between w-full gap-4 md:flex-row-reverse">
<Card title="SPIFFS" description="Statistics">
{data ? (
<div className="flex">
<div className="mx-auto my-4 bg-gray-500 rounded-3xl">
<div></div>
{JSON.stringify(data.data.filesystem.used)} bytes total
</div>
</div>
) : (
<div>Loading...</div>
)}
</Card>
<Card
title="Files"
description="SPIFFS Contents"
buttons={<IconButton icon={<FiUploadCloud className="w-8 h-8" />} />}
className="md:w-1/3"
>
{data ? (
<div className="flex flex-col my-4 space-y-2">
{data.data.files.map((file: IFile) => (
<div
key={file.name}
className="flex justify-between mx-4 bg-gray-300 rounded-md dark:bg-secondaryDark "
>
<div className="flex p-2 max-h-12">
<div className="flex w-12">
{/* <FileIcon
extension={
(file.nameModified ?? file.name).split('.')[
(file.nameModified ?? file.name).split('.').length -
1
]
}
{...defaultStyles[
(file.nameModified ?? file.name).split('.')[
(file.nameModified ?? file.name).split('.').length -
1
] as DefaultExtensionType
]}
/> */}
</div>
<a
href={`http://${connectionUrl}/${file.name.replace(
'static/',
'',
)}`}
className="my-auto font-semibold"
>
{file.nameModified ?? file.name}
</a>
</div>
<IconButton
className="mx-2 my-auto"
// confirmAction={async (): Promise<void> => {
// await fetch(
// `http://${connectionUrl}/json/spiffs/delete/static?remove=${file.name}`,
// {
// method: 'DELETE',
// },
// );
// }}
icon={<FiTrash className="w-5 h-5" />}
/>
</div>
))}
</div>
) : (
<div>Loading...</div>
)}
</Card>
</div>
</PrimaryTemplate>
);
};

59
src/pages/Plugins/Index.tsx

@ -1,59 +0,0 @@
import type React from 'react';
import {
FiAlignLeft,
FiBell,
FiFastForward,
FiFileText,
FiRss,
} from 'react-icons/fi';
import { PageLayout } from '@components/templates/PageLayout';
import { ExternalNotification } from './ExternalNotification';
import { Files } from './Files';
import { RangeTest } from './RangeTest';
import { Serial } from './Serial';
import { StoreAndForward } from './StoreAndForward';
export const Plugins = (): JSX.Element => {
return (
<PageLayout
title="Plugins"
sidebarItems={[
{
title: 'Range Test',
description: 'Test the range of your Meshtastic node',
icon: <FiRss className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'File Browser',
description: 'HTTP only file browser',
icon: <FiFileText className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'External Notification',
description: 'External hardware alerts',
icon: <FiBell className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Serial',
description: 'Send serial data over the mesh',
icon: <FiAlignLeft className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Store & Forward',
description: 'Retrive message history',
icon: <FiFastForward className="flex-shrink-0 w-6 h-6" />,
},
]}
panels={[
<RangeTest key={1} />,
<Files key={2} />,
<ExternalNotification key={3} />,
<Serial key={4} />,
<StoreAndForward key={5} />,
]}
/>
);
};

106
src/pages/Plugins/RangeTest.tsx

@ -1,106 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface RangeTestProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const RangeTest = ({
navOpen,
setNavOpen,
}: RangeTestProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
},
});
React.useEffect(() => {
reset({
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchRangeTestPluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Range Test"
tagline="Plugin"
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="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Range Test Plugin Enabled?"
{...register('rangeTestPluginEnabled')}
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSave')}
/>
<Input
type="number"
label="Message Interval"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSender', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

133
src/pages/Plugins/Serial.tsx

@ -1,133 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface SerialProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Serial = ({ navOpen, setNavOpen }: SerialProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
},
});
React.useEffect(() => {
reset({
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchSerialPluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Serial"
tagline="Plugin"
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="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('serialpluginEnabled')}
/>
<Checkbox
label="Echo"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

98
src/pages/Plugins/StoreAndForward.tsx

@ -1,98 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface StoreAndForwardProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const StoreAndForward = ({
navOpen,
setNavOpen,
}: StoreAndForwardProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
},
});
React.useEffect(() => {
reset({
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchStoreForwardPluginEnabled = useWatch({
control,
name: 'storeForwardPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Serial"
tagline="Plugin"
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="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('storeForwardPluginEnabled')}
/>
<Input
type="number"
label="Number of records"
disabled={!watchStoreForwardPluginEnabled}
{...register('storeForwardPluginRecords', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

4
src/pages/settings/Channels.tsx

@ -11,7 +11,6 @@ import {
import { Tooltip } from '@app/components/generic/Tooltip';
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
import { FormFooter } from '@components/FormFooter';
import { Loading } from '@components/generic/Loading';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
@ -21,11 +20,12 @@ import {
Checkbox,
IconButton,
Input,
Loading,
Select,
} from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { ChannelsSidebar } from '../../components/pages/settings/channels/ChannelsSidebar';
import { ChannelsSidebar } from '../../components/pages/settings/radio/channels/ChannelsSidebar';
export interface ChannelsProps {
navOpen?: boolean;

12
src/pages/settings/Index.tsx

@ -4,6 +4,7 @@ import {
FiLayers,
FiLayout,
FiMapPin,
FiPackage,
FiRadio,
FiUser,
FiWifi,
@ -15,6 +16,7 @@ import { PageLayout } from '@components/templates/PageLayout';
import { Channels } from './Channels';
import { Interface } from './Interface';
import { Plugins } from './Plugins';
import { Position } from './Position';
import { Power } from './Power';
import { Radio } from './Radio';
@ -31,8 +33,9 @@ export const Settings = (): JSX.Element => {
<User key={4} />,
<Power key={5} />,
<Radio key={6} />,
<Channels key={8} />,
<Interface key={7} />,
<Channels key={7} />,
<Plugins key={8} />,
<Interface key={9} />,
];
const sidebarItems: SidebarItemProps[] = [
@ -56,6 +59,11 @@ export const Settings = (): JSX.Element => {
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Plugins',
description: 'Plugins',
icon: <FiPackage className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Language and UI settings',

130
src/pages/settings/Plugins.tsx

@ -0,0 +1,130 @@
import React from 'react';
import {
FiAlignLeft,
FiBell,
FiExternalLink,
FiFastForward,
FiMenu,
FiRss,
} from 'react-icons/fi';
import { PluginsSidebar } from '@app/components/pages/settings/plugins/PluginsSidebar';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Button, Card, IconButton } from '@meshtastic/components';
export type Plugin =
| 'Range Test'
| 'External Notifications'
| 'Serial'
| 'Store & Forward';
export interface PluginsProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Plugins = ({ navOpen, setNavOpen }: PluginsProps): JSX.Element => {
const [sidebarOpen, setSidebarOpen] = React.useState(false);
const [selectedPlugin, setSelectedPlugin] = React.useState<
Plugin | undefined
>();
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const plugins: {
name: Plugin;
description: string;
enabled: boolean;
icon: JSX.Element;
}[] = [
{
name: 'Range Test',
description: 'Test the range of your Meshtastic node',
enabled: preferences.rangeTestPluginEnabled,
icon: <FiRss />,
},
{
name: 'External Notifications',
description: 'External hardware alerts',
enabled: preferences.extNotificationPluginEnabled,
icon: <FiBell />,
},
{
name: 'Serial',
description: 'Send serial data over the mesh',
enabled: preferences.serialpluginEnabled,
icon: <FiAlignLeft />,
},
{
name: 'Store & Forward',
description: 'Retrive message history',
enabled: preferences.storeForwardPluginEnabled,
icon: <FiFastForward />,
},
];
return (
<>
<PrimaryTemplate
title="Plugins"
tagline="Settings"
leftButton={
<Button
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
>
<Card
title="Basic settings"
description="Device name and user parameters"
>
<div className="w-full max-w-3xl p-10 space-y-2 md:max-w-xl">
{plugins.map((plugin, index) => (
<div
key={index}
onClick={(): void => {
setSelectedPlugin(plugin.name);
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 ${
selectedPlugin === plugin.name
? 'border-primary dark:border-primary'
: ''
}`}
>
<div className="flex my-auto space-x-2">
<div
className={`h-3 my-auto w-3 rounded-full ${
plugin.enabled ? 'bg-green-500' : 'bg-gray-400'
}`}
/>
<div className="flex gap-2">
<div className="my-auto">{plugin.icon}</div>
{plugin.name}
</div>
</div>
<div className="flex gap-2">
<IconButton active icon={<FiExternalLink />} />
</div>
</div>
))}
</div>
</Card>
</PrimaryTemplate>
{sidebarOpen && (
<PluginsSidebar
closeSidebar={(): void => {
setSidebarOpen(false);
}}
plugin={selectedPlugin}
/>
)}
</>
);
};
Loading…
Cancel
Save