Browse Source

more state changes

pull/101/head
Sacha Weatherstone 3 years ago
parent
commit
0894fd76f5
No known key found for this signature in database GPG Key ID: 7AB2D7E206124B31
  1. 1
      src/components/Dialog/DialogManager.tsx
  2. 1
      src/components/Dialog/ImportDialog.tsx
  3. 13
      src/components/Dialog/QRDialog.tsx
  4. 18
      src/components/Form/DynamicForm.tsx
  5. 187
      src/components/PageComponents/Channel.tsx
  6. 16
      src/core/stores/deviceStore.ts
  7. 11
      src/pages/Channels.tsx
  8. 49
      src/pages/Messages.tsx
  9. 19
      src/validation/channel.ts
  10. 10
      src/validation/config/network.ts

1
src/components/Dialog/DialogManager.tsx

@ -22,7 +22,6 @@ export const DialogManager = (): JSX.Element => {
onOpenChange={(open) => { onOpenChange={(open) => {
setDialogOpen("import", open); setDialogOpen("import", open);
}} }}
channels={channels}
loraConfig={config.lora} loraConfig={config.lora}
/> />
<ShutdownDialog <ShutdownDialog

1
src/components/Dialog/ImportDialog.tsx

@ -20,7 +20,6 @@ export interface ImportDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
loraConfig?: Protobuf.Config_LoRaConfig; loraConfig?: Protobuf.Config_LoRaConfig;
channels: Protobuf.Channel[];
} }
export const ImportDialog = ({ export const ImportDialog = ({

13
src/components/Dialog/QRDialog.tsx

@ -12,14 +12,14 @@ import {
DialogTitle DialogTitle
} from "@components/UI/Dialog.js"; } from "@components/UI/Dialog.js";
import { ClipboardIcon } from "lucide-react"; import { ClipboardIcon } from "lucide-react";
import { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf, Types } from "@meshtastic/meshtasticjs";
import { Label } from "@components/UI/Label.js"; import { Label } from "@components/UI/Label.js";
export interface QRDialogProps { export interface QRDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
loraConfig?: Protobuf.Config_LoRaConfig; loraConfig?: Protobuf.Config_LoRaConfig;
channels: Protobuf.Channel[]; channels: Map<Types.ChannelNumber, Protobuf.Channel>;
} }
export const QRDialog = ({ export const QRDialog = ({
@ -31,9 +31,12 @@ export const QRDialog = ({
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]); const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [QRCodeURL, setQRCodeURL] = useState<string>(""); const [QRCodeURL, setQRCodeURL] = useState<string>("");
const filteredChannels = Array.from(channels.values()).filter((channel) =>
selectedChannels.includes(channel.index)
);
useEffect(() => { useEffect(() => {
const channelsToEncode = channels const channelsToEncode = filteredChannels
.filter((channel) => selectedChannels.includes(channel.index))
.map((channel) => channel.settings) .map((channel) => channel.settings)
.filter((ch): ch is Protobuf.ChannelSettings => !!ch); .filter((ch): ch is Protobuf.ChannelSettings => !!ch);
const encoded = new Protobuf.ChannelSet( const encoded = new Protobuf.ChannelSet(
@ -62,7 +65,7 @@ export const QRDialog = ({
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="flex gap-3 px-4 py-5 sm:p-6"> <div className="flex gap-3 px-4 py-5 sm:p-6">
<div className="flex w-40 flex-col gap-1"> <div className="flex w-40 flex-col gap-1">
{channels.map((channel) => ( {filteredChannels.map((channel) => (
<div key={channel.index}> <div key={channel.index}>
<Label> <Label>
{channel.settings?.name.length {channel.settings?.name.length

18
src/components/Form/DynamicForm.tsx

@ -10,6 +10,7 @@ import { H4 } from "@components/UI/Typography/H4.js";
import { Subtle } from "@components/UI/Typography/Subtle.js"; import { Subtle } from "@components/UI/Typography/Subtle.js";
import { DynamicFormField, FieldProps } from "./DynamicFormField.js"; import { DynamicFormField, FieldProps } from "./DynamicFormField.js";
import { FieldWrapper } from "./FormWrapper.js"; import { FieldWrapper } from "./FormWrapper.js";
import { Button } from "../UI/Button.js";
interface DisabledBy<T> { interface DisabledBy<T> {
fieldName: Path<T>; fieldName: Path<T>;
@ -33,6 +34,8 @@ export interface GenericFormElementProps<T extends FieldValues, Y> {
export interface DynamicFormProps<T extends FieldValues> { export interface DynamicFormProps<T extends FieldValues> {
onSubmit: SubmitHandler<T>; onSubmit: SubmitHandler<T>;
submitType?: "onChange" | "onSubmit";
hasSubmitButton?: boolean;
defaultValues?: DeepPartial<T>; defaultValues?: DeepPartial<T>;
fieldGroups: { fieldGroups: {
label: string; label: string;
@ -42,12 +45,14 @@ export interface DynamicFormProps<T extends FieldValues> {
} }
export function DynamicForm<T extends FieldValues>({ export function DynamicForm<T extends FieldValues>({
fieldGroups,
onSubmit, onSubmit,
defaultValues submitType = "onChange",
hasSubmitButton,
defaultValues,
fieldGroups
}: DynamicFormProps<T>) { }: DynamicFormProps<T>) {
const { handleSubmit, control, getValues } = useForm<T>({ const { handleSubmit, control, getValues } = useForm<T>({
mode: "onChange", mode: submitType,
defaultValues: defaultValues defaultValues: defaultValues
}); });
@ -68,7 +73,11 @@ export function DynamicForm<T extends FieldValues>({
return ( return (
<form <form
className="space-y-8 divide-y divide-gray-200" className="space-y-8 divide-y divide-gray-200"
onChange={handleSubmit(onSubmit)} {...(submitType === "onSubmit"
? { onSubmit: handleSubmit(onSubmit) }
: {
onChange: handleSubmit(onSubmit)
})}
> >
{fieldGroups.map((fieldGroup, index) => ( {fieldGroups.map((fieldGroup, index) => (
<div <div
@ -92,6 +101,7 @@ export function DynamicForm<T extends FieldValues>({
))} ))}
</div> </div>
))} ))}
{hasSubmitButton && <Button type="submit">Submit</Button>}
</form> </form>
); );
} }

187
src/components/PageComponents/Channel.tsx

@ -1,7 +1,5 @@
import { useEffect, useState } from "react";
import { fromByteArray, toByteArray } from "base64-js"; import { fromByteArray, toByteArray } from "base64-js";
import type { ChannelSettingsValidation } from "@app/validation/channelSettings.js"; import type { ChannelValidation } from "@app/validation/channel.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
import { DynamicForm } from "../Form/DynamicForm.js"; import { DynamicForm } from "../Form/DynamicForm.js";
@ -10,10 +8,6 @@ export interface SettingsPanelProps {
} }
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => { export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
const { connection, addChannel } = useDevice();
const [keySize, setKeySize] = useState<128 | 256>(256);
const [pskHidden, setPskHidden] = useState(true);
// const { // const {
// register, // register,
// handleSubmit, // handleSubmit,
@ -81,116 +75,23 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
// ); // );
// }); // });
const onSubmit = (data: ChannelSettingsValidation) => { const onSubmit = (data: ChannelValidation) => {
console.log(data); console.log(data);
}; };
return ( return (
// <div className="p-3"> <DynamicForm<ChannelValidation>
// <form onSubmit={onSubmit}>
// {channel?.index !== 0 && (
// <>
// <Controller
// name="enabled"
// control={control}
// render={({ field: { value, ...rest } }) => (
// <>
// <Label>Enabled</Label>
// <Switch
// // label="Enabled"
// // description="Description"
// checked={value}
// {...rest}
// />
// </>
// )}
// />
// <Label>Name</Label>
// <Input
// // 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: <RefreshCwIcon size={16} />,
// 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> */}
// <Label>Pre-Shared Key</Label>
// <Input
// // width="100%"
// // label="Pre-Shared Key"
// // description="Channel key to encrypt data"
// type={pskHidden ? "password" : "text"}
// action={{
// icon: pskHidden ? EyeIcon : EyeOffIcon,
// onClick: () => {
// setPskHidden(!pskHidden);
// }
// }}
// // error={errors.psk?.message}
// {...register("psk")}
// />
// <Controller
// name="uplinkEnabled"
// control={control}
// render={({ field: { value, ...rest } }) => (
// <>
// <Label>Uplink Enabled</Label>
// <Switch
// // label="Uplink Enabled"
// // description="Send packets to designated MQTT server"
// checked={value}
// {...rest}
// />
// </>
// )}
// />
// <Controller
// name="downlinkEnabled"
// control={control}
// render={({ field: { value, ...rest } }) => (
// <>
// <Label>Downlink Enabled</Label>
// <Switch
// // label="Downlink Enabled"
// // description="Recieve packets to designated MQTT server"
// checked={value}
// {...rest}
// />
// </>
// )}
// />
// </form>
<DynamicForm<ChannelSettingsValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
submitType="onSubmit"
hasSubmitButton={true}
defaultValues={{ defaultValues={{
enabled: [ ...channel,
Protobuf.Channel_Role.SECONDARY, ...{
Protobuf.Channel_Role.PRIMARY settings: {
].find((role) => role === channel?.role) ...channel?.settings,
? true psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
: false, }
...channel?.settings, }
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
}} }}
fieldGroups={[ fieldGroups={[
{ {
@ -198,80 +99,52 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
description: "Settings for the Bluetooth module", description: "Settings for the Bluetooth module",
fields: [ fields: [
{ {
type: "toggle", type: "select",
name: "enabled", name: "role",
label: "Enabled", label: "Role",
description: "Description" description: "Description",
properties: {
enumValue: Protobuf.Channel_Role
}
}, },
{ {
type: "password", type: "password",
name: "psk", name: "settings.psk",
label: "pre-Shared Key", label: "pre-Shared Key",
description: "Description", description: "Description",
disabledBy: [
{
fieldName: "enabled"
}
],
properties: { properties: {
// act // act
} }
}, },
{ {
type: "number", type: "number",
name: "channelNum", name: "settings.channelNum",
label: "Channel Number", label: "Channel Number",
description: "Description", description: "Description"
disabledBy: [
{
fieldName: "enabled"
}
]
}, },
{ {
type: "text", type: "text",
name: "name", name: "settings.name",
label: "Name", label: "Name",
description: "Description", description: "Description"
disabledBy: [
{
fieldName: "enabled"
}
]
}, },
{ {
type: "number", type: "number",
name: "id", name: "settings.id",
label: "ID", label: "ID",
description: "Description", description: "Description"
disabledBy: [
{
fieldName: "enabled"
}
]
}, },
{ {
type: "toggle", type: "toggle",
name: "uplinkEnabled", name: "settings.uplinkEnabled",
label: "Uplink Enabled", label: "Uplink Enabled",
description: "Description", description: "Description"
disabledBy: [
{
fieldName: "enabled"
}
]
}, },
{ {
type: "toggle", type: "toggle",
name: "downlinkEnabled", name: "settings.downlinkEnabled",
label: "Downlink Enabled", label: "Downlink Enabled",
description: "Description", description: "Description"
disabledBy: [
{
fieldName: "enabled"
}
]
} }
] ]
} }

16
src/core/stores/deviceStore.ts

@ -30,7 +30,7 @@ export type DialogVariant =
export interface Device { export interface Device {
id: number; id: number;
status: Types.DeviceStatusEnum; status: Types.DeviceStatusEnum;
channels: Protobuf.Channel[]; channels: Map<Types.ChannelNumber, Protobuf.Channel>;
config: Protobuf.LocalConfig; config: Protobuf.LocalConfig;
moduleConfig: Protobuf.LocalModuleConfig; moduleConfig: Protobuf.LocalModuleConfig;
workingConfig: Protobuf.Config[]; workingConfig: Protobuf.Config[];
@ -108,7 +108,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
draft.devices.set(id, { draft.devices.set(id, {
id, id,
status: Types.DeviceStatusEnum.DEVICE_DISCONNECTED, status: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
channels: [], channels: new Map(),
config: new Protobuf.LocalConfig(), config: new Protobuf.LocalConfig(),
moduleConfig: new Protobuf.LocalModuleConfig(), moduleConfig: new Protobuf.LocalModuleConfig(),
workingConfig: [], workingConfig: [],
@ -336,16 +336,10 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {
const device = draft.devices.get(id); const device = draft.devices.get(id);
if (device) { if (!device) {
const channelIndex = device.channels.findIndex( return;
(c) => c.index === channel.index
);
if (channelIndex !== -1) {
device.channels[channelIndex] = channel;
} else {
device.channels.push(channel);
}
} }
device.channels.set(channel.index, channel);
}) })
); );
}, },

11
src/pages/Channels.tsx

@ -26,14 +26,15 @@ export const ChannelsPage = (): JSX.Element => {
Types.ChannelNumber.PRIMARY Types.ChannelNumber.PRIMARY
); );
const currentChannel = channels.get(activeChannel);
const allChannels = Array.from(channels.values());
return ( return (
<> <>
<Sidebar></Sidebar> <Sidebar></Sidebar>
<PageLayout <PageLayout
label={`Channel: ${ label={`Channel: ${
channels[activeChannel] currentChannel ? getChannelName(currentChannel) : "Loading..."
? getChannelName(channels[activeChannel])
: "Loading..."
}`} }`}
actions={[ actions={[
{ {
@ -54,13 +55,13 @@ export const ChannelsPage = (): JSX.Element => {
> >
<Tabs defaultValue="0"> <Tabs defaultValue="0">
<TabsList> <TabsList>
{channels.map((channel) => ( {allChannels.map((channel) => (
<TabsTrigger key={channel.index} value={channel.index.toString()}> <TabsTrigger key={channel.index} value={channel.index.toString()}>
{getChannelName(channel)} {getChannelName(channel)}
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
{channels.map((channel) => ( {allChannels.map((channel) => (
<TabsContent key={channel.index} value={channel.index.toString()}> <TabsContent key={channel.index} value={channel.index.toString()}>
<Channel key={channel.index} channel={channel} /> <Channel key={channel.index} channel={channel} />
</TabsContent> </TabsContent>

49
src/pages/Messages.tsx

@ -20,31 +20,34 @@ export const MessagesPage = (): JSX.Element => {
const filteredNodes = Array.from(nodes.values()).filter( const filteredNodes = Array.from(nodes.values()).filter(
(n) => n.num !== hardware.myNodeNum (n) => n.num !== hardware.myNodeNum
); );
const allChannels = Array.from(channels.values());
const filteredChannels = allChannels.filter(
(ch) => ch.role !== Protobuf.Channel_Role.DISABLED
);
const currentChannel = channels.get(activeChat);
return ( return (
<> <>
<Sidebar> <Sidebar>
<SidebarSection label="Channels"> <SidebarSection label="Channels">
{channels {filteredChannels.map((channel) => (
.filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED) <SidebarButton
.map((channel) => ( key={channel.index}
<SidebarButton label={
key={channel.index} channel.settings?.name.length
label={ ? channel.settings?.name
channel.settings?.name.length : channel.index === 0
? channel.settings?.name ? "Primary"
: channel.index === 0 : `Ch ${channel.index}`
? "Primary" }
: `Ch ${channel.index}` active={activeChat === channel.index}
} onClick={() => {
active={activeChat === channel.index} setChatType("broadcast");
onClick={() => { setActiveChat(channel.index);
setChatType("broadcast"); }}
setActiveChat(channel.index); element={<HashIcon size={16} className="mr-2" />}
}} />
element={<HashIcon size={16} className="mr-2" />} ))}
/>
))}
</SidebarSection> </SidebarSection>
<SidebarSection label="Peers"> <SidebarSection label="Peers">
{filteredNodes.map((node) => ( {filteredNodes.map((node) => (
@ -63,14 +66,14 @@ export const MessagesPage = (): JSX.Element => {
</Sidebar> </Sidebar>
<PageLayout <PageLayout
label={`Messages: ${ label={`Messages: ${
chatType === "broadcast" && channels[activeChat] chatType === "broadcast" && currentChannel
? getChannelName(channels[activeChat]) ? getChannelName(currentChannel)
: chatType === "direct" && nodes.get(activeChat) : chatType === "direct" && nodes.get(activeChat)
? nodes.get(activeChat)?.user?.longName ?? "Unknown" ? nodes.get(activeChat)?.user?.longName ?? "Unknown"
: "Loading..." : "Loading..."
}`} }`}
> >
{channels.map( {allChannels.map(
(channel) => (channel) =>
activeChat === channel.index && ( activeChat === channel.index && (
<ChannelChat <ChannelChat

19
src/validation/channelSettings.ts → src/validation/channel.ts

@ -7,15 +7,24 @@ import {
Length Length
} from "class-validator"; } from "class-validator";
import type { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
export class ChannelSettingsValidation export class ChannelValidation
implements Omit<Protobuf.Channel, keyof Protobuf.native.Message | "settings">
{
@IsNumber()
index: number;
settings: Channel_SettingsValidation;
@IsEnum(Protobuf.Channel_Role)
role: Protobuf.Channel_Role;
}
export class Channel_SettingsValidation
implements implements
Omit<Protobuf.ChannelSettings, keyof Protobuf.native.Message | "psk"> Omit<Protobuf.ChannelSettings, keyof Protobuf.native.Message | "psk">
{ {
@IsBoolean()
enabled: boolean;
@IsNumber() @IsNumber()
channelNum: number; channelNum: number;

10
src/validation/config/network.ts

@ -1,4 +1,11 @@
import { IsBoolean, IsEnum, IsIP, IsOptional, Length } from "class-validator"; import {
IsBoolean,
IsEnum,
IsIP,
IsOptional,
IsString,
Length
} from "class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
@ -31,6 +38,7 @@ export class NetworkValidation
ipv4Config: NetworkValidation_IpV4Config; ipv4Config: NetworkValidation_IpV4Config;
@IsString()
rsyslogServer: string; rsyslogServer: string;
} }

Loading…
Cancel
Save