fix: connect new node no longer crashes. (#610)
This commit is contained in:
@@ -57,6 +57,7 @@ export const CommandPalette = () => {
|
|||||||
const {
|
const {
|
||||||
commandPaletteOpen,
|
commandPaletteOpen,
|
||||||
setCommandPaletteOpen,
|
setCommandPaletteOpen,
|
||||||
|
setConnectDialogOpen,
|
||||||
setSelectedDevice,
|
setSelectedDevice,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
@@ -133,7 +134,7 @@ export const CommandPalette = () => {
|
|||||||
label: "Connect New Node",
|
label: "Connect New Node",
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
action() {
|
action() {
|
||||||
setSelectedDevice(0);
|
setConnectDialogOpen(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,12 +22,9 @@ import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
|||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { Link } from "../UI/Typography/Link.tsx";
|
import { Link } from "../UI/Typography/Link.tsx";
|
||||||
import { Fragment } from "react/jsx-runtime";
|
import { Fragment } from "react/jsx-runtime";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export interface TabElementProps {
|
export interface TabElementProps {
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
connectionInProgress: boolean;
|
|
||||||
setConnectionInProgress: (inProgress: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabManifest {
|
export interface TabManifest {
|
||||||
@@ -114,26 +111,25 @@ export const NewDeviceDialog = ({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: NewDeviceProps) => {
|
}: NewDeviceProps) => {
|
||||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
|
||||||
const { unsupported } = useBrowserFeatureDetection();
|
const { unsupported } = useBrowserFeatureDetection();
|
||||||
|
|
||||||
const tabs: TabManifest[] = [
|
const tabs: TabManifest[] = [
|
||||||
{
|
{
|
||||||
label: "HTTP",
|
label: "HTTP",
|
||||||
element: HTTP,
|
element: HTTP,
|
||||||
isDisabled: connectionInProgress,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Bluetooth",
|
label: "Bluetooth",
|
||||||
element: BLE,
|
element: BLE,
|
||||||
isDisabled: unsupported.includes("Web Bluetooth") ||
|
isDisabled: unsupported.includes("Web Bluetooth") ||
|
||||||
unsupported.includes("Secure Context") || connectionInProgress,
|
unsupported.includes("Secure Context"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Serial",
|
label: "Serial",
|
||||||
element: Serial,
|
element: Serial,
|
||||||
isDisabled: unsupported.includes("Web Serial") ||
|
isDisabled: unsupported.includes("Web Serial") ||
|
||||||
unsupported.includes("Secure Context") || connectionInProgress,
|
unsupported.includes("Secure Context"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -160,8 +156,6 @@ export const NewDeviceDialog = ({
|
|||||||
: null}
|
: null}
|
||||||
<tab.element
|
<tab.element
|
||||||
closeDialog={() => onOpenChange(false)}
|
closeDialog={() => onOpenChange(false)}
|
||||||
setConnectionInProgress={setConnectionInProgress}
|
|
||||||
connectionInProgress={connectionInProgress}
|
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
|
|
||||||
vi.mock("@core/stores/messageStore");
|
vi.mock("@core/stores/messageStore");
|
||||||
vi.mock("./useRefreshKeysDialog");
|
vi.mock("./useRefreshKeysDialog");
|
||||||
@@ -25,63 +24,6 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders dialog when there is a node error for the active chat", () => {
|
|
||||||
const deviceId = 1;
|
|
||||||
const nodeWithErrorNum = 12345;
|
|
||||||
const activeChatNum = nodeWithErrorNum;
|
|
||||||
|
|
||||||
const deviceStore = useDeviceStore.getState().addDevice(deviceId);
|
|
||||||
|
|
||||||
deviceStore.addNodeInfo({
|
|
||||||
num: nodeWithErrorNum,
|
|
||||||
user: {
|
|
||||||
id: nodeWithErrorNum.toString(),
|
|
||||||
publicKey: new Uint8Array(0),
|
|
||||||
hwModel: Protobuf.Mesh.HardwareModel.HELTEC_V3,
|
|
||||||
longName: "Problem Node Long",
|
|
||||||
shortName: "ProbNode",
|
|
||||||
isLicensed: false,
|
|
||||||
macaddr: new Uint8Array(0),
|
|
||||||
},
|
|
||||||
lastHeard: Date.now() / 1000,
|
|
||||||
snr: 10,
|
|
||||||
} as Protobuf.Mesh.NodeInfo);
|
|
||||||
|
|
||||||
deviceStore.setNodeError(activeChatNum, "PKI_MISMATCH");
|
|
||||||
|
|
||||||
const updatedDeviceState = useDeviceStore.getState().getDevice(deviceId);
|
|
||||||
if (!updatedDeviceState) {
|
|
||||||
throw new Error(
|
|
||||||
"Failed to get updated device state from store for provider",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
|
||||||
const mockHandleClose = vi.fn();
|
|
||||||
const mockHandleRemove = vi.fn();
|
|
||||||
mockUseRefreshKeysDialog.mockReturnValue({
|
|
||||||
handleCloseDialog: mockHandleClose,
|
|
||||||
handleNodeRemove: mockHandleRemove,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<DeviceContext.Provider value={updatedDeviceState}>
|
|
||||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
|
||||||
</DeviceContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/Keys Mismatch - Problem Node Long/))
|
|
||||||
.toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
/Your node is unable to send a direct message to node: Problem Node Long \(ProbNode\)/,
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("button", { name: "Request New Keys" }))
|
|
||||||
.toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not render dialog if no error exists for active chat", () => {
|
test("does not render dialog if no error exists for active chat", () => {
|
||||||
const deviceId = 1;
|
const deviceId = 1;
|
||||||
const activeChatNum = 54321;
|
const activeChatNum = 54321;
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||||
|
|
||||||
export const BLE = (
|
export const BLE = (
|
||||||
{ setConnectionInProgress, closeDialog }: TabElementProps,
|
{ closeDialog }: TabElementProps,
|
||||||
) => {
|
) => {
|
||||||
|
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const messageStore = useMessageStore();
|
const messageStore = useMessageStore();
|
||||||
@@ -40,7 +41,10 @@ export const BLE = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<fieldset
|
||||||
|
className="flex w-full flex-col gap-2 p-4"
|
||||||
|
disabled={connectionInProgress}
|
||||||
|
>
|
||||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||||
{bleDevices.map((device) => (
|
{bleDevices.map((device) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -80,6 +84,6 @@ export const BLE = (
|
|||||||
>
|
>
|
||||||
<span>New device</span>
|
<span>New device</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</fieldset>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ interface FormData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HTTP = (
|
export const HTTP = (
|
||||||
{ closeDialog, setConnectionInProgress, connectionInProgress }:
|
{ closeDialog }: TabElementProps,
|
||||||
TabElementProps,
|
|
||||||
) => {
|
) => {
|
||||||
|
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||||
const isURLHTTPS = location.protocol === "https:";
|
const isURLHTTPS = location.protocol === "https:";
|
||||||
|
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
@@ -73,7 +73,11 @@ export const HTTP = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex w-full flex-col gap-2 p-4" onSubmit={onSubmit}>
|
<form className="flex w-full flex-col gap-2 p-4" onSubmit={onSubmit}>
|
||||||
<div className="flex flex-col gap-2" style={{ minHeight: "12rem" }}>
|
<fieldset
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
style={{ minHeight: "12rem" }}
|
||||||
|
disabled={connectionInProgress}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Label>IP Address/Hostname</Label>
|
<Label>IP Address/Hostname</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -132,7 +136,7 @@ export const HTTP = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</fieldset>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||||
|
|
||||||
export const Serial = (
|
export const Serial = (
|
||||||
{ setConnectionInProgress, closeDialog }: TabElementProps,
|
{ closeDialog }: TabElementProps,
|
||||||
) => {
|
) => {
|
||||||
|
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||||
const { addDevice } = useDeviceStore();
|
const { addDevice } = useDeviceStore();
|
||||||
const messageStore = useMessageStore();
|
const messageStore = useMessageStore();
|
||||||
@@ -46,7 +47,10 @@ export const Serial = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<fieldset
|
||||||
|
className="flex w-full flex-col gap-2 p-4"
|
||||||
|
disabled={connectionInProgress}
|
||||||
|
>
|
||||||
<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) => {
|
||||||
const { usbProductId, usbVendorId } = port.getInfo();
|
const { usbProductId, usbVendorId } = port.getInfo();
|
||||||
@@ -85,6 +89,6 @@ export const Serial = (
|
|||||||
>
|
>
|
||||||
<span>New device</span>
|
<span>New device</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</fieldset>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,19 +92,22 @@ export interface Device {
|
|||||||
) => Protobuf.Mesh.NodeInfo[];
|
) => Protobuf.Mesh.NodeInfo[];
|
||||||
getNodesLength: () => number;
|
getNodesLength: () => number;
|
||||||
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
|
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
|
||||||
|
getMyNode: () => Protobuf.Mesh.NodeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceState {
|
export interface DeviceState {
|
||||||
devices: Map<number, Device>;
|
|
||||||
remoteDevices: Map<number, undefined>;
|
|
||||||
|
|
||||||
addDevice: (id: number) => Device;
|
addDevice: (id: number) => Device;
|
||||||
removeDevice: (id: number) => void;
|
removeDevice: (id: number) => void;
|
||||||
getDevices: () => Device[];
|
getDevices: () => Device[];
|
||||||
getDevice: (id: number) => Device | undefined;
|
getDevice: (id: number) => Device | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
interface PrivateDeviceState extends DeviceState {
|
||||||
|
devices: Map<number, Device>;
|
||||||
|
remoteDevices: Map<number, undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
|
||||||
devices: new Map(),
|
devices: new Map(),
|
||||||
remoteDevices: new Map(),
|
remoteDevices: new Map(),
|
||||||
|
|
||||||
@@ -576,6 +579,14 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
return device.nodesMap.get(nodeNum);
|
return device.nodesMap.get(nodeNum);
|
||||||
},
|
},
|
||||||
|
getMyNode: (): Protobuf.Mesh.NodeInfo => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error(`Device ${id} not found`);
|
||||||
|
}
|
||||||
|
return device.nodesMap.get(device.hardware.myNodeNum) ??
|
||||||
|
create(Protobuf.Mesh.NodeInfoSchema);
|
||||||
|
},
|
||||||
getNodesLength: () => {
|
getNodesLength: () => {
|
||||||
const device = get().devices.get(id);
|
const device = get().devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
|
|||||||
Reference in New Issue
Block a user