Move plugins to settings page

This commit is contained in:
Sacha Weatherstone
2022-01-18 00:39:55 +11:00
parent b929ca0136
commit 161d432330
32 changed files with 834 additions and 1016 deletions
-135
View File
@@ -1,135 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface ExternalNotificationProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const ExternalNotification = ({
navOpen,
setNavOpen,
}: ExternalNotificationProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs:
preferences.extNotificationPluginOutputMs,
},
});
React.useEffect(() => {
reset({
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchExternalNotificationPluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="External Notification"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};
-128
View File
@@ -1,128 +0,0 @@
import type React from 'react';
// import { DefaultExtensionType, defaultStyles, FileIcon } from 'react-file-icon';
import { FiMenu, FiTrash, FiUploadCloud } from 'react-icons/fi';
import useSWR from 'swr';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connectionUrl } from '@core/connection';
import fetcher from '@core/utils/fetcher';
import { Card, IconButton } from '@meshtastic/components';
export interface RangeTestProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
interface IFile {
name: string;
nameModified: string;
size: number;
}
interface IFiles {
data: {
files: IFile[];
filesystem: {
free: number;
total: number;
used: number;
};
};
status: boolean;
}
export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
const { data } = useSWR<IFiles>(
`http://${connectionUrl}/json/spiffs/browse/static`,
fetcher,
);
return (
<PrimaryTemplate
title="File Browser"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
>
<div className="flex flex-col justify-between w-full gap-4 md:flex-row-reverse">
<Card title="SPIFFS" description="Statistics">
{data ? (
<div className="flex">
<div className="mx-auto my-4 bg-gray-500 rounded-3xl">
<div></div>
{JSON.stringify(data.data.filesystem.used)} bytes total
</div>
</div>
) : (
<div>Loading...</div>
)}
</Card>
<Card
title="Files"
description="SPIFFS Contents"
buttons={<IconButton icon={<FiUploadCloud className="w-8 h-8" />} />}
className="md:w-1/3"
>
{data ? (
<div className="flex flex-col my-4 space-y-2">
{data.data.files.map((file: IFile) => (
<div
key={file.name}
className="flex justify-between mx-4 bg-gray-300 rounded-md dark:bg-secondaryDark "
>
<div className="flex p-2 max-h-12">
<div className="flex w-12">
{/* <FileIcon
extension={
(file.nameModified ?? file.name).split('.')[
(file.nameModified ?? file.name).split('.').length -
1
]
}
{...defaultStyles[
(file.nameModified ?? file.name).split('.')[
(file.nameModified ?? file.name).split('.').length -
1
] as DefaultExtensionType
]}
/> */}
</div>
<a
href={`http://${connectionUrl}/${file.name.replace(
'static/',
'',
)}`}
className="my-auto font-semibold"
>
{file.nameModified ?? file.name}
</a>
</div>
<IconButton
className="mx-2 my-auto"
// confirmAction={async (): Promise<void> => {
// await fetch(
// `http://${connectionUrl}/json/spiffs/delete/static?remove=${file.name}`,
// {
// method: 'DELETE',
// },
// );
// }}
icon={<FiTrash className="w-5 h-5" />}
/>
</div>
))}
</div>
) : (
<div>Loading...</div>
)}
</Card>
</div>
</PrimaryTemplate>
);
};
-59
View File
@@ -1,59 +0,0 @@
import type React from 'react';
import {
FiAlignLeft,
FiBell,
FiFastForward,
FiFileText,
FiRss,
} from 'react-icons/fi';
import { PageLayout } from '@components/templates/PageLayout';
import { ExternalNotification } from './ExternalNotification';
import { Files } from './Files';
import { RangeTest } from './RangeTest';
import { Serial } from './Serial';
import { StoreAndForward } from './StoreAndForward';
export const Plugins = (): JSX.Element => {
return (
<PageLayout
title="Plugins"
sidebarItems={[
{
title: 'Range Test',
description: 'Test the range of your Meshtastic node',
icon: <FiRss className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'File Browser',
description: 'HTTP only file browser',
icon: <FiFileText className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'External Notification',
description: 'External hardware alerts',
icon: <FiBell className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Serial',
description: 'Send serial data over the mesh',
icon: <FiAlignLeft className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Store & Forward',
description: 'Retrive message history',
icon: <FiFastForward className="flex-shrink-0 w-6 h-6" />,
},
]}
panels={[
<RangeTest key={1} />,
<Files key={2} />,
<ExternalNotification key={3} />,
<Serial key={4} />,
<StoreAndForward key={5} />,
]}
/>
);
};
-106
View File
@@ -1,106 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface RangeTestProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const RangeTest = ({
navOpen,
setNavOpen,
}: RangeTestProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
},
});
React.useEffect(() => {
reset({
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchRangeTestPluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Range Test"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Range Test Plugin Enabled?"
{...register('rangeTestPluginEnabled')}
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSave')}
/>
<Input
type="number"
label="Message Interval"
disabled={!watchRangeTestPluginEnabled}
{...register('rangeTestPluginSender', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};
-133
View File
@@ -1,133 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface SerialProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Serial = ({ navOpen, setNavOpen }: SerialProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
},
});
React.useEffect(() => {
reset({
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchSerialPluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Serial"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('serialpluginEnabled')}
/>
<Checkbox
label="Echo"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};
-98
View File
@@ -1,98 +0,0 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@components/FormFooter';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Card, Checkbox, IconButton, Input } from '@meshtastic/components';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface StoreAndForwardProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const StoreAndForward = ({
navOpen,
setNavOpen,
}: StoreAndForwardProps): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
},
});
React.useEffect(() => {
reset({
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data, async (): Promise<void> => {
//add loading indicator
reset({ ...data });
await Promise.resolve();
});
});
//todo, add loading indicator
const watchStoreForwardPluginEnabled = useWatch({
control,
name: 'storeForwardPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Serial"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('storeForwardPluginEnabled')}
/>
<Input
type="number"
label="Number of records"
disabled={!watchStoreForwardPluginEnabled}
{...register('storeForwardPluginRecords', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};
+2 -2
View File
@@ -11,7 +11,6 @@ import {
import { Tooltip } from '@app/components/generic/Tooltip';
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
import { FormFooter } from '@components/FormFooter';
import { Loading } from '@components/generic/Loading';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
@@ -21,11 +20,12 @@ import {
Checkbox,
IconButton,
Input,
Loading,
Select,
} from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { ChannelsSidebar } from '../../components/pages/settings/channels/ChannelsSidebar';
import { ChannelsSidebar } from '../../components/pages/settings/radio/channels/ChannelsSidebar';
export interface ChannelsProps {
navOpen?: boolean;
+10 -2
View File
@@ -4,6 +4,7 @@ import {
FiLayers,
FiLayout,
FiMapPin,
FiPackage,
FiRadio,
FiUser,
FiWifi,
@@ -15,6 +16,7 @@ import { PageLayout } from '@components/templates/PageLayout';
import { Channels } from './Channels';
import { Interface } from './Interface';
import { Plugins } from './Plugins';
import { Position } from './Position';
import { Power } from './Power';
import { Radio } from './Radio';
@@ -31,8 +33,9 @@ export const Settings = (): JSX.Element => {
<User key={4} />,
<Power key={5} />,
<Radio key={6} />,
<Channels key={8} />,
<Interface key={7} />,
<Channels key={7} />,
<Plugins key={8} />,
<Interface key={9} />,
];
const sidebarItems: SidebarItemProps[] = [
@@ -56,6 +59,11 @@ export const Settings = (): JSX.Element => {
description: 'Manage channels',
icon: <FiLayers className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Plugins',
description: 'Plugins',
icon: <FiPackage className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Interface',
description: 'Language and UI settings',
+130
View File
@@ -0,0 +1,130 @@
import React from 'react';
import {
FiAlignLeft,
FiBell,
FiExternalLink,
FiFastForward,
FiMenu,
FiRss,
} from 'react-icons/fi';
import { PluginsSidebar } from '@app/components/pages/settings/plugins/PluginsSidebar';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { Button, Card, IconButton } from '@meshtastic/components';
export type Plugin =
| 'Range Test'
| 'External Notifications'
| 'Serial'
| 'Store & Forward';
export interface PluginsProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Plugins = ({ navOpen, setNavOpen }: PluginsProps): JSX.Element => {
const [sidebarOpen, setSidebarOpen] = React.useState(false);
const [selectedPlugin, setSelectedPlugin] = React.useState<
Plugin | undefined
>();
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const plugins: {
name: Plugin;
description: string;
enabled: boolean;
icon: JSX.Element;
}[] = [
{
name: 'Range Test',
description: 'Test the range of your Meshtastic node',
enabled: preferences.rangeTestPluginEnabled,
icon: <FiRss />,
},
{
name: 'External Notifications',
description: 'External hardware alerts',
enabled: preferences.extNotificationPluginEnabled,
icon: <FiBell />,
},
{
name: 'Serial',
description: 'Send serial data over the mesh',
enabled: preferences.serialpluginEnabled,
icon: <FiAlignLeft />,
},
{
name: 'Store & Forward',
description: 'Retrive message history',
enabled: preferences.storeForwardPluginEnabled,
icon: <FiFastForward />,
},
];
return (
<>
<PrimaryTemplate
title="Plugins"
tagline="Settings"
leftButton={
<Button
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
>
<Card
title="Basic settings"
description="Device name and user parameters"
>
<div className="w-full max-w-3xl p-10 space-y-2 md:max-w-xl">
{plugins.map((plugin, index) => (
<div
key={index}
onClick={(): void => {
setSelectedPlugin(plugin.name);
setSidebarOpen(true);
}}
className={`flex justify-between p-2 border border-gray-300 dark:border-gray-600 bg-gray-100 rounded-md dark:bg-secondaryDark shadow-md ${
selectedPlugin === plugin.name
? 'border-primary dark:border-primary'
: ''
}`}
>
<div className="flex my-auto space-x-2">
<div
className={`h-3 my-auto w-3 rounded-full ${
plugin.enabled ? 'bg-green-500' : 'bg-gray-400'
}`}
/>
<div className="flex gap-2">
<div className="my-auto">{plugin.icon}</div>
{plugin.name}
</div>
</div>
<div className="flex gap-2">
<IconButton active icon={<FiExternalLink />} />
</div>
</div>
))}
</div>
</Card>
</PrimaryTemplate>
{sidebarOpen && (
<PluginsSidebar
closeSidebar={(): void => {
setSidebarOpen(false);
}}
plugin={selectedPlugin}
/>
)}
</>
);
};