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:
co-authored by
philon-
parent
01fa030ef9
commit
1946000d14
+113
-68
@@ -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={[
|
||||
|
||||
Reference in New Issue
Block a user