Browse Source

Consolidate forms & other minor fixes

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
ea5ab9376f
  1. 15
      package.json
  2. 474
      pnpm-lock.yaml
  3. 5
      src/App.tsx
  4. 2
      src/components/Tabs.tsx
  5. 11
      src/components/generic/Card.tsx
  6. 6
      src/components/generic/Modal.tsx
  7. 21
      src/components/generic/Sidebar/CollapsibleSection.tsx
  8. 20
      src/components/generic/form/Form.tsx
  9. 18
      src/components/layout/Sidebar/Settings/Channels.tsx
  10. 37
      src/components/layout/Sidebar/Settings/Device.tsx
  11. 56
      src/components/layout/Sidebar/Settings/Display.tsx
  12. 131
      src/components/layout/Sidebar/Settings/GPS.tsx
  13. 156
      src/components/layout/Sidebar/Settings/Index.tsx
  14. 65
      src/components/layout/Sidebar/Settings/LoRa.tsx
  15. 158
      src/components/layout/Sidebar/Settings/Position.tsx
  16. 75
      src/components/layout/Sidebar/Settings/Power.tsx
  17. 56
      src/components/layout/Sidebar/Settings/User.tsx
  18. 50
      src/components/layout/Sidebar/Settings/WiFi.tsx
  19. 18
      src/components/layout/Sidebar/Settings/channels/Channels.tsx
  20. 4
      src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx
  21. 99
      src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx
  22. 84
      src/components/layout/Sidebar/Settings/modules/ExternalNotifications.tsx
  23. 68
      src/components/layout/Sidebar/Settings/modules/MQTT.tsx
  24. 39
      src/components/layout/Sidebar/Settings/modules/RangeTest.tsx
  25. 95
      src/components/layout/Sidebar/Settings/modules/Serial.tsx
  26. 82
      src/components/layout/Sidebar/Settings/modules/StoreForward.tsx
  27. 83
      src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
  28. 99
      src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx
  29. 102
      src/components/layout/Sidebar/Settings/plugins/Serial.tsx
  30. 97
      src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx
  31. 11
      src/components/layout/index.tsx
  32. 5
      src/components/menu/BottomNav.tsx
  33. 13
      src/components/modals/VersionInfo.tsx
  34. 2
      src/core/slices/mapSlice.ts
  35. 6
      src/pages/Extensions/Debug.tsx
  36. 4
      src/pages/Extensions/FileBrowser.tsx
  37. 5
      src/pages/Extensions/Index.tsx
  38. 6
      src/pages/Extensions/Info.tsx
  39. 8
      src/pages/Extensions/Logs.tsx
  40. 2
      src/pages/Nodes/NodeCard.tsx
  41. 2
      src/pages/Nodes/index.tsx
  42. 2
      src/pages/NotFound.tsx

15
package.json

@ -4,7 +4,7 @@
"description": "Meshtastic web client", "description": "Meshtastic web client",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)", "package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)",
@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/eslint-config": "^1.0.6", "@meshtastic/eslint-config": "^1.0.6",
"@meshtastic/meshtasticjs": "^0.6.48", "@meshtastic/meshtasticjs": "^0.6.50",
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
@ -31,9 +31,8 @@
"prettier": "^2.5.1", "prettier": "^2.5.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-flow-renderer": "^10.0.0-next.45", "react-flow-renderer": "^10.0.0-next.48",
"react-hook-form": "^7.27.1", "react-hook-form": "^7.27.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
@ -49,9 +48,9 @@
}, },
"devDependencies": { "devDependencies": {
"@hookform/devtools": "^4.0.2", "@hookform/devtools": "^4.0.2",
"@types/mapbox-gl": "^2.6.2", "@types/mapbox-gl": "^2.6.3",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.13",
"@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.12", "@types/web-bluetooth": "^0.0.12",
"@vitejs/plugin-react": "^1.2.0", "@vitejs/plugin-react": "^1.2.0",
@ -63,9 +62,9 @@
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.6.2", "typescript": "^4.6.2",
"unimported": "^1.19.1", "unimported": "^1.19.1",
"vite": "^2.8.5", "vite": "^2.8.6",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.5.0" "workbox-window": "^6.5.1"
} }
} }

474
pnpm-lock.yaml

File diff suppressed because it is too large

5
src/App.tsx

@ -1,12 +1,11 @@
import type React from 'react'; import type React from 'react';
import { Map } from '@app/pages/Map';
import { Connection } from '@components/Connection'; import { Connection } from '@components/Connection';
import { ContextMenu } from '@components/generic/ContextMenu';
import { BottomNav } from '@components/menu/BottomNav'; import { BottomNav } from '@components/menu/BottomNav';
import { useRoute } from '@core/router'; import { useRoute } from '@core/router';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Extensions } from '@pages/Extensions/Index'; import { Extensions } from '@pages/Extensions/Index';
import { Map } from '@pages/Map';
import { Messages } from '@pages/Messages'; import { Messages } from '@pages/Messages';
import { Nodes } from '@pages/Nodes'; import { Nodes } from '@pages/Nodes';
import { NotFound } from '@pages/NotFound'; import { NotFound } from '@pages/NotFound';
@ -17,7 +16,6 @@ export const App = (): JSX.Element => {
return ( return (
<div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}> <div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}>
<ContextMenu>
<Connection /> <Connection />
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex min-h-0 w-full flex-grow"> <div className="flex min-h-0 w-full flex-grow">
@ -29,7 +27,6 @@ export const App = (): JSX.Element => {
</div> </div>
<BottomNav /> <BottomNav />
</div> </div>
</ContextMenu>
</div> </div>
); );
}; };

2
src/components/Tabs.tsx

@ -1,6 +1,6 @@
import type React from 'react'; import type React from 'react';
import { Tab, TabProps } from './Tab'; import { Tab, TabProps } from '@components/Tab';
export interface TabsProps { export interface TabsProps {
tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[]; tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[];

11
src/components/generic/Card.tsx

@ -1,7 +1,6 @@
import type React from 'react'; import type React from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import Draggable from 'react-draggable';
export interface CardProps { export interface CardProps {
className?: string; className?: string;
@ -9,30 +8,23 @@ export interface CardProps {
actions?: React.ReactNode; actions?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
border?: boolean; border?: boolean;
draggable?: boolean;
} }
export const Card = ({ export const Card = ({
className, className,
title, title,
actions, actions,
draggable,
border, border,
children, children,
}: CardProps): JSX.Element => { }: CardProps): JSX.Element => {
return ( return (
<Draggable handle=".handle" disabled={!draggable}>
<div <div
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${ className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
border ? 'border border-gray-400 dark:border-gray-600' : '' border ? 'border border-gray-400 dark:border-gray-600' : ''
} ${className ?? ''}`} } ${className ?? ''}`}
> >
{(title || actions) && ( {(title || actions) && (
<div <div className="w-full select-none justify-between rounded-t-md border-b border-gray-400 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-tertiaryDark dark:text-white">
className={`w-full select-none justify-between rounded-t-md border-b border-gray-400 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-tertiaryDark dark:text-white ${
draggable ? 'cursor-move' : ''
}`}
>
<div className="handle flex h-8 justify-between"> <div className="handle flex h-8 justify-between">
<div className="my-auto ml-2 truncate">{title}</div> <div className="my-auto ml-2 truncate">{title}</div>
{actions} {actions}
@ -52,6 +44,5 @@ export const Card = ({
{children} {children}
</m.div> </m.div>
</div> </div>
</Draggable>
); );
}; };

6
src/components/generic/Modal.tsx

@ -3,11 +3,10 @@ import type React from 'react';
import { AnimatePresence, m } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { FiX } from 'react-icons/fi'; import { FiX } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Card, CardProps } from '@components/generic/Card';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton } from './button/IconButton';
import { Card, CardProps } from './Card';
export interface ModalProps extends CardProps { export interface ModalProps extends CardProps {
open: boolean; open: boolean;
bgDismiss?: boolean; bgDismiss?: boolean;
@ -51,7 +50,6 @@ export const Modal = ({
<div className="inline-block w-full max-w-3xl align-middle"> <div className="inline-block w-full max-w-3xl align-middle">
<Card <Card
border border
draggable
actions={ actions={
<div className="flex gap-2"> <div className="flex gap-2">
{actions} {actions}

21
src/components/generic/Sidebar/CollapsibleSection.tsx

@ -7,12 +7,14 @@ import { FiArrowUp } from 'react-icons/fi';
export interface CollapsibleSectionProps { export interface CollapsibleSectionProps {
title: string; title: string;
icon?: JSX.Element; icon?: JSX.Element;
status?: boolean;
children: JSX.Element; children: JSX.Element;
} }
export const CollapsibleSection = ({ export const CollapsibleSection = ({
title, title,
icon, icon,
status,
children, children,
}: CollapsibleSectionProps): JSX.Element => { }: CollapsibleSectionProps): JSX.Element => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -21,6 +23,7 @@ export const CollapsibleSection = ({
<m.div> <m.div>
<m.div <m.div
layout layout
onClick={toggleOpen}
className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b bg-gray-200 p-2 text-sm font-medium dark:border-primaryDark dark:bg-tertiaryDark dark:text-gray-400 ${ className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b bg-gray-200 p-2 text-sm font-medium dark:border-primaryDark dark:bg-tertiaryDark dark:text-gray-400 ${
open open
? 'border-l-primary dark:border-l-primary' ? 'border-l-primary dark:border-l-primary'
@ -29,13 +32,25 @@ export const CollapsibleSection = ({
> >
<m.div <m.div
layout layout
onClick={toggleOpen}
whileHover={{ scale: 1.01 }} whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }} whileTap={{ scale: 0.99 }}
className="flex justify-between gap-2 " className="my-auto flex justify-between gap-2"
> >
<m.div className="flex gap-2"> <m.div className="flex gap-2">
<m.div className="my-auto">{icon}</m.div> <m.div className="my-auto flex gap-2">
{status !== undefined ? (
<>
{icon}
<div
className={`h-3 w-3 rounded-full ${
status ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</>
) : (
<>{icon}</>
)}
</m.div>
{title} {title}
</m.div> </m.div>
<m.div <m.div

20
src/components/generic/form/Form.tsx

@ -1,22 +1,36 @@
import type React from 'react'; import type React from 'react';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Loading } from '@components/generic/Loading'; import { Loading } from '@components/generic/Loading';
export interface FormProps { export interface FormProps {
loading?: boolean; submit: () => Promise<void>;
loading: boolean;
dirty: boolean;
children: React.ReactNode; children: React.ReactNode;
} }
export const Form = ({ loading, children }: FormProps): JSX.Element => { export const Form = ({
submit,
loading,
dirty,
children,
}: FormProps): JSX.Element => {
return ( return (
<form <form
onSubmit={(e): void => { onSubmit={(e): void => {
e.preventDefault(); e.preventDefault();
}} }}
className="relative flex-grow gap-3 p-2"
> >
{loading && <Loading />} {loading && <Loading />}
{children} {children}
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton disabled={dirty} onClick={submit} icon={<FiSave />} />
</div>
</div>
</form> </form>
); );
}; };

18
src/components/layout/Sidebar/Settings/Channels.tsx

@ -2,10 +2,9 @@ import type React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select'; import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
@ -59,7 +58,7 @@ export const Channels = (): JSX.Element => {
label="Use Presets" label="Use Presets"
onChange={(e): void => setUsePreset(e.target.checked)} onChange={(e): void => setUsePreset(e.target.checked)}
/> />
<form onSubmit={onSubmit}> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
{usePreset ? ( {usePreset ? (
<Select <Select
label="Preset" label="Preset"
@ -103,18 +102,7 @@ export const Channels = (): JSX.Element => {
suffix="dBm" suffix="dBm"
{...register('settings.txPower', { valueAsNumber: true })} {...register('settings.txPower', { valueAsNumber: true })}
/> />
</form> </Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</> </>
)} )}
</> </>

37
src/components/layout/Sidebar/Settings/Radio.tsx → src/components/layout/Sidebar/Settings/Device.tsx

@ -2,16 +2,15 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Select } from '@components/generic/form/Select'; import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Radio = (): JSX.Element => { export const Device = (): JSX.Element => {
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
); );
@ -34,28 +33,18 @@ export const Radio = (): JSX.Element => {
}); });
}); });
return ( return (
<> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<form className="space-y-2" onSubmit={onSubmit}> <Checkbox
<Checkbox label="Is Router" {...register('isRouter')} /> label="Serial Console Disabled"
<Select {...register('serialDisabled')}
label="Region"
optionsEnum={Protobuf.RegionCode}
{...register('region', { valueAsNumber: true })}
/> />
<Checkbox label="Debug Log" {...register('debugLogEnabled')} /> <Checkbox label="Factory Reset Device" {...register('factoryReset')} />
<Checkbox label="Serial Disabled" {...register('serialDisabled')} /> <Checkbox label="Debug Log Enabled" {...register('debugLogEnabled')} />
</form> <Select
<div className="flex w-full bg-white dark:bg-secondaryDark"> label="Role"
<div className="ml-auto p-2"> optionsEnum={Protobuf.Role}
<IconButton {...register('role', { valueAsNumber: true })}
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/> />
</div> </Form>
</div>
</>
); );
}; };

56
src/components/layout/Sidebar/Settings/Display.tsx

@ -0,0 +1,56 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(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.GpsCoordinateFormat}
{...register('gpsFormat', { valueAsNumber: true })}
/>
</Form>
);
};

131
src/components/layout/Sidebar/Settings/GPS.tsx

@ -0,0 +1,131 @@
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 { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const GPS = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...preferences,
positionBroadcastSecs:
preferences.positionBroadcastSecs === 0
? preferences.isRouter
? 43200
: 900
: preferences.positionBroadcastSecs,
},
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(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="Use Smart Position"
{...register('positionBroadcastSmart')}
/>
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Select
label="Location Sharing"
optionsEnum={Protobuf.LocationSharing}
{...register('locationShare', { valueAsNumber: true })}
/>
<Select
label="GPS Mode"
optionsEnum={Protobuf.GpsOperation}
{...register('gpsOperation', { valueAsNumber: true })}
/>
<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.PositionFlags)
.filter((value) => typeof value[1] !== 'number')
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.PositionFlags.POS_UNDEFINED,
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
value={bitwiseDecode(value, Protobuf.PositionFlags).map(
(flag) => {
return {
value: flag,
label: Protobuf.PositionFlags[flag].replace('POS_', ''),
};
},
)}
onChange={(e: { value: number; label: string }[]): void =>
onChange(bitwiseEncode(e.map((v) => v.value)))
}
labelledBy="Select"
/>
</div>
);
}}
/>
</Form>
);
};

156
src/components/layout/Sidebar/Settings/Index.tsx

@ -2,36 +2,43 @@ import type React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { import {
FiActivity,
FiAlignLeft, FiAlignLeft,
FiBell, FiBell,
FiFastForward, FiFastForward,
FiLayers, FiLayers,
FiLayout, FiLayout,
FiMapPin, FiMapPin,
FiMessageSquare,
FiPackage, FiPackage,
FiRadio, FiPower,
FiRss, FiRss,
FiSmartphone,
FiTv,
FiUser, FiUser,
FiWifi, FiWifi,
FiZap,
} from 'react-icons/fi'; } from 'react-icons/fi';
import { useAppSelector } from '@app/hooks/useAppSelector.js';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection'; import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection'; import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay'; import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { Channels } from '@components/layout/Sidebar/Settings/Channels'; import { Channels } from '@components/layout/Sidebar/Settings/Channels';
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup'; import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
import { Display } from '@components/layout/Sidebar/Settings/Display';
import { GPS } from '@components/layout/Sidebar/Settings/GPS';
import { Interface } from '@components/layout/Sidebar/Settings/Interface'; import { Interface } from '@components/layout/Sidebar/Settings/Interface';
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/plugins/ExternalNotifications'; import { LoRa } from '@components/layout/Sidebar/Settings/LoRa';
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/RangeTest'; import { CannedMessage } from '@components/layout/Sidebar/Settings/modules/CannedMessage';
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/Serial'; import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/modules/ExternalNotifications';
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/StoreForward'; import { MQTT } from '@components/layout/Sidebar/Settings/modules/MQTT';
import { Position } from '@components/layout/Sidebar/Settings/Position'; 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 { Power } from '@components/layout/Sidebar/Settings/Power';
import { Radio } from '@components/layout/Sidebar/Settings/Radio';
import { User } from '@components/layout/Sidebar/Settings/User'; import { User } from '@components/layout/Sidebar/Settings/User';
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi'; import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
import { useAppSelector } from '@hooks/useAppSelector';
export interface SettingsProps { export interface SettingsProps {
open: boolean; open: boolean;
@ -39,13 +46,15 @@ export interface SettingsProps {
} }
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => { export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
const [pluginsOpen, setPluginsOpen] = useState(false); const [modulesOpen, setModulesOpen] = useState(false);
const [channelsOpen, setChannelsOpen] = useState(false); const [channelsOpen, setChannelsOpen] = useState(false);
const { const {
rangeTestPluginEnabled, rangeTestModuleEnabled,
extNotificationPluginEnabled, extNotificationModuleEnabled,
serialpluginEnabled, serialmoduleEnabled,
storeForwardPluginEnabled, storeForwardModuleEnabled,
mqttDisabled,
cannedMessageModuleEnabled,
} = useAppSelector((state) => state.meshtastic.radio.preferences); } = useAppSelector((state) => state.meshtastic.radio.preferences);
const hasGps = true; const hasGps = true;
@ -61,20 +70,26 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
}} }}
direction="y" direction="y"
> >
<CollapsibleSection icon={<FiWifi />} title="WiFi & MQTT">
<WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiMapPin />} title="Position">
<Position />
</CollapsibleSection>
<CollapsibleSection icon={<FiUser />} title="User"> <CollapsibleSection icon={<FiUser />} title="User">
<User /> <User />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection icon={<FiZap />} title="Power"> <CollapsibleSection icon={<FiSmartphone />} title="Device">
<WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiMapPin />} title="GPS">
<GPS />
</CollapsibleSection>
<CollapsibleSection icon={<FiPower />} title="Power">
<Power /> <Power />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection icon={<FiRadio />} title="Radio"> <CollapsibleSection icon={<FiWifi />} title="WiFi">
<Radio /> <WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiTv />} title="Display">
<Display />
</CollapsibleSection>
<CollapsibleSection icon={<FiRss />} title="LoRa">
<LoRa />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection icon={<FiLayers />} title="Primary Channel"> <CollapsibleSection icon={<FiLayers />} title="Primary Channel">
<Channels /> <Channels />
@ -88,87 +103,76 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
/> />
<ExternalSection <ExternalSection
onClick={(): void => { onClick={(): void => {
setPluginsOpen(true); setModulesOpen(true);
}} }}
icon={<FiPackage />} icon={<FiPackage />}
title="Plugins" title="Modules"
/> />
<CollapsibleSection icon={<FiLayout />} title="Interface"> <CollapsibleSection icon={<FiLayout />} title="Interface">
<Interface /> <Interface />
</CollapsibleSection> </CollapsibleSection>
</SidebarOverlay> </SidebarOverlay>
{/* Plugins */} {/* Modules */}
<SidebarOverlay <SidebarOverlay
title="Plugins" title="Modules"
open={pluginsOpen} open={modulesOpen}
close={(): void => { close={(): void => {
setPluginsOpen(false); setModulesOpen(false);
}} }}
direction="x" direction="x"
> >
<CollapsibleSection <CollapsibleSection
title="Range Test" icon={<FiWifi />}
icon={ title="MQTT"
<div className="flex gap-2"> status={!mqttDisabled}
<FiRss />
<div
className={`h-3 w-3 rounded-full ${
rangeTestPluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<RangeTestSettingsPanel /> <MQTT />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection <CollapsibleSection
title="External Notifications" icon={<FiAlignLeft />}
icon={ title="Serial"
<div className="flex gap-2"> status={serialmoduleEnabled}
<FiBell />
<div
className={`h-3 w-3 rounded-full ${
extNotificationPluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<ExternalNotificationsSettingsPlanel /> <SerialSettingsPanel />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection <CollapsibleSection
title="Serial" icon={<FiBell />}
icon={ title="External Notifications"
<div className="flex gap-2"> status={extNotificationModuleEnabled}
<FiAlignLeft />
<div
className={`h-3 w-3 rounded-full ${
serialpluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<SerialSettingsPanel /> <ExternalNotificationsSettingsPlanel />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection <CollapsibleSection
icon={<FiFastForward />}
title="Store & Forward" title="Store & Forward"
icon={ status={storeForwardModuleEnabled}
<div className="flex gap-2">
<FiFastForward />
<div
className={`h-3 w-3 rounded-full ${
storeForwardPluginEnabled ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
}
> >
<StoreForwardSettingsPanel /> <StoreForwardSettingsPanel />
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection
icon={<FiRss />}
title="Range Test"
status={rangeTestModuleEnabled}
>
<RangeTestSettingsPanel />
</CollapsibleSection>
<CollapsibleSection
icon={<FiActivity />}
title="Telemetry"
status={true}
>
<Telemetry />
</CollapsibleSection>
<CollapsibleSection
icon={<FiMessageSquare />}
title="Canned Message"
status={cannedMessageModuleEnabled}
>
<CannedMessage />
</CollapsibleSection>
</SidebarOverlay> </SidebarOverlay>
{/* End Plugins */} {/* End Modules */}
{/* Channels */} {/* Channels */}
<SidebarOverlay <SidebarOverlay

65
src/components/layout/Sidebar/Settings/LoRa.tsx

@ -0,0 +1,65 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Input
label="Hop Count"
type="number"
suffix="Hops"
{...register('hopLimit', { valueAsNumber: true })}
/>
<Checkbox label="Transmit Disabled" {...register('isLoraTxDisabled')} />
<Checkbox label="Router Mode" {...register('isRouter')} />
<Input
label="Send Owner Interval"
type="number"
suffix="Seconds"
{...register('sendOwnerInterval', { valueAsNumber: true })}
/>
<Input
label="Frequency Offset"
type="number"
suffix="Hz"
{...register('frequencyOffset', { valueAsNumber: true })}
/>
<Select
label="Region"
optionsEnum={Protobuf.RegionCode}
{...register('region', { valueAsNumber: true })}
/>
</Form>
);
};

158
src/components/layout/Sidebar/Settings/Position.tsx

@ -1,158 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MultiSelect } from 'react-multi-select-component';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Label } from '@components/generic/form/Label';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { bitwiseEncode } from '@core/utils/bitwise';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Position = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...preferences,
positionBroadcastSecs:
preferences.positionBroadcastSecs === 0
? preferences.isRouter
? 43200
: 900
: preferences.positionBroadcastSecs,
},
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const encode = (enums: Protobuf.PositionFlags[]): number => {
return enums.reduce((acc, curr) => acc | curr, 0);
};
const decode = (value: number): Protobuf.PositionFlags[] => {
const enumValues = Object.keys(Protobuf.PositionFlags)
.map(Number)
.filter(Boolean);
return enumValues.map((b) => value & b).filter(Boolean);
};
return (
<>
<form className="space-y-2" onSubmit={onSubmit}>
<Input
label="Broadcast Interval"
type="number"
suffix="Seconds"
{...register('positionBroadcastSecs', { 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.PositionFlags)
.filter((value) => typeof value[1] !== 'number')
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.PositionFlags.POS_UNDEFINED,
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
value={decode(value).map((flag) => {
return {
value: flag,
label: Protobuf.PositionFlags[flag].replace('POS_', ''),
};
})}
onChange={(e: { value: number; label: string }[]): void =>
onChange(bitwiseEncode(e.map((v) => v.value)))
}
labelledBy="Select"
/>
</div>
);
}}
/>
<Input
label="Position Type (DEBUG)"
type="number"
disabled
{...register('positionFlags', { valueAsNumber: true })}
/>
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Select
label="Location Sharing"
optionsEnum={Protobuf.LocationSharing}
{...register('locationShare', { valueAsNumber: true })}
/>
<Select
label="GPS Mode"
optionsEnum={Protobuf.GpsOperation}
{...register('gpsOperation', { valueAsNumber: true })}
/>
<Select
label="Display Format"
optionsEnum={Protobuf.GpsCoordinateFormat}
{...register('gpsFormat', { valueAsNumber: true })}
/>
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
<Input
label="Max DOP"
type="number"
{...register('gpsMaxDop', { valueAsNumber: true })}
/>
<Input
label="Last GPS Attempt"
disabled
{...register('gpsAttemptTime', { valueAsNumber: true })}
/>
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};

75
src/components/layout/Sidebar/Settings/Power.tsx

@ -2,10 +2,10 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; 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 { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -37,14 +37,12 @@ export const Power = (): JSX.Element => {
}); });
}); });
return ( return (
<> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<form className="space-y-2" onSubmit={onSubmit}>
<Select <Select
label="Charge current" label="Charge current"
optionsEnum={Protobuf.ChargeCurrent} optionsEnum={Protobuf.ChargeCurrent}
{...register('chargeCurrent', { valueAsNumber: true })} {...register('chargeCurrent', { valueAsNumber: true })}
/> />
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
<Checkbox <Checkbox
label="Powered by low power source (solar)" label="Powered by low power source (solar)"
disabled={preferences.isRouter} disabled={preferences.isRouter}
@ -53,18 +51,61 @@ export const Power = (): JSX.Element => {
} }
{...register('isLowPower')} {...register('isLowPower')}
/> />
</form> <Checkbox label="Always Powered" {...register('isAlwaysPowered')} />
<div className="flex w-full bg-white dark:bg-secondaryDark"> <Input
<div className="ml-auto p-2"> label="Shutdown on battery delay"
<IconButton type="number"
disabled={!formState.isDirty} suffix="Seconds"
onClick={async (): Promise<void> => { {...register('onBatteryShutdownAfterSecs', { valueAsNumber: true })}
await onSubmit();
}}
icon={<FiSave />}
/> />
</div> <Checkbox label="Power Saving" {...register('isPowerSaving')} />
</div> <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="Phone SDS Timeout"
suffix="Seconds"
type="number"
{...register('phoneSdsTimeoutSec', { 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>
); );
}; };

56
src/components/layout/Sidebar/Settings/User.tsx

@ -2,11 +2,10 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { base16 } from 'rfc4648'; import { base16 } from 'rfc4648';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select'; import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
@ -67,18 +66,10 @@ export const User = (): JSX.Element => {
}); });
return ( return (
<> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<form className="space-y-2" onSubmit={onSubmit}>
<Input label="Device ID" value={node?.user?.id} disabled /> <Input label="Device ID" value={node?.user?.id} disabled />
<Input <Input label="Device Name" {...register('longName')} />
label="Hardware" <Input label="Short Name" maxLength={3} {...register('shortName')} />
value={
Protobuf.HardwareModel[
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
/>
<Input <Input
label="Mac Address" label="Mac Address"
defaultValue={ defaultValue={
@ -89,19 +80,26 @@ export const User = (): JSX.Element => {
} }
disabled disabled
/> />
<Input label="Device Name" {...register('longName')} /> <Input
<Input label="Short Name" maxLength={3} {...register('shortName')} /> label="Hardware (DEPRECATED)"
value={
Protobuf.HardwareModel[
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
/>
<Checkbox label="Licenced Operator?" {...register('isLicensed')} /> <Checkbox label="Licenced Operator?" {...register('isLicensed')} />
<Select <Select
label="Team" label="Team (DEPRECATED)"
optionsEnum={Protobuf.Team} optionsEnum={Protobuf.Team}
{...register('team', { valueAsNumber: true })} {...register('team', { valueAsNumber: true })}
/> />
<Input <Input
label="Antenna Azimuth" label="Transmit Power"
suffix="°" suffix="dBm"
type="number" type="number"
{...register('antAzimuth', { valueAsNumber: true })} {...register('txPowerDbm', { valueAsNumber: true })}
/> />
<Input <Input
label="Antenna Gain" label="Antenna Gain"
@ -110,23 +108,11 @@ export const User = (): JSX.Element => {
{...register('antGainDbi', { valueAsNumber: true })} {...register('antGainDbi', { valueAsNumber: true })}
/> />
<Input <Input
label="Transmit Power" label="Antenna Azimuth"
suffix="dBm" suffix="°"
type="number" type="number"
{...register('txPowerDbm', { valueAsNumber: true })} {...register('antAzimuth', { valueAsNumber: true })}
/>
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/> />
</div> </Form>
</div>
</>
); );
}; };

50
src/components/layout/Sidebar/Settings/WiFi.tsx

@ -2,10 +2,9 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -21,18 +20,12 @@ export const WiFi = (): JSX.Element => {
defaultValues: preferences, defaultValues: preferences,
}); });
const watchWifiApMode = useWatch({ const WifiApMode = useWatch({
control, control,
name: 'wifiApMode', name: 'wifiApMode',
defaultValue: false, defaultValue: false,
}); });
const watchMQTTDisabled = useWatch({
control,
name: 'mqttDisabled',
defaultValue: false,
});
useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);
@ -46,51 +39,20 @@ export const WiFi = (): JSX.Element => {
}); });
}); });
return ( return (
<> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<form className="space-y-2" onSubmit={onSubmit}>
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} /> <Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
<Input <Input
label="WiFi SSID" label="WiFi SSID"
disabled={watchWifiApMode} disabled={WifiApMode}
{...register('wifiSsid')} {...register('wifiSsid')}
/> />
<Input <Input
type="password" type="password"
autoComplete="off" autoComplete="off"
label="WiFi PSK" label="WiFi PSK"
disabled={watchWifiApMode} disabled={WifiApMode}
{...register('wifiPassword')} {...register('wifiPassword')}
/> />
<Checkbox label="Disable MQTT" {...register('mqttDisabled')} /> </Form>
<Input
label="MQTT Server Address"
disabled={watchMQTTDisabled}
{...register('mqttServer')}
/>
<Input
label="MQTT Username"
disabled={watchMQTTDisabled}
{...register('mqttUsername')}
/>
<Input
label="MQTT Password"
type="password"
autoComplete="off"
disabled={watchMQTTDisabled}
{...register('mqttPassword')}
/>
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

18
src/components/layout/Sidebar/Settings/channels/Channels.tsx

@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import { fromByteArray, toByteArray } from 'base64-js'; import { fromByteArray, toByteArray } from 'base64-js';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md'; import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
import { IconButton } from '@components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
@ -18,7 +17,7 @@ export interface SettingsPanelProps {
channel: Protobuf.Channel; channel: Protobuf.Channel;
} }
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => { export const Channels = ({ channel }: SettingsPanelProps): JSX.Element => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [keySize, setKeySize] = useState<128 | 256>(256); const [keySize, setKeySize] = useState<128 | 256>(256);
const [pskHidden, setPskHidden] = useState(true); const [pskHidden, setPskHidden] = useState(true);
@ -75,8 +74,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
}); });
return ( return (
<div className="flex w-full flex-col"> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Form loading={loading}>
{channel?.index !== 0 && ( {channel?.index !== 0 && (
<> <>
<Checkbox <Checkbox
@ -125,17 +123,5 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} /> <Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} /> <Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
</Form> </Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</div>
); );
}; };

4
src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx

@ -1,7 +1,7 @@
import type React from 'react'; import type React from 'react';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection'; import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { SettingsPanel } from '@components/layout/Sidebar/Settings/channels/Channels'; import { Channels } from '@components/layout/Sidebar/Settings/channels/Channels';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
@ -33,7 +33,7 @@ export const ChannelsGroup = (): JSX.Element => {
/> />
} }
> >
<SettingsPanel channel={channel} /> <Channels channel={channel} />
</CollapsibleSection> </CollapsibleSection>
</div> </div>
); );

99
src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx

@ -0,0 +1,99 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const moduleEnabled = useWatch({
control,
name: 'rotary1Enabled',
defaultValue: false,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Module Enabled"
{...register('cannedMessageModuleEnabled')}
/>
<Checkbox
label="Rotary Encoder #1 Enabled"
{...register('rotary1Enabled')}
/>
<Input
label="Encoder #1 Pin A"
type="number"
disabled={moduleEnabled}
{...register('rotary1PinA', { valueAsNumber: true })}
/>
<Input
label="Encoder #1 Pin B"
type="number"
disabled={moduleEnabled}
{...register('rotary1PinB', { valueAsNumber: true })}
/>
<Input
label="Endoer #1 Pin Press"
type="number"
disabled={moduleEnabled}
{...register('rotary1PinPress', { valueAsNumber: true })}
/>
<Select
label="Clockwise event"
disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventCw', { valueAsNumber: true })}
/>
<Select
label="Counter Clockwise event"
disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventCcw', { valueAsNumber: true })}
/>
<Select
label="Press event"
disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventPress', { valueAsNumber: true })}
/>
<Input
label="Allow Input Source"
disabled={moduleEnabled}
{...register('cannedMessageModuleAllowInputSource')}
/>
<Checkbox
label="Send Bell"
{...register('cannedMessageModuleSendBell')}
/>
</Form>
);
};

84
src/components/layout/Sidebar/Settings/modules/ExternalNotifications.tsx

@ -0,0 +1,84 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const moduleEnabled = useWatch({
control,
name: 'extNotificationModuleEnabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Module Enabled"
{...register('extNotificationModuleEnabled')}
/>
<Input
type="number"
label="Output MS"
suffix="ms"
disabled={!moduleEnabled}
{...register('extNotificationModuleOutputMs', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output"
disabled={!moduleEnabled}
{...register('extNotificationModuleOutput', {
valueAsNumber: true,
})}
/>
<Checkbox
label="Active"
disabled={!moduleEnabled}
{...register('extNotificationModuleActive')}
/>
<Checkbox
label="Message"
disabled={!moduleEnabled}
{...register('extNotificationModuleAlertMessage')}
/>
<Checkbox
label="Bell"
disabled={!moduleEnabled}
{...register('extNotificationModuleAlertBell')}
/>
</Form>
);
};

68
src/components/layout/Sidebar/Settings/modules/MQTT.tsx

@ -0,0 +1,68 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const moduleEnabled = useWatch({
control,
name: 'mqttDisabled',
defaultValue: false,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Disabled" {...register('mqttDisabled')} />
<Input
label="MQTT Server Address"
disabled={moduleEnabled}
{...register('mqttServer')}
/>
<Input
label="MQTT Username"
disabled={moduleEnabled}
{...register('mqttUsername')}
/>
<Input
label="MQTT Password"
type="password"
autoComplete="off"
disabled={moduleEnabled}
{...register('mqttPassword')}
/>
<Checkbox
label="Encryption Enabled"
disabled={moduleEnabled}
{...register('mqttEncryptionEnabled')}
/>
</Form>
);
};

39
src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx → src/components/layout/Sidebar/Settings/modules/RangeTest.tsx

@ -2,9 +2,7 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form'; import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
@ -37,45 +35,32 @@ export const RangeTestSettingsPanel = (): JSX.Element => {
}); });
}); });
const pluginEnabled = useWatch({ const moduleEnabled = useWatch({
control, control,
name: 'rangeTestPluginEnabled', name: 'rangeTestModuleEnabled',
defaultValue: false, defaultValue: false,
}); });
return ( return (
<> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Form loading={loading}>
<Checkbox <Checkbox
label="Range Test Plugin Enabled?" label="Module Enabled"
{...register('rangeTestPluginEnabled')} {...register('rangeTestModuleEnabled')}
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!pluginEnabled}
{...register('rangeTestPluginSave')}
/> />
<Input <Input
type="number" type="number"
label="Message Interval" label="Message Interval"
disabled={!pluginEnabled} disabled={!moduleEnabled}
suffix="Seconds" suffix="Seconds"
{...register('rangeTestPluginSender', { {...register('rangeTestModuleSender', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
</Form> <Checkbox
<div className="flex w-full bg-white dark:bg-secondaryDark"> label="Save CSV to storage"
<div className="ml-auto p-2"> disabled={!moduleEnabled}
<IconButton {...register('rangeTestModuleSave')}
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/> />
</div> </Form>
</div>
</>
); );
}; };

95
src/components/layout/Sidebar/Settings/modules/Serial.tsx

@ -0,0 +1,95 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const moduleEnabled = useWatch({
control,
name: 'serialmoduleEnabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('serialmoduleEnabled')} />
<Checkbox
label="Echo"
disabled={!moduleEnabled}
{...register('serialmoduleEcho')}
/>
<Input
type="number"
label="RX"
disabled={!moduleEnabled}
{...register('serialmoduleRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!moduleEnabled}
{...register('serialmoduleTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!moduleEnabled}
{...register('serialmoduleBaud', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!moduleEnabled}
{...register('serialmoduleTimeout', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!moduleEnabled}
{...register('serialmoduleMode', {
valueAsNumber: true,
})}
/>
</Form>
);
};

82
src/components/layout/Sidebar/Settings/modules/StoreForward.tsx

@ -0,0 +1,82 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const moduleEnabled = useWatch({
control,
name: 'storeForwardModuleEnabled',
defaultValue: false,
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Module Enabled"
{...register('storeForwardModuleEnabled')}
/>
<Checkbox
label="Heartbeat Enabled"
disabled={!moduleEnabled}
{...register('storeForwardModuleHeartbeat')}
/>
<Input
type="number"
label="Number of records"
suffix="Records"
disabled={!moduleEnabled}
{...register('storeForwardModuleRecords', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return max"
disabled={!moduleEnabled}
{...register('storeForwardModuleHistoryReturnMax', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return window"
disabled={!moduleEnabled}
{...register('storeForwardModuleHistoryReturnWindow', {
valueAsNumber: true,
})}
/>
</Form>
);
};

83
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx

@ -0,0 +1,83 @@
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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox
label="Measurement Enabled"
{...register('telemetryModuleMeasurementEnabled')}
/>
<Checkbox
label="Displayed on Screen"
{...register('telemetryModuleScreenEnabled')}
/>
<Input
label="Read Error Count Threshold"
type="number"
{...register('telemetryModuleReadErrorCountThreshold', {
valueAsNumber: true,
})}
/>
<Input
label="Update Interval"
type="number"
{...register('telemetryModuleUpdateInterval', {
valueAsNumber: true,
})}
/>
<Input
label="Recovery Interval"
type="number"
{...register('telemetryModuleRecoveryInterval', {
valueAsNumber: true,
})}
/>
<Checkbox
label="Display Farenheit"
{...register('telemetryModuleDisplayFarenheit')}
/>
<Select
label="Sensor Type"
optionsEnum={Protobuf.RadioConfig_UserPreferences_TelemetrySensorType}
{...register('telemetryModuleSensorType', { valueAsNumber: true })}
/>
<Input
label="Sensor Pin"
type="number"
{...register('telemetryModuleSensorPin', { valueAsNumber: true })}
/>
</Form>
);
};

99
src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx

@ -1,99 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
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 { 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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!pluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
suffix="ms"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};

102
src/components/layout/Sidebar/Settings/plugins/Serial.tsx

@ -1,102 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
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 { 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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox label="Plugin Enabled" {...register('serialpluginEnabled')} />
<Checkbox
label="Echo"
disabled={!pluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!pluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!pluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!pluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!pluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};

97
src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx

@ -1,97 +0,0 @@
import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
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 { 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 preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'storeForwardPluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('storeForwardPluginEnabled')}
/>
<Checkbox
label="Heartbeat Enabled"
disabled={!pluginEnabled}
{...register('storeForwardPluginHeartbeat')}
/>
<Input
type="number"
label="Number of records"
suffix="Records"
disabled={!pluginEnabled}
{...register('storeForwardPluginRecords', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return max"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnMax', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return window"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnWindow', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};

11
src/components/layout/index.tsx

@ -6,13 +6,12 @@ import { FiMessageCircle, FiSettings } from 'react-icons/fi';
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri'; import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
import { VscExtensions } from 'react-icons/vsc'; import { VscExtensions } from 'react-icons/vsc';
import { routes, useRoute } from '@app/core/router'; import { ErrorFallback } from '@components/ErrorFallback';
import { IconButton } from '@components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { Sidebar } from '@components/layout/Sidebar'; import { Sidebar } from '@components/layout/Sidebar';
import type { TabProps } from '@components/Tab';
import { ErrorFallback } from '../ErrorFallback'; import { Tabs } from '@components/Tabs';
import type { TabProps } from '../Tab'; import { routes, useRoute } from '@core/router';
import { Tabs } from '../Tabs';
export interface LayoutProps { export interface LayoutProps {
title: string; title: string;
@ -59,7 +58,7 @@ export const Layout = ({
]; ];
return ( return (
<div className="relative flex w-full bg-white dark:bg-secondaryDark "> <div className="relative flex w-full overflow-hidden bg-white dark:bg-secondaryDark">
<div className="flex flex-grow"> <div className="flex flex-grow">
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}> <Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark"> <div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">

5
src/components/menu/BottomNav.tsx

@ -18,6 +18,8 @@ import {
RiArrowUpLine, RiArrowUpLine,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { BottomNavItem } from '@components/menu/BottomNavItem';
import { VersionInfo } from '@components/modals/VersionInfo';
import { import {
connType, connType,
openConnectionModal, openConnectionModal,
@ -28,9 +30,6 @@ import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import { VersionInfo } from '../modals/VersionInfo';
import { BottomNavItem } from './BottomNavItem';
export const BottomNav = (): JSX.Element => { export const BottomNav = (): JSX.Element => {
const [showVersionInfo, setShowVersionInfo] = useState(false); const [showVersionInfo, setShowVersionInfo] = useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

13
src/components/modals/VersionInfo.tsx

@ -4,14 +4,13 @@ import { useEffect } from 'react';
import { MdUpgrade } from 'react-icons/md'; import { MdUpgrade } from 'react-icons/md';
import useSWR from 'swr'; import useSWR from 'swr';
import { connectionUrl } from '@app/core/connection.js'; import { IconButton } from '@components/generic/button/IconButton';
import { setUpdateAvaliable } from '@app/core/slices/appSlice';
import { fetcher } from '@app/core/utils/fetcher';
import { useAppDispatch } from '@app/hooks/useAppDispatch';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Modal } from '@components/generic/Modal'; import { Modal } from '@components/generic/Modal';
import { connectionUrl } from '@core/connection';
import { IconButton } from '../generic/button/IconButton'; import { setUpdateAvaliable } from '@core/slices/appSlice';
import { fetcher } from '@core/utils/fetcher';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
export interface Commit { export interface Commit {
sha: string; sha: string;

2
src/core/slices/mapSlice.ts

@ -1,6 +1,6 @@
import { LngLat } from 'mapbox-gl'; import { LngLat } from 'mapbox-gl';
import type { MapStyleName } from '@pages/Map/styles'; import type { MapStyleName } from '@core/mapStyles';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';

6
src/pages/Extensions/Debug.tsx

@ -1,8 +1,8 @@
import type React from 'react'; import type React from 'react';
import { Button } from '@app/components/generic/button/Button'; import { Button } from '@components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@components/generic/Card';
import { connection } from '@app/core/connection.js'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
export const Debug = (): JSX.Element => { export const Debug = (): JSX.Element => {

4
src/pages/Extensions/FileBrowser.tsx

@ -4,8 +4,8 @@ import { AnimatePresence, m } from 'framer-motion';
import { FiFilePlus } from 'react-icons/fi'; import { FiFilePlus } from 'react-icons/fi';
import useSWR from 'swr'; import useSWR from 'swr';
import { Button } from '@app/components/generic/button/Button'; import { Button } from '@components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@components/generic/Card';
import { fetcher } from '@core/utils/fetcher'; import { fetcher } from '@core/utils/fetcher';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';

5
src/pages/Extensions/Index.tsx

@ -8,12 +8,11 @@ import { VscDebug, VscExtensions } from 'react-icons/vsc';
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection'; import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
import { Layout } from '@components/layout'; import { Layout } from '@components/layout';
import { Debug } from '@pages/Extensions/Debug';
import { FileBrowser } from '@pages/Extensions/FileBrowser'; import { FileBrowser } from '@pages/Extensions/FileBrowser';
import { Info } from '@pages/Extensions/Info'; import { Info } from '@pages/Extensions/Info';
import { Logs } from '@pages/Extensions/Logs'; import { Logs } from '@pages/Extensions/Logs';
import { Debug } from './Debug';
export const Extensions = (): JSX.Element => { export const Extensions = (): JSX.Element => {
const [selectedExtension, setSelectedExtension] = useState< const [selectedExtension, setSelectedExtension] = useState<
'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug' 'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug'
@ -24,7 +23,7 @@ export const Extensions = (): JSX.Element => {
title="Extensions" title="Extensions"
icon={<VscExtensions />} icon={<VscExtensions />}
sidebarContents={ sidebarContents={
<div className="absolute flex h-full w-full flex-col dark:bg-primaryDark"> <div className="absolute flex w-full flex-col dark:bg-primaryDark">
<ExternalSection <ExternalSection
onClick={(): void => { onClick={(): void => {
setSelectedExtension('info'); setSelectedExtension('info');

6
src/pages/Extensions/Info.tsx

@ -3,9 +3,9 @@ import type React from 'react';
import { FiRefreshCw } from 'react-icons/fi'; import { FiRefreshCw } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { IconButton } from '@app/components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { Card } from '@app/components/generic/Card'; import { Card } from '@components/generic/Card';
import { CopyButton } from '@app/components/menu/buttons/CopyButton'; import { CopyButton } from '@components/menu/buttons/CopyButton';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';

8
src/pages/Extensions/Logs.tsx

@ -3,10 +3,10 @@ import type React from 'react';
import { AnimatePresence, m } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { FiArrowRight, FiPaperclip, FiTrash } from 'react-icons/fi'; import { FiArrowRight, FiPaperclip, FiTrash } from 'react-icons/fi';
import { IconButton } from '@app/components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { Card } from '@app/components/generic/Card'; import { Card } from '@components/generic/Card';
import { clearLogs } from '@app/core/slices/meshtasticSlice'; import { clearLogs } from '@core/slices/meshtasticSlice';
import { useAppDispatch } from '@app/hooks/useAppDispatch'; import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, Types } from '@meshtastic/meshtasticjs';

2
src/pages/Nodes/NodeCard.tsx

@ -15,10 +15,10 @@ import { IoTelescope } from 'react-icons/io5';
import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md'; import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { Tooltip } from '@app/components/generic/Tooltip';
import { IconButton } from '@components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection'; import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay'; import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { Tooltip } from '@components/generic/Tooltip';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem'; import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { CopyButton } from '@components/menu/buttons/CopyButton'; import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Node } from '@core/slices/meshtasticSlice'; import type { Node } from '@core/slices/meshtasticSlice';

2
src/pages/Nodes/index.tsx

@ -8,8 +8,8 @@ import { BiCrown } from 'react-icons/bi';
import { FiSettings } from 'react-icons/fi'; import { FiSettings } from 'react-icons/fi';
import { RiMindMap } from 'react-icons/ri'; import { RiMindMap } from 'react-icons/ri';
import { Tooltip } from '@app/components/generic/Tooltip';
import { IconButton } from '@components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { Tooltip } from '@components/generic/Tooltip';
import { Layout } from '@components/layout'; import { Layout } from '@components/layout';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem'; import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';

2
src/pages/NotFound.tsx

@ -1,6 +1,6 @@
import type React from 'react'; import type React from 'react';
import { Card } from '@app/components/generic/Card'; import { Card } from '@components/generic/Card';
export const NotFound = (): JSX.Element => { export const NotFound = (): JSX.Element => {
return ( return (

Loading…
Cancel
Save