Move plugins to settings page
This commit is contained in:
+1
-3
@@ -13,7 +13,6 @@ import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Messages } from '@pages/Messages';
|
||||
import { Nodes } from '@pages/Nodes';
|
||||
import { NotFound } from '@pages/NotFound';
|
||||
import { Plugins } from '@pages/Plugins/Index';
|
||||
import { Settings } from '@pages/settings/Index';
|
||||
|
||||
import { ErrorFallback } from './components/ErrorFallback';
|
||||
@@ -67,7 +66,7 @@ export const App = (): JSX.Element => {
|
||||
<div className="flex">
|
||||
<Logo />
|
||||
</div>
|
||||
<Navigation className="flex" />
|
||||
<Navigation />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +80,6 @@ export const App = (): JSX.Element => {
|
||||
<Nodes />
|
||||
</MapboxProvider>
|
||||
)}
|
||||
{route.name === 'plugins' && <Plugins />}
|
||||
{route.name === 'settings' && <Settings />}
|
||||
{route.name === false && <NotFound />}
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type React from 'react';
|
||||
|
||||
export const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 z-10 flex rounded-md backdrop-filter backdrop-blur-sm">
|
||||
<div className="m-auto text-lg font-medium text-gray-400">Loading</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const Sidebar = ({
|
||||
</div>
|
||||
</div>
|
||||
{children ?? (
|
||||
<div className="flex flex-grow bg-gray-100 dark:bg-primaryDark">
|
||||
<div className="flex flex-grow bg-gray-50 dark:bg-primaryDark">
|
||||
<div className="m-auto text-lg font-medium">Please select item</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Loading } from '@meshtastic/components';
|
||||
|
||||
export interface FormProps {
|
||||
loading?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Form = ({ loading, children }: FormProps): JSX.Element => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="relative flex-grow gap-3 p-2"
|
||||
>
|
||||
{loading && <Loading />}
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { Tooltip } from '../generic/Tooltip';
|
||||
import { VersionInfo } from '../modals/VersionInfo';
|
||||
|
||||
// export interface BottomNavProps {
|
||||
|
||||
@@ -99,6 +100,12 @@ export const BottomNav = (): JSX.Element => {
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<VersionInfo
|
||||
visible={showVersionInfo}
|
||||
onclose={(): void => {
|
||||
setShowVersionInfo(false);
|
||||
}}
|
||||
/>
|
||||
<Tooltip contents={`Current Commit`}>
|
||||
<div
|
||||
onClick={(): void => {
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { FiGrid, FiMessageSquare, FiPackage, FiSettings } from 'react-icons/fi';
|
||||
import { FiGrid, FiMessageSquare, FiSettings } from 'react-icons/fi';
|
||||
import type { Link } from 'type-route';
|
||||
|
||||
import { routes, useRoute } from '@core/router';
|
||||
|
||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||
|
||||
export type NavigationProps = DefaultDivProps;
|
||||
|
||||
export const Navigation = ({
|
||||
onClick,
|
||||
className,
|
||||
...props
|
||||
}: NavigationProps): JSX.Element => {
|
||||
export const Navigation = (): JSX.Element => {
|
||||
const route = useRoute();
|
||||
|
||||
return (
|
||||
@@ -34,13 +26,6 @@ export const Navigation = ({
|
||||
link={routes.nodes().link}
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
name="Plugins"
|
||||
icon={<FiPackage className="w-5 h-5 my-auto group-active:scale-90" />}
|
||||
active={route.name === 'plugins'}
|
||||
link={routes.plugins().link}
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
name="Settings"
|
||||
icon={<FiSettings className="w-5 h-5 my-auto group-active:scale-90" />}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Modal } from '@components/generic/Modal';
|
||||
import { Card } from '@meshtastic/components';
|
||||
|
||||
export interface VersionInfoProps {
|
||||
visible: boolean;
|
||||
onclose: () => void;
|
||||
}
|
||||
|
||||
export const VersionInfo = ({
|
||||
visible,
|
||||
onclose,
|
||||
}: VersionInfoProps): JSX.Element => {
|
||||
// const { data } = useSWR<CommitHistory>(
|
||||
// `query {
|
||||
// repository(owner: "meshtastic", name: "meshtastic-web") {
|
||||
// ref(qualifiedName: "master") {
|
||||
// name
|
||||
// target {
|
||||
// ... on Commit {
|
||||
// history(first: 4) {
|
||||
// edges {
|
||||
// node {
|
||||
// abbreviatedOid
|
||||
// message
|
||||
// author {
|
||||
// avatarUrl
|
||||
// name
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }`,
|
||||
// fetcher,
|
||||
// );
|
||||
// console.log(data);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
onClose={(): void => {
|
||||
onclose();
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<div className="w-full max-w-3xl p-10">Version Info</div>
|
||||
{/* {data?.sha} */}
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { FiCode, FiSliders } from 'react-icons/fi';
|
||||
|
||||
import { TabButton } from '@app/components/TabButton';
|
||||
import type { Plugin } from '@app/pages/settings/Plugins';
|
||||
import { Sidebar } from '@components/generic/Sidebar';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
import { ExternalNotificationsDebugPanel } from './panels/ExternalNotifications/DebugPanel';
|
||||
import { ExternalNotificationsSettingsPlanel } from './panels/ExternalNotifications/SettingsPlanel';
|
||||
import { RangeTestDebugPanel } from './panels/RangeTest/DebugPanel';
|
||||
import { RangeTestSettingsPanel } from './panels/RangeTest/SettingsPanel';
|
||||
import { SerialDebugPanel } from './panels/Serial/DebugPanel';
|
||||
import { SerialSettingsPanel } from './panels/Serial/SettingsPanel';
|
||||
import { StoreForwardDebugPanel } from './panels/StoreForward/DebugPanel';
|
||||
import { StoreForwardSettingsPanel } from './panels/StoreForward/SettingsPanel';
|
||||
|
||||
export interface PluginsSidebarProps {
|
||||
plugin?: Plugin;
|
||||
closeSidebar: () => void;
|
||||
}
|
||||
|
||||
export const PluginsSidebar = ({
|
||||
plugin,
|
||||
closeSidebar,
|
||||
}: PluginsSidebarProps): JSX.Element => {
|
||||
return (
|
||||
<Sidebar
|
||||
title={plugin ?? 'Please select plugin'}
|
||||
tagline={plugin ? 'settings' : '...'}
|
||||
closeSidebar={closeSidebar}
|
||||
>
|
||||
{plugin && (
|
||||
<Tab.Group>
|
||||
<div className="shadow-md">
|
||||
<Tab.List className="flex justify-between border-b border-gray-300 dark:border-gray-600">
|
||||
<TabButton>
|
||||
<FiSliders />
|
||||
</TabButton>
|
||||
<TabButton>
|
||||
<FiCode />
|
||||
</TabButton>
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels className="flex flex-grow overflow-y-auto bg-gray-100 dark:bg-primaryDark">
|
||||
{plugin === 'Range Test' && (
|
||||
<>
|
||||
<RangeTestSettingsPanel />
|
||||
<RangeTestDebugPanel />
|
||||
</>
|
||||
)}
|
||||
{plugin === 'External Notifications' && (
|
||||
<>
|
||||
<ExternalNotificationsSettingsPlanel />
|
||||
<ExternalNotificationsDebugPanel />
|
||||
</>
|
||||
)}
|
||||
{plugin === 'Serial' && (
|
||||
<>
|
||||
<SerialSettingsPanel />
|
||||
<SerialDebugPanel />
|
||||
</>
|
||||
)}
|
||||
{plugin === 'Store & Forward' && (
|
||||
<>
|
||||
<StoreForwardSettingsPanel />
|
||||
<StoreForwardDebugPanel />
|
||||
</>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
export const ExternalNotificationsDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
extNotificationPluginActive: preferences.extNotificationPluginActive,
|
||||
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell,
|
||||
extNotificationPluginAlertMessage:
|
||||
preferences.extNotificationPluginAlertMessage,
|
||||
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
|
||||
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
|
||||
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Panel className="relative">
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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 (
|
||||
<Tab.Panel className="flex flex-col w-full">
|
||||
<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="p-2 ml-auto">
|
||||
<IconButton
|
||||
disabled={!formState.isDirty}
|
||||
onClick={async (): Promise<void> => {
|
||||
await onSubmit();
|
||||
}}
|
||||
icon={<FiSave />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
export const RangeTestDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
|
||||
rangeTestPluginSave: preferences.rangeTestPluginSave,
|
||||
rangeTestPluginSender: preferences.rangeTestPluginSender,
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Panel className="relative">
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const RangeTestSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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: 'rangeTestPluginEnabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tab.Panel className="flex flex-col w-full">
|
||||
<Form loading={loading}>
|
||||
<Checkbox
|
||||
label="Range Test Plugin Enabled?"
|
||||
{...register('rangeTestPluginEnabled')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Range Test Plugin Save?"
|
||||
disabled={!pluginEnabled}
|
||||
{...register('rangeTestPluginSave')}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Message Interval"
|
||||
disabled={!pluginEnabled}
|
||||
suffix="Seconds"
|
||||
{...register('rangeTestPluginSender', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</Form>
|
||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
||||
<div className="p-2 ml-auto">
|
||||
<IconButton
|
||||
disabled={!formState.isDirty}
|
||||
onClick={async (): Promise<void> => {
|
||||
await onSubmit();
|
||||
}}
|
||||
icon={<FiSave />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
export const SerialDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
serialpluginEnabled: preferences.serialpluginEnabled,
|
||||
serialpluginEcho: preferences.serialpluginEcho,
|
||||
serialpluginMode: preferences.serialpluginMode,
|
||||
serialpluginRxd: preferences.serialpluginRxd,
|
||||
serialpluginTxd: preferences.serialpluginTxd,
|
||||
serialpluginTimeout: preferences.serialpluginTimeout,
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Panel className="relative">
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const SerialSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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 (
|
||||
<Tab.Panel className="flex flex-col w-full">
|
||||
<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="p-2 ml-auto">
|
||||
<IconButton
|
||||
disabled={!formState.isDirty}
|
||||
onClick={async (): Promise<void> => {
|
||||
await onSubmit();
|
||||
}}
|
||||
icon={<FiSave />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type React from 'react';
|
||||
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { CopyButton } from '@components/menu/buttons/CopyButton';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
export const StoreForwardDebugPanel = (): JSX.Element => {
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const debugData = {
|
||||
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
|
||||
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat,
|
||||
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
|
||||
storeForwardPluginHistoryReturnMax:
|
||||
preferences.storeForwardPluginHistoryReturnMax,
|
||||
storeForwardPluginHistoryReturnWindow:
|
||||
preferences.storeForwardPluginHistoryReturnWindow,
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Panel className="relative">
|
||||
<div className="fixed right-0 m-2">
|
||||
<CopyButton data={JSON.stringify(debugData)} />
|
||||
</div>
|
||||
<JSONPretty className="max-w-sm" data={debugData} />
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { Checkbox, IconButton, Input } from '@meshtastic/components';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const StoreForwardSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const preferences = useAppSelector(
|
||||
(state) => state.meshtastic.radio.preferences,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
React.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 (
|
||||
<Tab.Panel className="flex flex-col w-full">
|
||||
<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="p-2 ml-auto">
|
||||
<IconButton
|
||||
disabled={!formState.isDirty}
|
||||
onClick={async (): Promise<void> => {
|
||||
await onSubmit();
|
||||
}}
|
||||
icon={<FiSave />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
||||
|
||||
import { Loading } from '@app/components/generic/Loading';
|
||||
import { Form } from '@app/components/generic/form/Form';
|
||||
import { connection } from '@app/core/connection';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
|
||||
@@ -73,8 +73,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Tab.Panel className="flex flex-col w-full">
|
||||
{loading && <Loading />}
|
||||
<form className="flex-grow gap-3 p-2">
|
||||
<Form loading={loading}>
|
||||
{channel?.index !== 0 && (
|
||||
<>
|
||||
<Checkbox
|
||||
@@ -122,7 +121,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
/>
|
||||
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
|
||||
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
|
||||
</form>
|
||||
</Form>
|
||||
<div className="flex w-full bg-white dark:bg-secondaryDark">
|
||||
<div className="p-2 ml-auto">
|
||||
<IconButton
|
||||
@@ -3,6 +3,5 @@ import { createRouter, defineRoute } from 'type-route';
|
||||
export const { RouteProvider, useRoute, routes } = createRouter({
|
||||
messages: defineRoute('/'),
|
||||
nodes: defineRoute('/nodes'),
|
||||
plugins: defineRoute('/plugins'),
|
||||
settings: defineRoute('/settings'),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user