Initial 1.3.x rework

This commit is contained in:
Sacha Weatherstone
2022-07-26 08:52:23 +10:00
parent ff6019098b
commit a2280ab96e
163 changed files with 6383 additions and 14397 deletions
+74
View File
@@ -0,0 +1,74 @@
import type React from "react";
import { majorScale, Pane } from "evergreen-ui";
import { useAppStore } from "@app/core/stores/appStore.js";
import { DeviceWrapper } from "@app/DeviceWrapper.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { NoDevice } from "../misc/NoDevice.js";
import { Progress } from "../Progress.js";
import { Header } from "./Header.js";
import { Sidebar } from "./Sidebar/index.js";
export interface AppLayoutProps {
children: React.ReactNode;
}
export const AppLayout = ({ children }: AppLayoutProps): JSX.Element => {
const { getDevices } = useDeviceStore();
const { selectedDevice } = useAppStore();
const devices = getDevices();
return (
<Pane
width="100vw"
display="flex"
background="tint1"
flexDirection="column"
minHeight="100vh"
>
<Header />
<Pane display="flex" flex={1} height="100%" width="100%">
{devices.length ? (
devices.map((device, index) => (
<Pane
key={index}
width="100%"
height="100%"
display={index === selectedDevice ? "grid" : "none"}
gap={majorScale(3)}
gridTemplateColumns="16rem 1fr"
>
<DeviceWrapper device={device}>
{device && device.ready ? (
<>
<Sidebar />
<Pane height="100%" display="flex">
{children}
</Pane>
</>
) : (
<>
<Pane
width="100%"
flexGrow={1}
margin={majorScale(3)}
borderRadius={majorScale(1)}
background="white"
elevation={1}
/>
<Progress />
</>
)}
</DeviceWrapper>
</Pane>
))
) : (
<NoDevice />
)}
</Pane>
</Pane>
);
};
+147
View File
@@ -0,0 +1,147 @@
import type React from "react";
import { useState } from "react";
import {
Button,
CrossIcon,
GlobeIcon,
IconButton,
Link,
majorScale,
Pane,
PlusIcon,
StatusIndicator,
Tab,
Tablist,
Tooltip,
} from "evergreen-ui";
import { FiGithub } from "react-icons/fi";
import { useAppStore } from "@app/core/stores/appStore.js";
import { NewDevice } from "@components/SlideSheets/NewDevice.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Types } from "@meshtastic/meshtasticjs";
export const Header = (): JSX.Element => {
const { getDevices, removeDevice } = useDeviceStore();
const [newConnectionOpen, setNewConnectionOpen] = useState(false);
const { selectedDevice, setSelectedDevice } = useAppStore();
return (
<Pane
is="nav"
width="100%"
position="sticky"
top={0}
backgroundColor="white"
zIndex={10}
height={majorScale(8)}
flexShrink={0}
display="flex"
alignItems="center"
borderBottom="muted"
>
<NewDevice
open={newConnectionOpen}
onClose={() => {
setNewConnectionOpen(false);
}}
/>
<Pane
display="flex"
alignItems="center"
width={majorScale(12)}
marginRight={majorScale(22)}
>
<Link href="/">
<Pane
is="img"
width={100}
height={28}
src="/Logo_Black.svg"
cursor="pointer"
/>
</Link>
</Pane>
<Tablist display="flex" marginX={majorScale(4)}>
{getDevices().map((device, index) => (
<Tab
key={index}
gap={majorScale(1)}
isSelected={index === selectedDevice}
onSelect={() => {
setSelectedDevice(index);
}}
>
<Hashicon value={device.hardware.myNodeNum.toString()} size={20} />
{device.nodes.find((n) => n.data.num === device.hardware.myNodeNum)
?.data.user?.shortName ?? "UNK"}
<StatusIndicator
color={
[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(device.status)
? "success"
: [
Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONNECTED,
].includes(device.status)
? "warning"
: "danger"
}
/>
</Tab>
))}
</Tablist>
<Pane
display="flex"
marginLeft="auto"
gap={majorScale(1)}
marginRight={majorScale(2)}
>
<Tooltip content="Connect new device">
<Button
display="inline-flex"
marginY="auto"
appearance="primary"
iconBefore={<PlusIcon />}
onClick={() => {
setNewConnectionOpen(true);
}}
>
New
</Button>
</Tooltip>
{getDevices().length !== 0 && (
<Tooltip content="Disconnect active device">
<Button
iconAfter={CrossIcon}
onClick={() => {
removeDevice(selectedDevice ?? 0);
}}
>
Disconnect
</Button>
</Tooltip>
)}
<Tooltip content="Visit GitHub">
<Link
target="_blank"
href="https://github.com/meshtastic/meshtastic-web"
>
<IconButton icon={FiGithub} />
</Link>
</Tooltip>
<Tooltip content="Visit Meshtastic.org">
<Link target="_blank" href="https://meshtastic.org/">
<IconButton icon={GlobeIcon} />
</Link>
</Tooltip>
</Pane>
</Pane>
);
};
@@ -0,0 +1,103 @@
import type React from "react";
import {
Badge,
Heading,
Link,
majorScale,
MapMarkerIcon,
Pane,
} from "evergreen-ui";
import { FiBluetooth, FiTerminal, FiWifi } from "react-icons/fi";
import { toMGRS } from "@app/core/utils/toMGRS.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Types } from "@meshtastic/meshtasticjs";
export const DeviceCard = (): JSX.Element => {
const { hardware, nodes, status, connection } = useDevice();
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
return (
<Pane
display="flex"
flexGrow={1}
flexDirection="column"
marginTop="auto"
gap={majorScale(1)}
>
<Pane display="flex" gap={majorScale(2)}>
<Hashicon value={hardware.myNodeNum.toString()} size={42} />
<Pane flexGrow={1}>
<Heading>{myNode?.data.user?.longName}</Heading>
<Link
target="_blank"
href="https://github.com/meshtastic/meshtastic-web/releases/"
>
<Badge
color="green"
width="100%"
marginRight={8}
display="flex"
marginTop={4}
>
{hardware.firmwareVersion}
</Badge>
</Link>
</Pane>
</Pane>
<Pane display="flex" gap={majorScale(1)}>
<MapMarkerIcon />
<Badge
color={myNode?.data.position?.latitudeI ? "green" : "red"}
display="flex"
width="100%"
>
{toMGRS(
myNode?.data.position?.latitudeI,
myNode?.data.position?.longitudeI
)}
</Badge>
</Pane>
<Pane display="flex" gap={majorScale(1)}>
{connection?.connType === "ble" && <FiBluetooth />}
{connection?.connType === "http" && <FiWifi />}
{connection?.connType === "serial" && <FiTerminal />}
<Badge
color={
[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(status)
? "green"
: [
Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONNECTED,
].includes(status)
? "orange"
: "red"
}
display="flex"
width="100%"
>
{[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(status)
? "Connected"
: [
Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONNECTED,
].includes(status)
? "Connecting"
: "Disconnected"}
</Badge>
</Pane>
</Pane>
);
};
@@ -1,62 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Device = (): JSX.Element => {
const deviceConfig = useAppSelector(
(state) => state.meshtastic.radio.config.device,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.Config_DeviceConfig>({
defaultValues: deviceConfig,
});
useEffect(() => {
reset(deviceConfig);
}, [reset, deviceConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setConfig(
{
payloadVariant: {
oneofKind: 'device',
device: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Serial Console Disabled"
{...register('serialDisabled')}
/>
<Checkbox label="Factory Reset Device" {...register('factoryReset')} />
<Checkbox label="Enabled Debug Log" {...register('debugLogEnabled')} />
<Checkbox
label="Disable Serial COnsole"
{...register('serialDisabled')}
/>
<Select
label="Role"
optionsEnum={Protobuf.Config_DeviceConfig_Role}
{...register('role', { valueAsNumber: true })}
/>
</Form>
);
};
@@ -1,64 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Display = (): JSX.Element => {
const displayConfig = useAppSelector(
(state) => state.meshtastic.radio.config.display,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.Config_DisplayConfig>({
defaultValues: displayConfig,
});
useEffect(() => {
reset(displayConfig);
}, [reset, displayConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setConfig(
{
payloadVariant: {
oneofKind: 'display',
display: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Input
label="Screen Timeout"
type="number"
suffix="Seconds"
{...register('screenOnSecs', { valueAsNumber: true })}
/>
<Input
label="Carousel Delay"
type="number"
suffix="Seconds"
{...register('autoScreenCarouselSecs', { valueAsNumber: true })}
/>
<Select
label="GPS Display Units"
optionsEnum={Protobuf.Config_DisplayConfig_GpsCoordinateFormat}
{...register('gpsFormat', { valueAsNumber: true })}
/>
</Form>
);
};
@@ -1,182 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import {
FiActivity,
FiAlignLeft,
FiBell,
FiFastForward,
FiLayers,
FiLayout,
FiMapPin,
FiMessageSquare,
FiPackage,
FiPower,
FiRss,
FiSmartphone,
FiTv,
FiUser,
FiWifi,
} from 'react-icons/fi';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
import { Display } from '@components/layout/Sidebar/Settings/Display';
import { Position } from '@app/components/layout/Sidebar/Settings/Position';
import { Interface } from '@components/layout/Sidebar/Settings/Interface';
import { LoRa } from '@components/layout/Sidebar/Settings/LoRa';
import { CannedMessage } from '@components/layout/Sidebar/Settings/modules/CannedMessage';
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/modules/ExternalNotifications';
import { MQTT } from '@components/layout/Sidebar/Settings/modules/MQTT';
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/modules/RangeTest';
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/modules/Serial';
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/modules/StoreForward';
import { Telemetry } from '@components/layout/Sidebar/Settings/modules/Telemetry';
import { Power } from '@components/layout/Sidebar/Settings/Power';
import { User } from '@components/layout/Sidebar/Settings/User';
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
import { useAppSelector } from '@hooks/useAppSelector';
export interface SettingsProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
const [modulesOpen, setModulesOpen] = useState(false);
const [channelsOpen, setChannelsOpen] = useState(false);
const moduleConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig,
);
const hasGps = true;
const hasWifi = true;
return (
<>
<SidebarOverlay
title="Settings"
open={open}
close={(): void => {
setOpen(false);
}}
direction="y"
>
<CollapsibleSection icon={<FiUser />} title="User">
<User />
</CollapsibleSection>
<CollapsibleSection icon={<FiSmartphone />} title="Device">
<WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiMapPin />} title="Position">
<Position />
</CollapsibleSection>
<CollapsibleSection icon={<FiPower />} title="Power">
<Power />
</CollapsibleSection>
<CollapsibleSection icon={<FiWifi />} title="WiFi">
<WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiTv />} title="Display">
<Display />
</CollapsibleSection>
<CollapsibleSection icon={<FiRss />} title="LoRa">
<LoRa />
</CollapsibleSection>
<ExternalSection
onClick={(): void => {
setChannelsOpen(true);
}}
icon={<FiLayers />}
title="Channels"
/>
<ExternalSection
onClick={(): void => {
setModulesOpen(true);
}}
icon={<FiPackage />}
title="Modules"
/>
<CollapsibleSection icon={<FiLayout />} title="Interface">
<Interface />
</CollapsibleSection>
</SidebarOverlay>
{/* Modules */}
<SidebarOverlay
title="Modules"
open={modulesOpen}
close={(): void => {
setModulesOpen(false);
}}
direction="x"
>
<CollapsibleSection
icon={<FiWifi />}
title="MQTT"
status={!moduleConfig.mqtt.disabled}
>
<MQTT />
</CollapsibleSection>
<CollapsibleSection
icon={<FiAlignLeft />}
title="Serial"
status={moduleConfig.serial.enabled}
>
<SerialSettingsPanel />
</CollapsibleSection>
<CollapsibleSection
icon={<FiBell />}
title="External Notifications"
status={moduleConfig.extNotification.enabled}
>
<ExternalNotificationsSettingsPlanel />
</CollapsibleSection>
<CollapsibleSection
icon={<FiFastForward />}
title="Store & Forward"
status={moduleConfig.storeForward.enabled}
>
<StoreForwardSettingsPanel />
</CollapsibleSection>
<CollapsibleSection
icon={<FiRss />}
title="Range Test"
status={moduleConfig.rangeTest.enabled}
>
<RangeTestSettingsPanel />
</CollapsibleSection>
<CollapsibleSection
icon={<FiActivity />}
title="Telemetry"
status={true}
>
<Telemetry />
</CollapsibleSection>
<CollapsibleSection
icon={<FiMessageSquare />}
title="Canned Message"
status={moduleConfig.cannedMessage.enabled}
>
<CannedMessage />
</CollapsibleSection>
</SidebarOverlay>
{/* End Modules */}
{/* Channels */}
<SidebarOverlay
title="Channels"
open={channelsOpen}
close={(): void => {
setChannelsOpen(false);
}}
direction="x"
>
<ChannelsGroup />
</SidebarOverlay>
{/* End Channels */}
</>
);
};
@@ -1,28 +0,0 @@
import type React from 'react';
import { Select } from '@components/generic/form/Select';
export const Interface = (): JSX.Element => {
return (
<Select
label="Language"
options={[
{
name: 'English',
value: 'en',
},
{
name: '日本',
value: 'jp',
},
{
name: 'Português',
value: 'pt',
},
]}
onChange={(e): void => {
console.log('changed language');
}}
/>
);
};
@@ -1,141 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const LoRa = (): JSX.Element => {
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware.myNodeNum,
);
const loraConfig = useAppSelector(
(state) => state.meshtastic.radio.config.lora,
);
const [loading, setLoading] = useState(false);
const [usePreset, setUsePreset] = useState(true);
// const { register, handleSubmit, formState, reset } =
// useForm<Protobuf.RadioConfig_UserPreferences>({
// defaultValues: preferences,
// });
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.Config_LoRaConfig>({
defaultValues: loraConfig,
});
useEffect(() => {
reset(loraConfig);
}, [reset, loraConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
const packet = Protobuf.AdminMessage.create({
variant: {
oneofKind: 'setConfig',
setConfig: {
payloadVariant: {
oneofKind: 'lora',
lora: data,
},
},
},
});
void connection.sendPacket(
Protobuf.AdminMessage.toBinary(packet),
Protobuf.PortNum.ADMIN_APP,
myNodeNum,
true,
0,
true,
false,
async (num) => {
return await Promise.resolve();
},
);
// void connection.setPreferences(data, async () => {
// reset({ ...data });
// setLoading(false);
// await Promise.resolve();
// });
});
return (
<>
<Checkbox
checked={usePreset}
label="Use Presets"
onChange={(e): void => setUsePreset(e.target.checked)}
/>
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
{usePreset ? (
<Select
label="Preset"
optionsEnum={Protobuf.Config_LoRaConfig_ModemPreset}
{...register('modemPreset', {
valueAsNumber: true,
})}
/>
) : (
<>
<Input
label="Bandwidth"
type="number"
suffix="MHz"
{...register('bandwidth', {
valueAsNumber: true,
})}
/>
<Input
label="Spread Factor"
type="number"
suffix="CPS"
min={7}
max={12}
{...register('spreadFactor', {
valueAsNumber: true,
})}
/>
<Input
label="Coding Rate"
type="number"
{...register('codingRate', {
valueAsNumber: true,
})}
/>
</>
)}
<Input
label="Transmit Power"
type="number"
suffix="dBm"
{...register('txPower', { valueAsNumber: true })}
/>
<Input
label="Hop Count"
type="number"
suffix="Hops"
{...register('hopLimit', { valueAsNumber: true })}
/>
<Checkbox label="Transmit Disabled" {...register('txDisabled')} />
<Input
label="Frequency Offset"
type="number"
suffix="Hz"
{...register('frequencyOffset', { valueAsNumber: true })}
/>
<Select
label="Region"
optionsEnum={Protobuf.Config_LoRaConfig_RegionCode}
{...register('region', { valueAsNumber: true })}
/>
</Form>
</>
);
};
@@ -1,140 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { MultiSelect } from 'react-multi-select-component';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Label } from '@components/generic/form/Label';
import { connection } from '@core/connection';
import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Position = (): JSX.Element => {
const positionConfig = useAppSelector(
(state) => state.meshtastic.radio.config.position,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.Config_PositionConfig>({
defaultValues: positionConfig,
// defaultValues: {
// ...preferences,
// positionBroadcastSecs:
// preferences.positionBroadcastSecs === 0
// ? preferences.role === Protobuf.Role.Router
// ? 43200
// : 900
// : preferences.positionBroadcastSecs,
// },
});
useEffect(() => {
reset(positionConfig);
}, [reset, positionConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setConfig(
{
payloadVariant: {
oneofKind: 'position',
position: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Input
label="Broadcast Interval"
type="number"
suffix="Seconds"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Checkbox
label="Disable Smart Position"
{...register('positionBroadcastSmartDisabled')}
/>
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Checkbox
label="Disable Location Sharing"
{...register('locationShareDisabled')}
/>
<Checkbox label="Disable GPS" {...register('gpsDisabled')} />
<Input
label="GPS Update Interval"
type="number"
suffix="Seconds"
{...register('gpsUpdateInterval', { valueAsNumber: true })}
/>
<Input
label="Last GPS Attempt"
disabled
{...register('gpsAttemptTime', { valueAsNumber: true })}
/>
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
<Input
label="Max DOP"
type="number"
{...register('gpsMaxDop', { valueAsNumber: true })}
/>
<Controller
name="positionFlags"
control={control}
render={({ field, fieldState }): JSX.Element => {
const { value, onChange, ...rest } = field;
const { error } = fieldState;
const label = 'Position Flags';
return (
<div className="w-full">
{label && <Label label={label} error={error?.message} />}
<MultiSelect
options={Object.entries(
Protobuf.Config_PositionConfig_PositionFlags,
)
.filter((value) => typeof value[1] !== 'number')
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.Config_PositionConfig_PositionFlags
.POS_UNDEFINED,
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
value={bitwiseDecode(
value,
Protobuf.Config_PositionConfig_PositionFlags,
).map((flag) => {
return {
value: flag,
label: Protobuf.Config_PositionConfig_PositionFlags[
flag
].replace('POS_', ''),
};
})}
onChange={(e: { value: number; label: string }[]): void =>
onChange(bitwiseEncode(e.map((v) => v.value)))
}
labelledBy="Select"
/>
</div>
);
}}
/>
</Form>
);
};
@@ -1,124 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Power = (): JSX.Element => {
const powerConfig = useAppSelector(
(state) => state.meshtastic.radio.config.power,
);
const deviceConfig = useAppSelector(
(state) => state.meshtastic.radio.config.device,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.Config_PowerConfig>({
defaultValues: powerConfig,
// defaultValues: {
// ...preferences,
// isLowPower:
// preferences.role === Protobuf.Role.Router
// ? true
// : preferences.isLowPower,
// },
});
useEffect(() => {
reset(powerConfig);
}, [reset, powerConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setConfig(
{
payloadVariant: {
oneofKind: 'power',
power: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Select
label="Charge current"
optionsEnum={Protobuf.Config_PowerConfig_ChargeCurrent}
{...register('chargeCurrent', { valueAsNumber: true })}
/>
<Checkbox
label="Powered by low power source (solar)"
disabled={
deviceConfig.role === Protobuf.Config_DeviceConfig_Role.Router
}
validationMessage={
deviceConfig.role === Protobuf.Config_DeviceConfig_Role.Router
? 'Enabled by default in router mode'
: ''
}
{...register('isLowPower')}
/>
<Checkbox label="Always Powered" {...register('isAlwaysPowered')} />
<Input
label="Shutdown on battery delay"
type="number"
suffix="Seconds"
{...register('onBatteryShutdownAfterSecs', { valueAsNumber: true })}
/>
<Checkbox label="Power Saving" {...register('isPowerSaving')} />
<Input
label="ADC Multiplier Override ratio"
type="number"
{...register('adcMultiplierOverride', { valueAsNumber: true })}
/>
<Input
label="Minumum Wake Time"
suffix="Seconds"
type="number"
{...register('minWakeSecs', { valueAsNumber: true })}
/>
<Input
label="Phone Timeout"
suffix="Seconds"
type="number"
{...register('phoneTimeoutSecs', { valueAsNumber: true })}
/>
<Input
label="Mesh SDS Timeout"
suffix="Seconds"
type="number"
{...register('meshSdsTimeoutSecs', { valueAsNumber: true })}
/>
<Input
label="SDS"
suffix="Seconds"
type="number"
{...register('sdsSecs', { valueAsNumber: true })}
/>
<Input
label="LS"
suffix="Seconds"
type="number"
{...register('lsSecs', { valueAsNumber: true })}
/>
<Input
label="Wait Bluetooth"
suffix="Seconds"
type="number"
{...register('waitBluetoothSecs', { valueAsNumber: true })}
/>
</Form>
);
};
@@ -1,106 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { base16 } from 'rfc4648';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const User = (): JSX.Element => {
const [loading, setLoading] = useState(false);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const node = useAppSelector((state) => state.meshtastic.nodes).find(
(node) => node.data.num === myNodeNum,
);
const { register, handleSubmit, formState, reset } = useForm<{
longName: string;
shortName: string;
isLicensed: boolean;
antAzimuth: number;
antGainDbi: number;
txPowerDbm: number;
}>({
defaultValues: {
longName: node?.data.user?.longName,
shortName: node?.data.user?.shortName,
isLicensed: node?.data.user?.isLicensed,
antAzimuth: node?.data.user?.antAzimuth,
antGainDbi: node?.data.user?.antGainDbi,
txPowerDbm: node?.data.user?.txPowerDbm,
},
});
useEffect(() => {
reset({
longName: node?.data.user?.longName,
shortName: node?.data.user?.shortName,
isLicensed: node?.data.user?.isLicensed,
});
}, [reset, node]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
if (node?.data.user) {
void connection.setOwner({ ...node.data.user, ...data }, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
}
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Input label="Device ID" value={node?.data.user?.id} disabled />
<Input label="Device Name" {...register('longName')} />
<Input label="Short Name" maxLength={3} {...register('shortName')} />
<Input
label="Mac Address"
defaultValue={
base16
.stringify(node?.data.user?.macaddr ?? [])
.match(/.{1,2}/g)
?.join(':') ?? ''
}
disabled
/>
<Input
label="Hardware (DEPRECATED)"
value={
Protobuf.HardwareModel[
node?.data.user?.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
/>
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
<Input
label="Transmit Power"
suffix="dBm"
type="number"
{...register('txPowerDbm', { valueAsNumber: true })}
/>
<Input
label="Antenna Gain"
suffix="dBi"
type="number"
{...register('antGainDbi', { valueAsNumber: true })}
/>
<Input
label="Antenna Azimuth"
suffix="°"
type="number"
{...register('antAzimuth', { valueAsNumber: true })}
/>
</Form>
);
};
@@ -1,62 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const WiFi = (): JSX.Element => {
const wifiConfig = useAppSelector(
(state) => state.meshtastic.radio.config.wifi,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.Config_WiFiConfig>({
defaultValues: wifiConfig,
});
const WifiApMode = useWatch({
control,
name: 'apMode',
defaultValue: false,
});
useEffect(() => {
reset(wifiConfig);
}, [reset, wifiConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setConfig(
{
payloadVariant: {
oneofKind: 'wifi',
wifi: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Enable WiFi AP" {...register('apMode')} />
<Input label="WiFi SSID" disabled={WifiApMode} {...register('ssid')} />
<Input
type="password"
autoComplete="off"
label="WiFi PSK"
disabled={WifiApMode}
{...register('psk')}
/>
</Form>
);
};
@@ -1,127 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { fromByteArray, toByteArray } from 'base64-js';
import { useForm } from 'react-hook-form';
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface SettingsPanelProps {
channel: Protobuf.Channel;
}
export const Channels = ({ channel }: SettingsPanelProps): JSX.Element => {
const [loading, setLoading] = useState(false);
const [keySize, setKeySize] = useState<128 | 256>(256);
const [pskHidden, setPskHidden] = useState(true);
const { register, handleSubmit, setValue, formState, reset } = useForm<
Omit<Protobuf.ChannelSettings, 'psk'> & { psk: string; enabled: boolean }
>({
defaultValues: {
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel?.role)
? true
: false,
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
},
});
useEffect(() => {
reset({
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel?.role)
? true
: false,
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
});
}, [channel, reset]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
const channelData = Protobuf.Channel.create({
role:
channel?.role === Protobuf.Channel_Role.PRIMARY
? Protobuf.Channel_Role.PRIMARY
: data.enabled
? Protobuf.Channel_Role.SECONDARY
: Protobuf.Channel_Role.DISABLED,
index: channel?.index,
settings: {
...data,
psk: toByteArray(data.psk ?? ''),
},
});
await connection.setChannel(channelData, (): Promise<void> => {
reset({ ...data });
setLoading(false);
return Promise.resolve();
});
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
{channel?.index !== 0 && (
<>
<Checkbox
label="Enabled"
{...register('enabled', { valueAsNumber: true })}
/>
<Input label="Name" {...register('name')} />
</>
)}
<Select
label="Key Size"
options={[
{ name: '128 Bit', value: 128 },
{ name: '256 Bit', value: 256 },
]}
value={keySize}
onChange={(e): void => {
setKeySize(parseInt(e.target.value) as 128 | 256);
}}
/>
<Input
label="Pre-Shared Key"
type={pskHidden ? 'password' : 'text'}
disabled
action={
<>
<IconButton
onClick={(): void => {
setPskHidden(!pskHidden);
}}
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
/>
<IconButton
onClick={(): void => {
const key = new Uint8Array(keySize);
crypto.getRandomValues(key);
setValue('psk', fromByteArray(key));
}}
icon={<MdRefresh />}
/>
</>
}
{...register('psk')}
/>
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
</Form>
);
};
@@ -1,43 +0,0 @@
import type React from 'react';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { Channels } from '@components/layout/Sidebar/Settings/channels/Channels';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const ChannelsGroup = (): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
return (
<>
{channels.map((channel) => {
return (
<div key={channel.index}>
<CollapsibleSection
title={
channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `Channel: ${channel.index}`
}
icon={
<div
className={`h-3 w-3 rounded-full ${
channel.role === Protobuf.Channel_Role.PRIMARY
? 'bg-orange-500'
: channel.role === Protobuf.Channel_Role.SECONDARY
? 'bg-green-500'
: 'bg-gray-500'
}`}
/>
}
>
<Channels channel={channel} />
</CollapsibleSection>
</div>
);
})}
</>
);
};
@@ -1,102 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const CannedMessage = (): JSX.Element => {
const cannedMessageConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.cannedMessage,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_CannedMessageConfig>({
defaultValues: cannedMessageConfig,
});
const moduleEnabled = useWatch({
control,
name: 'rotary1Enabled',
defaultValue: false,
});
useEffect(() => {
reset(cannedMessageConfig);
}, [reset, cannedMessageConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'cannedMessage',
cannedMessage: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('enabled')} />
<Checkbox
label="Rotary Encoder #1 Enabled"
{...register('rotary1Enabled')}
/>
<Input
label="Encoder Pin A"
type="number"
disabled={moduleEnabled}
{...register('inputbrokerPinA', { valueAsNumber: true })}
/>
<Input
label="Encoder Pin B"
type="number"
disabled={moduleEnabled}
{...register('inputbrokerPinB', { valueAsNumber: true })}
/>
<Input
label="Endoer Pin Press"
type="number"
disabled={moduleEnabled}
{...register('inputbrokerPinPress', { valueAsNumber: true })}
/>
<Select
label="Clockwise event"
disabled={moduleEnabled}
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
{...register('inputbrokerEventCw', { valueAsNumber: true })}
/>
<Select
label="Counter Clockwise event"
disabled={moduleEnabled}
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
{...register('inputbrokerEventCcw', { valueAsNumber: true })}
/>
<Select
label="Press event"
disabled={moduleEnabled}
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
{...register('inputbrokerEventPress', { valueAsNumber: true })}
/>
<Checkbox label="Up Down enabled" {...register('updown1Enabled')} />
<Input
label="Allow Input Source"
disabled={moduleEnabled}
{...register('allowInputSource')}
/>
<Checkbox label="Send Bell" {...register('sendBell')} />
</Form>
);
};
@@ -1,89 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
const [loading, setLoading] = useState(false);
const extNotificationConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.extNotification,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_ExternalNotificationConfig>({
defaultValues: extNotificationConfig,
});
useEffect(() => {
reset(extNotificationConfig);
}, [reset, extNotificationConfig]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'externalNotification',
externalNotification: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
const moduleEnabled = useWatch({
control,
name: 'enabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('enabled')} />
<Input
type="number"
label="Output MS"
suffix="ms"
disabled={!moduleEnabled}
{...register('outputMs', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output"
disabled={!moduleEnabled}
{...register('output', {
valueAsNumber: true,
})}
/>
<Checkbox
label="Active"
disabled={!moduleEnabled}
{...register('active')}
/>
<Checkbox
label="Message"
disabled={!moduleEnabled}
{...register('alertMessage')}
/>
<Checkbox
label="Bell"
disabled={!moduleEnabled}
{...register('alertBell')}
/>
</Form>
);
};
@@ -1,76 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const MQTT = (): JSX.Element => {
const mqttConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.mqtt,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_MQTTConfig>({
defaultValues: mqttConfig,
});
const moduleEnabled = useWatch({
control,
name: 'disabled',
defaultValue: false,
});
useEffect(() => {
reset(mqttConfig);
}, [reset, mqttConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'mqtt',
mqtt: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Disabled" {...register('disabled')} />
<Input
label="MQTT Server Address"
disabled={moduleEnabled}
{...register('address')}
/>
<Input
label="MQTT Username"
disabled={moduleEnabled}
{...register('username')}
/>
<Input
label="MQTT Password"
type="password"
autoComplete="off"
disabled={moduleEnabled}
{...register('password')}
/>
<Checkbox
label="Encryption Enabled"
disabled={moduleEnabled}
{...register('encryptionEnabled')}
/>
</Form>
);
};
@@ -1,71 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const RangeTestSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = useState(false);
const rangeTestConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.rangeTest,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_RangeTestConfig>({
defaultValues: rangeTestConfig,
});
useEffect(() => {
reset(rangeTestConfig);
}, [reset, rangeTestConfig]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'rangeTest',
rangeTest: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
const moduleEnabled = useWatch({
control,
name: 'enabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('enabled')} />
<Input
type="number"
label="Message Interval"
disabled={!moduleEnabled}
suffix="Seconds"
{...register('sender', {
valueAsNumber: true,
})}
/>
<Checkbox
label="Save CSV to storage"
disabled={!moduleEnabled}
{...register('save')}
/>
</Form>
);
};
@@ -1,99 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const SerialSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = useState(false);
const serialConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.serial,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_SerialConfig>({
defaultValues: serialConfig,
});
useEffect(() => {
reset(serialConfig);
}, [reset, serialConfig]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'serial',
serial: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
const moduleEnabled = useWatch({
control,
name: 'enabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('enabled')} />
<Checkbox label="Echo" disabled={!moduleEnabled} {...register('echo')} />
<Input
type="number"
label="RX"
disabled={!moduleEnabled}
{...register('rxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX Pin"
disabled={!moduleEnabled}
{...register('txd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Baud Rate"
disabled={!moduleEnabled}
{...register('baud', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!moduleEnabled}
{...register('timeout', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!moduleEnabled}
{...register('mode', {
valueAsNumber: true,
})}
/>
</Form>
);
};
@@ -1,87 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const StoreForwardSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = useState(false);
const storeForwardConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.storeForward,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_StoreForwardConfig>({
defaultValues: storeForwardConfig,
});
useEffect(() => {
reset(storeForwardConfig);
}, [reset, storeForwardConfig]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'storeForward',
storeForward: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
const moduleEnabled = useWatch({
control,
name: 'enabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('enabled')} />
<Checkbox
label="Heartbeat Enabled"
disabled={!moduleEnabled}
{...register('heartbeat')}
/>
<Input
type="number"
label="Number of records"
suffix="Records"
disabled={!moduleEnabled}
{...register('records', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return max"
disabled={!moduleEnabled}
{...register('historyReturnMax', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return window"
disabled={!moduleEnabled}
{...register('historyReturnWindow', {
valueAsNumber: true,
})}
/>
</Form>
);
};
@@ -1,97 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Telemetry = (): JSX.Element => {
const telemetryConfig = useAppSelector(
(state) => state.meshtastic.radio.moduleConfig.telemetry,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.ModuleConfig_TelemetryConfig>({
defaultValues: telemetryConfig,
});
useEffect(() => {
reset(telemetryConfig);
}, [reset, telemetryConfig]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setModuleConfig(
{
payloadVariant: {
oneofKind: 'telemetry',
telemetry: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
},
);
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Measurement Enabled"
{...register('environmentMeasurementEnabled')}
/>
<Checkbox
label="Displayed on Screen"
{...register('environmentScreenEnabled')}
/>
<Input
label="Read Error Count Threshold"
type="number"
{...register('environmentReadErrorCountThreshold', {
valueAsNumber: true,
})}
/>
<Input
label="Update Interval"
suffix="Seconds"
type="number"
{...register('environmentUpdateInterval', {
valueAsNumber: true,
})}
/>
<Input
label="Recovery Interval"
suffix="Seconds"
type="number"
{...register('environmentRecoveryInterval', {
valueAsNumber: true,
})}
/>
<Checkbox
label="Display Farenheit"
{...register('environmentDisplayFahrenheit')}
/>
<Select
label="Sensor Type"
optionsEnum={Protobuf.TelemetrySensorType}
{...register('environmentSensorType', {
valueAsNumber: true,
})}
/>
<Input
label="Sensor Pin"
type="number"
{...register('environmentSensorPin', {
valueAsNumber: true,
})}
/>
</Form>
);
};
@@ -1,38 +0,0 @@
import type React from 'react';
import { m } from 'framer-motion';
export interface SidebarItemProps {
selected: boolean;
setSelected: () => void;
actions?: React.ReactNode;
children: React.ReactNode;
}
export const SidebarItem = ({
selected,
setSelected,
actions,
children,
}: SidebarItemProps): JSX.Element => {
return (
<div
onClick={(): void => {
setSelected();
}}
className={`mx-2 flex cursor-pointer select-none rounded-md border bg-gray-200 p-2 shadow-md first:mt-2 last:mb-2 hover:border-primary dark:bg-tertiaryDark dark:hover:border-primary ${
selected ? 'border-primary' : 'border-gray-400 dark:border-gray-600'
}`}
>
<m.div
className="flex w-full justify-between"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<div className="flex gap-2">{children}</div>
<div className="flex gap-1">{actions}</div>
</m.div>
</div>
);
};
+111 -24
View File
@@ -1,33 +1,120 @@
import type React from 'react';
import type React from "react";
import { useState } from "react";
import { Settings } from '@components/layout/Sidebar/Settings/Index';
import { useAppSelector } from '@hooks/useAppSelector';
import {
ArrayIcon,
GlobeIcon,
IconComponent,
InboxIcon,
InfoSignIcon,
LabTestIcon,
LayersIcon,
majorScale,
Pane,
SettingsIcon,
Tab,
Tablist,
} from "evergreen-ui";
export interface SidebarProps {
children: React.ReactNode;
setSettingsOpen: (settingsOpen: boolean) => void;
settingsOpen: boolean;
import { PeersDialog } from "@app/components/Dialog/PeersDialog.js";
import { Page, useDevice } from "@app/core/stores/deviceStore.js";
import { DeviceCard } from "./DeviceCard.js";
interface NavLink {
name: string;
icon: IconComponent;
page: Page;
disabled?: boolean;
}
export const Sidebar = ({
settingsOpen,
setSettingsOpen,
children,
}: SidebarProps): JSX.Element => {
const appState = useAppSelector((state) => state.app);
export const Sidebar = (): JSX.Element => {
const { activePage, setActivePage } = useDevice();
const [PeersDialogOpen, setPeersDialogOpen] = useState(false);
const navLinks: NavLink[] = [
{
name: "Messages",
icon: InboxIcon,
page: "messages",
},
{
name: "Map",
icon: GlobeIcon,
page: "map",
disabled: true,
},
{
name: "Extensions",
icon: LabTestIcon,
page: "extensions",
},
{
name: "Config",
icon: SettingsIcon,
page: "config",
},
{
name: "Channels",
icon: LayersIcon,
page: "channels",
},
{
name: "Info",
icon: InfoSignIcon,
page: "info",
},
];
return (
<div
className={`absolute z-20 h-full w-full flex-grow flex-col md:relative md:flex md:w-96 ${
appState.mobileNavOpen ? 'flex' : 'hidden'
}`}
<Pane
display="flex"
flexDirection="column"
width="100%"
flexGrow={1}
margin={majorScale(3)}
padding={majorScale(2)}
borderRadius={majorScale(1)}
background="white"
elevation={1}
>
<div className="flex h-full w-full flex-col drop-shadow-xl dark:bg-primaryDark">
<div className="relative flex-grow gap-1">
<div className="absolute h-full w-full">{children}</div>
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
</div>
</div>
</div>
<Tablist>
{navLinks.map((Link) => (
<Tab
key={Link.name}
gap={majorScale(2)}
disabled={Link.disabled}
direction="vertical"
isSelected={Link.page === activePage}
onSelect={() => {
setActivePage(Link.page);
}}
>
<Link.icon />
{Link.name}
</Tab>
))}
<Tab
gap={5}
direction="vertical"
isSelected={PeersDialogOpen}
onSelect={() => {
setPeersDialogOpen(true);
}}
>
<ArrayIcon />
Peers
</Tab>
</Tablist>
<PeersDialog
isOpen={PeersDialogOpen}
close={() => {
setPeersDialogOpen(false);
}}
/>
<Pane display="flex" flexGrow={1}>
<DeviceCard />
</Pane>
</Pane>
);
};
-91
View File
@@ -1,91 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { FiMessageCircle, FiSettings } from 'react-icons/fi';
import { RiRoadMapLine } from 'react-icons/ri';
import { VscExtensions } from 'react-icons/vsc';
import { ErrorFallback } from '@components/ErrorFallback';
import { IconButton } from '@components/generic/button/IconButton';
import { Sidebar } from '@components/layout/Sidebar';
import type { TabProps } from '@components/Tab';
import { Tabs } from '@components/Tabs';
import { routes, useRoute } from '@core/router';
export interface LayoutProps {
title: string;
icon: React.ReactNode;
sidebarContents: React.ReactNode;
children: React.ReactNode;
}
export const Layout = ({
title,
icon,
sidebarContents,
children,
}: LayoutProps): JSX.Element => {
const [settingsOpen, setSettingsOpen] = useState(false);
const route = useRoute();
const tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[] = [
{
title: 'Messages',
icon: <FiMessageCircle />,
link: routes.messages().link,
active: route.name === 'messages',
},
{
title: 'Map',
icon: <RiRoadMapLine />,
link: routes.map().link,
active: route.name === 'map',
},
{
title: 'Extensions',
icon: <VscExtensions />,
link: routes.extensions().link,
active: route.name === 'extensions',
},
];
return (
<div className="relative flex w-full overflow-hidden bg-white dark:bg-secondaryDark">
<div className="flex flex-grow">
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
<div className="flex h-10 gap-1">
<div className="my-auto">
<IconButton icon={icon} />
</div>
<div className="my-auto text-lg font-medium dark:text-white">
{title}
</div>
</div>
</div>
<div className="flex flex-col gap-2">{sidebarContents}</div>
</Sidebar>
</div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<div className="flex h-full w-full flex-col bg-gray-300 dark:bg-secondaryDark">
<div className="flex w-full bg-white pt-1 dark:bg-primaryDark">
<div className="z-10 -mr-2 h-8">
<IconButton
className="m-1"
icon={<FiSettings />}
onClick={(): void => {
setSettingsOpen(!settingsOpen);
}}
active={settingsOpen}
/>
</div>
<Tabs tabs={tabs} />
</div>
<div className="flex flex-grow">{children}</div>
</div>
</ErrorBoundary>
</div>
);
};
@@ -0,0 +1,74 @@
import type React from "react";
import { IconComponent, majorScale, Pane, Tab, Tablist } from "evergreen-ui";
export interface Tab {
key: number;
name: string;
icon: IconComponent;
element: () => JSX.Element;
disabled?: boolean;
}
export interface TabbedContentProps {
active: number;
setActive: (index: number) => void;
tabs: Tab[];
actions?: (() => JSX.Element)[];
}
export const TabbedContent = ({
active,
setActive,
tabs,
actions,
}: TabbedContentProps): JSX.Element => {
return (
<Pane
margin={majorScale(3)}
borderRadius={majorScale(1)}
background="white"
elevation={1}
display="flex"
flexGrow={1}
flexDirection="column"
padding={majorScale(2)}
gap={majorScale(2)}
>
<Pane borderBottom="muted" paddingBottom={majorScale(2)}>
<Pane display="flex">
<Tablist>
{tabs.map((Entry) => (
<Tab
key={Entry.key}
disabled={Entry.disabled}
gap={5}
onSelect={() => setActive(Entry.key)}
isSelected={active === Entry.key}
>
<Entry.icon />
{Entry.name}
</Tab>
))}
</Tablist>
<Pane marginLeft="auto">
{actions?.map((Action, index) => (
<Action key={index} />
))}
</Pane>
</Pane>
</Pane>
{tabs.map((Entry) => (
<Pane
key={Entry.key}
display={active === Entry.key ? "flex" : "none"}
flexDirection="column"
flexGrow={1}
>
<Entry.element />
</Pane>
))}
</Pane>
);
};