Browse Source

Form improvements & ui cleanup

pull/2/head
Sacha Weatherstone 5 years ago
parent
commit
6f1f91e70a
  1. 36
      src/components/FormFooter.tsx
  2. 26
      src/components/generic/Card.tsx
  3. 12
      src/components/generic/IconButton.tsx
  4. 4
      src/components/generic/SidebarItem.tsx
  5. 2
      src/components/generic/form/Checkbox.tsx
  6. 2
      src/components/generic/form/InputWrapper.tsx
  7. 2
      src/components/generic/form/Select.tsx
  8. 16
      src/components/templates/PrimaryTemplate.tsx
  9. 1
      src/pages/Messages.tsx
  10. 40
      src/pages/Nodes/Node.tsx
  11. 2
      src/pages/Plugins/Files.tsx
  12. 35
      src/pages/Plugins/RangeTest.tsx
  13. 28
      src/pages/settings/Channels.tsx
  14. 26
      src/pages/settings/Connection.tsx
  15. 93
      src/pages/settings/Index.tsx
  16. 2
      src/pages/settings/Interface.tsx
  17. 127
      src/pages/settings/Position.tsx
  18. 92
      src/pages/settings/Power.tsx
  19. 119
      src/pages/settings/Radio.tsx
  20. 59
      src/pages/settings/User.tsx
  21. 93
      src/pages/settings/WiFi.tsx

36
src/components/FormFooter.tsx

@ -0,0 +1,36 @@
import type React from 'react';
import { FiSave, FiXCircle } from 'react-icons/fi';
import { IconButton } from './generic/IconButton';
export interface FormFooterProps {
dirty: boolean;
clearAction: () => void;
saveAction: () => void;
}
export const FormFooter = ({
dirty,
clearAction,
saveAction,
}: FormFooterProps): JSX.Element => {
return (
<div className="flex float-right gap-2">
<IconButton
icon={<FiXCircle className="w-5 h-5" />}
disabled={!dirty}
onClick={(): void => {
clearAction();
}}
/>
<IconButton
disabled={!dirty}
onClick={(): void => {
saveAction();
}}
icon={<FiSave className="w-5 h-5" />}
/>
</div>
);
};

26
src/components/generic/Card.tsx

@ -3,10 +3,11 @@ import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
interface CardProps extends DefaultDivProps { interface CardProps extends DefaultDivProps {
title: string; title?: string;
description: string | JSX.Element; description?: string | JSX.Element;
buttons?: JSX.Element; buttons?: JSX.Element;
lgPlaceholder?: JSX.Element; lgPlaceholder?: JSX.Element;
loading?: boolean;
} }
export const Card = ({ export const Card = ({
@ -23,15 +24,22 @@ export const Card = ({
className={`flex flex-col flex-auto dark:text-white border-y md:border shadow-md select-none dark:bg-primaryDark border-gray-300 dark:border-transparent md:rounded-3xl ${className}`} className={`flex flex-col flex-auto dark:text-white border-y md:border shadow-md select-none dark:bg-primaryDark border-gray-300 dark:border-transparent md:rounded-3xl ${className}`}
{...props} {...props}
> >
<div className="flex items-center justify-between mx-10 mt-10"> {(title || description) && (
<div className="flex flex-col"> <div className="flex items-center justify-between mx-10 mt-10">
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight text-black md:text-3xl dark:text-white"> <div className="flex flex-col">
{title} {title && (
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight text-black md:text-3xl dark:text-white">
{title}
</div>
)}
{description && (
<div className="font-medium text-gray-400">{description}</div>
)}
</div> </div>
<div className="font-medium text-gray-400">{description}</div> {buttons}
</div> </div>
{buttons} )}
</div>
<div className="flex"> <div className="flex">
<div className={`${lgPlaceholder ? 'w-full xl:w-2/3' : 'w-full'}`}> <div className={`${lgPlaceholder ? 'w-full xl:w-2/3' : 'w-full'}`}>
{children} {children}

12
src/components/generic/IconButton.tsx

@ -4,17 +4,27 @@ type DefaulButtonProps = JSX.IntrinsicElements['button'];
export interface IconButtonProps extends DefaulButtonProps { export interface IconButtonProps extends DefaulButtonProps {
icon: React.ReactNode; icon: React.ReactNode;
active?: boolean;
} }
export const IconButton = ({ export const IconButton = ({
icon, icon,
active,
disabled,
...props ...props
}: IconButtonProps): JSX.Element => { }: IconButtonProps): JSX.Element => {
return ( return (
<div className="my-auto text-gray-500 dark:text-gray-400"> <div className="my-auto text-gray-500 dark:text-gray-400">
<button <button
type="button" type="button"
className="p-2 transition duration-200 ease-in-out rounded-md active:scale-95 hover:bg-gray-200 dark:hover:bg-gray-600" disabled={disabled}
className={`p-2 transition duration-200 ease-in-out rounded-md active:scale-95 ${
active
? 'bg-gray-200 dark:bg-gray-600'
: 'hover:bg-gray-200 dark:hover:bg-gray-600'
} ${
disabled ? 'text-gray-400 dark:text-gray-700 cursor-not-allowed' : ''
}`}
{...props} {...props}
> >
{icon} {icon}

4
src/components/generic/SidebarItem.tsx

@ -18,12 +18,12 @@ export const SidebarItem = ({
}: SidebarItemProps): JSX.Element => { }: SidebarItemProps): JSX.Element => {
return ( return (
<div <div
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${ className={`flex p-3 cursor-pointer select-none dark:hover:bg-primaryDark ${
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark' selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
}`} }`}
{...props} {...props}
> >
<div className="text-gray-500 dark:text-gray-400">{icon}</div> <div className="my-auto text-gray-500 dark:text-gray-400">{icon}</div>
<div className="ml-3 text-left"> <div className="ml-3 text-left">
<div className="font-medium text-left">{title}</div> <div className="font-medium text-left">{title}</div>
<div className="mt-0.5 text-gray-400 text-sm">{description}</div> <div className="mt-0.5 text-gray-400 text-sm">{description}</div>

2
src/components/generic/form/Checkbox.tsx

@ -27,7 +27,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
id={id} id={id}
className={`appearance-none w-8 h-8 border rounded-md focus:outline-none checked:bg-primary checked:border-transparent transition duration-200 ease-in-out border-gray-400 dark:border-gray-200 ${ className={`appearance-none w-8 h-8 border rounded-md focus:outline-none checked:bg-primary checked:border-transparent transition duration-200 ease-in-out border-gray-400 dark:border-gray-200 ${
props.disabled props.disabled
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400' ? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400 dark:border-gray-700'
: '' : ''
} ${ } ${
error error

2
src/components/generic/form/InputWrapper.tsx

@ -14,7 +14,7 @@ export const InputWrapper = ({
<div <div
className={`flex w-full border-gray-400 dark:border-gray-200 border-y border rounded-md transition duration-200 ease-in-out ${ className={`flex w-full border-gray-400 dark:border-gray-200 border-y border rounded-md transition duration-200 ease-in-out ${
disabled disabled
? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400' ? 'bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-400 dark:border-gray-700'
: '' : ''
} ${ } ${
error error

2
src/components/generic/form/Select.tsx

@ -30,7 +30,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
<select <select
ref={ref} ref={ref}
className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${ className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
small ? 'p-1' : 'h-10 px-2' small ? 'm-1' : 'h-10 mx-2'
}`} }`}
disabled={ disabled={
props.disabled props.disabled

16
src/components/templates/PrimaryTemplate.tsx

@ -4,7 +4,8 @@ export interface PrimaryTemplateProps {
children: React.ReactNode; children: React.ReactNode;
title: string; title: string;
tagline: string; tagline: string;
button?: JSX.Element; leftButton?: JSX.Element;
rightButton?: JSX.Element;
footer?: JSX.Element; footer?: JSX.Element;
} }
@ -12,13 +13,13 @@ export const PrimaryTemplate = ({
children, children,
title, title,
tagline, tagline,
button, leftButton,
rightButton,
footer, footer,
}: PrimaryTemplateProps): JSX.Element => { }: PrimaryTemplateProps): JSX.Element => {
return ( return (
<div className="flex flex-col flex-auto h-full min-w-0"> <div className="flex flex-col flex-auto h-full min-w-0">
<div className="flex p-4 bg-white border-b border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark"> <div className="flex p-4 bg-white border-b border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<a className="font-medium whitespace-nowrap text-primary"> <a className="font-medium whitespace-nowrap text-primary">
{tagline} {tagline}
@ -27,13 +28,16 @@ export const PrimaryTemplate = ({
{title} {title}
</h2> </h2>
</div> </div>
{rightButton}
</div> </div>
<div className="flex-auto flex-grow py-6 overflow-y-auto bg-white md:p-10 dark:bg-secondaryDark"> <div className="flex-auto flex-grow py-6 overflow-y-auto bg-white md:p-5 dark:bg-secondaryDark">
{children} {children}
</div> </div>
{footer && ( {footer && (
<div className="flex p-4 bg-white border-t border-gray-300 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark"> <div className="flex px-4 py-2 bg-white border-t border-gray-300 md: md:py-4 md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{button && <div className="pr-2 m-auto md:hidden">{button}</div>} {leftButton && (
<div className="pr-2 m-auto md:hidden">{leftButton}</div>
)}
<div className="flex-1 min-w-0">{footer}</div> <div className="flex-1 min-w-0">{footer}</div>
</div> </div>
)} )}

1
src/pages/Messages.tsx

@ -11,7 +11,6 @@ import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => { export const Messages = (): JSX.Element => {
const messages = useAppSelector((state) => state.meshtastic.messages); const messages = useAppSelector((state) => state.meshtastic.messages);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users); const users = useAppSelector((state) => state.meshtastic.users);
const channels = useAppSelector((state) => state.meshtastic.channels); const channels = useAppSelector((state) => state.meshtastic.channels);

40
src/pages/Nodes/Node.tsx

@ -1,11 +1,12 @@
import 'react-json-pretty/themes/acai.css'; import 'react-json-pretty/themes/acai.css';
import type React from 'react'; import React from 'react';
import { FiMenu, FiTerminal } from 'react-icons/fi'; import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import { Cover } from '@app/components/generic/Cover';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
@ -28,11 +29,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
const position = useAppSelector( const position = useAppSelector(
(state) => state.meshtastic.positionPackets, (state) => state.meshtastic.positionPackets,
).find((position) => position.packet.from === node.num)?.data; ).find((position) => position.packet.from === node.num)?.data;
const [debug, setDebug] = React.useState(false);
return ( return (
<PrimaryTemplate <PrimaryTemplate
title={user ? user.longName : node.num.toString()} title={user ? user.longName : node.num.toString()}
tagline="Node" tagline="Node"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -40,12 +42,28 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
}} }}
/> />
} }
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={<></>}
> >
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<div className="justify-between space-y-2 md:space-y-0 md:space-x-2 md:flex"> <div className="flex flex-col justify-between gap-4 md:flex-row">
<StatCard <StatCard
title="Last heard" title="Last heard"
value={<TimeAgo date={new Date(node.lastHeard * 1000)} />} value={
node.lastHeard ? (
<TimeAgo date={new Date(node.lastHeard * 1000)} />
) : (
'Never'
)
}
/> />
<StatCard title="SNR" value={node.snr.toString()} /> <StatCard title="SNR" value={node.snr.toString()} />
</div> </div>
@ -53,20 +71,12 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
title="Position" title="Position"
description={new Date(node.lastHeard * 1000).toLocaleString()} description={new Date(node.lastHeard * 1000).toLocaleString()}
> >
<Cover enabled={debug} content={<JSONPretty data={node} />} />
<div className="p-10"> <div className="p-10">
<JSONPretty data={position} /> <JSONPretty data={position} />
</div> </div>
</Card> </Card>
<Card <Card title="Settings" description="Remote node settings">
title="Settings"
description="Remote node settings"
lgPlaceholder={
<div className="w-full h-full text-black dark:text-white">
<FiTerminal className="w-24 h-24 m-auto" />
<div className="text-center">Placeholder</div>
</div>
}
>
<div className="p-10"> <div className="p-10">
<form className="space-y-4"> <form className="space-y-4">
<Input label={'Device Name'} /> <Input label={'Device Name'} />

2
src/pages/Plugins/Files.tsx

@ -54,7 +54,7 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
<PrimaryTemplate <PrimaryTemplate
title="File Browser" title="File Browser"
tagline="Plugin" tagline="Plugin"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {

35
src/pages/Plugins/RangeTest.tsx

@ -1,12 +1,12 @@
import type React from 'react'; import type React from 'react';
import { useForm } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FiMenu, FiSave } from 'react-icons/fi'; import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection'; import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
@ -26,7 +26,7 @@ export const RangeTest = ({
const { t } = useTranslation(); const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences); const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit, formState } = const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({ useForm<RadioConfig_UserPreferences>({
defaultValues: { defaultValues: {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled, rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
@ -36,16 +36,20 @@ export const RangeTest = ({
}); });
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
console.log(data);
void connection.setPreferences(data); void connection.setPreferences(data);
}); });
const watchRangeTestPluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return ( return (
<PrimaryTemplate <PrimaryTemplate
title="Range Test" title="Range Test"
tagline="Plugin" tagline="Plugin"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -54,16 +58,11 @@ export const RangeTest = ({
/> />
} }
footer={ footer={
<Button <FormFooter
className="px-10 ml-auto" dirty={formState.isDirty}
icon={<FiSave className="w-5 h-5" />} saveAction={onSubmit}
disabled={!formState.isDirty} clearAction={reset}
onClick={onSubmit} />
active
border
>
{t('strings.save_changes')}
</Button>
} }
> >
<div className="w-full space-y-4"> <div className="w-full space-y-4">
@ -76,11 +75,13 @@ export const RangeTest = ({
/> />
<Checkbox <Checkbox
label="Range Test Plugin Save?" label="Range Test Plugin Save?"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSave')} {...register('rangeTestPluginSave')}
/> />
<Input <Input
type="number" type="number"
label="Message Interval" label="Message Interval"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSender', { {...register('rangeTestPluginSender', {
valueAsNumber: true, valueAsNumber: true,
})} })}

28
src/pages/settings/Channels.tsx

@ -30,7 +30,7 @@ export const Channels = ({
<PrimaryTemplate <PrimaryTemplate
title="Channels" title="Channels"
tagline="Settings" tagline="Settings"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -38,6 +38,15 @@ export const Channels = ({
}} }}
/> />
} }
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={ footer={
<Button <Button
className="px-10 ml-auto" className="px-10 ml-auto"
@ -50,22 +59,7 @@ export const Channels = ({
} }
> >
<div className="space-y-4"> <div className="space-y-4">
<Card <Card>
title="Manage Channels"
description="Edit channel throughput and other settings"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Cover enabled={debug} content={<JSONPretty data={channels} />} /> <Cover enabled={debug} content={<JSONPretty data={channels} />} />
<div className="w-full p-4 space-y-2 md:p-10"> <div className="w-full p-4 space-y-2 md:p-10">
{channels.map((channel) => ( {channels.map((channel) => (

26
src/pages/settings/Connection.tsx

@ -2,8 +2,9 @@ import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FiCheck, FiMenu, FiSave } from 'react-icons/fi'; import { FiCheck, FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection, setConnection } from '@app/core/connection'; import { connection, setConnection } from '@app/core/connection';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button'; import { Button } from '@components/generic/Button';
@ -52,7 +53,7 @@ export const Connection = ({
); );
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride); const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const { register, handleSubmit, formState } = useForm<{ const { register, handleSubmit, formState, reset } = useForm<{
method: connType; method: connType;
}>({ }>({
defaultValues: { defaultValues: {
@ -121,7 +122,7 @@ export const Connection = ({
<PrimaryTemplate <PrimaryTemplate
title="Connection" title="Connection"
tagline="Settings" tagline="Settings"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -130,21 +131,14 @@ export const Connection = ({
/> />
} }
footer={ footer={
<Button <FormFooter
className="px-10 ml-auto" dirty={formState.isDirty}
icon={<FiSave className="w-5 h-5" />} saveAction={onSubmit}
disabled={!formState.isDirty} clearAction={reset}
active />
border
>
{t('strings.save_changes')}
</Button>
} }
> >
<Card <Card>
title="Connection Settings"
description="HTTP, BLE and Serial Options"
>
<div className="w-full max-w-3xl p-10 md:max-w-xl"> <div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}> <form className="space-y-2" onSubmit={onSubmit}>
<Select <Select

93
src/pages/settings/Index.tsx

@ -4,55 +4,80 @@ import {
FiLayers, FiLayers,
FiLayout, FiLayout,
FiLink2, FiLink2,
FiRss, FiMapPin,
FiSmartphone, FiRadio,
FiUser,
FiWifi,
FiZap,
} from 'react-icons/fi'; } from 'react-icons/fi';
import { PageLayout } from '@components/templates/PageLayout'; import { PageLayout } from '@components/templates/PageLayout';
import { Channels } from './Channels'; import { Channels } from './Channels';
import { Connection } from './Connection'; import { Connection } from './Connection';
import { Device } from './Device';
import { Interface } from './Interface'; import { Interface } from './Interface';
import { Position } from './Position';
import { Power } from './Power';
import { Radio } from './Radio'; import { Radio } from './Radio';
import { User } from './User';
import { WiFi } from './WiFi';
export const Settings = (): JSX.Element => { export const Settings = (): JSX.Element => {
const sidebarItems = [
{
title: 'Connection',
description: 'Connection method and parameters',
icon: <FiLink2 className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'WiFi',
description: 'WiFi credentials and mode',
icon: <FiWifi className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Position',
description: 'Position settings and flags',
icon: <FiMapPin className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'User',
description: 'Device name and details',
icon: <FiUser className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Power',
description: 'Power and sleep settings',
icon: <FiZap className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Radio',
description: 'LoRa settings',
icon: <FiRadio className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Language and UI settings',
icon: <FiLayout className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Channels',
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
];
return ( return (
<PageLayout <PageLayout
title="Settings" title="Settings"
sidebarItems={[ sidebarItems={sidebarItems}
{
title: 'Connection',
description: 'Method and peramaters for connecting to the device',
icon: <FiLink2 className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Device',
description: 'Device settings, such as device name and wifi settings',
icon: <FiSmartphone className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Radio',
description: 'Adjust radio power and frequency settings',
icon: <FiRss className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Change language and other UI settings',
icon: <FiLayout className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Channels',
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
]}
panels={[ panels={[
<Connection key={1} />, <Connection key={1} />,
<Device key={2} />, <WiFi key={2} />,
<Radio key={3} />, <Position key={3} />,
<Interface key={4} />, <User key={4} />,
<Channels key={5} />, <Power key={5} />,
<Radio key={6} />,
<Interface key={7} />,
<Channels key={8} />,
]} ]}
/> />
); );

2
src/pages/settings/Interface.tsx

@ -24,7 +24,7 @@ export const Interface = ({
<PrimaryTemplate <PrimaryTemplate
title="Interface" title="Interface"
tagline="Settings" tagline="Settings"
button={ leftButton={
<Button <Button
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {

127
src/pages/settings/Position.tsx

@ -0,0 +1,127 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PositionProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Position = ({
navOpen,
setNavOpen,
}: PositionProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...radioConfig,
positionBroadcastSecs:
radioConfig.positionBroadcastSecs === 0
? radioConfig.isRouter
? 43200
: 900
: radioConfig.positionBroadcastSecs,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="Position"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Input
label={'Broadcast Interval (seconds)'}
type="number"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select
label="Position Type"
optionsEnum={Protobuf.PositionFlags}
{...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>
</Card>
</PrimaryTemplate>
);
};

92
src/pages/settings/Power.tsx

@ -0,0 +1,92 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Select } from '@app/components/generic/form/Select';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PowerProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Power = ({ navOpen, setNavOpen }: PowerProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...radioConfig,
isLowPower: radioConfig.isRouter ? true : radioConfig.isLowPower,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="Power"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Select
label={'Charge current'}
optionsEnum={Protobuf.ChargeCurrent}
{...register('chargeCurrent', { valueAsNumber: true })}
/>
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
<Checkbox
label="Powered by low power source (solar)"
disabled={radioConfig.isRouter}
validationMessage={
radioConfig.isRouter ? 'Enabled by default in router mode' : ''
}
{...register('isLowPower')}
/>
</form>
</div>
</Card>
</PrimaryTemplate>
);
};

119
src/pages/settings/Radio.tsx

@ -2,16 +2,15 @@ import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu, FiSave, FiXCircle } from 'react-icons/fi'; import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection'; import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover'; import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select'; import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton'; import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
@ -39,7 +38,7 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
<PrimaryTemplate <PrimaryTemplate
title="Radio" title="Radio"
tagline="Settings" tagline="Settings"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -47,108 +46,28 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
}} }}
/> />
} }
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={ footer={
<div className="flex space-x-2"> <FormFooter
<IconButton dirty={formState.isDirty}
icon={<FiXCircle className="w-5 h-5" />} saveAction={onSubmit}
disabled={formState.isDirty} clearAction={reset}
onClick={(): void => { />
reset();
}}
/>
<Button
className="px-10 ml-auto"
icon={<FiSave className="w-5 h-5" />}
disabled={!formState.isDirty}
onClick={onSubmit}
active
border
>
{t('strings.save_changes')}
</Button>
</div>
} }
> >
<Card <Card>
title="Basic settings"
description="Device name and user parameters"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} /> <Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl"> <div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}> <form className="space-y-2" onSubmit={onSubmit}>
<div>Power</div> <Checkbox label="Is Router" {...register('isRouter')} />
<hr />
<Input
label={'Charge current'}
disabled
{...register('chargeCurrent', { valueAsNumber: true })}
/>
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
<Checkbox label="Low Power" {...register('isLowPower')} />
<hr />
<div>WiFi</div>
<Input label={t('strings.wifi_ssid')} {...register('wifiSsid')} />
<Input
type="password"
label={t('strings.wifi_psk')}
{...register('wifiPassword')}
/>
<hr />
<div>Position</div>
<Input
label={'Broadcast Interval (seconds)'}
type="number"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select
label="Position Type"
optionsEnum={Protobuf.PositionFlags}
{...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 })}
/>
<hr />
<div>Other</div>
<Select <Select
label="Region" label="Region"
optionsEnum={Protobuf.RegionCode} optionsEnum={Protobuf.RegionCode}

59
src/pages/settings/Device.tsx → src/pages/settings/User.tsx

@ -2,13 +2,13 @@ import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu, FiSave } from 'react-icons/fi'; import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection'; import { connection } from '@app/core/connection';
import { addUser } from '@app/core/slices/meshtasticSlice'; import { addUser } from '@app/core/slices/meshtasticSlice';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover'; import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
@ -18,12 +18,12 @@ import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export interface DeviceProps { export interface UserProps {
navOpen?: boolean; navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>; setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
} }
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => { export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const [debug, setDebug] = React.useState(false); const [debug, setDebug] = React.useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -31,7 +31,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
const user = useAppSelector((state) => state.meshtastic.users).find( const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === myNodeInfo.myNodeNum, (user) => user.packet.from === myNodeInfo.myNodeNum,
); );
const { register, handleSubmit, formState } = useForm<{ const { register, handleSubmit, formState, reset } = useForm<{
longName: string; longName: string;
shortName: string; shortName: string;
isLicensed: boolean; isLicensed: boolean;
@ -46,7 +46,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
}); });
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
// TODO: can be remove once getUser is implemented // TODO: can be removed once getUser is implemented
if (user) { if (user) {
void connection.setOwner({ ...user.data, ...data }); void connection.setOwner({ ...user.data, ...data });
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } })); dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
@ -55,9 +55,9 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
return ( return (
<PrimaryTemplate <PrimaryTemplate
title="Device" title="User"
tagline="Settings" tagline="Settings"
button={ leftButton={
<IconButton <IconButton
icon={<FiMenu className="w-5 h-5" />} icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => { onClick={(): void => {
@ -65,35 +65,24 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
}} }}
/> />
} }
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={ footer={
<Button <FormFooter
className="px-10 ml-auto" dirty={formState.isDirty}
icon={<FiSave className="w-5 h-5" />} saveAction={onSubmit}
disabled={!formState.isDirty} clearAction={reset}
onClick={onSubmit} />
active
border
>
{t('strings.save_changes')}
</Button>
} }
> >
<Card <Card>
title="Basic settings"
description="Device name and user parameters"
buttons={
<Button
border
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
icon={<FiCode />}
>
Debug
</Button>
}
>
<Cover enabled={debug} content={<JSONPretty data={user} />} /> <Cover enabled={debug} content={<JSONPretty data={user} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl"> <div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}> <form className="space-y-2" onSubmit={onSubmit}>
@ -117,7 +106,7 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<Select <Select
label="Team" label="Team"
optionsEnum={Protobuf.Team} optionsEnum={Protobuf.Team}
{...register('team')} {...register('team', { valueAsNumber: true })}
/> />
</form> </form>
</div> </div>

93
src/pages/settings/WiFi.tsx

@ -0,0 +1,93 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Checkbox } from '@app/components/generic/form/Checkbox';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface WiFiProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const WiFi = ({ navOpen, setNavOpen }: WiFiProps): JSX.Element => {
const { t } = useTranslation();
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
const [debug, setDebug] = React.useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: radioConfig,
});
const watchWifiApMode = useWatch({
control,
name: 'wifiApMode',
defaultValue: false,
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
return (
<PrimaryTemplate
title="WiFi"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
rightButton={
<IconButton
icon={<FiCode className="w-5 h-5" />}
active={debug}
onClick={(): void => {
setDebug(!debug);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<Card>
<Cover enabled={debug} content={<JSONPretty data={radioConfig} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
<Input
label={t('strings.wifi_ssid')}
disabled={watchWifiApMode}
{...register('wifiSsid')}
/>
<Input
type="password"
label={t('strings.wifi_psk')}
disabled={watchWifiApMode}
{...register('wifiPassword')}
/>
</form>
</div>
</Card>
</PrimaryTemplate>
);
};
Loading…
Cancel
Save