Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72fc3ea337 | ||
|
|
82f4784107 | ||
|
|
4f9fb9976d | ||
|
|
42068ad3d8 | ||
|
|
62f8c4509e | ||
|
|
d699764546 | ||
|
|
8549d56c21 | ||
|
|
4b532fc7f8 | ||
|
|
06d2c393ce | ||
|
|
cecdf9758b | ||
|
|
02cb4f2584 | ||
|
|
8cfcd7b1af | ||
|
|
c0cb059f52 | ||
|
|
a2a9b37238 | ||
|
|
57d0d27bbb | ||
|
|
0e92dd9bea | ||
|
|
c16ebf3917 | ||
|
|
3d3a08a23f | ||
|
|
4d1227a942 | ||
|
|
a8ee273b24 | ||
|
|
3ee7a57480 | ||
|
|
2f2c777c56 | ||
|
|
2f36118e9d | ||
|
|
a6d161581f | ||
|
|
d05ea5a2cc | ||
|
|
471db94242 | ||
|
|
2654e4fbc9 | ||
|
|
f2aa5bfbee | ||
|
|
3b018b0c70 | ||
|
|
921db10d91 | ||
|
|
bf4f593e3a | ||
|
|
1e061a1e19 | ||
|
|
9b9f537e2c | ||
|
|
985cce0b0d | ||
|
|
3fe38eb506 | ||
|
|
51081d3052 | ||
|
|
c08f6d16bb | ||
|
|
62ad4c49f8 | ||
|
|
3b0a1e6108 | ||
|
|
c2f2205626 | ||
|
|
87c729d694 | ||
|
|
8e4f60edf3 | ||
|
|
8811eee9f5 | ||
|
|
2af93f1acd | ||
|
|
78a35544c7 | ||
|
|
3ad2d650b0 | ||
|
|
2e12b27566 | ||
|
|
989fad7e17 | ||
|
|
f7a2e5f76b | ||
|
|
2b81fc47e2 | ||
|
|
b522113cd7 | ||
|
|
fce642c24e | ||
|
|
bf425a8ec7 | ||
|
|
a7d0d36086 | ||
|
|
5e72510bef | ||
|
|
88efdc4758 | ||
|
|
41acc4d25d | ||
|
|
d10c010b9a | ||
|
|
80ab44c8db | ||
|
|
7895df2d9f | ||
|
|
c780437355 | ||
|
|
9d4aa05316 | ||
|
|
354d04592b | ||
|
|
9bea6870bb | ||
|
|
d0bd02980d | ||
|
|
7d1135b9dc | ||
|
|
f66332b3e3 | ||
|
|
05a6b6293e | ||
|
|
b9a8a2ba6c | ||
|
|
c16070f02b | ||
|
|
3e6653a98f | ||
|
|
59126ca939 | ||
|
|
4bde402a53 | ||
|
|
4cf91272de | ||
|
|
c711c39aa9 | ||
|
|
c4342f1a2b | ||
|
|
afc45588fa | ||
|
|
22cd5aa88d | ||
|
|
8c4c8a760e | ||
|
|
0911df6b0d | ||
|
|
06c20fa950 | ||
|
|
22d900a831 | ||
|
|
c6cc5e5e6f | ||
|
|
f0d8db1c87 | ||
|
|
8c37be4af3 | ||
|
|
076dae80b7 | ||
|
|
7d4001ea9d | ||
|
|
8ed3ce8203 | ||
|
|
ebd5a3d3a6 | ||
|
|
1cdf18747d | ||
|
|
6c1f140ad1 | ||
|
|
4c4be2e18f |
@@ -1,6 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: push
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -21,42 +24,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Package Output
|
|
||||||
run: pnpm package
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
|
||||||
with:
|
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
automatic_release_tag: "latest"
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
./dist/build.tar
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Buildah Build
|
|
||||||
id: build-container
|
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
|
||||||
containerfiles: |
|
|
||||||
./Containerfile
|
|
||||||
image: ${{github.event.repository.full_name}}
|
|
||||||
tags: latest ${{ github.sha }}
|
|
||||||
oci: true
|
|
||||||
platforms: linux/amd64, linux/arm64
|
|
||||||
|
|
||||||
- name: Push To Registry
|
|
||||||
id: push-to-registry
|
|
||||||
uses: redhat-actions/push-to-registry@v2
|
|
||||||
with:
|
|
||||||
image: ${{ steps.build-container.outputs.image }}
|
|
||||||
tags: ${{ steps.build-container.outputs.tags }}
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Print image url
|
|
||||||
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
|
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Compress build
|
||||||
|
run: pnpm package
|
||||||
|
|
||||||
|
- name: Archive compressed build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist/build.tar
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: 'Release'
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Package Output
|
||||||
|
run: pnpm package
|
||||||
|
|
||||||
|
- name: Archive compressed build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist/build.tar
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Buildah Build
|
||||||
|
id: build-container
|
||||||
|
uses: redhat-actions/buildah-build@v2
|
||||||
|
with:
|
||||||
|
containerfiles: |
|
||||||
|
./Containerfile
|
||||||
|
image: ${{github.event.repository.full_name}}
|
||||||
|
tags: latest ${{ github.sha }}
|
||||||
|
oci: true
|
||||||
|
platforms: linux/amd64, linux/arm64
|
||||||
|
|
||||||
|
- name: Push To Registry
|
||||||
|
id: push-to-registry
|
||||||
|
uses: redhat-actions/push-to-registry@v2
|
||||||
|
with:
|
||||||
|
image: ${{ steps.build-container.outputs.image }}
|
||||||
|
tags: ${{ steps.build-container.outputs.tags }}
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Print image url
|
||||||
|
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
|
||||||
+12
-10
@@ -5,11 +5,12 @@
|
|||||||
"description": "Meshtastic web client",
|
"description": "Meshtastic web client",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"build": "rsbuild build",
|
||||||
"build": "tsc && pnpm check && vite build ",
|
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
"check:fix": "pnpm check --write",
|
"check:fix": "pnpm check --write",
|
||||||
"preview": "vite preview",
|
"dev": "rsbuild dev --open",
|
||||||
|
"format": "biome format --write",
|
||||||
|
"preview": "rsbuild preview",
|
||||||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
|
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^1.10.0",
|
"@bufbuild/protobuf": "^1.10.0",
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@meshtastic/js": "2.3.7-1",
|
"@meshtastic/js": "2.3.7-5",
|
||||||
|
"@noble/curves": "^1.5.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.0",
|
"@radix-ui/react-checkbox": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "^3.6.0",
|
||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -59,18 +61,20 @@
|
|||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"timeago-react": "^3.0.6",
|
"timeago-react": "^3.0.6",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"zustand": "4.5.2"
|
"zustand": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.8.2",
|
"@biomejs/biome": "^1.8.2",
|
||||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1",
|
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
|
||||||
|
"@rsbuild/core": "^1.0.10",
|
||||||
|
"@rsbuild/plugin-react": "^1.0.3",
|
||||||
"@types/chrome": "^0.0.263",
|
"@types/chrome": "^0.0.263",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/w3c-web-serial": "^1.0.6",
|
"@types/w3c-web-serial": "^1.0.6",
|
||||||
"@types/web-bluetooth": "^0.0.20",
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"gzipper": "^7.2.0",
|
"gzipper": "^7.2.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
@@ -78,8 +82,6 @@
|
|||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2"
|
||||||
"vite": "^5.3.1",
|
|
||||||
"vite-plugin-environment": "^1.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+987
-453
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineConfig } from "@rsbuild/core";
|
||||||
|
import { pluginReact } from "@rsbuild/plugin-react";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
let hash = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
hash = execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
} catch (error) {
|
||||||
|
hash = "DEV";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [pluginReact()],
|
||||||
|
source: {
|
||||||
|
define: {
|
||||||
|
"process.env.COMMIT_HASH": JSON.stringify(hash),
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
"@app": "./src",
|
||||||
|
"@pages": "./src/pages",
|
||||||
|
"@components": "./src/components",
|
||||||
|
"@core": "./src/core",
|
||||||
|
"@layouts": "./src/layouts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
title: "Meshtastic Web",
|
||||||
|
},
|
||||||
|
});
|
||||||
+16
-11
@@ -1,14 +1,15 @@
|
|||||||
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||||
import { PageRouter } from "@app/PageRouter.js";
|
import { PageRouter } from "@app/PageRouter.tsx";
|
||||||
import { CommandPalette } from "@components/CommandPalette.js";
|
import { CommandPalette } from "@components/CommandPalette.tsx";
|
||||||
import { DeviceSelector } from "@components/DeviceSelector.js";
|
import { DeviceSelector } from "@components/DeviceSelector.tsx";
|
||||||
import { DialogManager } from "@components/Dialog/DialogManager.js";
|
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||||
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js";
|
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
|
||||||
import { Toaster } from "@components/Toaster.js";
|
import { Toaster } from "@components/Toaster.tsx";
|
||||||
import { ThemeController } from "@components/generic/ThemeController.js";
|
import Footer from "@components/UI/Footer.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { ThemeController } from "@components/generic/ThemeController.tsx";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { Dashboard } from "@pages/Dashboard/index.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
|
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||||
import { MapProvider } from "react-map-gl";
|
import { MapProvider } from "react-map-gl";
|
||||||
|
|
||||||
export const App = (): JSX.Element => {
|
export const App = (): JSX.Element => {
|
||||||
@@ -40,7 +41,11 @@ export const App = (): JSX.Element => {
|
|||||||
<PageRouter />
|
<PageRouter />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeviceContext } from "@core/stores/deviceStore.js";
|
import { DeviceContext } from "@core/stores/deviceStore.ts";
|
||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.ts";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export interface DeviceWrapperProps {
|
export interface DeviceWrapperProps {
|
||||||
|
|||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { ChannelsPage } from "@pages/Channels.js";
|
import { ChannelsPage } from "@pages/Channels.tsx";
|
||||||
import { ConfigPage } from "@pages/Config/index.js";
|
import { ConfigPage } from "@pages/Config/index.tsx";
|
||||||
import { MapPage } from "@pages/Map.js";
|
import { MapPage } from "@pages/Map.tsx";
|
||||||
import { MessagesPage } from "@pages/Messages.js";
|
import { MessagesPage } from "@pages/Messages.tsx";
|
||||||
import { NodesPage } from "@pages/Nodes.js";
|
import { NodesPage } from "@pages/Nodes.tsx";
|
||||||
|
|
||||||
export const PageRouter = (): JSX.Element => {
|
export const PageRouter = (): JSX.Element => {
|
||||||
const { activePage } = useDevice();
|
const { activePage } = useDevice();
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@components/UI/Command.js";
|
} from "@components/UI/Command.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { useCommandState } from "cmdk";
|
import { useCommandState } from "cmdk";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.js";
|
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.tsx";
|
||||||
import { Separator } from "@components/UI/Seperator.js";
|
import { Separator } from "@components/UI/Seperator.tsx";
|
||||||
import { Code } from "@components/UI/Typography/Code.js";
|
import { Code } from "@components/UI/Typography/Code.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
|
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
export const DialogManager = (): JSX.Element => {
|
export const DialogManager = (): JSX.Element => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Checkbox } from "@components/UI/Checkbox.js";
|
import { Checkbox } from "@components/UI/Checkbox.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,11 +7,11 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Switch } from "@components/UI/Switch.js";
|
import { Switch } from "@components/UI/Switch.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { toByteArray } from "base64-js";
|
import { toByteArray } from "base64-js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { BLE } from "@components/PageComponents/Connect/BLE.js";
|
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
|
||||||
import { HTTP } from "@components/PageComponents/Connect/HTTP.js";
|
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
|
||||||
import { Serial } from "@components/PageComponents/Connect/Serial.js";
|
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@components/UI/Tabs.js";
|
} from "@components/UI/Tabs.tsx";
|
||||||
import { Link } from "@components/UI/Typography/Link.js";
|
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
|
|
||||||
export interface TabElementProps {
|
export interface TabElementProps {
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
|
||||||
|
export interface PkiRegenerateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PkiRegenerateDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
}: PkiRegenerateDialogProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Regenerate Key pair?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to regenerate key pair?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="destructive" onClick={() => onSubmit()}>
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Checkbox } from "@components/UI/Checkbox.js";
|
import { Checkbox } from "@components/UI/Checkbox.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Protobuf, type Types } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
import { fromByteArray } from "base64-js";
|
import { fromByteArray } from "base64-js";
|
||||||
import { ClipboardIcon } from "lucide-react";
|
import { ClipboardIcon } from "lucide-react";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
|
|
||||||
export interface RemoveNodeDialogProps {
|
export interface RemoveNodeDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { ClockIcon, PowerIcon } from "lucide-react";
|
import { ClockIcon, PowerIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
DynamicFormField,
|
DynamicFormField,
|
||||||
type FieldProps,
|
type FieldProps,
|
||||||
} from "@components/Form/DynamicFormField.js";
|
} from "@components/Form/DynamicFormField.tsx";
|
||||||
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { H4 } from "@components/UI/Typography/H4.js";
|
import { H4 } from "@components/UI/Typography/H4.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import {
|
import {
|
||||||
type Control,
|
type Control,
|
||||||
type DefaultValues,
|
type DefaultValues,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
GenericInput,
|
GenericInput,
|
||||||
type InputFieldProps,
|
type InputFieldProps,
|
||||||
} from "@components/Form/FormInput.js";
|
} from "@components/Form/FormInput.tsx";
|
||||||
import {
|
import {
|
||||||
PasswordGenerator,
|
PasswordGenerator,
|
||||||
type PasswordGeneratorProps,
|
type PasswordGeneratorProps,
|
||||||
} from "@components/Form/FormPasswordGenerator.js";
|
} from "@components/Form/FormPasswordGenerator.tsx";
|
||||||
import {
|
import {
|
||||||
type SelectFieldProps,
|
type SelectFieldProps,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
} from "@components/Form/FormSelect.js";
|
} from "@components/Form/FormSelect.tsx";
|
||||||
import {
|
import {
|
||||||
type ToggleFieldProps,
|
type ToggleFieldProps,
|
||||||
ToggleInput,
|
ToggleInput,
|
||||||
} from "@components/Form/FormToggle.js";
|
} from "@components/Form/FormToggle.tsx";
|
||||||
import type { Control, FieldValues } from "react-hook-form";
|
import type { Control, FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export type FieldProps<T> =
|
export type FieldProps<T> =
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import type {
|
import type {
|
||||||
BaseFormBuilderProps,
|
BaseFormBuilderProps,
|
||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import type { ChangeEventHandler } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "text" | "number" | "password";
|
type: "text" | "number" | "password";
|
||||||
|
inputChange?: ChangeEventHandler;
|
||||||
properties?: {
|
properties?: {
|
||||||
|
value?: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
step?: number;
|
step?: number;
|
||||||
@@ -24,22 +29,38 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
disabled,
|
disabled,
|
||||||
field,
|
field,
|
||||||
}: GenericFormElementProps<T, InputFieldProps<T>>) {
|
}: GenericFormElementProps<T, InputFieldProps<T>>) {
|
||||||
|
const [passwordShown, setPasswordShown] = useState(false);
|
||||||
|
const togglePasswordVisiblity = () => {
|
||||||
|
setPasswordShown(!passwordShown);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={field.name}
|
name={field.name}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange, ...rest } }) => (
|
render={({ field: { value, onChange, ...rest } }) => (
|
||||||
<Input
|
<Input
|
||||||
type={field.type}
|
type={
|
||||||
|
field.type === "password" && passwordShown ? "text" : field.type
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
field.type === "password"
|
||||||
|
? {
|
||||||
|
icon: passwordShown ? EyeOff : Eye,
|
||||||
|
onClick: togglePasswordVisiblity,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
step={field.properties?.step}
|
step={field.properties?.step}
|
||||||
value={field.type === "number" ? Number.parseFloat(value) : value}
|
value={field.type === "number" ? Number.parseFloat(value) : value}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
if (field.inputChange) field.inputChange(e);
|
||||||
onChange(
|
onChange(
|
||||||
field.type === "number"
|
field.type === "number"
|
||||||
? Number.parseFloat(e.target.value)
|
? Number.parseFloat(e.target.value)
|
||||||
: e.target.value,
|
: e.target.value,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
{...field.properties}
|
{...field.properties}
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type {
|
import type {
|
||||||
BaseFormBuilderProps,
|
BaseFormBuilderProps,
|
||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { Generator } from "@components/UI/Generator.js";
|
import { Generator } from "@components/UI/Generator.tsx";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "passwordGenerator";
|
type: "passwordGenerator";
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
devicePSKBitCount: number;
|
devicePSKBitCount: number;
|
||||||
inputChange: ChangeEventHandler;
|
inputChange: ChangeEventHandler;
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
@@ -20,14 +23,28 @@ export function PasswordGenerator<T extends FieldValues>({
|
|||||||
field,
|
field,
|
||||||
disabled,
|
disabled,
|
||||||
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
||||||
|
const [passwordShown, setPasswordShown] = useState(false);
|
||||||
|
const togglePasswordVisiblity = () => {
|
||||||
|
setPasswordShown(!passwordShown);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={field.name}
|
name={field.name}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, ...rest } }) => (
|
render={({ field: { value, ...rest } }) => (
|
||||||
<Generator
|
<Generator
|
||||||
hide={field.hide}
|
type={field.hide && !passwordShown ? "password" : "text"}
|
||||||
|
action={
|
||||||
|
field.hide
|
||||||
|
? {
|
||||||
|
icon: passwordShown ? EyeOff : Eye,
|
||||||
|
onClick: togglePasswordVisiblity,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
devicePSKBitCount={field.devicePSKBitCount}
|
devicePSKBitCount={field.devicePSKBitCount}
|
||||||
|
bits={field.bits}
|
||||||
inputChange={field.inputChange}
|
inputChange={field.inputChange}
|
||||||
selectChange={field.selectChange}
|
selectChange={field.selectChange}
|
||||||
buttonClick={field.buttonClick}
|
buttonClick={field.buttonClick}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type {
|
import type {
|
||||||
BaseFormBuilderProps,
|
BaseFormBuilderProps,
|
||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.js";
|
} from "@components/UI/Select.tsx";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
BaseFormBuilderProps,
|
BaseFormBuilderProps,
|
||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { Switch } from "@components/UI/Switch.js";
|
import { Switch } from "@components/UI/Switch.tsx";
|
||||||
import type { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
|
|
||||||
export interface FieldWrapperProps {
|
export interface FieldWrapperProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ChannelValidation } from "@app/validation/channel.js";
|
import type { ChannelValidation } from "@app/validation/channel.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
@@ -111,10 +111,14 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
type: "select",
|
type: "select",
|
||||||
name: "role",
|
name: "role",
|
||||||
label: "Role",
|
label: "Role",
|
||||||
|
disabled: channel.index === 0,
|
||||||
description:
|
description:
|
||||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.Channel.Channel_Role,
|
enumValue:
|
||||||
|
channel.index === 0
|
||||||
|
? { PRIMARY: 1 }
|
||||||
|
: { DISABLED: 0, SECONDARY: 2 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -127,6 +131,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
inputChange: inputChangeEvent,
|
inputChange: inputChangeEvent,
|
||||||
selectChange: selectChangeEvent,
|
selectChange: selectChangeEvent,
|
||||||
buttonClick: clickEvent,
|
buttonClick: clickEvent,
|
||||||
|
hide: true,
|
||||||
properties: {
|
properties: {
|
||||||
value: pass,
|
value: pass,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.js";
|
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Bluetooth = (): JSX.Element => {
|
export const Bluetooth = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DeviceValidation } from "@app/validation/config/device.js";
|
import type { DeviceValidation } from "@app/validation/config/device.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Device = (): JSX.Element => {
|
export const Device = (): JSX.Element => {
|
||||||
@@ -32,7 +32,22 @@ export const Device = (): JSX.Element => {
|
|||||||
label: "Role",
|
label: "Role",
|
||||||
description: "What role the device performs on the mesh",
|
description: "What role the device performs on the mesh",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
|
enumValue: {
|
||||||
|
Client: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||||
|
"Client Mute":
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_MUTE,
|
||||||
|
Router: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||||
|
Repeater: Protobuf.Config.Config_DeviceConfig_Role.REPEATER,
|
||||||
|
Tracker: Protobuf.Config.Config_DeviceConfig_Role.TRACKER,
|
||||||
|
Sensor: Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||||
|
TAK: Protobuf.Config.Config_DeviceConfig_Role.TAK,
|
||||||
|
"Client Hidden":
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_HIDDEN,
|
||||||
|
"Lost and Found":
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
|
||||||
|
"TAK Tracker":
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||||
|
},
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DisplayValidation } from "@app/validation/config/display.js";
|
import type { DisplayValidation } from "@app/validation/config/display.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Display = (): JSX.Element => {
|
export const Display = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LoRaValidation } from "@app/validation/config/lora.js";
|
import type { LoRaValidation } from "@app/validation/config/lora.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const LoRa = (): JSX.Element => {
|
export const LoRa = (): JSX.Element => {
|
||||||
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
|
|||||||
label: "Ignore MQTT",
|
label: "Ignore MQTT",
|
||||||
description: "Don't forward MQTT messages over the mesh",
|
description: "Don't forward MQTT messages over the mesh",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "configOkToMqtt",
|
||||||
|
label: "OK to MQTT",
|
||||||
|
description:
|
||||||
|
"When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { NetworkValidation } from "@app/validation/config/network.js";
|
import type { NetworkValidation } from "@app/validation/config/network.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
import {
|
||||||
|
convertIntToIpAddress,
|
||||||
|
convertIpAddressToInt,
|
||||||
|
} from "@core/utils/ip.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Network = (): JSX.Element => {
|
export const Network = (): JSX.Element => {
|
||||||
@@ -13,9 +17,12 @@ export const Network = (): JSX.Element => {
|
|||||||
case: "network",
|
case: "network",
|
||||||
value: {
|
value: {
|
||||||
...data,
|
...data,
|
||||||
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config(
|
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config({
|
||||||
data.ipv4Config,
|
ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0,
|
||||||
),
|
gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0,
|
||||||
|
subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0,
|
||||||
|
dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -25,7 +32,19 @@ export const Network = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<DynamicForm<NetworkValidation>
|
<DynamicForm<NetworkValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
defaultValues={config.network}
|
defaultValues={{
|
||||||
|
...config.network,
|
||||||
|
ipv4Config: {
|
||||||
|
ip: convertIntToIpAddress(config.network?.ipv4Config?.ip ?? 0),
|
||||||
|
gateway: convertIntToIpAddress(
|
||||||
|
config.network?.ipv4Config?.gateway ?? 0,
|
||||||
|
),
|
||||||
|
subnet: convertIntToIpAddress(
|
||||||
|
config.network?.ipv4Config?.subnet ?? 0,
|
||||||
|
),
|
||||||
|
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
|
||||||
|
},
|
||||||
|
}}
|
||||||
fieldGroups={[
|
fieldGroups={[
|
||||||
{
|
{
|
||||||
label: "WiFi Config",
|
label: "WiFi Config",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PositionValidation } from "@app/validation/config/position.js";
|
import type { PositionValidation } from "@app/validation/config/position.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Position = (): JSX.Element => {
|
export const Position = (): JSX.Element => {
|
||||||
@@ -77,12 +77,6 @@ export const Position = (): JSX.Element => {
|
|||||||
label: "Enable Pin",
|
label: "Enable Pin",
|
||||||
description: "GPS module enable pin override",
|
description: "GPS module enable pin override",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "number",
|
|
||||||
name: "channelPrecision",
|
|
||||||
label: "Channel Precision",
|
|
||||||
description: "GPS channel precision",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PowerValidation } from "@app/validation/config/power.js";
|
import type { PowerValidation } from "@app/validation/config/power.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Power = (): JSX.Element => {
|
export const Power = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { DynamicForm } from "@app/components/Form/DynamicForm.js";
|
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
||||||
import type { SecurityValidation } from "@app/validation/config/security.js";
|
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import {
|
||||||
|
getX25519PrivateKey,
|
||||||
|
getX25519PublicKey,
|
||||||
|
} from "@app/core/utils/x25519";
|
||||||
|
import type { SecurityValidation } from "@app/validation/config/security.tsx";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -15,7 +19,7 @@ export const Security = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
|
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
|
||||||
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
|
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
|
||||||
config.security?.privateKey.length ?? 16,
|
config.security?.privateKey.length ?? 32,
|
||||||
);
|
);
|
||||||
const [privateKeyValidationText, setPrivateKeyValidationText] =
|
const [privateKeyValidationText, setPrivateKeyValidationText] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
@@ -23,14 +27,11 @@ export const Security = (): JSX.Element => {
|
|||||||
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
||||||
);
|
);
|
||||||
const [adminKey, setAdminKey] = useState<string>(
|
const [adminKey, setAdminKey] = useState<string>(
|
||||||
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
|
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
|
||||||
);
|
|
||||||
const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false);
|
|
||||||
const [adminKeyBitCount, setAdminKeyBitCount] = useState<number>(
|
|
||||||
config.security?.adminKey.length ?? 16,
|
|
||||||
);
|
);
|
||||||
const [adminKeyValidationText, setAdminKeyValidationText] =
|
const [adminKeyValidationText, setAdminKeyValidationText] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const onSubmit = (data: SecurityValidation) => {
|
const onSubmit = (data: SecurityValidation) => {
|
||||||
if (privateKeyValidationText || adminKeyValidationText) return;
|
if (privateKeyValidationText || adminKeyValidationText) return;
|
||||||
@@ -41,7 +42,7 @@ export const Security = (): JSX.Element => {
|
|||||||
case: "security",
|
case: "security",
|
||||||
value: {
|
value: {
|
||||||
...data,
|
...data,
|
||||||
adminKey: toByteArray(adminKey),
|
adminKey: [toByteArray(adminKey)],
|
||||||
privateKey: toByteArray(privateKey),
|
privateKey: toByteArray(privateKey),
|
||||||
publicKey: toByteArray(publicKey),
|
publicKey: toByteArray(publicKey),
|
||||||
},
|
},
|
||||||
@@ -50,73 +51,86 @@ export const Security = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickEvent = (
|
const validateKey = (
|
||||||
setKey: (value: React.SetStateAction<string>) => void,
|
|
||||||
bitCount: number,
|
|
||||||
setValidationText: (
|
|
||||||
value: React.SetStateAction<string | undefined>,
|
|
||||||
) => void,
|
|
||||||
) => {
|
|
||||||
setKey(
|
|
||||||
btoa(
|
|
||||||
cryptoRandomString({
|
|
||||||
length: bitCount ?? 0,
|
|
||||||
type: "alphanumeric",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setValidationText(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePass = (
|
|
||||||
input: string,
|
input: string,
|
||||||
count: number,
|
count: number,
|
||||||
setValidationText: (
|
setValidationText: (
|
||||||
value: React.SetStateAction<string | undefined>,
|
value: React.SetStateAction<string | undefined>,
|
||||||
) => void,
|
) => void,
|
||||||
) => {
|
) => {
|
||||||
|
try {
|
||||||
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
||||||
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
} else {
|
} else {
|
||||||
setValidationText(undefined);
|
setValidationText(undefined);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const privateKeyClickEvent = () => {
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pkiRegenerate = () => {
|
||||||
|
const privateKey = getX25519PrivateKey();
|
||||||
|
const publicKey = getX25519PublicKey(privateKey);
|
||||||
|
|
||||||
|
setPrivateKey(fromByteArray(privateKey));
|
||||||
|
setPublicKey(fromByteArray(publicKey));
|
||||||
|
validateKey(
|
||||||
|
fromByteArray(privateKey),
|
||||||
|
privateKeyBitCount,
|
||||||
|
setPrivateKeyValidationText,
|
||||||
|
);
|
||||||
|
|
||||||
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const privateKeyInputChangeEvent = (
|
const privateKeyInputChangeEvent = (
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
const psk = e.currentTarget?.value;
|
const privateKeyB64String = e.target.value;
|
||||||
setPrivateKey(psk);
|
setPrivateKey(privateKeyB64String);
|
||||||
validatePass(psk, privateKeyBitCount, setPrivateKeyValidationText);
|
validateKey(
|
||||||
|
privateKeyB64String,
|
||||||
|
privateKeyBitCount,
|
||||||
|
setPrivateKeyValidationText,
|
||||||
|
);
|
||||||
|
|
||||||
|
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
|
||||||
|
setPublicKey(fromByteArray(publicKey));
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const psk = e.currentTarget?.value;
|
const psk = e.currentTarget?.value;
|
||||||
setAdminKey(psk);
|
setAdminKey(psk);
|
||||||
validatePass(psk, privateKeyBitCount, setAdminKeyValidationText);
|
validateKey(psk, privateKeyBitCount, setAdminKeyValidationText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const privateKeySelectChangeEvent = (e: string) => {
|
const privateKeySelectChangeEvent = (e: string) => {
|
||||||
const count = Number.parseInt(e);
|
const count = Number.parseInt(e);
|
||||||
setPrivateKeyBitCount(count);
|
setPrivateKeyBitCount(count);
|
||||||
validatePass(privateKey, count, setPrivateKeyValidationText);
|
validateKey(privateKey, count, setPrivateKeyValidationText);
|
||||||
};
|
|
||||||
|
|
||||||
const adminKeySelectChangeEvent = (e: string) => {
|
|
||||||
const count = Number.parseInt(e);
|
|
||||||
setAdminKeyBitCount(count);
|
|
||||||
validatePass(privateKey, count, setAdminKeyValidationText);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<DynamicForm<SecurityValidation>
|
<DynamicForm<SecurityValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
submitType="onChange"
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
...config.security,
|
...config.security,
|
||||||
...{
|
...{
|
||||||
adminKey: adminKey,
|
adminKey: adminKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
|
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||||
|
isManaged: config.security?.isManaged ?? false,
|
||||||
|
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||||
|
serialEnabled: config.security?.serialEnabled ?? false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fieldGroups={[
|
fieldGroups={[
|
||||||
@@ -129,23 +143,13 @@ export const Security = (): JSX.Element => {
|
|||||||
name: "privateKey",
|
name: "privateKey",
|
||||||
label: "Private Key",
|
label: "Private Key",
|
||||||
description: "Used to create a shared key with a remote device",
|
description: "Used to create a shared key with a remote device",
|
||||||
|
bits: [{ text: "256 bit", value: "32", key: "bit256" }],
|
||||||
validationText: privateKeyValidationText,
|
validationText: privateKeyValidationText,
|
||||||
devicePSKBitCount: privateKeyBitCount,
|
devicePSKBitCount: privateKeyBitCount,
|
||||||
inputChange: privateKeyInputChangeEvent,
|
inputChange: privateKeyInputChangeEvent,
|
||||||
selectChange: privateKeySelectChangeEvent,
|
selectChange: privateKeySelectChangeEvent,
|
||||||
hide: !privateKeyVisible,
|
hide: !privateKeyVisible,
|
||||||
buttonClick: () =>
|
buttonClick: privateKeyClickEvent,
|
||||||
clickEvent(
|
|
||||||
setPrivateKey,
|
|
||||||
privateKeyBitCount,
|
|
||||||
setPrivateKeyValidationText,
|
|
||||||
),
|
|
||||||
disabledBy: [
|
|
||||||
{
|
|
||||||
fieldName: "adminChannelEnabled",
|
|
||||||
invert: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
properties: {
|
properties: {
|
||||||
value: privateKey,
|
value: privateKey,
|
||||||
action: {
|
action: {
|
||||||
@@ -161,12 +165,15 @@ export const Security = (): JSX.Element => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
description:
|
description:
|
||||||
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
|
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
|
||||||
|
properties: {
|
||||||
|
value: publicKey,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Admin Settings",
|
label: "Admin Settings",
|
||||||
description: "Settings for Admin ",
|
description: "Settings for Admin",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -183,29 +190,18 @@ export const Security = (): JSX.Element => {
|
|||||||
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "passwordGenerator",
|
type: "text",
|
||||||
name: "adminKey",
|
name: "adminKey",
|
||||||
label: "Admin Key",
|
label: "Admin Key",
|
||||||
description:
|
description:
|
||||||
"The public key authorized to send admin messages to this node",
|
"The public key authorized to send admin messages to this node",
|
||||||
validationText: adminKeyValidationText,
|
validationText: adminKeyValidationText,
|
||||||
devicePSKBitCount: adminKeyBitCount,
|
|
||||||
inputChange: adminKeyInputChangeEvent,
|
inputChange: adminKeyInputChangeEvent,
|
||||||
selectChange: adminKeySelectChangeEvent,
|
disabledBy: [
|
||||||
hide: !adminKeyVisible,
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
buttonClick: () =>
|
],
|
||||||
clickEvent(
|
|
||||||
setAdminKey,
|
|
||||||
adminKeyBitCount,
|
|
||||||
setAdminKeyValidationText,
|
|
||||||
),
|
|
||||||
disabledBy: [{ fieldName: "adminChannelEnabled" }],
|
|
||||||
properties: {
|
properties: {
|
||||||
value: adminKey,
|
value: adminKey,
|
||||||
action: {
|
|
||||||
icon: adminKeyVisible ? EyeOff : Eye,
|
|
||||||
onClick: () => setAdminKeyVisible(!adminKeyVisible),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -214,17 +210,12 @@ export const Security = (): JSX.Element => {
|
|||||||
label: "Logging Settings",
|
label: "Logging Settings",
|
||||||
description: "Settings for Logging",
|
description: "Settings for Logging",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "bluetoothLoggingEnabled",
|
|
||||||
label: "Allow Bluetooth Logging",
|
|
||||||
description: "Enables device (serial style logs) over Bluetooth",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "debugLogApiEnabled",
|
name: "debugLogApiEnabled",
|
||||||
label: "Enable Debug Log API",
|
label: "Enable Debug Log API",
|
||||||
description: "Output live debug logging over serial",
|
description:
|
||||||
|
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -236,5 +227,11 @@ export const Security = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<PkiRegenerateDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={() => setDialogOpen(false)}
|
||||||
|
onSubmit={() => pkiRegenerate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { BleConnection, Constants } from "@meshtastic/js";
|
import { BleConnection, Constants } from "@meshtastic/js";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Switch } from "@components/UI/Switch.js";
|
import { Switch } from "@components/UI/Switch.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { HttpConnection } from "@meshtastic/js";
|
import { HttpConnection } from "@meshtastic/js";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { SerialConnection } from "@meshtastic/js";
|
import { SerialConnection } from "@meshtastic/js";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||||
import {
|
import {
|
||||||
type MessageWithState,
|
type MessageWithState,
|
||||||
useDevice,
|
useDevice,
|
||||||
} from "@app/core/stores/deviceStore.js";
|
} from "@app/core/stores/deviceStore.ts";
|
||||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
import { Message } from "@components/PageComponents/Messages/Message.tsx";
|
||||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
||||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.js";
|
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
||||||
import type { Protobuf, Types } from "@meshtastic/js";
|
import type { Protobuf, Types } from "@meshtastic/js";
|
||||||
import { InboxIcon } from "lucide-react";
|
import { InboxIcon } from "lucide-react";
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ export const ChannelChat = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="pl-3 pr-3 pt-3 pb-1">
|
||||||
<MessageInput to={to} channel={channel} />
|
<MessageInput to={to} channel={channel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MessageWithState } from "@app/core/stores/deviceStore.js";
|
import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { Types } from "@meshtastic/js";
|
import type { Types } from "@meshtastic/js";
|
||||||
import { SendIcon } from "lucide-react";
|
import { SendIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
|
||||||
export interface TraceRouteProps {
|
export interface TraceRouteProps {
|
||||||
from?: Protobuf.Mesh.NodeInfo;
|
from?: Protobuf.Mesh.NodeInfo;
|
||||||
@@ -24,7 +25,10 @@ export const TraceRoute = ({
|
|||||||
<div className="ml-5 flex">
|
<div className="ml-5 flex">
|
||||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||||
{to?.user?.longName}↔
|
{to?.user?.longName}↔
|
||||||
{route.map((hop) => `${nodes.get(hop)?.user?.longName ?? "Unknown"}↔`)}
|
{route.map((hop) => {
|
||||||
|
const node = nodes.get(hop);
|
||||||
|
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`;
|
||||||
|
})}
|
||||||
{from?.user?.longName}
|
{from?.user?.longName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.js";
|
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const AmbientLighting = (): JSX.Element => {
|
export const AmbientLighting = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AudioValidation } from "@app/validation/moduleConfig/audio.js";
|
import type { AudioValidation } from "@app/validation/moduleConfig/audio.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Audio = (): JSX.Element => {
|
export const Audio = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
|
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const CannedMessage = (): JSX.Element => {
|
export const CannedMessage = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.js";
|
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const DetectionSensor = (): JSX.Element => {
|
export const DetectionSensor = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
|
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const ExternalNotification = (): JSX.Element => {
|
export const ExternalNotification = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.js";
|
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const MQTT = (): JSX.Element => {
|
export const MQTT = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.js";
|
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const NeighborInfo = (): JSX.Element => {
|
export const NeighborInfo = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.js";
|
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Paxcounter = (): JSX.Element => {
|
export const Paxcounter = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
|
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const RangeTest = (): JSX.Element => {
|
export const RangeTest = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SerialValidation } from "@app/validation/moduleConfig/serial.js";
|
import type { SerialValidation } from "@app/validation/moduleConfig/serial.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Serial = (): JSX.Element => {
|
export const Serial = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
|
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.ts";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const StoreForward = (): JSX.Element => {
|
export const StoreForward = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
|
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const Telemetry = (): JSX.Element => {
|
export const Telemetry = (): JSX.Element => {
|
||||||
@@ -87,7 +87,7 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
description: "How often to send Power data over the mesh",
|
description: "How often to send Power data over the mesh",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "toggle",
|
||||||
name: "powerScreenEnabled",
|
name: "powerScreenEnabled",
|
||||||
label: "Power Screen Enabled",
|
label: "Power Screen Enabled",
|
||||||
description: "Enable the Power Telemetry Screen",
|
description: "Enable the Power Telemetry Screen",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
|
import Footer from "./UI/Footer";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -7,6 +8,7 @@ export interface PageLayoutProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions?: {
|
actions?: {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
iconClasses?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -18,6 +20,7 @@ export const PageLayout = ({
|
|||||||
children,
|
children,
|
||||||
}: PageLayoutProps): JSX.Element => {
|
}: PageLayoutProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="relative flex h-full w-full flex-col">
|
<div className="relative flex h-full w-full flex-col">
|
||||||
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
|
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +40,7 @@ export const PageLayout = ({
|
|||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon />
|
<action.icon className={action.iconClasses} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,11 +50,13 @@ export const PageLayout = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-col overflow-y-auto",
|
"flex h-full w-full flex-col overflow-y-auto",
|
||||||
!noPadding && "p-3",
|
!noPadding && "pl-3 pr-3 ",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { Page } from "@core/stores/deviceStore.js";
|
import type { Page } from "@core/stores/deviceStore.ts";
|
||||||
import {
|
import {
|
||||||
BatteryMediumIcon,
|
BatteryMediumIcon,
|
||||||
CpuIcon,
|
CpuIcon,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from "@components/UI/Toast.js";
|
} from "@components/UI/Toast.tsx";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast();
|
const { toasts } = useToast();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
|
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Command as CommandPrimitive } from "cmdk";
|
|||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@components/UI/Dialog.js";
|
import { Dialog, DialogContent } from "@components/UI/Dialog.tsx";
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
|
||||||
|
|
||||||
|
const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
className={`flex flex- justify-center p-2 ${className}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--backgroundPrimary)",
|
||||||
|
color: "var(--textPrimary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ color: "var(--link)" }}
|
||||||
|
>
|
||||||
|
Powered by ▲ Vercel
|
||||||
|
</a>{" "}
|
||||||
|
| Meshtastic® is a registered trademark of Meshtastic LLC. |{" "}
|
||||||
|
<a
|
||||||
|
href="https://meshtastic.org/docs/legal"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ color: "var(--link)" }}
|
||||||
|
>
|
||||||
|
Legal Information
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.js";
|
} from "@components/UI/Select.tsx";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||||
hide?: boolean;
|
type: "text" | "password";
|
||||||
devicePSKBitCount?: number;
|
devicePSKBitCount?: number;
|
||||||
value: string;
|
value: string;
|
||||||
variant: "default" | "invalid";
|
variant: "default" | "invalid";
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
@@ -30,11 +31,16 @@ export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
|||||||
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
hide = true,
|
type,
|
||||||
devicePSKBitCount,
|
devicePSKBitCount,
|
||||||
variant,
|
variant,
|
||||||
value,
|
value,
|
||||||
buttonText,
|
buttonText,
|
||||||
|
bits = [
|
||||||
|
{ text: "256 bit", value: "32", key: "bit256" },
|
||||||
|
{ text: "128 bit", value: "16", key: "bit128" },
|
||||||
|
{ text: "8 bit", value: "1", key: "bit8" },
|
||||||
|
],
|
||||||
selectChange,
|
selectChange,
|
||||||
inputChange,
|
inputChange,
|
||||||
buttonClick,
|
buttonClick,
|
||||||
@@ -44,40 +50,54 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Invokes onChange event on the input element when the value changes from the parent component
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
const setValue = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLInputElement.prototype,
|
||||||
|
"value",
|
||||||
|
)?.set;
|
||||||
|
|
||||||
|
if (!setValue) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
setValue.call(inputRef.current, value);
|
||||||
|
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}, [value]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
type={hide ? "password" : "text"}
|
type={type}
|
||||||
id="pskInput"
|
id="pskInput"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={inputChange}
|
onChange={inputChange}
|
||||||
action={action}
|
action={action}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={devicePSKBitCount?.toString()}
|
value={devicePSKBitCount?.toString()}
|
||||||
onValueChange={(e) => selectChange(e)}
|
onValueChange={(e) => selectChange(e)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="!max-w-max">
|
<SelectTrigger className="!max-w-max">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem key="bit256" value="32">
|
{bits.map(({ text, value, key }) => (
|
||||||
256 bit
|
<SelectItem key={key} value={value}>
|
||||||
</SelectItem>
|
{text}
|
||||||
<SelectItem key="bit128" value="16">
|
|
||||||
128 bit
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="bit8" value="1">
|
|
||||||
8 bit
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="success"
|
variant="success"
|
||||||
onClick={buttonClick}
|
onClick={buttonClick}
|
||||||
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { type VariantProps, cva } from "class-variance-authority";
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export interface InputProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, variant, prefix, suffix, action, ...props }, ref) => {
|
({ className, value, variant, prefix, suffix, action, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
@@ -45,6 +45,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className,
|
className,
|
||||||
inputVariants({ variant }),
|
inputVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
|
value={value}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const MenubarMenu = MenubarPrimitive.Menu;
|
const MenubarMenu = MenubarPrimitive.Menu;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root;
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|||||||
import { Check, ChevronDown } from "lucide-react";
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root;
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { H4 } from "@components/UI/Typography/H4.js";
|
import { H4 } from "@components/UI/Typography/H4.tsx";
|
||||||
|
|
||||||
export interface SidebarSectionProps {
|
export interface SidebarSectionProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export interface SidebarButtonProps {
|
export interface SidebarButtonProps {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
const Switch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root;
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 dark:bg-slate-800",
|
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 mt-2 dark:bg-slate-800",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type VariantProps, cva } from "class-variance-authority";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider;
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider;
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
|
|
||||||
export interface H4Props {
|
export interface H4Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
|
|
||||||
export interface SubtleProps {
|
export interface SubtleProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export interface ThemeControllerProps {
|
export interface ThemeControllerProps {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type ReactNode, useSyncExternalStore } from "react";
|
import { type ReactNode, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js";
|
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.tsx";
|
||||||
|
|
||||||
const TOAST_LIMIT = 1;
|
const TOAST_LIMIT = 1;
|
||||||
const TOAST_REMOVE_DELAY = 1000000;
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
@@ -80,7 +80,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
...t,
|
...t,
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
: t,
|
: t
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -190,4 +190,4 @@ function useToast() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useToast, toast };
|
export { toast, useToast };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf, type Types } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
|
|
||||||
export const subscribeAll = (
|
export const subscribeAll = (
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function convertIntToIpAddress(int: number): string {
|
||||||
|
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertIpAddressToInt(ip: string): number | null {
|
||||||
|
return (
|
||||||
|
ip
|
||||||
|
.split(".")
|
||||||
|
.reverse()
|
||||||
|
.reduce((ipnum, octet) => {
|
||||||
|
return (ipnum << 8) + Number.parseInt(octet);
|
||||||
|
}, 0) >>> 0
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { x25519 } from "@noble/curves/ed25519";
|
||||||
|
|
||||||
|
export function getX25519PrivateKey(): Uint8Array {
|
||||||
|
const key = x25519.utils.randomPrivateKey();
|
||||||
|
|
||||||
|
key[0] &= 248;
|
||||||
|
key[31] &= 127;
|
||||||
|
key[31] |= 64;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getX25519PublicKey(privateKey: Uint8Array): Uint8Array {
|
||||||
|
return x25519.getPublicKey(privateKey);
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import "maplibre-gl/dist/maplibre-gl.css";
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import { App } from "@app/App.js";
|
import { App } from "@app/App.tsx";
|
||||||
|
|
||||||
const container = document.getElementById("root") as HTMLElement;
|
const container = document.getElementById("root") as HTMLElement;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@app/components/UI/Tabs.js";
|
} from "@app/components/UI/Tabs.tsx";
|
||||||
import { Channel } from "@components/PageComponents/Channel.js";
|
import { Channel } from "@components/PageComponents/Channel.tsx";
|
||||||
import { PageLayout } from "@components/PageLayout.js";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Types } from "@meshtastic/js";
|
import { Types } from "@meshtastic/js";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import { ImportIcon, QrCodeIcon } from "lucide-react";
|
import { ImportIcon, QrCodeIcon } from "lucide-react";
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.js";
|
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
|
||||||
import { Device } from "@components/PageComponents/Config/Device.js";
|
import { Device } from "@components/PageComponents/Config/Device.tsx";
|
||||||
import { Display } from "@components/PageComponents/Config/Display.js";
|
import { Display } from "@components/PageComponents/Config/Display.tsx";
|
||||||
import { LoRa } from "@components/PageComponents/Config/LoRa.js";
|
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
|
||||||
import { Network } from "@components/PageComponents/Config/Network.js";
|
import { Network } from "@components/PageComponents/Config/Network.tsx";
|
||||||
import { Position } from "@components/PageComponents/Config/Position.js";
|
import { Position } from "@components/PageComponents/Config/Position.tsx";
|
||||||
import { Power } from "@components/PageComponents/Config/Power.js";
|
import { Power } from "@components/PageComponents/Config/Power.tsx";
|
||||||
import { Security } from "@components/PageComponents/Config/Security.js";
|
import { Security } from "@components/PageComponents/Config/Security.tsx";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@components/UI/Tabs.js";
|
} from "@components/UI/Tabs.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
export const DeviceConfig = (): JSX.Element => {
|
export const DeviceConfig = (): JSX.Element => {
|
||||||
const { metadata } = useDevice();
|
const { metadata } = useDevice();
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { AmbientLighting } from "@app/components/PageComponents/ModuleConfig/AmbientLighting.js";
|
import { AmbientLighting } from "@app/components/PageComponents/ModuleConfig/AmbientLighting.tsx";
|
||||||
import { DetectionSensor } from "@app/components/PageComponents/ModuleConfig/DetectionSensor.js";
|
import { DetectionSensor } from "@app/components/PageComponents/ModuleConfig/DetectionSensor.tsx";
|
||||||
import { NeighborInfo } from "@app/components/PageComponents/ModuleConfig/NeighborInfo.js";
|
import { NeighborInfo } from "@app/components/PageComponents/ModuleConfig/NeighborInfo.tsx";
|
||||||
import { Audio } from "@components/PageComponents/ModuleConfig/Audio.js";
|
import { Audio } from "@components/PageComponents/ModuleConfig/Audio.tsx";
|
||||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
|
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.tsx";
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.tsx";
|
||||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.tsx";
|
||||||
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
|
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.tsx";
|
||||||
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
|
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.tsx";
|
||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.tsx";
|
||||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.tsx";
|
||||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.tsx";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@components/UI/Tabs.js";
|
} from "@components/UI/Tabs.tsx";
|
||||||
|
|
||||||
export const ModuleConfig = (): JSX.Element => {
|
export const ModuleConfig = (): JSX.Element => {
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
import { useDevice } from "@app/core/stores/deviceStore.ts";
|
||||||
import { PageLayout } from "@components/PageLayout.js";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
|
||||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.js";
|
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
|
||||||
import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react";
|
import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
import { useAppStore } from "@app/core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
|
import { useDeviceStore } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Separator } from "@components/UI/Seperator.js";
|
import { Separator } from "@components/UI/Seperator.tsx";
|
||||||
import { H3 } from "@components/UI/Typography/H3.js";
|
import { H3 } from "@components/UI/Typography/H3.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import {
|
import {
|
||||||
BluetoothIcon,
|
BluetoothIcon,
|
||||||
ListPlusIcon,
|
ListPlusIcon,
|
||||||
@@ -21,6 +21,7 @@ export const Dashboard = () => {
|
|||||||
const devices = useMemo(() => getDevices(), [getDevices]);
|
const devices = useMemo(() => getDevices(), [getDevices]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -83,7 +84,7 @@ export const Dashboard = () => {
|
|||||||
<div className="m-auto flex flex-col gap-3 text-center">
|
<div className="m-auto flex flex-col gap-3 text-center">
|
||||||
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
|
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
|
||||||
<H3>No Devices</H3>
|
<H3>No Devices</H3>
|
||||||
<Subtle>Connect atleast one device to get started</Subtle>
|
<Subtle>Connect at least one device to get started</Subtle>
|
||||||
<Button
|
<Button
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
onClick={() => setConnectDialogOpen(true)}
|
onClick={() => setConnectDialogOpen(true)}
|
||||||
@@ -95,5 +96,6 @@ export const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-9
@@ -1,12 +1,13 @@
|
|||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.ts";
|
||||||
import { PageLayout } from "@components/PageLayout.js";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { bbox, lineString } from "@turf/turf";
|
import { bbox, lineString } from "@turf/turf";
|
||||||
import {
|
import {
|
||||||
BoxSelectIcon,
|
BoxSelectIcon,
|
||||||
@@ -180,7 +181,8 @@ export const MapPage = (): JSX.Element => {
|
|||||||
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
||||||
<Hashicon value={node.num.toString()} size={22} />
|
<Hashicon value={node.num.toString()} size={22} />
|
||||||
<Subtle className={cn(zoom < 12 && "hidden")}>
|
<Subtle className={cn(zoom < 12 && "hidden")}>
|
||||||
{node.user?.longName}
|
{node.user?.longName ||
|
||||||
|
`!${numberToHexUnpadded(node.num)}`}
|
||||||
</Subtle>
|
</Subtle>
|
||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|||||||
+33
-11
@@ -1,14 +1,15 @@
|
|||||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js";
|
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
|
||||||
import { PageLayout } from "@components/PageLayout.js";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, Types } from "@meshtastic/js";
|
||||||
import { getChannelName } from "@pages/Channels.js";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { HashIcon, WaypointsIcon } from "lucide-react";
|
import { getChannelName } from "@pages/Channels.tsx";
|
||||||
|
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MessagesPage = (): JSX.Element => {
|
export const MessagesPage = (): JSX.Element => {
|
||||||
@@ -28,6 +29,8 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
const currentChannel = channels.get(activeChat);
|
const currentChannel = channels.get(activeChat);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const node = nodes.get(activeChat);
|
||||||
|
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -56,7 +59,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
{filteredNodes.map((node) => (
|
{filteredNodes.map((node) => (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
key={node.num}
|
key={node.num}
|
||||||
label={node.user?.longName ?? "Unknown"}
|
label={node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`}
|
||||||
active={activeChat === node.num}
|
active={activeChat === node.num}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChatType("direct");
|
setChatType("direct");
|
||||||
@@ -67,17 +70,35 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
<div className="flex flex-col flex-grow">
|
||||||
<PageLayout
|
<PageLayout
|
||||||
label={`Messages: ${
|
label={`Messages: ${
|
||||||
chatType === "broadcast" && currentChannel
|
chatType === "broadcast" && currentChannel
|
||||||
? getChannelName(currentChannel)
|
? getChannelName(currentChannel)
|
||||||
: chatType === "direct" && nodes.get(activeChat)
|
: chatType === "direct" && nodes.get(activeChat)
|
||||||
? nodes.get(activeChat)?.user?.longName ?? "Unknown"
|
? nodes.get(activeChat)?.user?.longName ?? nodeHex
|
||||||
: "Loading..."
|
: "Loading..."
|
||||||
}`}
|
}`}
|
||||||
actions={
|
actions={
|
||||||
chatType === "direct"
|
chatType === "direct"
|
||||||
? [
|
? [
|
||||||
|
{
|
||||||
|
icon: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? LockIcon
|
||||||
|
: LockOpenIcon,
|
||||||
|
iconClasses: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "text-green-600"
|
||||||
|
: "text-yellow-300",
|
||||||
|
async onClick() {
|
||||||
|
const targetNode = nodes.get(activeChat)?.num;
|
||||||
|
if (targetNode === undefined) return;
|
||||||
|
toast({
|
||||||
|
title: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "Chat is using PKI encryption."
|
||||||
|
: "Chat is using PSK encryption.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: WaypointsIcon,
|
icon: WaypointsIcon,
|
||||||
async onClick() {
|
async onClick() {
|
||||||
@@ -121,6 +142,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+22
-9
@@ -1,13 +1,15 @@
|
|||||||
|
import Footer from "@app/components/UI/Footer";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.tsx";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { Table } from "@components/generic/Table/index.js";
|
import { Table } from "@components/generic/Table/index.tsx";
|
||||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
@@ -27,7 +29,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="w-full overflow-y-auto">
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="overflow-y-auto h-full">
|
||||||
<Table
|
<Table
|
||||||
headings={[
|
headings={[
|
||||||
{ title: "", type: "blank", sortable: false },
|
{ title: "", type: "blank", sortable: false },
|
||||||
@@ -36,6 +39,7 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{ title: "MAC Address", type: "normal", sortable: true },
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
{ title: "Last Heard", type: "normal", sortable: true },
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
{ title: "SNR", type: "normal", sortable: true },
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
{ title: "Connection", type: "normal", sortable: true },
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
{ title: "Remove", type: "normal", sortable: false },
|
{ title: "Remove", type: "normal", sortable: false },
|
||||||
]}
|
]}
|
||||||
@@ -47,7 +51,7 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
? `Meshtastic ${base16
|
? `Meshtastic ${base16
|
||||||
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||||
.toLowerCase()}`
|
.toLowerCase()}`
|
||||||
: `UNK: ${node.num}`)}
|
: `!${numberToHexUnpadded(node.num)}`)}
|
||||||
</h1>,
|
</h1>,
|
||||||
|
|
||||||
<Mono key="model">
|
<Mono key="model">
|
||||||
@@ -71,6 +75,13 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||||
{(node.snr + 10) * 5}raw
|
{(node.snr + 10) * 5}raw
|
||||||
</Mono>,
|
</Mono>,
|
||||||
|
<Mono key="pki">
|
||||||
|
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||||
|
<LockIcon className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<LockOpenIcon className="text-yellow-300" />
|
||||||
|
)}
|
||||||
|
</Mono>,
|
||||||
<Mono key="hops">
|
<Mono key="hops">
|
||||||
{node.lastHeard !== 0
|
{node.lastHeard !== 0
|
||||||
? node.viaMqtt === false && node.hopsAway === 0
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
@@ -95,6 +106,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,4 +60,7 @@ export class LoRaValidation
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
ignoreMqtt: boolean;
|
ignoreMqtt: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
configOkToMqtt: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,21 +41,24 @@ export class NetworkValidation
|
|||||||
|
|
||||||
export class NetworkValidationIpV4Config
|
export class NetworkValidationIpV4Config
|
||||||
implements
|
implements
|
||||||
Omit<Protobuf.Config.Config_NetworkConfig_IpV4Config, keyof Message>
|
Omit<
|
||||||
|
Protobuf.Config.Config_NetworkConfig_IpV4Config,
|
||||||
|
keyof Message | "ip" | "gateway" | "subnet" | "dns"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
@IsIP()
|
@IsIP()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
ip: number;
|
ip: string;
|
||||||
|
|
||||||
@IsIP()
|
@IsIP()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
gateway: number;
|
gateway: string;
|
||||||
|
|
||||||
@IsIP()
|
@IsIP()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
subnet: number;
|
subnet: string;
|
||||||
|
|
||||||
@IsIP()
|
@IsIP()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
dns: number;
|
dns: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,4 @@ export class PositionValidation
|
|||||||
|
|
||||||
@IsEnum(Protobuf.Config.Config_PositionConfig_GpsMode)
|
@IsEnum(Protobuf.Config.Config_PositionConfig_GpsMode)
|
||||||
gpsMode: Protobuf.Config.Config_PositionConfig_GpsMode;
|
gpsMode: Protobuf.Config.Config_PositionConfig_GpsMode;
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
channelPrecision: number[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IsArray, IsBoolean, IsNumber, IsString, IsUrl } from "class-validator";
|
import { IsArray, IsBoolean, IsNumber, IsString, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import type { RasterSource } from "@core/stores/appStore.js";
|
import type { RasterSource } from "@core/stores/appStore.ts";
|
||||||
|
|
||||||
export class MapValidation {
|
export class MapValidation {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
|
|||||||
+2
-1
@@ -34,6 +34,7 @@
|
|||||||
"@types/w3c-web-serial"
|
"@types/w3c-web-serial"
|
||||||
],
|
],
|
||||||
"strictPropertyInitialization": false,
|
"strictPropertyInitialization": false,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
import { visualizer } from "rollup-plugin-visualizer";
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import EnvironmentPlugin from "vite-plugin-environment";
|
import EnvironmentPlugin from "vite-plugin-environment";
|
||||||
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
|
|
||||||
let hash = "";
|
let hash = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user