Merge branch 'master' into feature/node-quick-options
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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:
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
|
||||
Web Bluetooth
|
||||
</Link>
|
||||
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
|
||||
Web Serial
|
||||
</Link>
|
||||
</Subtle>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user