refactor: switch to using Bun (#718)

This commit is contained in:
Dan Ditomaso
2025-07-16 22:30:10 -04:00
committed by GitHub
parent acfd7ebd2d
commit 704d06cfe7
401 changed files with 35294 additions and 39107 deletions
@@ -1,8 +1,8 @@
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Ensure the path is correct for import
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
vi.mock("@core/stores/messageStore", () => ({
useMessageStore: vi.fn(() => ({
@@ -26,9 +26,7 @@ describe("DeleteMessagesDialog", () => {
});
it("calls onOpenChange with false when the close button (X) is clicked", () => {
render(
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
);
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
const closeButton = screen.queryByTestId("dialog-close-button");
if (!closeButton) {
throw new Error(
@@ -41,15 +39,15 @@ describe("DeleteMessagesDialog", () => {
});
it("renders the dialog when open is true", () => {
render(
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
);
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
expect(screen.getByText("Clear All Messages")).toBeInTheDocument();
expect(screen.getByText(/This action will clear all message history./))
.toBeInTheDocument();
expect(
screen.getByText(/This action will clear all message history./),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Clear Messages" }))
.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Clear Messages" }),
).toBeInTheDocument();
});
it("does not render the dialog when open is false", () => {
@@ -60,18 +58,14 @@ describe("DeleteMessagesDialog", () => {
});
it("calls onOpenChange with false when the dismiss button is clicked", () => {
render(
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
);
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
it("calls deleteAllMessages and onOpenChange with false when the clear messages button is clicked", () => {
render(
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
);
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Clear Messages" }));
expect(mockClearAllMessages).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
@@ -9,8 +9,8 @@ import {
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { AlertTriangleIcon } from "lucide-react";
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
import { useTranslation } from "react-i18next";
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
export interface DeleteMessagesDialogProps {
open: boolean;
@@ -41,11 +41,7 @@ export const DeleteMessagesDialog = ({
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
<Button
variant="outline"
onClick={handleCloseDialog}
name="dismiss"
>
<Button variant="outline" onClick={handleCloseDialog} name="dismiss">
{t("button.dismiss")}
</Button>
<Button
@@ -1,5 +1,5 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { create } from "@bufbuild/protobuf";
import { GenericInput } from "@components/Form/FormInput.tsx";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
@@ -10,13 +10,13 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { zodResolver } from "@hookform/resolvers/zod";
import { Protobuf } from "@meshtastic/core";
import { useForm } from "react-hook-form";
import { GenericInput } from "@components/Form/FormInput.tsx";
import { useTranslation } from "react-i18next";
import { Label } from "../UI/Label.tsx";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Label } from "../UI/Label.tsx";
export interface User {
longName: string;
@@ -76,15 +76,11 @@ export const DeviceNameDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{t("deviceName.title")}</DialogTitle>
<DialogDescription>
{t("deviceName.description")}
</DialogDescription>
<DialogDescription>{t("deviceName.description")}</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<div>
<Label htmlFor="longName">
{t("deviceName.longName")}
</Label>
<Label htmlFor="longName">{t("deviceName.longName")}</Label>
<GenericInput
control={control}
field={{
@@ -104,9 +100,7 @@ export const DeviceNameDialog = ({
/>
</div>
<div>
<Label htmlFor="shortName">
{t("deviceName.shortName")}
</Label>
<Label htmlFor="shortName">{t("deviceName.shortName")}</Label>
<GenericInput
control={control}
field={{
@@ -134,10 +128,7 @@ export const DeviceNameDialog = ({
>
{t("button.reset")}
</Button>
<Button
type="submit"
name="save"
>
<Button type="submit" name="save">
{t("button.save")}
</Button>
</DialogFooter>
@@ -1,16 +1,16 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
import { RebootOTADialog } from "@components/Dialog/RebootOTADialog.tsx";
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
export const DialogManager = () => {
const { channels, config, dialog, setDialogOpen } = useDevice();
@@ -1,6 +1,5 @@
import { create, fromBinary } from "@bufbuild/protobuf";
import { Button } from "@components/UI/Button.tsx";
import { Checkbox } from "../UI/Checkbox/index.tsx";
import {
Dialog,
DialogClose,
@@ -18,6 +17,7 @@ import { Protobuf } from "@meshtastic/core";
import { toByteArray } from "base64-js";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Checkbox } from "../UI/Checkbox/index.tsx";
export interface ImportDialogProps {
open: boolean;
@@ -25,10 +25,7 @@ export interface ImportDialogProps {
loraConfig?: Protobuf.Config.Config_LoRaConfig;
}
export const ImportDialog = ({
open,
onOpenChange,
}: ImportDialogProps) => {
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const { t } = useTranslation("dialog");
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
@@ -69,7 +66,7 @@ export const ImportDialog = ({
setValidUrl(false);
setChannelSet(undefined);
}
}, [importDialogInput]);
}, [importDialogInput, t]);
const apply = () => {
channelSet?.settings.map(
@@ -77,9 +74,10 @@ export const ImportDialog = ({
connection?.setChannel(
create(Protobuf.Channel.ChannelSchema, {
index,
role: index === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
role:
index === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
}),
);
@@ -104,9 +102,7 @@ export const ImportDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{t("import.title")}</DialogTitle>
<DialogDescription>
{t("import.description")}
</DialogDescription>
<DialogDescription>{t("import.description")}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<Label>{t("import.channelSetUrl")}</Label>
@@ -127,25 +123,21 @@ export const ImportDialog = ({
checked={channelSet?.loraConfig?.usePreset ?? true}
/>
</div>
{
/* <Select
{/* <Select
label="Modem Preset"
disabled
value={channelSet?.loraConfig?.modemPreset}
>
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
</Select> */
}
</Select> */}
</div>
{
/* <Select
{/* <Select
label="Region"
disabled
value={channelSet?.loraConfig?.region}
>
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
</Select> */
}
</Select> */}
<span className="text-md block font-medium text-text-primary">
{t("import.channels")}
@@ -1,4 +1,7 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogClose,
@@ -7,9 +10,6 @@ import {
DialogHeader,
DialogTitle,
} from "../UI/Dialog.tsx";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
export interface LocationResponseDialogProps {
location: Types.PacketMetadata<Protobuf.Mesh.Position> | undefined;
@@ -26,16 +26,19 @@ export const LocationResponseDialog = ({
const { getNode } = useDevice();
const from = getNode(location?.from ?? 0);
const longName = from?.user?.longName ??
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const shortName = from?.user?.shortName ??
const shortName =
from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
const position = location?.data;
const hasCoordinates = position &&
const hasCoordinates =
position &&
typeof position.latitudeI === "number" &&
typeof position.longitudeI === "number" &&
typeof position.altitude === "number";
@@ -53,40 +56,38 @@ export const LocationResponseDialog = ({
</DialogTitle>
</DialogHeader>
<DialogDescription>
{hasCoordinates
? (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p>
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
position.latitudeI ?? 0 / 1e7
}&mlon=${position.longitudeI ?? 0 / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{" "}
{position.latitudeI ?? 0 / 1e7},{" "}
{position.longitudeI ?? 0 / 1e7}
</a>
</p>
<p>
{t("locationResponse.altitude")} {position.altitude}
{(position.altitude ?? 0) < 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</p>
</span>
</div>
)
: (
// Optional: Show a message if coordinates are not available
<p className="text-textPrimary">
{t("locationResponse.noCoordinates")}
</p>
)}
{hasCoordinates ? (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p>
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
position.latitudeI ?? 0 / 1e7
}&mlon=${position.longitudeI ?? 0 / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{" "}
{position.latitudeI ?? 0 / 1e7},{" "}
{position.longitudeI ?? 0 / 1e7}
</a>
</p>
<p>
{t("locationResponse.altitude")} {position.altitude}
{(position.altitude ?? 0) < 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</p>
</span>
</div>
) : (
// Optional: Show a message if coordinates are not available
<p className="text-textPrimary">
{t("locationResponse.noCoordinates")}
</p>
)}
</DialogDescription>
</DialogContent>
</Dialog>
@@ -1,4 +1,5 @@
import { Button } from "@components/UI/Button.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import {
Dialog,
DialogClose,
@@ -8,9 +9,8 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Trans, useTranslation } from "react-i18next";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
export interface ManagedModeDialogProps {
open: boolean;
@@ -36,7 +36,7 @@ export const ManagedModeDialog = ({
<Trans
i18nKey="managedMode.description"
components={{
"bold": <p className="font-bold inline" />,
bold: <p className="font-bold inline" />,
}}
/>
</DialogDescription>
@@ -1,7 +1,3 @@
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@core/hooks/useBrowserFeatureDetection.ts";
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
@@ -18,6 +14,10 @@ import {
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@core/hooks/useBrowserFeatureDetection.ts";
import { AlertCircle } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "../UI/Typography/Link.tsx";
@@ -45,24 +45,23 @@ interface FeatureErrorProps {
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
"Web Bluetooth": {
href:
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth",
},
"Web Serial": {
href:
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebSerial",
},
"Secure Context": {
href:
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
i18nKey: "newDeviceDialog.validation.requiresSecureContext",
},
};
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
if (missingFeatures.length === 0) return null;
if (missingFeatures.length === 0) {
return null;
}
const browserFeatures = missingFeatures.filter(
(feature) => feature !== "Secure Context",
@@ -70,11 +69,11 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature =
(tabId === "BLE" && browserFeatures.includes("Web Bluetooth"))
tabId === "BLE" && browserFeatures.includes("Web Bluetooth")
? "Web Bluetooth"
: (tabId === "Serial" && browserFeatures.includes("Web Serial"))
? "Web Serial"
: undefined;
: tabId === "Serial" && browserFeatures.includes("Web Serial")
? "Web Serial"
: undefined;
return (
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm text-slate-500 dark:text-slate-400">
@@ -97,9 +96,11 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
{needsFeature && needsSecureContext && " "}
{needsSecureContext && (
<Trans
i18nKey={browserFeatures.length > 0
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
: "newDeviceDialog.validation.requiresSecureContext"}
i18nKey={
browserFeatures.length > 0
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
: "newDeviceDialog.validation.requiresSecureContext"
}
components={{
"0": (
<Link
@@ -117,10 +118,7 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
);
};
export const NewDeviceDialog = ({
open,
onOpenChange,
}: NewDeviceProps) => {
export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
const { t } = useTranslation("dialog");
const { unsupported } = useBrowserFeatureDetection();
@@ -135,14 +133,16 @@ export const NewDeviceDialog = ({
id: "BLE",
label: t("newDeviceDialog.tabBluetooth"),
element: BLE,
isDisabled: unsupported.includes("Web Bluetooth") ||
isDisabled:
unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
},
{
id: "Serial",
label: t("newDeviceDialog.tabSerial"),
element: Serial,
isDisabled: unsupported.includes("Web Serial") ||
isDisabled:
unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
},
];
@@ -165,19 +165,11 @@ export const NewDeviceDialog = ({
{tabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<fieldset disabled={tab.isDisabled}>
{(tab.id !== "HTTP" &&
tab.isDisabled)
? (
<ErrorMessage
missingFeatures={unsupported}
tabId={tab.id}
/>
)
: (
<tab.element
closeDialog={() => onOpenChange(false)}
/>
)}
{tab.id !== "HTTP" && tab.isDisabled ? (
<ErrorMessage missingFeatures={unsupported} tabId={tab.id} />
) : (
<tab.element closeDialog={() => onOpenChange(false)} />
)}
</fieldset>
</TabsContent>
))}
@@ -1,22 +1,13 @@
import { useEffect, useState } from "react";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { DeviceImage } from "@components/generic/DeviceImage.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Uptime } from "@components/generic/Uptime.tsx";
import { toast } from "@core/hooks/useToast.ts";
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
import { cn } from "@core/utils/cn.ts";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@components/UI/Accordion.tsx";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogClose,
@@ -25,7 +16,23 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
import { toast } from "@core/hooks/useToast.ts";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useNavigate } from "@tanstack/react-router";
import {
BellIcon,
BellOffIcon,
@@ -35,16 +42,8 @@ import {
TrashIcon,
WaypointsIcon,
} from "lucide-react";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
export interface NodeDetailsDialogProps {
open: boolean;
@@ -72,21 +71,29 @@ export const NodeDetailsDialog = ({
);
useEffect(() => {
if (!node) return;
if (!node) {
return;
}
setIsFavoriteState(node?.isFavorite);
setIsIgnoredState(node?.isIgnored);
}, [node]);
if (!node) return;
if (!node) {
return;
}
function handleDirectMessage() {
if (!node) return;
if (!node) {
return;
}
navigate({ to: `/messages/direct/${node.num}` });
setDialogOpen("nodeDetails", false);
}
function handleRequestPosition() {
if (!node) return;
if (!node) {
return;
}
toast({
title: t("toast.requestingPosition.title", { ns: "ui" }),
@@ -94,13 +101,15 @@ export const NodeDetailsDialog = ({
connection?.requestPosition(node.num).then(() =>
toast({
title: t("toast.positionRequestSent.title", { ns: "ui" }),
})
}),
);
onOpenChange(false);
}
function handleTraceroute() {
if (!node) return;
if (!node) {
return;
}
toast({
title: t("toast.sendingTraceroute.title", { ns: "ui" }),
@@ -108,13 +117,15 @@ export const NodeDetailsDialog = ({
connection?.traceRoute(node.num).then(() =>
toast({
title: t("toast.tracerouteSent.title", { ns: "ui" }),
})
}),
);
onOpenChange(false);
}
function handleNodeRemove() {
if (!node) return;
if (!node) {
return;
}
setNodeNumToBeRemoved(node?.num);
setDialogOpen("nodeRemoval", true);
@@ -122,14 +133,18 @@ export const NodeDetailsDialog = ({
}
function handleToggleFavorite() {
if (!node) return;
if (!node) {
return;
}
updateFavorite({ nodeNum: node.num, isFavorite: !isFavoriteState });
setIsFavoriteState(!isFavoriteState);
}
function handleToggleIgnored() {
if (!node) return;
if (!node) {
return;
}
updateIgnored({ nodeNum: node.num, isIgnored: !isIgnoredState });
setIsIgnoredState(!isIgnoredState);
@@ -202,7 +217,7 @@ export const NodeDetailsDialog = ({
)}
/>
</Button>
<div className="flex flex-1 justify-start"></div>
<div className="flex flex-1 justify-start" />
<TooltipProvider delayDuration={300}>
<Tooltip>
@@ -255,7 +270,10 @@ export const NodeDetailsDialog = ({
<p className="text-lg font-semibold">
{t("nodeDetails.details")}
</p>
<p>{t("nodeDetails.nodeNumber")}{node.num}</p>
<p>
{t("nodeDetails.nodeNumber")}
{node.num}
</p>
<p>
{t("nodeDetails.nodeHexPrefix")}
{numberToHexUnpadded(node.num)}
@@ -268,21 +286,25 @@ export const NodeDetailsDialog = ({
</p>
<p>
{t("nodeDetails.lastHeard")}
{node.lastHeard === 0
? t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
: <TimeAgo timestamp={node.lastHeard * 1000} />}
{node.lastHeard === 0 ? (
t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
) : (
<TimeAgo timestamp={node.lastHeard * 1000} />
)}
</p>
<p>
{t("nodeDetails.hardware")}
{(Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
t("unknown.shortName"))
.replace(/_/g, " ")}
{(
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
t("unknown.shortName")
).replace(/_/g, " ")}
</p>
</div>
<DeviceImage
className="h-45 w-45 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
deviceType={Protobuf.Mesh
.HardwareModel[node.user?.hwModel ?? 0]}
deviceType={
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]
}
/>
</div>
</div>
@@ -293,36 +315,35 @@ export const NodeDetailsDialog = ({
{t("nodeDetails.position")}
</p>
{node.position
? (
<>
{node.position.latitudeI &&
node.position.longitudeI && (
<p>
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
node.position.latitudeI / 1e7
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
</a>
</p>
)}
{node.position.altitude && (
<p>
{t("locationResponse.altitude")}
{node.position.altitude}
{t("unit.meter.one")}
</p>
)}
</>
)
: <p>{t("unknown.shortName")}</p>}
{node.position ? (
<>
{node.position.latitudeI && node.position.longitudeI && (
<p>
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
node.position.latitudeI / 1e7
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
</a>
</p>
)}
{node.position.altitude && (
<p>
{t("locationResponse.altitude")}
{node.position.altitude}
{t("unit.meter.one")}
</p>
)}
</>
) : (
<p>{t("unknown.shortName")}</p>
)}
<Button
onClick={handleRequestPosition}
name="requestPosition"
@@ -338,14 +359,13 @@ export const NodeDetailsDialog = ({
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
{t("nodeDetails.deviceMetrics")}
</p>
{deviceMetricsMap.map(
(metric) =>
metric.value !== undefined && (
<p key={metric.key}>
{metric.label}: {metric.format(metric.value)}
</p>
),
)}
{deviceMetricsMap
.filter((metric) => metric.value !== undefined)
.map((metric) => (
<p key={metric.key}>
{metric.label}: {metric.format(metric?.value ?? 0)}
</p>
))}
{node.deviceMetrics.uptimeSeconds && (
<p>
{t("nodeDetails.uptime")}
@@ -1,5 +1,3 @@
import { useDevice } from "../../core/stores/deviceStore.ts";
import { Button } from "../UI/Button.tsx";
import {
Dialog,
DialogClose,
@@ -13,6 +11,8 @@ import { fromByteArray } from "base64-js";
import { DownloadIcon, PrinterIcon } from "lucide-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { useDevice } from "../../core/stores/deviceStore.ts";
import { Button } from "../UI/Button.tsx";
export interface PkiBackupDialogProps {
open: boolean;
@@ -30,7 +30,9 @@ export const PkiBackupDialog = ({
const decodeKeyData = React.useCallback(
(key: Uint8Array<ArrayBufferLike>) => {
if (!key) return "";
if (!key) {
return "";
}
return fromByteArray(key ?? new Uint8Array(0));
},
[],
@@ -41,20 +43,20 @@ export const PkiBackupDialog = ({
}, [setDialogOpen]);
const renderPrintWindow = React.useCallback(() => {
if (!privateKey || !publicKey) return;
if (!privateKey || !publicKey) {
return;
}
const printWindow = globalThis.open("", "_blank");
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>${
t("pkiBackup.header", {
interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})
}</title>
<title>${t("pkiBackup.header", {
interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})}</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
h1 { font-size: 18px; }
@@ -62,13 +64,11 @@ export const PkiBackupDialog = ({
</style>
</head>
<body>
<h1>${
t("pkiBackup.header", {
interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})
}</h1>
<h1>${t("pkiBackup.header", {
interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})}</h1>
<h3>${t("pkiBackup.secureBackup")}</h3>
<h3>${t("pkiBackup.publicKey")}</h3>
<p>${decodeKeyData(publicKey)}</p>
@@ -82,10 +82,12 @@ export const PkiBackupDialog = ({
printWindow.print();
closeDialog();
}
}, [decodeKeyData, privateKey, publicKey, closeDialog, t]);
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]);
const createDownloadKeyFile = React.useCallback(() => {
if (!privateKey || !publicKey) return;
if (!privateKey || !publicKey) {
return;
}
const decodedPrivateKey = decodeKeyData(privateKey);
const decodedPublicKey = decodeKeyData(publicKey);
@@ -116,7 +118,7 @@ export const PkiBackupDialog = ({
document.body.removeChild(link);
closeDialog();
URL.revokeObjectURL(url);
}, [decodeKeyData, privateKey, publicKey, closeDialog, t]);
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -124,9 +126,7 @@ export const PkiBackupDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
<DialogDescription>
{t("pkiBackup.secureBackup")}
</DialogDescription>
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
<DialogDescription>
<span className="font-bold break-before-auto">
{t("pkiBackup.loseKeysWarning")}
@@ -34,8 +34,7 @@ export const PkiRegenerateDialog = ({
const { t } = useTranslation("dialog");
const dialogText = {
title: text.title || t("pkiRegenerate.title"),
description: text.description ||
t("pkiRegenerate.description"),
description: text.description || t("pkiRegenerate.description"),
button: text.button || t("button.regenerate"),
};
return (
@@ -44,9 +43,7 @@ export const PkiRegenerateDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{dialogText.title}</DialogTitle>
<DialogDescription>
{dialogText.description}
</DialogDescription>
<DialogDescription>{dialogText.description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
+10 -19
View File
@@ -1,5 +1,4 @@
import { create, toBinary } from "@bufbuild/protobuf";
import { Checkbox } from "../UI/Checkbox/index.tsx";
import {
Dialog,
DialogClose,
@@ -14,8 +13,9 @@ import { Label } from "@components/UI/Label.tsx";
import { Protobuf, type Types } from "@meshtastic/core";
import { fromByteArray } from "base64-js";
import { useEffect, useMemo, useState } from "react";
import { QRCode } from "react-qrcode-logo";
import { useTranslation } from "react-i18next";
import { QRCode } from "react-qrcode-logo";
import { Checkbox } from "../UI/Checkbox/index.tsx";
export interface QRDialogProps {
open: boolean;
@@ -67,9 +67,7 @@ export const QRDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{t("qr.title")}</DialogTitle>
<DialogDescription>
{t("qr.description")}
</DialogDescription>
<DialogDescription>{t("qr.description")}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="flex gap-3 px-4 py-5 sm:p-6">
@@ -80,13 +78,11 @@ export const QRDialog = ({
{channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
? t("page.broadcastLabel", { ns: "channels" })
: `${
t("page.channelIndex", {
ns: "channels",
index: channel.index,
})
}${channel.index}`}
? t("page.broadcastLabel", { ns: "channels" })
: `${t("page.channelIndex", {
ns: "channels",
index: channel.index,
})}${channel.index}`}
</Label>
<Checkbox
key={channel.index}
@@ -94,9 +90,7 @@ export const QRDialog = ({
onChange={() => {
if (selectedChannels.includes(channel.index)) {
setSelectedChannels(
selectedChannels.filter((c) =>
c !== channel.index
),
selectedChannels.filter((c) => c !== channel.index),
);
} else {
setSelectedChannels([
@@ -140,10 +134,7 @@ export const QRDialog = ({
</div>
<DialogFooter>
<Label>{t("qr.sharableUrl")}</Label>
<Input
value={qrCodeUrl}
disabled
/>
<Input value={qrCodeUrl} disabled />
</DialogFooter>
</DialogContent>
</Dialog>
@@ -10,18 +10,15 @@ import {
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { RefreshCwIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export interface RebootDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const RebootDialog = ({
open,
onOpenChange,
}: RebootDialogProps) => {
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
@@ -32,12 +29,8 @@ export const RebootDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("reboot.title")}
</DialogTitle>
<DialogDescription>
{t("reboot.description")}
</DialogDescription>
<DialogTitle>{t("reboot.title")}</DialogTitle>
<DialogDescription>{t("reboot.description")}</DialogDescription>
</DialogHeader>
<div className="flex gap-2 p-4">
<Input
@@ -1,13 +1,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { RebootOTADialog } from "./RebootOTADialog.tsx";
import {
import type {
ButtonHTMLAttributes,
ClassAttributes,
InputHTMLAttributes,
ReactNode,
} from "react";
import { JSX } from "react/jsx-runtime";
import type { JSX } from "react/jsx-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RebootOTADialog } from "./RebootOTADialog.tsx";
const rebootOtaMock = vi.fn();
let mockConnection: { rebootOta: (delay: number) => void } | undefined = {
@@ -25,10 +25,9 @@ vi.mock("@components/UI/Button.tsx", async () => {
return {
...actual,
Button: (
props:
& JSX.IntrinsicAttributes
& ClassAttributes<HTMLButtonElement>
& ButtonHTMLAttributes<HTMLButtonElement>,
props: JSX.IntrinsicAttributes &
ClassAttributes<HTMLButtonElement> &
ButtonHTMLAttributes<HTMLButtonElement>,
) => <button {...props} />,
};
});
@@ -38,10 +37,9 @@ vi.mock("@components/UI/Input.tsx", async () => {
return {
...actual,
Input: (
props:
& JSX.IntrinsicAttributes
& ClassAttributes<HTMLInputElement>
& InputHTMLAttributes<HTMLInputElement>,
props: JSX.IntrinsicAttributes &
ClassAttributes<HTMLInputElement> &
InputHTMLAttributes<HTMLInputElement>,
) => <input {...props} />,
};
});
@@ -76,8 +74,9 @@ describe("RebootOTADialog", () => {
it("renders dialog with default input value", () => {
render(<RebootOTADialog open onOpenChange={() => {}} />);
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
expect(screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }))
.toBeInTheDocument();
expect(
screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }),
).toBeInTheDocument();
expect(screen.getByText(/reboot to ota mode now/i)).toBeInTheDocument();
});
@@ -1,5 +1,3 @@
import { useState } from "react";
import { ClockIcon, RefreshCwIcon } from "lucide-react";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
@@ -11,6 +9,8 @@ import {
} 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";
import { useTranslation } from "react-i18next";
export interface RebootOTADialogProps {
@@ -20,9 +20,10 @@ export interface RebootOTADialogProps {
const DEFAULT_REBOOT_DELAY = 5; // seconds
export const RebootOTADialog = (
{ open, onOpenChange }: RebootOTADialogProps,
) => {
export const RebootOTADialog = ({
open,
onOpenChange,
}: RebootOTADialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
@@ -39,13 +40,15 @@ export const RebootOTADialog = (
setInputValue(val);
const parsed = Number(val);
if (!isNaN(parsed) && parsed > 0) {
if (!Number.isNaN(parsed) && parsed > 0) {
setTime(parsed);
}
};
const handleRebootWithTimeout = async () => {
if (!connection) return;
if (!connection) {
return;
}
setIsScheduled(true);
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
@@ -63,7 +66,9 @@ export const RebootOTADialog = (
};
const handleInstantReboot = async () => {
if (!connection) return;
if (!connection) {
return;
}
await connection.rebootOta(DEFAULT_REBOOT_DELAY);
onOpenChange(false);
@@ -74,12 +79,8 @@ export const RebootOTADialog = (
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("rebootOta.title")}
</DialogTitle>
<DialogDescription>
{t("rebootOta.description")}
</DialogDescription>
<DialogTitle>{t("rebootOta.title")}</DialogTitle>
<DialogDescription>{t("rebootOta.description")}</DialogDescription>
</DialogHeader>
<div className="flex gap-2 p-2 items-center relative">
@@ -1,9 +1,9 @@
import { render } from "@testing-library/react";
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
import { render } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
vi.mock("@core/stores/messageStore");
vi.mock("./useRefreshKeysDialog");
@@ -12,8 +12,10 @@ const mockUseMessageStore = vi.mocked(useMessageStore);
const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
const getInitialState = () =>
useDeviceStore.getInitialState?.() ??
{ devices: new Map(), remoteDevices: new Map() };
useDeviceStore.getInitialState?.() ?? {
devices: new Map(),
remoteDevices: new Map(),
};
beforeEach(() => {
useDeviceStore.setState(getInitialState(), true);
@@ -31,7 +33,9 @@ test("does not render dialog if no error exists for active chat", () => {
useDeviceStore.getState().addDevice(deviceId);
const currentDeviceState = useDeviceStore.getState().getDevice(deviceId);
if (!currentDeviceState) throw new Error("Device not found");
if (!currentDeviceState) {
throw new Error("Device not found");
}
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
mockUseRefreshKeysDialog.mockReturnValue({
@@ -1,3 +1,4 @@
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogClose,
@@ -5,21 +6,21 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useTranslation } from "react-i18next";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
export interface RefreshKeysDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const RefreshKeysDialog = (
{ open, onOpenChange }: RefreshKeysDialogProps,
) => {
export const RefreshKeysDialog = ({
open,
onOpenChange,
}: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog");
const { activeChat } = useMessageStore();
const { nodeErrors, getNode } = useDevice();
@@ -40,9 +41,9 @@ export const RefreshKeysDialog = (
}),
description: `${t("refreshKeys.description.unableToSendDmPrefix")}${
nodeWithError?.user?.longName ?? ""
} (${nodeWithError?.user?.shortName ?? ""})${
t("refreshKeys.description.keyMismatchReasonSuffix")
}`,
} (${nodeWithError?.user?.shortName ?? ""})${t(
"refreshKeys.description.keyMismatchReasonSuffix",
)}`,
};
return (
@@ -69,9 +70,7 @@ export const RefreshKeysDialog = (
<p className="font-bold mb-0.5">
{t("refreshKeys.label.acceptNewKeys")}
</p>
<p>
{t("refreshKeys.description.acceptNewKeys")}
</p>
<p>{t("refreshKeys.description.acceptNewKeys")}</p>
</div>
<Button
variant="default"
@@ -1,6 +1,6 @@
import { useCallback } from "react";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { useCallback } from "react";
export function useRefreshKeysDialog() {
const { removeNode, setDialogOpen, clearNodeError, getNodeError } =
@@ -1,5 +1,3 @@
import { useAppStore } from "../../core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
@@ -11,7 +9,9 @@ import {
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useTranslation } from "react-i18next";
import { useAppStore } from "../../core/stores/appStore.ts";
export interface RemoveNodeDialogProps {
open: boolean;
@@ -38,9 +38,7 @@ export const RemoveNodeDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>{t("removeNode.title")}</DialogTitle>
<DialogDescription>
{t("removeNode.description")}
</DialogDescription>
<DialogDescription>{t("removeNode.description")}</DialogDescription>
</DialogHeader>
<div className="gap-4">
<form onSubmit={onSubmit}>
@@ -10,18 +10,15 @@ import {
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ClockIcon, PowerIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export interface ShutdownDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const ShutdownDialog = ({
open,
onOpenChange,
}: ShutdownDialogProps) => {
export const ShutdownDialog = ({ open, onOpenChange }: ShutdownDialogProps) => {
const { t } = useTranslation("dialog");
const { connection } = useDevice();
@@ -32,12 +29,8 @@ export const ShutdownDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("shutdown.title")}
</DialogTitle>
<DialogDescription>
{t("shutdown.description")}
</DialogDescription>
<DialogTitle>{t("shutdown.title")}</DialogTitle>
<DialogDescription>{t("shutdown.description")}</DialogDescription>
</DialogHeader>
<div className="flex gap-2 p-4">
@@ -1,4 +1,9 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
import {
Dialog,
DialogClose,
@@ -7,11 +12,6 @@ import {
DialogHeader,
DialogTitle,
} from "../UI/Dialog.tsx";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
import { useTranslation } from "react-i18next";
export interface TracerouteResponseDialogProps {
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
@@ -31,9 +31,11 @@ export const TracerouteResponseDialog = ({
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.from ?? 0);
const fromLongName = from?.user?.longName ??
const fromLongName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const fromShortName = from?.user?.shortName ??
const fromShortName =
from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
@@ -1,3 +1,5 @@
import { Button } from "@components/UI/Button.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import {
Dialog,
DialogClose,
@@ -8,11 +10,9 @@ import {
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useState } from "react";
import { eventBus } from "@core/utils/eventBus.ts";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export interface RouterRoleDialogProps {
@@ -20,9 +20,10 @@ export interface RouterRoleDialogProps {
onOpenChange: (open: boolean) => void;
}
export const UnsafeRolesDialog = (
{ open, onOpenChange }: RouterRoleDialogProps,
) => {
export const UnsafeRolesDialog = ({
open,
onOpenChange,
}: RouterRoleDialogProps) => {
const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false);
const { setDialogOpen } = useDevice();
@@ -1,3 +1,9 @@
import {
UNSAFE_ROLES,
useUnsafeRolesDialog,
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { eventBus } from "@core/utils/eventBus.ts";
import { renderHook } from "@testing-library/react";
import {
afterEach,
beforeEach,
@@ -7,18 +13,11 @@ import {
type Mock,
vi,
} from "vitest";
import { renderHook } from "@testing-library/react";
import {
UNSAFE_ROLES,
useUnsafeRolesDialog,
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { eventBus } from "@core/utils/eventBus.ts";
const mockNavigate = vi.fn();
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<
typeof import("@tanstack/react-router")
>();
const actual =
await importOriginal<typeof import("@tanstack/react-router")>();
return {
...actual,
useNavigate: () => mockNavigate,
@@ -75,9 +74,8 @@ describe("useUnsafeRolesDialog", () => {
const { result } = renderUnsafeRolesHook();
const safeRole = "SAFE_ROLE";
const validationResult = await result.current.validateRoleSelection(
safeRole,
);
const validationResult =
await result.current.validateRoleSelection(safeRole);
expect(validationResult).toBe(true);
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
@@ -151,9 +149,8 @@ describe("useUnsafeRolesDialog", () => {
mockDevice.setDialogOpen.mockClear();
(eventBus.on as Mock).mockClear();
const validationPromise = result.current.validateRoleSelection(
unsafeRole,
);
const validationPromise =
result.current.validateRoleSelection(unsafeRole);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
@@ -1,9 +1,9 @@
import { useCallback } from "react";
import { eventBus } from "@core/utils/eventBus.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { eventBus } from "@core/utils/eventBus.ts";
import { useCallback } from "react";
export const UNSAFE_ROLES = ["ROUTER", "ROUTER_LATE", "REPEATER"];
export type UnsafeRole = typeof UNSAFE_ROLES[number];
export type UnsafeRole = (typeof UNSAFE_ROLES)[number];
export const useUnsafeRolesDialog = () => {
const { setDialogOpen } = useDevice();
@@ -21,9 +21,11 @@ export const useUnsafeRolesDialog = () => {
setDialogOpen("unsafeRoles", true);
return new Promise((resolve) => {
const handleResponse = (
{ action }: { action: "confirm" | "dismiss" },
) => {
const handleResponse = ({
action,
}: {
action: "confirm" | "dismiss";
}) => {
eventBus.off("dialog:unsafeRoles", handleResponse);
resolve(action === "confirm");
};