New channel config (#807)

* Channel config rework

Add staged channel config with tabbed UI, import/export workflow, and global form state refactor

* Improve import dialog config comparison and UI labels

* Review fixes

* Improve state handling

* Fix default filter behaviour

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
Jeremy Gallant
2025-09-01 09:07:34 -04:00
committed by GitHub
co-authored by philon-
parent 01fa030ef9
commit 1946000d14
42 changed files with 597 additions and 456 deletions
@@ -11,13 +11,20 @@ import {
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@components/UI/Select.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
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";
import { Trans, useTranslation } from "react-i18next";
export interface ImportDialogProps {
open: boolean;
@@ -26,12 +33,15 @@ export interface ImportDialogProps {
}
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const { config, channels } = useDevice();
const { t } = useTranslation("dialog");
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
const [validUrl, setValidUrl] = useState<boolean>(false);
const [updateConfig, setUpdateConfig] = useState<boolean>(true);
const [importIndex, setImportIndex] = useState<number[]>([]);
const { connection } = useDevice();
const { setWorkingChannelConfig, setWorkingConfig } = useDevice();
useEffect(() => {
// the channel information is contained in the URL's fragment, which will be present after a
@@ -55,12 +65,17 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
)
.replace(/-/g, "+")
.replace(/_/g, "/");
setChannelSet(
fromBinary(
Protobuf.AppOnly.ChannelSetSchema,
toByteArray(paddedString),
),
const newChannelSet = fromBinary(
Protobuf.AppOnly.ChannelSetSchema,
toByteArray(paddedString),
);
const newImportChannelArray = newChannelSet.settings.map((_, idx) => idx);
setChannelSet(newChannelSet);
setImportIndex(newImportChannelArray);
setUpdateConfig(newChannelSet?.loraConfig !== undefined);
setValidUrl(true);
} catch (_error) {
setValidUrl(false);
@@ -71,29 +86,62 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const apply = () => {
channelSet?.settings.map(
(ch: Protobuf.Channel.ChannelSettings, index: number) => {
connection?.setChannel(
create(Protobuf.Channel.ChannelSchema, {
index,
role:
index === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
}),
);
if (importIndex[index] === -1) {
return;
}
const payload = create(Protobuf.Channel.ChannelSchema, {
index: importIndex[index],
role:
importIndex[index] === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
});
if (
!deepCompareConfig(
channels.get(importIndex[index] ?? 0),
payload,
true,
)
) {
setWorkingChannelConfig(payload);
}
},
);
if (channelSet?.loraConfig) {
connection?.setConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "lora",
value: channelSet.loraConfig,
},
}),
);
if (channelSet?.loraConfig && updateConfig) {
const payload = {
...config.lora,
...channelSet.loraConfig,
};
if (!deepCompareConfig(config.lora, payload, true)) {
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "lora",
value: payload,
},
}),
);
}
}
// Reset state after import
setImportDialogInput("");
setChannelSet(undefined);
setValidUrl(false);
setImportIndex([]);
setUpdateConfig(true);
onOpenChange(false);
};
const onSelectChange = (value: string, index: number) => {
const newImportIndex = [...importIndex];
newImportIndex[index] = Number.parseInt(value);
setImportIndex(newImportIndex);
};
return (
@@ -102,55 +150,85 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
<DialogClose />
<DialogHeader>
<DialogTitle>{t("import.title")}</DialogTitle>
<DialogDescription>{t("import.description")}</DialogDescription>
<DialogDescription>
<Trans
i18nKey={"import.description"}
components={{ italic: <i />, br: <br /> }}
/>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<Label>{t("import.channelSetUrl")}</Label>
<Input
value={importDialogInput}
suffix={validUrl ? "✅" : "❌"}
variant={
importDialogInput === ""
? "default"
: validUrl
? "dirty"
: "invalid"
}
onChange={(e) => {
setImportDialogInput(e.target.value);
}}
/>
{validUrl && (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-6 mt-2">
<div className="flex w-full gap-2">
<div className="w-36">
<Label>{t("import.usePreset")}</Label>
<div className=" flex items-center">
<Switch
disabled
checked={channelSet?.loraConfig?.usePreset ?? true}
className="ml-3 mr-4"
checked={updateConfig}
onCheckedChange={(next) => setUpdateConfig(next)}
/>
<Label className="">
{t("import.useLoraConfig")}
<span className="block pt-2 font-normal text-s">
{t("import.presetDescription")}
</span>
</Label>
</div>
{/* <Select
label="Modem Preset"
disabled
value={channelSet?.loraConfig?.modemPreset}
>
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
</Select> */}
</div>
{/* <Select
label="Region"
disabled
value={channelSet?.loraConfig?.region}
>
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
</Select> */}
<span className="text-md block font-medium text-text-primary">
{t("import.channels")}
</span>
<div className="flex w-40 flex-col gap-1">
{channelSet?.settings.map((channel) => (
<div className="flex justify-between" key={channel.id}>
<Label>
<div className="flex w-full flex-col gap-2">
<div className="flex items-center font-semibold text-sm">
<span className="flex-1">{t("import.channelName")}</span>
<span className="flex-1">{t("import.channelSlot")}</span>
</div>
{channelSet?.settings.map((channel, index) => (
<div
className="flex items-center"
key={`channel_${channel.id}_${index}`}
>
<Label className="flex-1">
{channel.name.length
? channel.name
: `${t("import.channelPrefix")}${channel.id}`}
</Label>
<Checkbox key={channel.id} />
<Select
onValueChange={(value) => onSelectChange(value, index)}
value={importIndex[index]?.toString()}
>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 8 }, (_, i) => i).map((i) => (
<SelectItem
key={`index_${i}`}
disabled={importIndex.includes(i) && index !== i}
value={i.toString()}
>
{i === 0
? t("import.primary")
: `${t("import.channelPrefix")}${i}`}
</SelectItem>
))}
<SelectItem value="-1">
{t("import.doNotImport")}
</SelectItem>
</SelectContent>
</Select>
</div>
))}
</div>
@@ -82,7 +82,7 @@ export const QRDialog = ({
: `${t("page.channelIndex", {
ns: "channels",
index: channel.index,
})}${channel.index}`}
})}`}
</Label>
<Checkbox
key={channel.index}
@@ -7,8 +7,6 @@ import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Heading } from "@components/UI/Typography/Heading.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { useAppStore } from "@core/stores";
import { dotPaths } from "@core/utils/dotPath.ts";
import { useEffect } from "react";
import {
type Control,
@@ -66,7 +64,6 @@ export interface DynamicFormProps<T extends FieldValues> {
fields: FieldProps<T>[];
}[];
validationSchema?: ZodType<T>;
formId?: string;
}
export type DynamicFormFormInit<T extends FieldValues> = (
@@ -83,10 +80,8 @@ export function DynamicForm<T extends FieldValues>({
values,
fieldGroups,
validationSchema,
formId,
}: DynamicFormProps<T>) {
const { t } = useTranslation();
const { addError, removeError } = useAppStore();
const internalMethods = useForm<T>({
mode: "onChange",
@@ -95,6 +90,7 @@ export function DynamicForm<T extends FieldValues>({
? createZodResolver(validationSchema)
: undefined,
shouldFocusError: false,
shouldUnregister: true,
resetOptions: { keepDefaultValues: true },
values,
});
@@ -110,23 +106,6 @@ export function DynamicForm<T extends FieldValues>({
}
}, [onFormInit, propMethods, internalMethods]);
useEffect(() => {
const errorKeys = Object.keys(formState.errors);
if (formId) {
if (errorKeys.length === 0) {
dotPaths(getValues()).forEach((key) => {
removeError(key);
});
removeError(formId);
} else {
errorKeys.forEach((key) => {
addError(key, "");
});
addError(formId, "");
}
}
}, [formState.errors, addError, formId, getValues, removeError]);
const isDisabled = (
disabledBy?: DisabledBy<T>[],
disabled?: boolean,
@@ -4,8 +4,7 @@ import type {
} from "@components/Form/DynamicForm.tsx";
import { Generator } from "@components/UI/Generator.tsx";
import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts";
import { useEffect } from "react";
import { Controller, type FieldValues, useFormContext } from "react-hook-form";
import { Controller, type FieldValues } from "react-hook-form";
import type { ButtonVariant } from "../UI/Button.tsx";
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
@@ -34,36 +33,29 @@ export function PasswordGenerator<T extends FieldValues>({
invalid,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
const { isVisible } = usePasswordVisibilityToggle();
const { trigger } = useFormContext();
useEffect(() => {
trigger(field.name);
}, [field.name, trigger]);
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
render={({ field: controllerField }) => (
<Generator
type={field.hide && !isVisible ? "password" : "text"}
id={field.id}
devicePSKBitCount={field.devicePSKBitCount}
bits={field.bits}
inputChange={(e) => {
if (field.inputChange) {
field.inputChange(e);
}
onChange(e);
const value = e.target.value;
field.inputChange?.(e); // call any external handler
controllerField.onChange(value); // ensure RHF receives just the value
}}
selectChange={field.selectChange ?? (() => {})}
value={value}
variant={invalid ? "invalid" : isDirty ? "dirty" : "default"}
actionButtons={field.actionButtons}
showPasswordToggle={field.showPasswordToggle}
showCopyButton={field.showCopyButton}
{...field.properties}
{...rest}
{...controllerField}
disabled={disabled}
/>
)}
@@ -70,6 +70,9 @@ export function SelectInput<T extends FieldValues>({
const handleValueChange = async (newValue: string) => {
const selectedKey = valueToKeyMap[newValue];
if (!selectedKey) {
return;
}
if (field.validate) {
const isValid = await field.validate(selectedKey);
@@ -1,118 +1,165 @@
import { makeChannelSchema } from "@app/validation/channel.ts";
import {
type ChannelValidation,
makeChannelSchema,
} from "@app/validation/channel.ts";
import { create } from "@bufbuild/protobuf";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { type DefaultValues, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { infer as zodInfer } from "zod/v4";
export interface SettingsPanelProps {
onFormInit: DynamicFormFormInit<ChannelValidation>;
channel: Protobuf.Channel.Channel;
}
export const Channel = ({ channel }: SettingsPanelProps) => {
const { config, connection, addChannel } = useDevice();
export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
const {
config,
setWorkingChannelConfig,
getWorkingChannelConfig,
removeWorkingChannelConfig,
} = useDevice();
const { t } = useTranslation(["channels", "ui", "dialog"]);
const { toast } = useToast();
const defaultConfig = channel;
const defaultValues = {
...defaultConfig,
...{
settings: {
...defaultConfig?.settings,
psk: fromByteArray(defaultConfig?.settings?.psk ?? new Uint8Array(0)),
moduleSettings: {
...defaultConfig?.settings?.moduleSettings,
positionPrecision:
defaultConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
},
},
},
};
const effectiveConfig = getWorkingChannelConfig(channel.index) ?? channel;
const formValues = {
...effectiveConfig,
...{
settings: {
...effectiveConfig?.settings,
psk: fromByteArray(effectiveConfig?.settings?.psk ?? new Uint8Array(0)),
moduleSettings: {
...effectiveConfig?.settings?.moduleSettings,
positionPrecision:
effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
},
},
},
};
const [preSharedDialogOpen, setPreSharedDialogOpen] =
useState<boolean>(false);
const [pass, setPass] = useState<string>(
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
);
const [byteCount, setBytes] = useState<number>(
channel?.settings?.psk.length ?? 16,
effectiveConfig?.settings?.psk.length ?? 16,
);
const ChannelValidationSchema = useMemo(() => {
return makeChannelSchema(byteCount);
}, [byteCount]);
type ChannelValidation = zodInfer<typeof ChannelValidationSchema>;
const formMethods = useForm<ChannelValidation>({
mode: "onChange",
defaultValues: defaultValues as DefaultValues<ChannelValidation>,
resolver: createZodResolver(ChannelValidationSchema),
shouldFocusError: false,
resetOptions: { keepDefaultValues: true },
values: formValues as ChannelValidation,
});
const { setValue, trigger, handleSubmit, formState } = formMethods;
useEffect(() => {
onFormInit?.(formMethods);
}, [onFormInit, formMethods]);
// Since byteCount is an independent state, we need to use the effective value
// from the channel config to ensure the form updates when the setting changes
const effectiveByteCount = effectiveConfig.settings?.psk.length ?? 16;
const lastEffectiveRef = useRef<number>(effectiveByteCount);
useEffect(() => {
if (effectiveByteCount !== lastEffectiveRef.current) {
lastEffectiveRef.current = effectiveByteCount;
setBytes(effectiveByteCount);
trigger("settings.psk");
}
}, [effectiveByteCount, trigger]);
const onSubmit = (data: ChannelValidation) => {
const channel = create(Protobuf.Channel.ChannelSchema, {
if (!formState.isReady) {
return;
}
const payload = {
...data,
settings: {
...data.settings,
psk: toByteArray(pass),
psk: toByteArray(data.settings.psk),
moduleSettings: create(Protobuf.Channel.ModuleSettingsSchema, {
...data.settings.moduleSettings,
positionPrecision: data.settings.moduleSettings.positionPrecision,
}),
},
});
connection?.setChannel(channel).then(() => {
console.debug(
t("toast.savedChannel.title", {
ns: "ui",
channelName: channel.settings?.name,
}),
);
toast({
title: t("toast.savedChannel.title", {
ns: "ui",
channelName: channel.settings?.name,
}),
});
addChannel(channel);
});
};
if (deepCompareConfig(channel, payload, true)) {
removeWorkingChannelConfig(channel.index);
return;
}
setWorkingChannelConfig(create(Protobuf.Channel.ChannelSchema, payload));
};
const preSharedKeyRegenerate = () => {
const preSharedKeyRegenerate = async () => {
const newPsk = btoa(
cryptoRandomString({
length: byteCount ?? 0,
length: byteCount ?? 16,
type: "alphanumeric",
}),
);
setPass(newPsk);
setValue("settings.psk", newPsk, { shouldDirty: true });
setPreSharedDialogOpen(false);
};
const preSharedClickEvent = () => {
setPreSharedDialogOpen(true);
};
const inputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
setPass(e.currentTarget?.value);
const valid = await trigger("settings.psk");
if (valid) {
handleSubmit(onSubmit)(); // manually invoke form submit
}
};
const selectChangeEvent = (e: string) => {
const count = Number.parseInt(e);
setBytes(count);
if (!Number.isNaN(count)) {
setBytes(count);
trigger("settings.psk");
}
};
return (
<>
<DynamicForm<ChannelValidation>
propMethods={formMethods}
onSubmit={onSubmit}
submitType="onSubmit"
validationSchema={ChannelValidationSchema}
hasSubmitButton
defaultValues={{
...channel,
...{
settings: {
...channel?.settings,
psk: pass,
moduleSettings: {
...channel?.settings?.moduleSettings,
positionPrecision:
channel?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10
: channel?.settings?.moduleSettings?.positionPrecision,
},
},
},
}}
fieldGroups={[
{
label: t("settings.label"),
@@ -140,19 +187,17 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
id: "channel-psk",
label: t("psk.label"),
description: t("psk.description"),
devicePSKBitCount: byteCount ?? 0,
inputChange: inputChangeEvent,
devicePSKBitCount: byteCount ?? 16,
selectChange: selectChangeEvent,
actionButtons: [
{
text: t("psk.generate"),
variant: "success",
onClick: preSharedClickEvent,
onClick: () => setPreSharedDialogOpen(true),
},
],
hide: true,
properties: {
value: pass,
showPasswordToggle: true,
showCopyButton: true,
},
@@ -44,7 +44,6 @@ export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={BluetoothValidationSchema}
formId="Config_BluetoothConfig"
defaultValues={config.bluetooth}
values={getEffectiveConfig("bluetooth")}
fieldGroups={[
@@ -46,7 +46,6 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={DeviceValidationSchema}
formId="Config_DeviceConfig"
defaultValues={config.device}
values={getEffectiveConfig("device")}
fieldGroups={[
@@ -43,7 +43,6 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={DisplayValidationSchema}
formId="Config_DisplayConfig"
defaultValues={config.display}
values={getEffectiveConfig("display")}
fieldGroups={[
@@ -44,7 +44,6 @@ export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={LoRaValidationSchema}
formId="Config_LoRaConfig"
defaultValues={config.lora}
values={getEffectiveConfig("lora")}
fieldGroups={[
@@ -62,7 +62,6 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={NetworkValidationSchema}
formId="Config_NetworkConfig"
defaultValues={{
...config.network,
ipv4Config: {
@@ -62,7 +62,6 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
}}
onFormInit={onFormInit}
validationSchema={PositionValidationSchema}
formId="Config_PositionConfig"
defaultValues={config.position}
values={getEffectiveConfig("position")}
fieldGroups={[
@@ -44,7 +44,6 @@ export const Power = ({ onFormInit }: PowerConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={PowerValidationSchema}
formId="Config_PowerConfig"
defaultValues={config.power}
values={getEffectiveConfig("power")}
fieldGroups={[
@@ -12,7 +12,7 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useAppStore, useDevice } from "@core/stores";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { Protobuf } from "@meshtastic/core";
@@ -35,21 +35,34 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
removeWorkingConfig,
} = useDevice();
const { removeError } = useAppStore();
const { t } = useTranslation("deviceConfig");
const securityConfig = getEffectiveConfig("security");
const defaultConfig = config.security;
const defaultValues = {
...securityConfig,
...defaultConfig,
...{
privateKey: fromByteArray(defaultConfig?.privateKey ?? new Uint8Array(0)),
publicKey: fromByteArray(defaultConfig?.publicKey ?? new Uint8Array(0)),
adminKey: [
fromByteArray(defaultConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
fromByteArray(defaultConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
fromByteArray(defaultConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
],
},
};
const effectiveConfig = getEffectiveConfig("security");
const formValues = {
...effectiveConfig,
...{
privateKey: fromByteArray(
securityConfig?.privateKey ?? new Uint8Array(0),
effectiveConfig?.privateKey ?? new Uint8Array(0),
),
publicKey: fromByteArray(securityConfig?.publicKey ?? new Uint8Array(0)),
publicKey: fromByteArray(effectiveConfig?.publicKey ?? new Uint8Array(0)),
adminKey: [
fromByteArray(securityConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
fromByteArray(securityConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
fromByteArray(securityConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
fromByteArray(effectiveConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
fromByteArray(effectiveConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
fromByteArray(effectiveConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
],
},
};
@@ -60,8 +73,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
resolver: createZodResolver(RawSecuritySchema),
shouldFocusError: false,
resetOptions: { keepDefaultValues: true },
values: formValues as RawSecurity,
});
const { setValue, formState } = formMethods;
const { setValue, trigger, handleSubmit, formState } = formMethods;
useEffect(() => {
onFormInit?.(formMethods);
@@ -108,19 +122,21 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
updatePublicKey(fromByteArray(privateKey));
};
const updatePublicKey = (privateKey: string) => {
const updatePublicKey = async (privateKey: string) => {
try {
const publicKey = fromByteArray(
getX25519PublicKey(toByteArray(privateKey)),
);
setValue("privateKey", privateKey);
setValue("publicKey", publicKey);
setValue("privateKey", privateKey, { shouldDirty: true });
setValue("publicKey", publicKey, { shouldDirty: true });
removeError("privateKey");
removeError("publicKey");
setPrivateKeyDialogOpen(false);
} catch (_e) {
setValue("privateKey", privateKey);
setValue("privateKey", privateKey, { shouldDirty: true });
}
const valid = await trigger(["privateKey", "publicKey"]);
if (valid) {
handleSubmit(onSubmit)(); // manually invoke form submit
}
};
@@ -137,7 +153,6 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
<DynamicForm<RawSecurity>
propMethods={formMethods}
onSubmit={onSubmit}
formId="Config_SecurityConfig"
fieldGroups={[
{
label: t("security.title"),
@@ -50,7 +50,6 @@ export const AmbientLighting = ({
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={AmbientLightingValidationSchema}
formId="ModuleConfig_AmbientLightingConfig"
defaultValues={moduleConfig.ambientLighting}
values={getEffectiveModuleConfig("ambientLighting")}
fieldGroups={[
@@ -48,7 +48,6 @@ export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={AudioValidationSchema}
formId="ModuleConfig_AudioConfig"
defaultValues={moduleConfig.audio}
values={getEffectiveModuleConfig("audio")}
fieldGroups={[
@@ -51,7 +51,6 @@ export const CannedMessage = ({
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={CannedMessageValidationSchema}
formId="ModuleConfig_CannedMessageConfig"
defaultValues={moduleConfig.cannedMessage}
values={getEffectiveModuleConfig("cannedMessage")}
fieldGroups={[
@@ -51,7 +51,6 @@ export const DetectionSensor = ({
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={DetectionSensorValidationSchema}
formId="ModuleConfig_DetectionSensorConfig"
defaultValues={moduleConfig.detectionSensor}
values={getEffectiveModuleConfig("detectionSensor")}
fieldGroups={[
@@ -51,7 +51,6 @@ export const ExternalNotification = ({
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={ExternalNotificationValidationSchema}
formId="ModuleConfig_ExternalNotificationConfig"
defaultValues={moduleConfig.externalNotification}
values={getEffectiveModuleConfig("externalNotification")}
fieldGroups={[
@@ -72,7 +72,6 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={MqttValidationSchema}
formId="ModuleConfig_MqttConfig"
defaultValues={populateDefaultValues(moduleConfig.mqtt)}
values={populateDefaultValues(getEffectiveModuleConfig("mqtt"))}
fieldGroups={[
@@ -49,7 +49,6 @@ export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={NeighborInfoValidationSchema}
formId="ModuleConfig_NeighborInfoConfig"
defaultValues={moduleConfig.neighborInfo}
values={getEffectiveModuleConfig("neighborInfo")}
fieldGroups={[
@@ -49,7 +49,6 @@ export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={PaxcounterValidationSchema}
formId="ModuleConfig_PaxcounterConfig"
defaultValues={moduleConfig.paxcounter}
values={getEffectiveModuleConfig("paxcounter")}
fieldGroups={[
@@ -50,7 +50,6 @@ export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={RangeTestValidationSchema}
formId="ModuleConfig_RangeTestConfig"
defaultValues={moduleConfig.rangeTest}
values={getEffectiveModuleConfig("rangeTest")}
fieldGroups={[
@@ -49,7 +49,6 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={SerialValidationSchema}
formId="ModuleConfig_SerialConfig"
defaultValues={moduleConfig.serial}
values={getEffectiveModuleConfig("serial")}
fieldGroups={[
@@ -49,7 +49,6 @@ export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={StoreForwardValidationSchema}
formId="ModuleConfig_StoreForwardConfig"
defaultValues={moduleConfig.storeForward}
values={getEffectiveModuleConfig("storeForward")}
fieldGroups={[
@@ -49,7 +49,6 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
onSubmit={onSubmit}
onFormInit={onFormInit}
validationSchema={TelemetryValidationSchema}
formId="ModuleConfig_TelemetryConfig"
defaultValues={moduleConfig.telemetry}
values={getEffectiveModuleConfig("telemetry")}
fieldGroups={[
-6
View File
@@ -13,7 +13,6 @@ import { cn } from "@core/utils/cn.ts";
import { useLocation, useNavigate } from "@tanstack/react-router";
import {
CircleChevronLeft,
LayersIcon,
type LucideIcon,
MapIcon,
MessageSquareIcon,
@@ -113,11 +112,6 @@ export const Sidebar = ({ children }: SidebarProps) => {
icon: SettingsIcon,
page: "config",
},
{
name: t("navigation.channels"),
icon: LayersIcon,
page: "channels",
},
{
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
icon: UsersIcon,
@@ -7,7 +7,6 @@ import {
SelectTrigger,
SelectValue,
} from "@components/UI/Select.tsx";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
export interface ActionButton {
@@ -48,7 +47,6 @@ const Generator = ({
showCopyButton,
...props
}: GeneratorProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const passwordRequiredBitSize = bits
@@ -76,23 +74,6 @@ const Generator = ({
},
];
// Invokes onChange event on the input element when the value changes from the parent component
useEffect(() => {
if (!inputRef.current) {
return;
}
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
if (!setValue) {
return;
}
inputRef.current.value = "";
setValue.call(inputRef.current, value);
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
}, [value]);
return (
<>
<Input
@@ -102,7 +83,6 @@ const Generator = ({
value={value}
onChange={inputChange}
disabled={disabled}
ref={inputRef}
showCopyButton={showCopyButton}
showPasswordToggle={showPasswordToggle}
/>