Merge branch 'master' into feature/node-quick-options

This commit is contained in:
Tilen Komel
2025-01-23 23:57:05 +01:00
107 changed files with 1776 additions and 1141 deletions
+5 -5
View File
@@ -1,5 +1,5 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
@@ -7,9 +7,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Protobuf } from "@meshtastic/js";
import { useForm } from "react-hook-form";
+14 -7
View File
@@ -1,10 +1,11 @@
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
import { QRDialog } from "@components/Dialog/QRDialog.js";
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
export const DialogManager = (): JSX.Element => {
const { channels, config, dialog, setDialogOpen } = useDevice();
@@ -49,6 +50,12 @@ export const DialogManager = (): JSX.Element => {
setDialogOpen("nodeRemoval", open);
}}
/>
<PkiBackupDialog
open={dialog.pkiBackup}
onOpenChange={(open) => {
setDialogOpen("pkiBackup", open);
}}
/>
</>
);
};
+7 -7
View File
@@ -1,5 +1,5 @@
import { Button } from "@components/UI/Button.js";
import { Checkbox } from "@components/UI/Checkbox.js";
import { Button } from "@components/UI/Button.tsx";
import { Checkbox } from "@components/UI/Checkbox.tsx";
import {
Dialog,
DialogContent,
@@ -7,11 +7,11 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
import { Switch } from "@components/UI/Switch.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { toByteArray } from "base64-js";
import { useEffect, useState } from "react";
+113 -60
View File
@@ -1,20 +1,26 @@
import { BLE } from "@components/PageComponents/Connect/BLE.js";
import { HTTP } from "@components/PageComponents/Connect/HTTP.js";
import { Serial } from "@components/PageComponents/Connect/Serial.js";
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@app/core/hooks/useBrowserFeatureDetection";
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
} from "@components/UI/Dialog.tsx";
import { AlertCircle, InfoIcon, } from "lucide-react";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.js";
import { Link } from "@components/UI/Typography/Link.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
} from "@components/UI/Tabs.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { Link } from "../UI/Typography/Link";
import { Fragment } from "react/jsx-runtime";
export interface TabElementProps {
closeDialog: () => void;
@@ -23,44 +29,114 @@ export interface TabElementProps {
export interface TabManifest {
label: string;
element: React.FC<TabElementProps>;
disabled: boolean;
disabledMessage: string;
disabledLink?: string;
isDisabled: boolean;
}
const tabs: TabManifest[] = [
{
label: "HTTP",
element: HTTP,
disabled: false,
disabledMessage: "Unsuported connection method",
},
{
label: "Bluetooth",
element: BLE,
disabled: !navigator.bluetooth,
disabledMessage:
"Web Bluetooth is currently only supported by Chromium-based browsers",
disabledLink:
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
},
{
label: "Serial",
element: Serial,
disabled: !navigator.serial,
disabledMessage:
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
},
];
export interface NewDeviceProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface FeatureErrorProps {
missingFeatures: BrowserFeature[];
}
const links: { [key: string]: string } = {
"Web Bluetooth":
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
"Web Serial":
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
"Secure Context":
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
};
const listFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction'
});
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
if (missingFeatures.length === 0) return null;
const browserFeatures = missingFeatures.filter(feature => feature !== "Secure Context");
const needsSecureContext = missingFeatures.includes("Secure Context");
const formatFeatureList = (features: string[]) => {
const parts = listFormatter.formatToParts(features);
return parts.map((part) => {
if (part.type === 'element') {
return (
<Link
key={part.value}
href={links[part.value]}
>
{part.value}
</Link>
);
}
return <Fragment key={part.value}>{part.value}</Fragment>;
});
};
return (
<Subtle className="flex flex-col items-start gap-2 text-black bg-red-200/80 p-4 rounded-md">
<div className="flex items-center gap-2 w-full">
<AlertCircle size={40} className="mr-2 flex-shrink-0" />
<div className="flex flex-col gap-3">
<p className="text-sm">
{browserFeatures.length > 0 && (
<>
This application requires {formatFeatureList(browserFeatures)}.
Please use a Chromium-based browser like Chrome or Edge.
</>
)}
{needsSecureContext && (
<>
{browserFeatures.length > 0 && " Additionally, it"}
{browserFeatures.length === 0 && "This application"} requires a{" "}
<Link
href={links["Secure Context"]}
>
secure context
</Link>
. Please connect using HTTPS or localhost.
</>
)}
</p>
</div>
</div>
</Subtle>
);
};
export const NewDeviceDialog = ({
open,
onOpenChange,
}: NewDeviceProps): JSX.Element => {
const { unsupported } = useBrowserFeatureDetection();
const tabs: TabManifest[] = [
{
label: "HTTP",
element: HTTP,
isDisabled: false,
},
{
label: "Bluetooth",
element: BLE,
isDisabled:
unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
},
{
label: "Serial",
element: Serial,
isDisabled:
unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
},
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
@@ -73,7 +149,6 @@ export const NewDeviceDialog = ({
<TabsTrigger
key={tab.label}
value={tab.label}
disabled={tab.disabled}
>
{tab.label}
</TabsTrigger>
@@ -81,35 +156,13 @@ export const NewDeviceDialog = ({
</TabsList>
{tabs.map((tab) => (
<TabsContent key={tab.label} value={tab.label}>
{tab.disabled ? (
<p className="text-sm text-slate-500 dark:text-slate-400">
{tab.disabledMessage}
</p>
) : (
<fieldset disabled={tab.isDisabled}>
{tab.isDisabled ? <ErrorMessage missingFeatures={unsupported} /> : null}
<tab.element closeDialog={() => onOpenChange(false)} />
)}
</fieldset>
</TabsContent>
))}
</Tabs>
{(!navigator.bluetooth || !navigator.serial) && (
<>
<Subtle>
Web Bluetooth and Web Serial are currently only supported by
Chromium-based browsers.
</Subtle>
<Subtle>
Read more:&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
Web Bluetooth
</Link>
&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
Web Serial
</Link>
</Subtle>
</>
)}
</DialogContent>
</Dialog>
);
+134
View File
@@ -0,0 +1,134 @@
import { useDevice } from "@app/core/stores/deviceStore";
import { Button } from "@components/UI/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { fromByteArray } from "base64-js";
import { DownloadIcon, PrinterIcon } from "lucide-react";
import React from "react";
export interface PkiBackupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const PkiBackupDialog = ({
open,
onOpenChange,
}: PkiBackupDialogProps) => {
const { config, setDialogOpen } = useDevice();
const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey;
const decodeKeyData = React.useCallback(
(key: Uint8Array<ArrayBufferLike>) => {
if (!key) return "";
return fromByteArray(key ?? new Uint8Array(0));
},
[],
);
const closeDialog = React.useCallback(() => {
setDialogOpen("pkiBackup", false);
}, [setDialogOpen]);
const renderPrintWindow = React.useCallback(() => {
if (!privateKey || !publicKey) return;
const printWindow = window.open("", "_blank");
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>=== MESHTASTIC KEYS ===</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
h1 { font-size: 18px; }
p { font-size: 14px; word-break: break-all; }
</style>
</head>
<body>
<h1>=== MESHTASTIC KEYS ===</h1>
<br>
<h2>Public Key:</h2>
<p>${decodeKeyData(publicKey)}</p>
<h2>Private Key:</h2>
<p>${decodeKeyData(privateKey)}</p>
<br>
<p>=== END OF KEYS ===</p>
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
closeDialog();
}
}, [decodeKeyData, privateKey, publicKey, closeDialog]);
const createDownloadKeyFile = React.useCallback(() => {
if (!privateKey || !publicKey) return;
const decodedPrivateKey = decodeKeyData(privateKey);
const decodedPublicKey = decodeKeyData(publicKey);
const formattedContent = [
"=== MESHTASTIC KEYS ===\n\n",
"Private Key:\n",
decodedPrivateKey,
"\n\nPublic Key:\n",
decodedPublicKey,
"\n\n=== END OF KEYS ===",
].join("");
const blob = new Blob([formattedContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "meshtastic_keys.txt";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
closeDialog();
URL.revokeObjectURL(url);
}, [decodeKeyData, privateKey, publicKey, closeDialog]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Backup Keys</DialogTitle>
<DialogDescription>
Its important to backup your public and private keys and store your
backup securely!
</DialogDescription>
<DialogDescription>
<span className="font-bold break-before-auto">
If you lose your keys, you will need to reset your device.
</span>
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-6">
<Button
variant={"default"}
onClick={() => createDownloadKeyFile()}
className=""
>
<DownloadIcon size={20} className="mr-2" />
Download
</Button>
<Button variant={"default"} onClick={() => renderPrintWindow()}>
<PrinterIcon size={20} className="mr-2" />
Print
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -1,4 +1,4 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
@@ -6,7 +6,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
} from "@components/UI/Dialog.tsx";
export interface PkiRegenerateDialogProps {
open: boolean;
+4 -4
View File
@@ -1,4 +1,4 @@
import { Checkbox } from "@components/UI/Checkbox.js";
import { Checkbox } from "@components/UI/Checkbox.tsx";
import {
Dialog,
DialogContent,
@@ -6,9 +6,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Protobuf, type Types } from "@meshtastic/js";
import { fromByteArray } from "base64-js";
import { ClipboardIcon } from "lucide-react";
+4 -4
View File
@@ -1,13 +1,13 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ClockIcon, RefreshCwIcon } from "lucide-react";
import { useState } from "react";
+4 -4
View File
@@ -1,6 +1,6 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
@@ -8,8 +8,8 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Label } from "@components/UI/Label.js";
} from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx";
export interface RemoveNodeDialogProps {
open: boolean;
+4 -4
View File
@@ -1,13 +1,13 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ClockIcon, PowerIcon } from "lucide-react";
import { useState } from "react";