Browse Source

Cleanup & start transition of moduleConfig

pull/82/head
Sacha Weatherstone 3 years ago
parent
commit
c184c5add6
No known key found for this signature in database GPG Key ID: 7AB2D7E206124B31
  1. 37
      src/Nav/NavBar.tsx
  2. 5
      src/Nav/PageNav.tsx
  3. 2
      src/PageRouter.tsx
  4. 4
      src/components/CommandPalette/GroupView.tsx
  5. 81
      src/components/CommandPalette/Index.tsx
  6. 6
      src/components/CommandPalette/SearchResult.tsx
  7. 4
      src/components/Dialog/RebootDialog.tsx
  8. 4
      src/components/Dialog/ShutdownDialog.tsx
  9. 8
      src/components/Drawer/index.tsx
  10. 6
      src/components/NewDevice.tsx
  11. 183
      src/components/PageComponents/Channel.tsx
  12. 4
      src/components/PageComponents/Connect/BLE.tsx
  13. 5
      src/components/PageComponents/Connect/HTTP.tsx
  14. 4
      src/components/PageComponents/Connect/Serial.tsx
  15. 8
      src/components/Widgets/DeviceWidget.tsx
  16. 5
      src/components/form/Button.tsx
  17. 2
      src/components/form/Form.tsx
  18. 6
      src/components/generic/TabbedContent.tsx
  19. 49
      src/components/generic/VerticalTabbedContent.tsx
  20. 1
      src/core/stores/deviceStore.ts
  21. 2
      src/pages/Channels.tsx
  22. 70
      src/pages/Config/DeviceConfig.tsx
  23. 52
      src/pages/Config/ModuleConfig.tsx
  24. 6
      src/pages/Config/index.tsx
  25. 13
      src/pages/Extensions/Environment.tsx
  26. 47
      src/pages/Extensions/FileBrowser.tsx
  27. 35
      src/pages/Extensions/Index.tsx
  28. 2
      src/pages/Messages.tsx

37
src/Nav/NavBar.tsx

@ -0,0 +1,37 @@
import { Button } from "@app/components/form/Button.js";
import { ChevronRightIcon, HomeIcon } from "@primer/octicons-react";
export interface NavBarProps {
breadcrumb: string[];
actions?: {
label: string;
onClick: () => void;
}[];
}
export const NavBar = ({ breadcrumb, actions }: NavBarProps): JSX.Element => {
return (
<div className="flex rounded-md bg-backgroundSecondary p-2">
<ol className="my-auto ml-2 flex gap-4 text-textSecondary">
<li className="cursor-pointer hover:brightness-disabled">
<HomeIcon className="h-5 w-5 flex-shrink-0" />
</li>
{breadcrumb.map((breadcrumb, index) => (
<li key={index} className="my-auto flex gap-4">
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 brightness-disabled" />
<span className="cursor-pointer text-sm font-medium hover:brightness-disabled">
{breadcrumb}
</span>
</li>
))}
</ol>
<div className="ml-auto">
{actions?.map((Action, index) => (
<Button key={index} onClick={Action.onClick}>
{Action.label}
</Button>
))}
</div>
</div>
);
};

5
src/Nav/PageNav.tsx

@ -30,11 +30,6 @@ export const PageNav = (): JSX.Element => {
icon: MapIcon, icon: MapIcon,
page: "map" page: "map"
}, },
{
name: "Extensions",
icon: BeakerIcon,
page: "extensions"
},
{ {
name: "Config", name: "Config",
icon: Cog8ToothIcon, icon: Cog8ToothIcon,

2
src/PageRouter.tsx

@ -1,7 +1,6 @@
import { useDevice } from "@core/providers/useDevice.js"; import { useDevice } from "@core/providers/useDevice.js";
import { ChannelsPage } from "@pages/Channels.js"; import { ChannelsPage } from "@pages/Channels.js";
import { ConfigPage } from "@pages/Config/index.js"; import { ConfigPage } from "@pages/Config/index.js";
import { ExtensionsPage } from "@pages/Extensions/Index.js";
import { MapPage } from "@pages/Map.js"; import { MapPage } from "@pages/Map.js";
import { MessagesPage } from "@pages/Messages.js"; import { MessagesPage } from "@pages/Messages.js";
import { PeersPage } from "@pages/Peers.js"; import { PeersPage } from "@pages/Peers.js";
@ -12,7 +11,6 @@ export const PageRouter = (): JSX.Element => {
<div className="flex-grow overflow-y-auto bg-backgroundPrimary"> <div className="flex-grow overflow-y-auto bg-backgroundPrimary">
{activePage === "messages" && <MessagesPage />} {activePage === "messages" && <MessagesPage />}
{activePage === "map" && <MapPage />} {activePage === "map" && <MapPage />}
{activePage === "extensions" && <ExtensionsPage />}
{activePage === "config" && <ConfigPage />} {activePage === "config" && <ConfigPage />}
{activePage === "channels" && <ChannelsPage />} {activePage === "channels" && <ChannelsPage />}
{activePage === "peers" && <PeersPage />} {activePage === "peers" && <PeersPage />}

4
src/components/CommandPalette/GroupView.tsx

@ -9,7 +9,7 @@ export interface GroupViewProps {
export const GroupView = ({ group }: GroupViewProps): JSX.Element => { export const GroupView = ({ group }: GroupViewProps): JSX.Element => {
return ( return (
<Combobox.Option <Combobox.Option
value={group.name} value={group.label}
className={({ active }) => className={({ active }) =>
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${ `flex cursor-default select-none items-center rounded-md px-3 py-2 ${
active ? "bg-backgroundPrimary text-textPrimary" : "" active ? "bg-backgroundPrimary text-textPrimary" : ""
@ -19,7 +19,7 @@ export const GroupView = ({ group }: GroupViewProps): JSX.Element => {
{({ active }) => ( {({ active }) => (
<> <>
<group.icon className="h-6 w-6" /> <group.icon className="h-6 w-6" />
<span className="ml-3 flex-auto truncate">{group.name}</span> <span className="ml-3 flex-auto truncate">{group.label}</span>
{active && <ChevronRightIcon className="h-5 text-textSecondary" />} {active && <ChevronRightIcon className="h-5 text-textSecondary" />}
</> </>
)} )}

81
src/components/CommandPalette/Index.tsx

@ -40,12 +40,12 @@ import { Blur } from "@components/generic/Blur.js";
import { ThemeController } from "@components/generic/ThemeController.js"; import { ThemeController } from "@components/generic/ThemeController.js";
export interface Group { export interface Group {
name: string; label: string;
icon: ComponentType<SVGProps<SVGSVGElement>>; icon: ComponentType<SVGProps<SVGSVGElement>>;
commands: Command[]; commands: Command[];
} }
export interface Command { export interface Command {
name: string; label: string;
icon: ComponentType<SVGProps<SVGSVGElement>>; icon: ComponentType<SVGProps<SVGSVGElement>>;
action?: () => void; action?: () => void;
subItems?: SubItem[]; subItems?: SubItem[];
@ -53,7 +53,7 @@ export interface Command {
} }
export interface SubItem { export interface SubItem {
name: string; label: string;
icon: JSX.Element; icon: JSX.Element;
action: () => void; action: () => void;
} }
@ -77,32 +77,25 @@ export const CommandPalette = (): JSX.Element => {
const groups: Group[] = [ const groups: Group[] = [
{ {
name: "Goto", label: "Goto",
icon: LinkIcon, icon: LinkIcon,
commands: [ commands: [
{ {
name: "Messages", label: "Messages",
icon: InboxIcon, icon: InboxIcon,
action() { action() {
setActivePage("messages"); setActivePage("messages");
} }
}, },
{ {
name: "Map", label: "Map",
icon: MapIcon, icon: MapIcon,
action() { action() {
setActivePage("map"); setActivePage("map");
} }
}, },
{ {
name: "Extensions", label: "Config",
icon: BeakerIcon,
action() {
setActivePage("extensions");
}
},
{
name: "Config",
icon: Cog8ToothIcon, icon: Cog8ToothIcon,
action() { action() {
setActivePage("config"); setActivePage("config");
@ -110,14 +103,14 @@ export const CommandPalette = (): JSX.Element => {
tags: ["settings"] tags: ["settings"]
}, },
{ {
name: "Channels", label: "Channels",
icon: Square3Stack3DIcon, icon: Square3Stack3DIcon,
action() { action() {
setActivePage("channels"); setActivePage("channels");
} }
}, },
{ {
name: "Peers", label: "Peers",
icon: UsersIcon, icon: UsersIcon,
action() { action() {
setActivePage("peers"); setActivePage("peers");
@ -126,15 +119,15 @@ export const CommandPalette = (): JSX.Element => {
] ]
}, },
{ {
name: "Manage", label: "Manage",
icon: DevicePhoneMobileIcon, icon: DevicePhoneMobileIcon,
commands: [ commands: [
{ {
name: "Switch Node", label: "Switch Node",
icon: ArrowsRightLeftIcon, icon: ArrowsRightLeftIcon,
subItems: getDevices().map((device) => { subItems: getDevices().map((device) => {
return { return {
name: label:
device.nodes.find( device.nodes.find(
(n) => n.data.num === device.hardware.myNodeNum (n) => n.data.num === device.hardware.myNodeNum
)?.data.user?.longName ?? device.hardware.myNodeNum.toString(), )?.data.user?.longName ?? device.hardware.myNodeNum.toString(),
@ -151,7 +144,7 @@ export const CommandPalette = (): JSX.Element => {
}) })
}, },
{ {
name: "Connect New Node", label: "Connect New Node",
icon: PlusIcon, icon: PlusIcon,
action() { action() {
setSelectedDevice(0); setSelectedDevice(0);
@ -160,22 +153,22 @@ export const CommandPalette = (): JSX.Element => {
] ]
}, },
{ {
name: "Contextual", label: "Contextual",
icon: CubeTransparentIcon, icon: CubeTransparentIcon,
commands: [ commands: [
{ {
name: "QR Code", label: "QR Code",
icon: QrCodeIcon, icon: QrCodeIcon,
subItems: [ subItems: [
{ {
name: "Generator", label: "Generator",
icon: <QueueListIcon className="w-4" />, icon: <QueueListIcon className="w-4" />,
action() { action() {
setDialogOpen("QR", true); setDialogOpen("QR", true);
} }
}, },
{ {
name: "Import", label: "Import",
icon: <ArrowDownOnSquareStackIcon className="w-4" />, icon: <ArrowDownOnSquareStackIcon className="w-4" />,
action() { action() {
setDialogOpen("import", true); setDialogOpen("import", true);
@ -184,7 +177,7 @@ export const CommandPalette = (): JSX.Element => {
] ]
}, },
{ {
name: "Disconnect", label: "Disconnect",
icon: XCircleIcon, icon: XCircleIcon,
action() { action() {
void connection?.disconnect(); void connection?.disconnect();
@ -193,21 +186,21 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Schedule Shutdown", label: "Schedule Shutdown",
icon: PowerIcon, icon: PowerIcon,
action() { action() {
setDialogOpen("shutdown", true); setDialogOpen("shutdown", true);
} }
}, },
{ {
name: "Schedule Reboot", label: "Schedule Reboot",
icon: ArrowPathIcon, icon: ArrowPathIcon,
action() { action() {
setDialogOpen("reboot", true); setDialogOpen("reboot", true);
} }
}, },
{ {
name: "Reset Peers", label: "Reset Peers",
icon: TrashIcon, icon: TrashIcon,
action() { action() {
if (connection) { if (connection) {
@ -220,7 +213,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Factory Reset", label: "Factory Reset",
icon: ArrowPathRoundedSquareIcon, icon: ArrowPathRoundedSquareIcon,
action() { action() {
if (connection) { if (connection) {
@ -235,18 +228,18 @@ export const CommandPalette = (): JSX.Element => {
] ]
}, },
{ {
name: "Debug", label: "Debug",
icon: BugAntIcon, icon: BugAntIcon,
commands: [ commands: [
{ {
name: "Reconfigure", label: "Reconfigure",
icon: ArrowPathIcon, icon: ArrowPathIcon,
action() { action() {
void connection?.configure(); void connection?.configure();
} }
}, },
{ {
name: "[WIP] Clear Messages", label: "[WIP] Clear Messages",
icon: ArchiveBoxXMarkIcon, icon: ArchiveBoxXMarkIcon,
action() { action() {
alert("This feature is not implemented"); alert("This feature is not implemented");
@ -255,22 +248,22 @@ export const CommandPalette = (): JSX.Element => {
] ]
}, },
{ {
name: "Application", label: "Application",
icon: WindowIcon, icon: WindowIcon,
commands: [ commands: [
{ {
name: "Toggle Dark Mode", label: "Toggle Dark Mode",
icon: MoonIcon, icon: MoonIcon,
action() { action() {
setDarkMode(!darkMode); setDarkMode(!darkMode);
} }
}, },
{ {
name: "Accent Color", label: "Accent Color",
icon: SwatchIcon, icon: SwatchIcon,
subItems: [ subItems: [
{ {
name: "Red", label: "Red",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -283,7 +276,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Orange", label: "Orange",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -296,7 +289,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Yellow", label: "Yellow",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -309,7 +302,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Green", label: "Green",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -322,7 +315,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Blue", label: "Blue",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -335,7 +328,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Purple", label: "Purple",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -348,7 +341,7 @@ export const CommandPalette = (): JSX.Element => {
} }
}, },
{ {
name: "Pink", label: "Pink",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
@ -386,7 +379,7 @@ export const CommandPalette = (): JSX.Element => {
return { return {
...group, ...group,
commands: group.commands.filter((command) => { commands: group.commands.filter((command) => {
const nameIncludes = `${group.name} ${command.name}` const nameIncludes = `${group.label} ${command.label}`
.toLowerCase() .toLowerCase()
.includes(query.toLowerCase()); .includes(query.toLowerCase());
@ -399,7 +392,7 @@ export const CommandPalette = (): JSX.Element => {
const subItemsInclude = ( const subItemsInclude = (
command.subItems command.subItems
?.map((s) => ?.map((s) =>
s.name.toLowerCase().includes(query.toLowerCase()) s.label.toLowerCase().includes(query.toLowerCase())
) )
.filter(Boolean) ?? [] .filter(Boolean) ?? []
).length; ).length;

6
src/components/CommandPalette/SearchResult.tsx

@ -11,7 +11,7 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
<div className="rounded-md border-2 border-backgroundPrimary py-2"> <div className="rounded-md border-2 border-backgroundPrimary py-2">
<div className="flex items-center px-3 py-2"> <div className="flex items-center px-3 py-2">
<group.icon className="text-gray-900 h-6 w-6 flex-none text-opacity-40" /> <group.icon className="text-gray-900 h-6 w-6 flex-none text-opacity-40" />
<span className="ml-3 flex-auto truncate">{group.name}</span> <span className="ml-3 flex-auto truncate">{group.label}</span>
</div> </div>
{group.commands.map((command, index) => ( {group.commands.map((command, index) => (
<div key={index}> <div key={index}>
@ -30,7 +30,7 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
active ? "text-opacity-100" : "" active ? "text-opacity-100" : ""
}`} }`}
/> />
<span className="ml-3">{command.name}</span> <span className="ml-3">{command.label}</span>
{active && ( {active && (
<ChevronRightIcon className="text-gray-400 ml-auto h-4" /> <ChevronRightIcon className="text-gray-400 ml-auto h-4" />
)} )}
@ -54,7 +54,7 @@ export const SearchResult = ({ group }: SearchResultProps): JSX.Element => {
{({ active }) => ( {({ active }) => (
<> <>
{item.icon} {item.icon}
<span className="ml-3">{item.name}</span> <span className="ml-3">{item.label}</span>
{active && ( {active && (
<ChevronRightIcon className="text-gray-400 ml-auto h-4" /> <ChevronRightIcon className="text-gray-400 ml-auto h-4" />
)} )}

4
src/components/Dialog/RebootDialog.tsx

@ -41,11 +41,13 @@ export const RebootDialog = ({
/> />
<Button <Button
className="w-24" className="w-24"
iconBefore={<ArrowPathIcon className="w-4" />}
onClick={() => { onClick={() => {
connection?.reboot(2).then(() => setDialogOpen("reboot", false)); connection?.reboot(2).then(() => setDialogOpen("reboot", false));
}} }}
> >
<span>
<ArrowPathIcon className="w-4" />
</span>
Now Now
</Button> </Button>
</div> </div>

4
src/components/Dialog/ShutdownDialog.tsx

@ -41,14 +41,14 @@ export const ShutdownDialog = ({
/> />
<Button <Button
className="w-24" className="w-24"
iconBefore={<PowerIcon className="w-4" />}
onClick={() => { onClick={() => {
connection connection
?.shutdown(2) ?.shutdown(2)
.then(() => setDialogOpen("shutdown", false)); .then(() => setDialogOpen("shutdown", false));
}} }}
> >
Now <PowerIcon className="w-4" />
<span>Now</span>
</Button> </Button>
</div> </div>
</Dialog> </Dialog>

8
src/components/Drawer/index.tsx

@ -10,9 +10,9 @@ export const Drawer = (): JSX.Element => {
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const tabs: TabType[] = [ const tabs: TabType[] = [
{ name: "Notifications", element: Notifications }, { label: "Notifications", element: Notifications },
{ name: "Metrics", element: Metrics }, { label: "Metrics", element: Metrics },
{ name: "Sensor", element: Sensor } { label: "Sensor", element: Sensor }
]; ];
return ( return (
<Tab.Group as="div"> <Tab.Group as="div">
@ -30,7 +30,7 @@ export const Drawer = (): JSX.Element => {
: "border-backgroundPrimary text-textSecondary" : "border-backgroundPrimary text-textSecondary"
}`} }`}
> >
<span className="m-auto select-none">{tab.name}</span> <span className="m-auto select-none">{tab.label}</span>
</div> </div>
)} )}
</Tab> </Tab>

6
src/components/NewDevice.tsx

@ -12,7 +12,7 @@ export const NewDevice = () => {
const tabs: TabType[] = [ const tabs: TabType[] = [
{ {
name: "Bluetooth", label: "Bluetooth",
element: BLE, element: BLE,
disabled: !navigator.bluetooth, disabled: !navigator.bluetooth,
disabledMessage: disabledMessage:
@ -21,13 +21,13 @@ export const NewDevice = () => {
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility" "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
}, },
{ {
name: "HTTP", label: "HTTP",
element: HTTP, element: HTTP,
disabled: false, disabled: false,
disabledMessage: "Unsuported connection method" disabledMessage: "Unsuported connection method"
}, },
{ {
name: "Serial", label: "Serial",
element: Serial, element: Serial,
disabled: !navigator.serial, disabled: !navigator.serial,
disabledMessage: disabledMessage:

183
src/components/PageComponents/Channel.tsx

@ -15,6 +15,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { classValidatorResolver } from "@hookform/resolvers/class-validator"; import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
import { NavBar } from "@app/Nav/NavBar.js";
export interface SettingsPanelProps { export interface SettingsPanelProps {
channel: Protobuf.Channel; channel: Protobuf.Channel;
@ -102,92 +103,106 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
}); });
return ( return (
<Form onSubmit={onSubmit}> <div className="flex flex-grow flex-col gap-2">
{channel?.index !== 0 && ( <NavBar
<> breadcrumb={["Channels", channel?.index.toString()]}
<Controller actions={[
name="enabled" {
control={control} label: "Apply",
render={({ field: { value, ...rest } }) => ( async onClick() {
<Toggle await onSubmit();
label="Enabled" }
description="Description"
checked={value}
{...rest}
/>
)}
/>
<Input
label="Name"
description="Max transmit power in dBm"
error={errors.name?.message}
{...register("name")}
/>
</>
)}
<Select
label="Key Size"
description="Desired size of generated key."
value={keySize}
onChange={(e): void => {
setKeySize(parseInt(e.target.value) as 128 | 256);
}}
action={{
icon: <ArrowPathIcon className="h-4" />,
action: () => {
const key = new Uint8Array(keySize / 8);
crypto.getRandomValues(key);
setValue("psk", fromByteArray(key), {
shouldDirty: true
});
} }
}} ]}
>
<option value={128}>128 Bit</option>
<option value={256}>256 Bit</option>
</Select>
<Input
width="100%"
label="Pre-Shared Key"
description="Channel key to encrypt data"
type={pskHidden ? "password" : "text"}
action={{
icon: pskHidden ? (
<EyeIcon className="w-4" />
) : (
<EyeSlashIcon className="w-4" />
),
action: () => {
setPskHidden(!pskHidden);
}
}}
error={errors.psk?.message}
{...register("psk")}
/>
<Controller
name="uplinkEnabled"
control={control}
render={({ field: { value, ...rest } }) => (
<Toggle
label="Uplink Enabled"
description="Send packets to designated MQTT server"
checked={value}
{...rest}
/>
)}
/> />
<Controller
name="downlinkEnabled" <Form onSubmit={onSubmit}>
control={control} {channel?.index !== 0 && (
render={({ field: { value, ...rest } }) => ( <>
<Toggle <Controller
label="Downlink Enabled" name="enabled"
description="Recieve packets to designated MQTT server" control={control}
checked={value} render={({ field: { value, ...rest } }) => (
{...rest} <Toggle
/> label="Enabled"
description="Description"
checked={value}
{...rest}
/>
)}
/>
<Input
label="Name"
description="Max transmit power in dBm"
error={errors.name?.message}
{...register("name")}
/>
</>
)} )}
/> <Select
</Form> label="Key Size"
description="Desired size of generated key."
value={keySize}
onChange={(e): void => {
setKeySize(parseInt(e.target.value) as 128 | 256);
}}
action={{
icon: <ArrowPathIcon className="h-4" />,
action: () => {
const key = new Uint8Array(keySize / 8);
crypto.getRandomValues(key);
setValue("psk", fromByteArray(key), {
shouldDirty: true
});
}
}}
>
<option value={128}>128 Bit</option>
<option value={256}>256 Bit</option>
</Select>
<Input
width="100%"
label="Pre-Shared Key"
description="Channel key to encrypt data"
type={pskHidden ? "password" : "text"}
action={{
icon: pskHidden ? (
<EyeIcon className="w-4" />
) : (
<EyeSlashIcon className="w-4" />
),
action: () => {
setPskHidden(!pskHidden);
}
}}
error={errors.psk?.message}
{...register("psk")}
/>
<Controller
name="uplinkEnabled"
control={control}
render={({ field: { value, ...rest } }) => (
<Toggle
label="Uplink Enabled"
description="Send packets to designated MQTT server"
checked={value}
{...rest}
/>
)}
/>
<Controller
name="downlinkEnabled"
control={control}
render={({ field: { value, ...rest } }) => (
<Toggle
label="Downlink Enabled"
description="Recieve packets to designated MQTT server"
checked={value}
{...rest}
/>
)}
/>
</Form>
</div>
); );
}; };

4
src/components/PageComponents/Connect/BLE.tsx

@ -51,7 +51,6 @@ export const BLE = (): JSX.Element => {
)} )}
</div> </div>
<Button <Button
iconBefore={<PlusCircleIcon className="w-4" />}
onClick={() => { onClick={() => {
void navigator.bluetooth void navigator.bluetooth
.requestDevice({ .requestDevice({
@ -65,7 +64,8 @@ export const BLE = (): JSX.Element => {
}); });
}} }}
> >
New device <PlusCircleIcon className="w-4" />
<span>New device</span>
</Button> </Button>
</div> </div>
); );

5
src/components/PageComponents/Connect/HTTP.tsx

@ -71,8 +71,9 @@ export const HTTP = (): JSX.Element => {
)} )}
/> />
</div> </div>
<Button iconBefore={<PlusCircleIcon className="w-4" />} type="submit"> <Button type="submit">
Connect <PlusCircleIcon className="w-4" />
<span>Connect</span>
</Button> </Button>
</form> </form>
); );

4
src/components/PageComponents/Connect/Serial.tsx

@ -64,14 +64,14 @@ export const Serial = (): JSX.Element => {
)} )}
</div> </div>
<Button <Button
iconBefore={<PlusCircleIcon className="w-4" />}
onClick={() => { onClick={() => {
void navigator.serial.requestPort().then((port) => { void navigator.serial.requestPort().then((port) => {
setSerialPorts(serialPorts.concat(port)); setSerialPorts(serialPorts.concat(port));
}); });
}} }}
> >
New device <PlusCircleIcon className="w-4" />
<span>New device</span>
</Button> </Button>
</div> </div>
); );

8
src/components/Widgets/DeviceWidget.tsx

@ -28,12 +28,8 @@ export const DeviceWidget = ({
{name} {name}
</span> </span>
<div className="my-auto ml-auto"> <div className="my-auto ml-auto">
<Button <Button onClick={disconnected ? reconnect : disconnect} size="sm">
onClick={disconnected ? reconnect : disconnect} <span>{disconnected ? "Reconnect" : "Disconnect"}</span>
size="sm"
iconBefore={<XCircleIcon className="h-4" />}
>
{disconnected ? "Reconnect" : "Disconnect"}
</Button> </Button>
</div> </div>
</div> </div>

5
src/components/form/Button.tsx

@ -1,13 +1,11 @@
import type { ButtonHTMLAttributes } from "react"; import type { ButtonHTMLAttributes, ComponentType, SVGProps } from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
iconBefore?: JSX.Element;
} }
export const Button = ({ export const Button = ({
size = "md", size = "md",
iconBefore,
children, children,
disabled, disabled,
className, className,
@ -30,7 +28,6 @@ export const Button = ({
{...rest} {...rest}
> >
<div className="m-auto flex shrink-0 items-center gap-2 font-medium"> <div className="m-auto flex shrink-0 items-center gap-2 font-medium">
{iconBefore}
{children} {children}
</div> </div>
</button> </button>

2
src/components/form/Form.tsx

@ -11,7 +11,7 @@ export const Form = ({
}: FormProps): JSX.Element => { }: FormProps): JSX.Element => {
return ( return (
<form <form
className="mr-2 w-full rounded-md bg-backgroundSecondary px-2" className="w-full rounded-md bg-backgroundSecondary px-2"
onSubmit={onSubmit} onSubmit={onSubmit}
onChange={onSubmit} onChange={onSubmit}
{...props} {...props}

6
src/components/generic/TabbedContent.tsx

@ -3,7 +3,7 @@ import { Mono } from "@components/generic/Mono";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
export interface TabType { export interface TabType {
name: string; label: string;
icon?: JSX.Element; icon?: JSX.Element;
element: () => JSX.Element; element: () => JSX.Element;
disabled?: boolean; disabled?: boolean;
@ -46,7 +46,7 @@ export const TabbedContent = ({
{entry.icon && ( {entry.icon && (
<div className="text-slate-500 m-auto">{entry.icon}</div> <div className="text-slate-500 m-auto">{entry.icon}</div>
)} )}
<span className="m-auto">{entry.name}</span> <span className="m-auto">{entry.label}</span>
</div> </div>
)} )}
</Tab> </Tab>
@ -65,7 +65,7 @@ export const TabbedContent = ({
</Tab.List> </Tab.List>
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
{tabs.map((entry, index) => ( {tabs.map((entry, index) => (
<Tab.Panel key={index} className="flex flex-grow"> <Tab.Panel key={index} className="m-2 flex flex-grow">
{!entry.disabled ? ( {!entry.disabled ? (
<entry.element /> <entry.element />
) : ( ) : (

49
src/components/generic/VerticalTabbedContent.tsx

@ -0,0 +1,49 @@
import { Fragment } from "react";
import { Mono } from "@components/generic/Mono";
import { Tab } from "@headlessui/react";
export interface TabType {
label: string;
element: () => JSX.Element;
disabled?: boolean;
}
export interface TabbedContentProps {
tabs: TabType[];
}
export const VerticalTabbedContent = ({
tabs
}: TabbedContentProps): JSX.Element => {
return (
<Tab.Group as="div" className="flex w-full gap-3">
<Tab.List className="flex w-44 flex-col">
{tabs.map((tab, index) => (
<Tab key={index} as={Fragment}>
{({ selected }) => (
<div
className={`flex cursor-pointer items-center border-l-4 p-4 text-sm font-medium ${
selected
? "border-accent bg-accentMuted bg-opacity-10 text-textPrimary"
: "border-backgroundPrimary text-textSecondary"
}`}
>
{tab.label}
<span className="ml-auto rounded-full bg-accent px-3 text-textPrimary">
3
</span>
</div>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels as={Fragment}>
{tabs.map((tab, index) => (
<Tab.Panel key={index} as={Fragment}>
<tab.element />
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
);
};

1
src/core/stores/deviceStore.ts

@ -8,7 +8,6 @@ import { Protobuf, Types } from "@meshtastic/meshtasticjs";
export type Page = export type Page =
| "messages" | "messages"
| "map" | "map"
| "extensions"
| "config" | "config"
| "channels" | "channels"
| "peers"; | "peers";

2
src/pages/Channels.tsx

@ -12,7 +12,7 @@ export const ChannelsPage = (): JSX.Element => {
const tabs: TabType[] = channels.map((channel) => { const tabs: TabType[] = channels.map((channel) => {
return { return {
name: channel.config.settings?.name.length label: channel.config.settings?.name.length
? channel.config.settings.name ? channel.config.settings.name
: channel.config.role === Protobuf.Channel_Role.PRIMARY : channel.config.role === Protobuf.Channel_Role.PRIMARY
? "Primary" ? "Primary"

70
src/pages/Config/DeviceConfig.tsx

@ -12,11 +12,13 @@ import { Tab } from "@headlessui/react";
import { ChevronRightIcon, HomeIcon } from "@heroicons/react/24/outline"; import { ChevronRightIcon, HomeIcon } from "@heroicons/react/24/outline";
import { Button } from "@components/form/Button.js"; import { Button } from "@components/form/Button.js";
import { CheckIcon } from "@primer/octicons-react"; import { CheckIcon } from "@primer/octicons-react";
import { NavBar } from "@app/Nav/NavBar.js";
import { VerticalTabbedContent } from "@app/components/generic/VerticalTabbedContent.js";
export const DeviceConfig = (): JSX.Element => { export const DeviceConfig = (): JSX.Element => {
const { hardware, workingConfig, connection } = useDevice(); const { hardware, workingConfig, connection } = useDevice();
const configSections = [ const tabs = [
{ {
label: "User", label: "User",
element: User element: User
@ -53,65 +55,23 @@ export const DeviceConfig = (): JSX.Element => {
]; ];
return ( return (
<div className="w-full"> <div className="flex flex-grow flex-col gap-2">
<div className="m-2 flex rounded-md bg-backgroundSecondary p-2"> <NavBar
<ol className="my-auto ml-2 flex gap-4 text-textSecondary"> breadcrumb={["Config"]}
<li className="cursor-pointer hover:brightness-disabled"> actions={[
<HomeIcon className="h-5 w-5 flex-shrink-0" /> {
</li> label: "Apply & Reboot",
{["Config", "User"].map((breadcrumb, index) => ( async onClick() {
<li key={index} className="flex gap-4">
<ChevronRightIcon className="h-5 w-5 flex-shrink-0 brightness-disabled" />
<span className="cursor-pointer text-sm font-medium hover:brightness-disabled">
{breadcrumb}
</span>
</li>
))}
</ol>
<div className="ml-auto">
<Button
onClick={async () => {
workingConfig.map(async (config) => { workingConfig.map(async (config) => {
await connection?.setConfig(config); await connection?.setConfig(config);
}); });
await connection?.commitEditSettings(); await connection?.commitEditSettings();
}} }
iconBefore={<CheckIcon className="w-4" />} }
> ]}
Apply & Reboot />
</Button>
</div>
</div>
<Tab.Group as="div" className="flex w-full gap-3"> <VerticalTabbedContent tabs={tabs} />
<Tab.List className="flex w-44 flex-col">
{configSections.map((Config, index) => (
<Tab key={index} as={Fragment}>
{({ selected }) => (
<div
className={`flex cursor-pointer items-center border-l-4 p-4 text-sm font-medium ${
selected
? "border-accent bg-accentMuted bg-opacity-10 text-textPrimary"
: "border-backgroundPrimary text-textSecondary"
}`}
>
{Config.label}
<span className="ml-auto rounded-full bg-accent px-3 text-textPrimary">
3
</span>
</div>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels as={Fragment}>
{configSections.map((Config, index) => (
<Tab.Panel key={index} as={Fragment}>
<Config.element />
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
</div> </div>
); );
}; };

52
src/pages/Config/ModuleConfig.tsx

@ -7,10 +7,14 @@ import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js"
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js"; import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js"; import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js"; import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
import { Tab } from "@headlessui/react"; import { useDevice } from "@app/core/providers/useDevice.js";
import { NavBar } from "@app/Nav/NavBar.js";
import { VerticalTabbedContent } from "@app/components/generic/VerticalTabbedContent.js";
export const ModuleConfig = (): JSX.Element => { export const ModuleConfig = (): JSX.Element => {
const configSections = [ const { workingModuleConfig, connection } = useDevice();
const tabs = [
{ {
label: "MQTT", label: "MQTT",
element: MQTT element: MQTT
@ -46,31 +50,23 @@ export const ModuleConfig = (): JSX.Element => {
]; ];
return ( return (
<Tab.Group as="div" className="flex w-full gap-3"> <div className="flex flex-grow flex-col gap-2">
<Tab.List className="flex w-44 flex-col gap-1"> <NavBar
{configSections.map((Config, index) => ( breadcrumb={["Module Config"]}
<Tab key={index} as={Fragment}> actions={[
{({ selected }) => ( {
<div label: "Apply & Reboot",
className={`flex cursor-pointer items-center rounded-md px-3 py-2 text-sm font-medium ${ async onClick() {
selected workingModuleConfig.map(async (moduleConfig) => {
? "bg-gray-100 text-gray-900" await connection?.setModuleConfig(moduleConfig);
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900" });
}`} await connection?.commitEditSettings();
> }
{Config.label} }
</div> ]}
)} />
</Tab>
))} <VerticalTabbedContent tabs={tabs} />
</Tab.List> </div>
<Tab.Panels as={Fragment}>
{configSections.map((Config, index) => (
<Tab.Panel key={index} as={Fragment}>
<Config.element />
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
); );
}; };

6
src/pages/Config/index.tsx

@ -14,17 +14,17 @@ export const ConfigPage = (): JSX.Element => {
const tabs: TabType[] = [ const tabs: TabType[] = [
{ {
name: "Device Config", label: "Device Config",
icon: <Cog8ToothIcon className="h-4" />, icon: <Cog8ToothIcon className="h-4" />,
element: DeviceConfig element: DeviceConfig
}, },
{ {
name: "Module Config", label: "Module Config",
icon: <CubeTransparentIcon className="h-4" />, icon: <CubeTransparentIcon className="h-4" />,
element: ModuleConfig element: ModuleConfig
}, },
{ {
name: "App Config", label: "App Config",
icon: <WindowIcon className="h-4" />, icon: <WindowIcon className="h-4" />,
element: AppConfig element: AppConfig
} }

13
src/pages/Extensions/Environment.tsx

@ -1,13 +0,0 @@
import { useDevice } from "@core/providers/useDevice.js";
export const Environment = (): JSX.Element => {
const { nodes } = useDevice();
return (
<div>
{nodes.map((node, index) => (
<div key={index}>{JSON.stringify(node.environmentMetrics)}</div>
))}
</div>
);
};

47
src/pages/Extensions/FileBrowser.tsx

@ -1,47 +0,0 @@
import { useEffect, useState } from "react";
export interface File {
nameModified: string;
name: string;
size: number;
}
export interface Files {
data: {
files: File[];
fileSystem: {
total: number;
used: number;
free: number;
};
};
status: string;
}
export const FileBrowser = (): JSX.Element => {
const [data, setData] = useState<Files>();
useEffect(() => {
void fetch("http://meshtastic.local/json/fs/browse/static").then(
async (res) => {
setData((await res.json()) as Files);
}
);
}, []);
return (
<div>
{data?.data.files.map((file) => (
<div key={file.name}>
<a
target="_blank"
rel="noopener noreferrer"
href={`http://meshtastic.local/${file.name.replace("static/", "")}`}
>
{file.name.replace("static/", "").replace(".gz", "")}
</a>
</div>
))}
</div>
);
};

35
src/pages/Extensions/Index.tsx

@ -1,35 +0,0 @@
import { TabbedContent, TabType } from "@components/generic/TabbedContent";
import { useDevice } from "@core/providers/useDevice.js";
import {
CloudIcon,
DocumentIcon,
SignalIcon
} from "@heroicons/react/24/outline";
import { Environment } from "@pages/Extensions/Environment.js";
import { FileBrowser } from "@pages/Extensions/FileBrowser";
export const ExtensionsPage = (): JSX.Element => {
const { hardware } = useDevice();
const tabs: TabType[] = [
{
name: "File Browser",
icon: <DocumentIcon className="h-4" />,
element: FileBrowser,
disabled: !hardware.hasWifi
},
{
name: "Range Test",
icon: <SignalIcon className="h-4" />,
element: FileBrowser,
disabled: !hardware.hasWifi
},
{
name: "Environment",
icon: <CloudIcon className="h-4" />,
element: Environment
}
];
return <TabbedContent tabs={tabs} />;
};

2
src/pages/Messages.tsx

@ -9,7 +9,7 @@ export const MessagesPage = (): JSX.Element => {
const tabs: TabType[] = channels.map((channel) => { const tabs: TabType[] = channels.map((channel) => {
return { return {
name: channel.config.settings?.name.length label: channel.config.settings?.name.length
? channel.config.settings?.name ? channel.config.settings?.name
: channel.config.index === 0 : channel.config.index === 0
? "Primary" ? "Primary"

Loading…
Cancel
Save