Merge branch 'meshtastic:master' into traceroute
This commit is contained in:
Vendored
+6
-3
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true
|
"editor.codeActionsOnSave": {
|
||||||
}
|
"quickfix.biome": "explicit"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.6.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": true
|
"ignoreUnknown": true,
|
||||||
|
"ignore": ["vercel.json"]
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"all": true
|
"recommended": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-34
@@ -6,8 +6,9 @@
|
|||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && pnpm check && vite build ",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
|
"check:fix": "pnpm check --write",
|
||||||
"preview": "vite preview",
|
"preview": "vite 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/)"
|
||||||
},
|
},
|
||||||
@@ -20,64 +21,64 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://meshtastic.org",
|
"homepage": "https://meshtastic.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^1.8.0",
|
"@bufbuild/protobuf": "^1.10.0",
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@meshtastic/js": "2.3.4-0",
|
"@meshtastic/js": "2.3.7-0",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.0.4",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.1.1",
|
||||||
"@turf/turf": "^6.5.0",
|
"@turf/turf": "^6.5.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"immer": "^10.0.4",
|
"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": "npm:empty-npm-package@^1.0.0",
|
||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.52.0",
|
||||||
"react-map-gl": "7.1.7",
|
"react-map-gl": "7.1.7",
|
||||||
"react-qrcode-logo": "^2.9.0",
|
"react-qrcode-logo": "^2.10.0",
|
||||||
"rfc4648": "^1.5.3",
|
"rfc4648": "^1.5.3",
|
||||||
"tailwind-merge": "^2.2.2",
|
"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",
|
||||||
"zustand": "4.5.2"
|
"zustand": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.6.3",
|
"@biomejs/biome": "^1.8.2",
|
||||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2",
|
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240613143006-244927bc441a.1",
|
||||||
"@types/chrome": "^0.0.263",
|
"@types/chrome": "^0.0.263",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.14.9",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.2.23",
|
"@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.2.1",
|
"@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",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.4",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.3",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.5.2",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.3.1",
|
||||||
"vite-plugin-environment": "^1.1.3"
|
"vite-plugin-environment": "^1.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4231
-3173
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
LayersIcon,
|
LayersIcon,
|
||||||
LayoutIcon,
|
LayoutIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LucideIcon,
|
type LucideIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
@@ -350,7 +350,7 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeydown);
|
window.addEventListener("keydown", handleKeydown);
|
||||||
return () => window.removeEventListener("keydown", handleKeydown);
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
}, []);
|
}, [setCommandPaletteOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog
|
<CommandDialog
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
LanguagesIcon,
|
LanguagesIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SunIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
SunIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const DeviceSelector = (): JSX.Element => {
|
export const DeviceSelector = (): JSX.Element => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
|
||||||
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js"
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
|
|
||||||
export const DialogManager = (): JSX.Element => {
|
export const DialogManager = (): JSX.Element => {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
} from "@components/UI/Dialog.js";
|
} from "@components/UI/Dialog.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.js";
|
||||||
import { Protobuf, 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";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { QRCode } from "react-qrcode-logo";
|
import { QRCode } from "react-qrcode-logo";
|
||||||
|
|
||||||
export interface QRDialogProps {
|
export interface QRDialogProps {
|
||||||
@@ -32,7 +32,7 @@ export const QRDialog = ({
|
|||||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
||||||
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
|
||||||
|
|
||||||
const allChannels = Array.from(channels.values());
|
const allChannels = useMemo(() => Array.from(channels.values()), [channels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const channelsToEncode = allChannels
|
const channelsToEncode = allChannels
|
||||||
@@ -50,8 +50,10 @@ export const QRDialog = ({
|
|||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, "-")
|
||||||
.replace(/\//g, "_");
|
.replace(/\//g, "_");
|
||||||
|
|
||||||
setQrCodeUrl(`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`);
|
setQrCodeUrl(
|
||||||
}, [channels, selectedChannels, qrCodeAdd, loraConfig]);
|
`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`,
|
||||||
|
);
|
||||||
|
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -97,18 +99,26 @@ export const QRDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={ "border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
|
className={`border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||||
|
qrCodeAdd
|
||||||
|
? "focus:ring-green-800 bg-green-800 text-white"
|
||||||
|
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||||
|
}`}
|
||||||
onClick={() => setQrCodeAdd(true)}
|
onClick={() => setQrCodeAdd(true)}
|
||||||
>
|
>
|
||||||
Add Channels
|
Add Channels
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={ "border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (!qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
|
className={`border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||||
|
!qrCodeAdd
|
||||||
|
? "focus:ring-green-800 bg-green-800 text-white"
|
||||||
|
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||||
|
}`}
|
||||||
onClick={() => setQrCodeAdd(false)}
|
onClick={() => setQrCodeAdd(false)}
|
||||||
>
|
>
|
||||||
Replace Channels
|
Replace Channels
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const RebootDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
||||||
action={{
|
action={{
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
onClick() {
|
onClick() {
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export const RemoveNodeDialog = ({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="destructive" onClick={() => onSubmit()}>Remove</Button>
|
<Button variant="destructive" onClick={() => onSubmit()}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const ShutdownDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => setTime(parseInt(e.target.value))}
|
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
||||||
suffix="Minutes"
|
suffix="Minutes"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
DynamicFormField,
|
DynamicFormField,
|
||||||
FieldProps,
|
type FieldProps,
|
||||||
} from "@components/Form/DynamicFormField.js";
|
} from "@components/Form/DynamicFormField.js";
|
||||||
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
import { FieldWrapper } from "@components/Form/FormWrapper.js";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { H4 } from "@components/UI/Typography/H4.js";
|
import { H4 } from "@components/UI/Typography/H4.js";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
import { Subtle } from "@components/UI/Typography/Subtle.js";
|
||||||
import {
|
import {
|
||||||
Control,
|
type Control,
|
||||||
DefaultValues,
|
type DefaultValues,
|
||||||
FieldValues,
|
type FieldValues,
|
||||||
Path,
|
type Path,
|
||||||
SubmitHandler,
|
type SubmitHandler,
|
||||||
useForm,
|
useForm,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export interface BaseFormBuilderProps<T> {
|
|||||||
disabledBy?: DisabledBy<T>[];
|
disabledBy?: DisabledBy<T>[];
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
properties?: {};
|
properties?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericFormElementProps<T extends FieldValues, Y> {
|
export interface GenericFormElementProps<T extends FieldValues, Y> {
|
||||||
@@ -94,9 +94,12 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fieldGroup.fields.map((field) => (
|
{fieldGroup.fields.map((field) => (
|
||||||
<FieldWrapper label={field.label} description={field.description}>
|
<FieldWrapper
|
||||||
|
key={field.label}
|
||||||
|
label={field.label}
|
||||||
|
description={field.description}
|
||||||
|
>
|
||||||
<DynamicFormField
|
<DynamicFormField
|
||||||
key={field.label}
|
|
||||||
field={field}
|
field={field}
|
||||||
control={control}
|
control={control}
|
||||||
disabled={isDisabled(field.disabledBy)}
|
disabled={isDisabled(field.disabledBy)}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { GenericInput, InputFieldProps } from "@components/Form/FormInput.js";
|
import {
|
||||||
import { SelectFieldProps, SelectInput } from "@components/Form/FormSelect.js";
|
GenericInput,
|
||||||
import { ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.js";
|
type InputFieldProps,
|
||||||
|
} from "@components/Form/FormInput.js";
|
||||||
|
import {
|
||||||
|
type SelectFieldProps,
|
||||||
|
SelectInput,
|
||||||
|
} from "@components/Form/FormSelect.js";
|
||||||
|
import {
|
||||||
|
type ToggleFieldProps,
|
||||||
|
ToggleInput,
|
||||||
|
} from "@components/Form/FormToggle.js";
|
||||||
import type { Control, FieldValues } from "react-hook-form";
|
import type { Control, FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export type FieldProps<T> =
|
export type FieldProps<T> =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Controller, 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";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.js";
|
} from "@components/UI/Select.js";
|
||||||
import { Controller, 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> {
|
||||||
type: "select" | "multiSelect";
|
type: "select" | "multiSelect";
|
||||||
@@ -40,7 +40,7 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(e) => onChange(parseInt(e))}
|
onValueChange={(e) => onChange(Number.parseInt(e))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={value?.toString()}
|
value={value?.toString()}
|
||||||
{...remainingProperties}
|
{...remainingProperties}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type {
|
|||||||
GenericFormElementProps,
|
GenericFormElementProps,
|
||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.js";
|
||||||
import { Switch } from "@components/UI/Switch.js";
|
import { Switch } from "@components/UI/Switch.js";
|
||||||
import { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import { Controller, FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "toggle";
|
type: "toggle";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type{ ChannelValidation } from "@app/validation/channel.js";
|
import type { ChannelValidation } from "@app/validation/channel.js";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||||
import { useToast } from "@core/hooks/useToast.js";
|
import { useToast } from "@core/hooks/useToast.js";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
@@ -20,8 +20,12 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
...data.settings,
|
...data.settings,
|
||||||
psk: toByteArray(data.settings.psk ?? ""),
|
psk: toByteArray(data.settings.psk ?? ""),
|
||||||
moduleSettings: {
|
moduleSettings: {
|
||||||
positionPrecision: data.settings.positionEnabled ? data.settings.preciseLocation ? 32 : data.settings.positionPrecision : 0,
|
positionPrecision: data.settings.positionEnabled
|
||||||
}
|
? data.settings.preciseLocation
|
||||||
|
? 32
|
||||||
|
: data.settings.positionPrecision
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
connection?.setChannel(channel).then(() => {
|
connection?.setChannel(channel).then(() => {
|
||||||
@@ -43,9 +47,16 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
settings: {
|
settings: {
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
positionEnabled: channel?.settings?.moduleSettings?.positionPrecision != undefined && channel?.settings?.moduleSettings?.positionPrecision > 0,
|
positionEnabled:
|
||||||
preciseLocation: channel?.settings?.moduleSettings?.positionPrecision == 32,
|
channel?.settings?.moduleSettings?.positionPrecision !==
|
||||||
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision == undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision
|
undefined &&
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision > 0,
|
||||||
|
preciseLocation:
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
||||||
|
positionPrecision:
|
||||||
|
channel?.settings?.moduleSettings?.positionPrecision === undefined
|
||||||
|
? 10
|
||||||
|
: channel?.settings?.moduleSettings?.positionPrecision,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -111,9 +122,32 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
description:
|
description:
|
||||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: config.display?.units == 0 ?
|
enumValue:
|
||||||
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } :
|
config.display?.units === 0
|
||||||
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 }
|
? {
|
||||||
|
"Within 23 km": 10,
|
||||||
|
"Within 12 km": 11,
|
||||||
|
"Within 5.8 km": 12,
|
||||||
|
"Within 2.9 km": 13,
|
||||||
|
"Within 1.5 km": 14,
|
||||||
|
"Within 700 m": 15,
|
||||||
|
"Within 350 m": 16,
|
||||||
|
"Within 200 m": 17,
|
||||||
|
"Within 90 m": 18,
|
||||||
|
"Within 50 m": 19,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"Within 15 miles": 10,
|
||||||
|
"Within 7.3 miles": 11,
|
||||||
|
"Within 3.6 miles": 12,
|
||||||
|
"Within 1.8 miles": 13,
|
||||||
|
"Within 0.9 miles": 14,
|
||||||
|
"Within 0.5 miles": 15,
|
||||||
|
"Within 0.2 miles": 16,
|
||||||
|
"Within 600 feet": 17,
|
||||||
|
"Within 300 feet": 18,
|
||||||
|
"Within 150 feet": 19,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const LoRa = (): JSX.Element => {
|
|||||||
label: "Hop Limit",
|
label: "Hop Limit",
|
||||||
description: "Maximum number of hops",
|
description: "Maximum number of hops",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7}
|
enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { 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.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { 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.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import { Label } from "@components/UI/Label.js";
|
import { Label } from "@components/UI/Label.js";
|
||||||
@@ -8,6 +8,7 @@ import { useDeviceStore } from "@core/stores/deviceStore.js";
|
|||||||
import { subscribeAll } from "@core/subscriptions.js";
|
import { subscribeAll } from "@core/subscriptions.js";
|
||||||
import { randId } from "@core/utils/randId.js";
|
import { randId } from "@core/utils/randId.js";
|
||||||
import { HttpConnection } from "@meshtastic/js";
|
import { HttpConnection } from "@meshtastic/js";
|
||||||
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||||
@@ -19,7 +20,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: ["client.meshtastic.org", "localhost"].includes(
|
ip: ["client.meshtastic.org", "localhost"].includes(
|
||||||
window.location.hostname
|
window.location.hostname,
|
||||||
)
|
)
|
||||||
? "meshtastic.local"
|
? "meshtastic.local"
|
||||||
: window.location.hostname,
|
: window.location.hostname,
|
||||||
@@ -33,10 +34,13 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
defaultValue: location.protocol === "https:",
|
defaultValue: location.protocol === "https:",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
setConnectionInProgress(true);
|
||||||
|
|
||||||
const id = randId();
|
const id = randId();
|
||||||
const device = addDevice(id);
|
const device = addDevice(id);
|
||||||
setSelectedDevice(id);
|
|
||||||
const connection = new HttpConnection(id);
|
const connection = new HttpConnection(id);
|
||||||
// TODO: Promise never resolves
|
// TODO: Promise never resolves
|
||||||
await connection.connect({
|
await connection.connect({
|
||||||
@@ -44,9 +48,10 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
fetchInterval: 2000,
|
fetchInterval: 2000,
|
||||||
tls: data.tls,
|
tls: data.tls,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedDevice(id);
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
|
|
||||||
closeDialog();
|
closeDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +63,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
// label="IP Address/Hostname"
|
// label="IP Address/Hostname"
|
||||||
prefix={tlsEnabled ? "https://" : "http://"}
|
prefix={tlsEnabled ? "https://" : "http://"}
|
||||||
placeholder="000.000.000.000 / meshtastic.local"
|
placeholder="000.000.000.000 / meshtastic.local"
|
||||||
|
disabled={connectionInProgress}
|
||||||
{...register("ip")}
|
{...register("ip")}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -69,7 +75,9 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
<Switch
|
<Switch
|
||||||
// label="Use TLS"
|
// label="Use TLS"
|
||||||
// description="Description"
|
// description="Description"
|
||||||
disabled={location.protocol === "https:"}
|
disabled={
|
||||||
|
location.protocol === "https:" || connectionInProgress
|
||||||
|
}
|
||||||
checked={value}
|
checked={value}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
@@ -77,8 +85,8 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit">
|
<Button type="submit" disabled={connectionInProgress}>
|
||||||
<span>Connect</span>
|
<span>{connectionInProgress ? "Connecting..." : "Connect"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { 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.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
@@ -48,19 +48,22 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<div className="flex w-full flex-col gap-2 p-4">
|
||||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||||
{serialPorts.map((port, index) => (
|
{serialPorts.map((port, index) => {
|
||||||
<Button
|
const { usbProductId, usbVendorId } = port.getInfo();
|
||||||
key={index}
|
return (
|
||||||
disabled={port.readable !== null}
|
<Button
|
||||||
onClick={async () => {
|
key={`${usbVendorId ?? "UNK"}-${usbProductId ?? "UNK"}-${index}`}
|
||||||
await onConnect(port);
|
disabled={port.readable !== null}
|
||||||
}}
|
onClick={async () => {
|
||||||
>
|
await onConnect(port);
|
||||||
{`# ${index} - ${port.getInfo().usbVendorId ?? "UNK"} - ${
|
}}
|
||||||
port.getInfo().usbProductId ?? "UNK"
|
>
|
||||||
}`}
|
{`# ${index} - ${usbVendorId ?? "UNK"} - ${
|
||||||
</Button>
|
usbProductId ?? "UNK"
|
||||||
))}
|
}`}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{serialPorts.length === 0 && (
|
{serialPorts.length === 0 && (
|
||||||
<Mono className="m-auto select-none">No devices paired yet.</Mono>
|
<Mono className="m-auto select-none">No devices paired yet.</Mono>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
||||||
import { MessageWithState, useDevice } from "@app/core/stores/deviceStore.js";
|
import {
|
||||||
|
type MessageWithState,
|
||||||
|
useDevice,
|
||||||
|
} from "@app/core/stores/deviceStore.js";
|
||||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.js";
|
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.js";
|
||||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export const DetectionSensor = (): JSX.Element => {
|
|||||||
label: "Minimum Broadcast Seconds",
|
label: "Minimum Broadcast Seconds",
|
||||||
description:
|
description:
|
||||||
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
|
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
},
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ import { DynamicForm } from "@components/Form/DynamicForm.js";
|
|||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
|
||||||
export const MQTT = (): JSX.Element => {
|
export const MQTT = (): JSX.Element => {
|
||||||
const { moduleConfig, setWorkingModuleConfig } = useDevice();
|
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: MqttValidation) => {
|
const onSubmit = (data: MqttValidation) => {
|
||||||
setWorkingModuleConfig(
|
setWorkingModuleConfig(
|
||||||
new Protobuf.ModuleConfig.ModuleConfig({
|
new Protobuf.ModuleConfig.ModuleConfig({
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
case: "mqtt",
|
case: "mqtt",
|
||||||
value: data,
|
value: {
|
||||||
|
...data,
|
||||||
|
mapReportSettings:
|
||||||
|
new Protobuf.ModuleConfig.ModuleConfig_MapReportSettings(
|
||||||
|
data.mapReportSettings,
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -70,7 +76,8 @@ export const MQTT = (): JSX.Element => {
|
|||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "encryptionEnabled",
|
name: "encryptionEnabled",
|
||||||
label: "Encryption Enabled",
|
label: "Encryption Enabled",
|
||||||
description: "Enable or disable MQTT encryption",
|
description:
|
||||||
|
"Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
@@ -151,10 +158,39 @@ export const MQTT = (): JSX.Element => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "select",
|
||||||
name: "mapReportSettings.positionPrecision",
|
name: "mapReportSettings.positionPrecision",
|
||||||
label: "Position Precision",
|
label: "Approximate Location",
|
||||||
description: "Precision of the position",
|
description:
|
||||||
|
"Position shared will be accurate within this distance",
|
||||||
|
properties: {
|
||||||
|
enumValue:
|
||||||
|
config.display?.units === 0
|
||||||
|
? {
|
||||||
|
"Within 23 km": 10,
|
||||||
|
"Within 12 km": 11,
|
||||||
|
"Within 5.8 km": 12,
|
||||||
|
"Within 2.9 km": 13,
|
||||||
|
"Within 1.5 km": 14,
|
||||||
|
"Within 700 m": 15,
|
||||||
|
"Within 350 m": 16,
|
||||||
|
"Within 200 m": 17,
|
||||||
|
"Within 90 m": 18,
|
||||||
|
"Within 50 m": 19,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"Within 15 miles": 10,
|
||||||
|
"Within 7.3 miles": 11,
|
||||||
|
"Within 3.6 miles": 12,
|
||||||
|
"Within 1.8 miles": 13,
|
||||||
|
"Within 0.9 miles": 14,
|
||||||
|
"Within 0.5 miles": 15,
|
||||||
|
"Within 0.2 miles": 16,
|
||||||
|
"Within 600 feet": 17,
|
||||||
|
"Within 300 feet": 18,
|
||||||
|
"Within 150 feet": 19,
|
||||||
|
},
|
||||||
|
},
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{
|
{
|
||||||
fieldName: "enabled",
|
fieldName: "enabled",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const NeighborInfo = (): JSX.Element => {
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "updateInterval",
|
name: "updateInterval",
|
||||||
label: "Update Interval",
|
label: "Update Interval",
|
||||||
description:
|
description:
|
||||||
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
|
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const Paxcounter = (): JSX.Element => {
|
|||||||
type: "number",
|
type: "number",
|
||||||
name: "paxcounterUpdateInterval",
|
name: "paxcounterUpdateInterval",
|
||||||
label: "Update Interval (seconds)",
|
label: "Update Interval (seconds)",
|
||||||
description: "How long to wait between sending paxcounter packets",
|
description:
|
||||||
|
"How long to wait between sending paxcounter packets",
|
||||||
properties: {
|
properties: {
|
||||||
suffix: "Seconds",
|
suffix: "Seconds",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.js";
|
||||||
import { AlignLeftIcon, LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { Subtle } from "@components/UI/Typography/Subtle.js";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import type { Page } from "@core/stores/deviceStore.js";
|
import type { Page } from "@core/stores/deviceStore.js";
|
||||||
import {
|
import {
|
||||||
|
BatteryMediumIcon,
|
||||||
|
CpuIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
LucideIcon,
|
type LucideIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
BatteryMediumIcon
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
@@ -20,8 +21,9 @@ export interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||||
const { hardware, nodes } = useDevice();
|
const { hardware, nodes, metadata } = useDevice();
|
||||||
const myNode = nodes.get(hardware.myNodeNum);
|
const myNode = nodes.get(hardware.myNodeNum);
|
||||||
|
const myMetadata = metadata.get(0);
|
||||||
const { activePage, setActivePage, setDialogOpen } = useDevice();
|
const { activePage, setActivePage, setDialogOpen } = useDevice();
|
||||||
|
|
||||||
interface NavLink {
|
interface NavLink {
|
||||||
@@ -77,12 +79,18 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-8 pb-6">
|
<div className="px-8 pb-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<BatteryMediumIcon size={24} viewBox={'0 0 28 24'}/>
|
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
|
||||||
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
|
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ZapIcon size={24} viewBox={'0 0 36 24'}/>
|
<ZapIcon size={24} viewBox={"0 0 36 24"} />
|
||||||
<Subtle>{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts</Subtle>
|
<Subtle>
|
||||||
|
{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts
|
||||||
|
</Subtle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CpuIcon size={24} viewBox={"0 0 36 24"} />
|
||||||
|
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ export function Toaster() {
|
|||||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
{toasts.map(({ id, title, description, action, ...props }) => (
|
||||||
<Toast key={id} {...props}>
|
<Toast key={id} {...props}>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
{title && <ToastTitle className="dark:text-white">{title}</ToastTitle>}
|
{title && (
|
||||||
{description && <ToastDescription className="dark:text-white-400">{description}</ToastDescription>}
|
<ToastTitle className="dark:text-white">{title}</ToastTitle>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription className="dark:text-white-400">
|
||||||
|
{description}
|
||||||
|
</ToastDescription>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{action}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { 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.js";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ChevronUpIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
export interface TableProps {
|
export interface TableProps {
|
||||||
headings: Heading[];
|
headings: Heading[];
|
||||||
@@ -12,6 +13,49 @@ export interface Heading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
||||||
|
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
||||||
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
|
|
||||||
|
const headingSort = (title: string) => {
|
||||||
|
if (sortColumn === title) {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortColumn(title);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedRows = rows.slice().sort((a, b) => {
|
||||||
|
if (!sortColumn) return 0;
|
||||||
|
|
||||||
|
const columnIndex = headings.findIndex((h) => h.title === sortColumn);
|
||||||
|
const aValue = a[columnIndex].props.children;
|
||||||
|
const bValue = b[columnIndex].props.children;
|
||||||
|
|
||||||
|
// Custom comparison for 'Last Heard' column
|
||||||
|
if (sortColumn === "Last Heard") {
|
||||||
|
const aTimestamp = aValue.props.timestamp ?? 0;
|
||||||
|
const bTimestamp = bValue.props.timestamp ?? 0;
|
||||||
|
|
||||||
|
if (aTimestamp < bTimestamp) {
|
||||||
|
return sortOrder === "asc" ? -1 : 1;
|
||||||
|
}
|
||||||
|
if (aTimestamp > bTimestamp) {
|
||||||
|
return sortOrder === "asc" ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default comparison for other columns
|
||||||
|
if (aValue < bValue) {
|
||||||
|
return sortOrder === "asc" ? -1 : 1;
|
||||||
|
}
|
||||||
|
if (aValue > bValue) {
|
||||||
|
return sortOrder === "asc" ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="min-w-full">
|
<table className="min-w-full">
|
||||||
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary">
|
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary">
|
||||||
@@ -25,11 +69,19 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
? "cursor-pointer hover:brightness-hover active:brightness-press"
|
? "cursor-pointer hover:brightness-hover active:brightness-press"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
|
onClick={() => heading.sortable && headingSort(heading.title)}
|
||||||
|
onKeyUp={() => heading.sortable && headingSort(heading.title)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{heading.title}
|
{heading.title}
|
||||||
{heading.sortable && (
|
{sortColumn === heading.title && (
|
||||||
<ChevronUpIcon size={16} className="my-auto" />
|
<>
|
||||||
|
{sortOrder === "asc" ? (
|
||||||
|
<ChevronUpIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon size={16} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
@@ -37,10 +89,12 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, index) => (
|
{sortedRows.map((row, index) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
{row.map((item, index) => (
|
{row.map((item, index) => (
|
||||||
<td
|
<td
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: OK because column order never changes.
|
||||||
key={index}
|
key={index}
|
||||||
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
|
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
|
||||||
>
|
>
|
||||||
|
|||||||
+20
-16
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, useEffect, useState } 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.js";
|
||||||
|
|
||||||
@@ -92,9 +92,9 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId);
|
addToRemoveQueue(toastId);
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
for (const toast of state.toasts) {
|
||||||
addToRemoveQueue(toast.id);
|
addToRemoveQueue(toast.id);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -130,9 +130,9 @@ let memoryState: State = { toasts: [] };
|
|||||||
|
|
||||||
function dispatch(action: Action) {
|
function dispatch(action: Action) {
|
||||||
memoryState = reducer(memoryState, action);
|
memoryState = reducer(memoryState, action);
|
||||||
listeners.forEach((listener) => {
|
for (const listener of listeners) {
|
||||||
listener(memoryState);
|
listener(memoryState);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
@@ -166,18 +166,22 @@ function toast({ ...props }: Toast) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useToast() {
|
const subscribe = (listener: () => void) => {
|
||||||
const [state, setState] = useState<State>(memoryState);
|
listeners.push(listener);
|
||||||
|
return function unsubscribe() {
|
||||||
|
const index = listeners.indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const getState = () => {
|
||||||
listeners.push(setState);
|
return memoryState;
|
||||||
return () => {
|
};
|
||||||
const index = listeners.indexOf(setState);
|
|
||||||
if (index > -1) {
|
function useToast() {
|
||||||
listeners.splice(index, 1);
|
const state = useSyncExternalStore(subscribe, getState);
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
currentPage: "messages",
|
currentPage: "messages",
|
||||||
rasterSources: [],
|
rasterSources: [],
|
||||||
commandPaletteOpen: false,
|
commandPaletteOpen: false,
|
||||||
darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches,
|
darkMode:
|
||||||
|
localStorage.getItem("theme-dark") !== null
|
||||||
|
? localStorage.getItem("theme-dark") === "true"
|
||||||
|
: window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||||
accent: "orange",
|
accent: "orange",
|
||||||
connectDialogOpen: false,
|
connectDialogOpen: false,
|
||||||
nodeNumToBeRemoved: 0,
|
nodeNumToBeRemoved: 0,
|
||||||
@@ -96,6 +99,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
setDarkMode: (enabled: boolean) => {
|
setDarkMode: (enabled: boolean) => {
|
||||||
|
localStorage.setItem("theme-dark", enabled.toString());
|
||||||
set(
|
set(
|
||||||
produce<AppState>((draft) => {
|
produce<AppState>((draft) => {
|
||||||
draft.darkMode = enabled;
|
draft.darkMode = enabled;
|
||||||
@@ -104,7 +108,7 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
},
|
},
|
||||||
setNodeNumToBeRemoved: (nodeNum) =>
|
setNodeNumToBeRemoved: (nodeNum) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
nodeNumToBeRemoved: nodeNum
|
nodeNumToBeRemoved: nodeNum,
|
||||||
})),
|
})),
|
||||||
setAccent(color) {
|
setAccent(color) {
|
||||||
set(
|
set(
|
||||||
|
|||||||
@@ -530,8 +530,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
device.nodes.delete(nodeNum);
|
device.nodes.delete(nodeNum);
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
setMessageState: (
|
setMessageState: (
|
||||||
type: "direct" | "broadcast",
|
type: "direct" | "broadcast",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.js";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, type Types } from "@meshtastic/js";
|
||||||
|
|
||||||
export const subscribeAll = (
|
export const subscribeAll = (
|
||||||
device: Device,
|
device: Device,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ export const DeviceConfig = (): JSX.Element => {
|
|||||||
<Tabs defaultValue="Device">
|
<Tabs defaultValue="Device">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger key={tab.label} value={tab.label}>
|
||||||
key={tab.label}
|
|
||||||
value={tab.label}
|
|
||||||
>
|
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { Audio } from "@components/PageComponents/ModuleConfig/Audio.js";
|
|||||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
|
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
||||||
|
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
|
||||||
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
|
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
|
||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
||||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
||||||
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
|
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
|
|||||||
+20
-16
@@ -14,20 +14,20 @@ import {
|
|||||||
ZoomInIcon,
|
ZoomInIcon,
|
||||||
ZoomOutIcon,
|
ZoomOutIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Marker, useMap } from "react-map-gl";
|
import { Marker, useMap } from "react-map-gl";
|
||||||
import MapGl from "react-map-gl/maplibre";
|
import MapGl from "react-map-gl/maplibre";
|
||||||
|
|
||||||
export const MapPage = (): JSX.Element => {
|
export const MapPage = (): JSX.Element => {
|
||||||
const { nodes, waypoints } = useDevice();
|
const { nodes, waypoints } = useDevice();
|
||||||
const { rasterSources } = useAppStore();
|
const { rasterSources, darkMode } = useAppStore();
|
||||||
const { default: map } = useMap();
|
const { default: map } = useMap();
|
||||||
|
|
||||||
const [zoom, setZoom] = useState(0);
|
const [zoom, setZoom] = useState(0);
|
||||||
|
|
||||||
const allNodes = Array.from(nodes.values());
|
const allNodes = Array.from(nodes.values());
|
||||||
|
|
||||||
const getBBox = () => {
|
const getBBox = useCallback(() => {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export const MapPage = (): JSX.Element => {
|
|||||||
if (center) {
|
if (center) {
|
||||||
map.easeTo(center);
|
map.easeTo(center);
|
||||||
}
|
}
|
||||||
};
|
}, [allNodes, map]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.on("zoom", () => {
|
map?.on("zoom", () => {
|
||||||
@@ -128,6 +128,11 @@ export const MapPage = (): JSX.Element => {
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
renderWorldCopies={false}
|
renderWorldCopies={false}
|
||||||
maxPitch={0}
|
maxPitch={0}
|
||||||
|
style={{
|
||||||
|
filter: darkMode
|
||||||
|
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchZoomRotate={false}
|
touchZoomRotate={false}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
@@ -160,20 +165,19 @@ export const MapPage = (): JSX.Element => {
|
|||||||
key={node.num}
|
key={node.num}
|
||||||
longitude={node.position.longitudeI / 1e7}
|
longitude={node.position.longitudeI / 1e7}
|
||||||
latitude={node.position.latitudeI / 1e7}
|
latitude={node.position.latitudeI / 1e7}
|
||||||
|
style={{ filter: darkMode ? "invert(1)" : "" }}
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
|
onClick={() => {
|
||||||
|
map?.easeTo({
|
||||||
|
zoom: 12,
|
||||||
|
center: [
|
||||||
|
(node.position?.longitudeI ?? 0) / 1e7,
|
||||||
|
(node.position?.latitudeI ?? 0) / 1e7,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
||||||
className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5"
|
|
||||||
onClick={() => {
|
|
||||||
map?.easeTo({
|
|
||||||
zoom: 12,
|
|
||||||
center: [
|
|
||||||
(node.position?.longitudeI ?? 0) / 1e7,
|
|
||||||
(node.position?.latitudeI ?? 0) / 1e7,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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}
|
||||||
|
|||||||
+36
-24
@@ -1,15 +1,15 @@
|
|||||||
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.js";
|
||||||
|
import { Button } from "@components/UI/Button.js";
|
||||||
import { Mono } from "@components/generic/Mono.js";
|
import { Mono } from "@components/generic/Mono.js";
|
||||||
import { Table } from "@components/generic/Table/index.js";
|
import { Table } from "@components/generic/Table/index.js";
|
||||||
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { base16 } from "rfc4648";
|
|
||||||
import { Button } from "@components/UI/Button.js";
|
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { Fragment } from "react";
|
||||||
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
export interface DeleteNoteDialogProps {
|
export interface DeleteNoteDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -40,8 +40,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{ title: "Remove", type: "normal", sortable: false },
|
{ title: "Remove", type: "normal", sortable: false },
|
||||||
]}
|
]}
|
||||||
rows={filteredNodes.map((node) => [
|
rows={filteredNodes.map((node) => [
|
||||||
<Hashicon size={24} value={node.num.toString()} />,
|
<Hashicon key="icon" size={24} value={node.num.toString()} />,
|
||||||
<h1>
|
<h1 key="header">
|
||||||
{node.user?.longName ??
|
{node.user?.longName ??
|
||||||
(node.user?.macaddr
|
(node.user?.macaddr
|
||||||
? `Meshtastic ${base16
|
? `Meshtastic ${base16
|
||||||
@@ -50,34 +50,46 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
: `UNK: ${node.num}`)}
|
: `UNK: ${node.num}`)}
|
||||||
</h1>,
|
</h1>,
|
||||||
|
|
||||||
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>,
|
<Mono key="model">
|
||||||
<Mono>
|
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
|
||||||
|
</Mono>,
|
||||||
|
<Mono key="addr">
|
||||||
{base16
|
{base16
|
||||||
.stringify(node.user?.macaddr ?? [])
|
.stringify(node.user?.macaddr ?? [])
|
||||||
.match(/.{1,2}/g)
|
.match(/.{1,2}/g)
|
||||||
?.join(":") ?? "UNK"}
|
?.join(":") ?? "UNK"}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
node.lastHeard === 0 ? (
|
<Fragment key="lastHeard">
|
||||||
<p>Never</p>
|
{node.lastHeard === 0 ? (
|
||||||
) : (
|
<p>Never</p>
|
||||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
) : (
|
||||||
),
|
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||||
<Mono>
|
)}
|
||||||
|
</Fragment>,
|
||||||
|
<Mono key="snr">
|
||||||
{node.snr}db/
|
{node.snr}db/
|
||||||
{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>
|
<Mono key="hops">
|
||||||
{node.lastHeard != 0 ?
|
{node.lastHeard !== 0
|
||||||
(node.viaMqtt === false && node.hopsAway === 0
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
? "Direct": node.hopsAway.toString() + " hops away")
|
? "Direct"
|
||||||
|
: `${node.hopsAway.toString()} hops away`
|
||||||
: "-"}
|
: "-"}
|
||||||
{node.viaMqtt === true? ", via MQTT": ""}
|
{node.viaMqtt === true ? ", via MQTT" : ""}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Button variant="destructive" onClick={() => {
|
<Button
|
||||||
setNodeNumToBeRemoved(node.num);
|
key="remove"
|
||||||
setDialogOpen("nodeRemoval", true)
|
variant="destructive"
|
||||||
}}><TrashIcon />Remove</Button>
|
onClick={() => {
|
||||||
|
setNodeNumToBeRemoved(node.num);
|
||||||
|
setDialogOpen("nodeRemoval", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
Remove
|
||||||
|
</Button>,
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Message } from "@bufbuild/protobuf";
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { IsBoolean, IsEnum, IsInt } from "class-validator";
|
import { IsBoolean, IsEnum, IsInt, IsString } from "class-validator";
|
||||||
|
|
||||||
export class DeviceValidation
|
export class DeviceValidation
|
||||||
implements Omit<Protobuf.Config.Config_DeviceConfig, keyof Message>
|
implements Omit<Protobuf.Config.Config_DeviceConfig, keyof Message>
|
||||||
@@ -34,4 +34,10 @@ export class DeviceValidation
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
disableTripleClick: boolean;
|
disableTripleClick: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
ledHeartbeatDisabled: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
tzdef: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,7 @@ export class DisplayValidation
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
wakeOnTapOrMotion: boolean;
|
wakeOnTapOrMotion: boolean;
|
||||||
|
|
||||||
|
@IsEnum(Protobuf.Config.Config_DisplayConfig_CompassOrientation)
|
||||||
|
compassOrientation: Protobuf.Config.Config_DisplayConfig_CompassOrientation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import type { Message } from "@bufbuild/protobuf";
|
|||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
|
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
|
||||||
|
|
||||||
const DeprecatedPositionValidationFields = ['gpsEnabled', 'gpsAttemptTime'];
|
const DeprecatedPositionValidationFields = ["gpsEnabled", "gpsAttemptTime"];
|
||||||
|
|
||||||
export class PositionValidation
|
export class PositionValidation
|
||||||
implements Omit<Protobuf.Config.Config_PositionConfig, keyof Message | typeof DeprecatedPositionValidationFields[number]>
|
implements
|
||||||
|
Omit<
|
||||||
|
Protobuf.Config.Config_PositionConfig,
|
||||||
|
keyof Message | (typeof DeprecatedPositionValidationFields)[number]
|
||||||
|
>
|
||||||
{
|
{
|
||||||
@IsInt()
|
@IsInt()
|
||||||
positionBroadcastSecs: number;
|
positionBroadcastSecs: number;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { Message } from "@bufbuild/protobuf";
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import { IsBoolean, IsString, Length, IsNumber } from "class-validator";
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
export class MqttValidation
|
export class MqttValidation
|
||||||
implements
|
implements
|
||||||
@@ -47,8 +53,10 @@ export class MqttValidationMapReportSettings
|
|||||||
Omit<Protobuf.ModuleConfig.ModuleConfig_MapReportSettings, keyof Message>
|
Omit<Protobuf.ModuleConfig.ModuleConfig_MapReportSettings, keyof Message>
|
||||||
{
|
{
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
publishIntervalSecs: number;
|
publishIntervalSecs: number;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
positionPrecision: number;
|
positionPrecision: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,10 @@ export class PaxcounterValidation
|
|||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
paxcounterUpdateInterval: number;
|
paxcounterUpdateInterval: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
bleThreshold: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
wifiThreshold: number;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -1,5 +1 @@
|
|||||||
{
|
{ "github": { "silent": true } }
|
||||||
"github": {
|
|
||||||
"silent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { resolve } from "path";
|
import { resolve } from "node:path";
|
||||||
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";
|
||||||
|
|||||||
Reference in New Issue
Block a user