2.0 Overhaul start
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button, LayerIcon, LayerOutlineIcon, Tooltip } from "evergreen-ui";
|
||||
import { IoQrCodeOutline } from "react-icons/io5";
|
||||
|
||||
import { Channel } from "@app/components/PageComponents/Channel.js";
|
||||
import { Button } from "@components/Button.js";
|
||||
import { QRDialog } from "@components/Dialog/QRDialog.js";
|
||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { QrCodeIcon } from "@heroicons/react/24/outline";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
import { Channel } from "@pages/Channels/Channel.js";
|
||||
|
||||
export const ChannelsPage = (): JSX.Element => {
|
||||
const { channels, config } = useDevice();
|
||||
@@ -21,10 +20,6 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
: channel.config.role === Protobuf.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.config.index}`,
|
||||
icon:
|
||||
channel.config.role !== Protobuf.Channel_Role.DISABLED
|
||||
? LayerIcon
|
||||
: LayerOutlineIcon,
|
||||
element: () => <Channel channel={channel.config} />,
|
||||
};
|
||||
});
|
||||
@@ -43,16 +38,15 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
tabs={tabs}
|
||||
actions={[
|
||||
() => (
|
||||
<Tooltip content="Open QR code generator">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setQRDialogOpen(true);
|
||||
}}
|
||||
iconBefore={IoQrCodeOutline}
|
||||
>
|
||||
QR Code
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="secondary"
|
||||
iconBefore={<QrCodeIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
setQRDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
QR Code
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
@@ -1,205 +0,0 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import {
|
||||
Button,
|
||||
EyeOffIcon,
|
||||
EyeOpenIcon,
|
||||
FormField,
|
||||
IconButton,
|
||||
majorScale,
|
||||
Pane,
|
||||
RefreshIcon,
|
||||
SelectField,
|
||||
Switch,
|
||||
TextInputField,
|
||||
Tooltip,
|
||||
} from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
const { connection } = useDevice();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||
const [pskHidden, setPskHidden] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty },
|
||||
reset,
|
||||
control,
|
||||
setValue,
|
||||
} = useForm<
|
||||
Omit<Protobuf.ChannelSettings, "psk"> & { psk: string; enabled: boolean }
|
||||
>({
|
||||
defaultValues: {
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
});
|
||||
}, [channel, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
const channelData = Protobuf.Channel.create({
|
||||
role:
|
||||
channel?.role === Protobuf.Channel_Role.PRIMARY
|
||||
? Protobuf.Channel_Role.PRIMARY
|
||||
: data.enabled
|
||||
? Protobuf.Channel_Role.SECONDARY
|
||||
: Protobuf.Channel_Role.DISABLED,
|
||||
index: channel?.index,
|
||||
settings: {
|
||||
...data,
|
||||
psk: toByteArray(data.psk ?? ""),
|
||||
},
|
||||
});
|
||||
|
||||
await connection?.setChannel(channelData, (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
{channel?.index !== 0 && (
|
||||
<>
|
||||
<FormField
|
||||
label="Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch
|
||||
height={24}
|
||||
marginLeft="auto"
|
||||
checked={value}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
label="Name"
|
||||
description="Max transmit power in dBm"
|
||||
{...register("name")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<SelectField
|
||||
width="100%"
|
||||
label="Key Size"
|
||||
description="Desired size of generated key."
|
||||
value={keySize}
|
||||
onChange={(e): void => {
|
||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||
}}
|
||||
>
|
||||
<option value={128}>128 Bit</option>
|
||||
<option value={256}>256 Bit</option>
|
||||
</SelectField>
|
||||
<Tooltip content="Generate new key">
|
||||
<IconButton
|
||||
marginTop={majorScale(6)}
|
||||
onClick={(
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
const key = new Uint8Array(keySize / 8);
|
||||
crypto.getRandomValues(key);
|
||||
setValue("psk", fromByteArray(key));
|
||||
}}
|
||||
icon={<RefreshIcon />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Pane>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<TextInputField
|
||||
width="100%"
|
||||
label="Pre-Shared Key"
|
||||
description="Max transmit power in dBm"
|
||||
type={pskHidden ? "password" : "text"}
|
||||
{...register("psk")}
|
||||
/>
|
||||
<Tooltip content={pskHidden ? "Show key" : "Hide key"}>
|
||||
<Button
|
||||
marginTop={majorScale(6)}
|
||||
width={majorScale(12)}
|
||||
onClick={(
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
setPskHidden(!pskHidden);
|
||||
}}
|
||||
iconBefore={pskHidden ? <EyeOpenIcon /> : <EyeOffIcon />}
|
||||
>
|
||||
{pskHidden ? "Show" : "Hide"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Pane>
|
||||
<FormField
|
||||
label="Uplink Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.uplinkEnabled?.message}
|
||||
validationMessage={errors.uplinkEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="uplinkEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Downlink Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.downlinkEnabled?.message}
|
||||
validationMessage={errors.downlinkEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="downlinkEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,12 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
export const AppConfig = (): JSX.Element => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const configSections = [
|
||||
{
|
||||
label: "Interface",
|
||||
@@ -26,31 +23,31 @@ export const AppConfig = (): JSX.Element => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Pane display="flex">
|
||||
<Pane flexBasis={150} marginRight={24}>
|
||||
<Tablist>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
direction="vertical"
|
||||
isSelected={index === selectedIndex}
|
||||
onSelect={() => setSelectedIndex(index)}
|
||||
>
|
||||
{Config.label}
|
||||
</Tab>
|
||||
))}
|
||||
</Tablist>
|
||||
</Pane>
|
||||
<Pane flex="1">
|
||||
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||
<Tab.List className="flex flex-col w-44 gap-1">
|
||||
{configSections.map((Config, index) => (
|
||||
<Pane
|
||||
key={index}
|
||||
display={index === selectedIndex ? "block" : "none"}
|
||||
>
|
||||
<Config.element />
|
||||
</Pane>
|
||||
<Tab key={index} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||
selected
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{Config.label}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Pane>
|
||||
</Pane>
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab.Panel key={index} as={Fragment}>
|
||||
<Config.element />
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Network } from "@app/components/PageComponents/Config/Network.js";
|
||||
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.js";
|
||||
@@ -12,9 +10,9 @@ import { Position } from "@components/PageComponents/Config/Position.js";
|
||||
import { Power } from "@components/PageComponents/Config/Power.js";
|
||||
import { User } from "@components/PageComponents/Config/User.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
export const DeviceConfig = (): JSX.Element => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const { hardware } = useDevice();
|
||||
|
||||
const configSections = [
|
||||
@@ -54,32 +52,31 @@ export const DeviceConfig = (): JSX.Element => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Pane display="flex">
|
||||
<Pane flexBasis={150} marginRight={24}>
|
||||
<Tablist>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
direction="vertical"
|
||||
isSelected={index === selectedIndex}
|
||||
onSelect={() => setSelectedIndex(index)}
|
||||
disabled={Config.disabled}
|
||||
>
|
||||
{Config.label}
|
||||
</Tab>
|
||||
))}
|
||||
</Tablist>
|
||||
</Pane>
|
||||
<Pane flex="1">
|
||||
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||
<Tab.List className="flex flex-col w-44 gap-1">
|
||||
{configSections.map((Config, index) => (
|
||||
<Pane
|
||||
key={index}
|
||||
display={index === selectedIndex ? "block" : "none"}
|
||||
>
|
||||
<Config.element />
|
||||
</Pane>
|
||||
<Tab key={index} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||
selected
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{Config.label}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Pane>
|
||||
</Pane>
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab.Panel key={index} as={Fragment}>
|
||||
<Config.element />
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Pane, Tab, Tablist } from "evergreen-ui";
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage";
|
||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||
@@ -10,6 +8,7 @@ import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js"
|
||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
||||
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
|
||||
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
export const ModuleConfig = (): JSX.Element => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -46,31 +45,31 @@ export const ModuleConfig = (): JSX.Element => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Pane display="flex">
|
||||
<Pane flexBasis={150} marginRight={24}>
|
||||
<Tablist>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
direction="vertical"
|
||||
isSelected={index === selectedIndex}
|
||||
onSelect={() => setSelectedIndex(index)}
|
||||
>
|
||||
{Config.label}
|
||||
</Tab>
|
||||
))}
|
||||
</Tablist>
|
||||
</Pane>
|
||||
<Pane flex="1">
|
||||
<Tab.Group as="div" className="flex gap-3 w-full">
|
||||
<Tab.List className="flex flex-col gap-1 w-44">
|
||||
{configSections.map((Config, index) => (
|
||||
<Pane
|
||||
key={index}
|
||||
display={index === selectedIndex ? "block" : "none"}
|
||||
>
|
||||
<Config.element />
|
||||
</Pane>
|
||||
<Tab key={index} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md cursor-pointer ${
|
||||
selected
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{Config.label}
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Pane>
|
||||
</Pane>
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
{configSections.map((Config, index) => (
|
||||
<Tab.Panel key={index} as={Fragment}>
|
||||
<Config.element />
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type React from "react";
|
||||
|
||||
import { ApplicationsIcon, CogIcon, CubeIcon } from "evergreen-ui";
|
||||
|
||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
CubeTransparentIcon,
|
||||
WindowIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { AppConfig } from "@pages/Config/AppConfig.js";
|
||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
||||
import { ModuleConfig } from "@pages/Config/ModuleConfig.js";
|
||||
@@ -11,17 +14,17 @@ export const ConfigPage = (): JSX.Element => {
|
||||
const tabs: TabType[] = [
|
||||
{
|
||||
name: "Device Config",
|
||||
icon: CogIcon,
|
||||
icon: <Cog8ToothIcon className="h-4" />,
|
||||
element: DeviceConfig,
|
||||
},
|
||||
{
|
||||
name: "Module Config",
|
||||
icon: CubeIcon,
|
||||
icon: <CubeTransparentIcon className="h-4" />,
|
||||
element: ModuleConfig,
|
||||
},
|
||||
{
|
||||
name: "App Config",
|
||||
icon: ApplicationsIcon,
|
||||
icon: <WindowIcon className="h-4" />,
|
||||
element: AppConfig,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type React from "react";
|
||||
|
||||
import { Pane } from "evergreen-ui";
|
||||
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
|
||||
export const Environment = (): JSX.Element => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<div>
|
||||
{nodes.map((node, index) => (
|
||||
<Pane key={index}>{JSON.stringify(node.environmentMetrics)}</Pane>
|
||||
<div key={index}>{JSON.stringify(node.environmentMetrics)}</div>
|
||||
))}
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Pane } from "evergreen-ui";
|
||||
|
||||
export interface File {
|
||||
nameModified: string;
|
||||
name: string;
|
||||
@@ -33,9 +31,9 @@ export const FileBrowser = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<div>
|
||||
{data?.data.files.map((file) => (
|
||||
<Pane key={file.name}>
|
||||
<div key={file.name}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -43,8 +41,8 @@ export const FileBrowser = (): JSX.Element => {
|
||||
>
|
||||
{file.name.replace("static/", "").replace(".gz", "")}
|
||||
</a>
|
||||
</Pane>
|
||||
</div>
|
||||
))}
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type React from "react";
|
||||
|
||||
import { DocumentIcon, GanttChartIcon, RainIcon } from "evergreen-ui";
|
||||
|
||||
import { TabbedContent, TabType } from "@components/layout/page/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";
|
||||
|
||||
@@ -13,19 +16,19 @@ export const ExtensionsPage = (): JSX.Element => {
|
||||
const tabs: TabType[] = [
|
||||
{
|
||||
name: "File Browser",
|
||||
icon: DocumentIcon,
|
||||
icon: <DocumentIcon className="h-4" />,
|
||||
element: FileBrowser,
|
||||
disabled: !hardware.hasWifi,
|
||||
},
|
||||
{
|
||||
name: "Range Test",
|
||||
icon: GanttChartIcon,
|
||||
icon: <SignalIcon className="h-4" />,
|
||||
element: FileBrowser,
|
||||
disabled: !hardware.hasWifi,
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
icon: RainIcon,
|
||||
icon: <CloudIcon className="h-4" />,
|
||||
element: Environment,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type React from "react";
|
||||
|
||||
import { Pane } from "evergreen-ui";
|
||||
import JSONPretty from "react-json-pretty";
|
||||
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -11,9 +10,9 @@ export const InfoPage = (): JSX.Element => {
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<div>
|
||||
<JSONPretty data={myNode} />
|
||||
<JSONPretty data={hardware} />
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +1,29 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
Heading,
|
||||
IconButton,
|
||||
LocateIcon,
|
||||
majorScale,
|
||||
MapMarkerIcon,
|
||||
Pane,
|
||||
Text,
|
||||
} from "evergreen-ui";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Map, Marker, useMap } from "react-map-gl";
|
||||
|
||||
import { IconButton } from "@app/components/IconButton.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { MapPinIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const MapPage = (): JSX.Element => {
|
||||
const { nodes, waypoints } = useDevice();
|
||||
const { current: map } = useMap();
|
||||
|
||||
return (
|
||||
<Pane
|
||||
margin={majorScale(3)}
|
||||
borderRadius={majorScale(1)}
|
||||
elevation={1}
|
||||
display="flex"
|
||||
flexGrow={1}
|
||||
flexDirection="column"
|
||||
gap={majorScale(2)}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
>
|
||||
<Pane
|
||||
position="absolute"
|
||||
zIndex={10}
|
||||
right={0}
|
||||
top={0}
|
||||
borderRadius={majorScale(1)}
|
||||
padding={majorScale(1)}
|
||||
margin={majorScale(1)}
|
||||
background="tint1"
|
||||
width={majorScale(28)}
|
||||
elevation={1}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Pane padding={majorScale(1)} background="tint2">
|
||||
<Heading>Title</Heading>
|
||||
</Pane>
|
||||
<Pane display="flex" flexDirection="column" gap={majorScale(1)}>
|
||||
<div className="flex-grow">
|
||||
<div className="absolute z-10 right-0 top-0 m-2 rounded-md p-2 shadow-md bg-white">
|
||||
<div className="font-medium text-lg p-1">Title</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{nodes.map((n) => (
|
||||
<Pane key={n.data.num} display="flex" gap={majorScale(1)}>
|
||||
<div key={n.data.num} className="flex gap-2">
|
||||
<Hashicon value={n.data.num.toString()} size={24} />
|
||||
<Text>{n.data.user?.longName}</Text>
|
||||
<div>{n.data.user?.longName}</div>
|
||||
<IconButton
|
||||
icon={LocateIcon}
|
||||
marginLeft="auto"
|
||||
size="small"
|
||||
icon={<MapPinIcon className="h-4" />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (n.data.position?.latitudeI) {
|
||||
map?.flyTo({
|
||||
@@ -68,10 +36,10 @@ export const MapPage = (): JSX.Element => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pane>
|
||||
</div>
|
||||
))}
|
||||
</Pane>
|
||||
</Pane>
|
||||
</div>
|
||||
</div>
|
||||
<Map
|
||||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
||||
mapLib={maplibregl}
|
||||
@@ -84,9 +52,9 @@ export const MapPage = (): JSX.Element => {
|
||||
latitude={wp.latitudeI / 1e7}
|
||||
anchor="bottom"
|
||||
>
|
||||
<Pane>
|
||||
<MapMarkerIcon />
|
||||
</Pane>
|
||||
<div>
|
||||
<MapPinIcon className="h-4" />
|
||||
</div>
|
||||
</Marker>
|
||||
))}
|
||||
{nodes.map((n) => {
|
||||
@@ -104,6 +72,6 @@ export const MapPage = (): JSX.Element => {
|
||||
}
|
||||
})}
|
||||
</Map>
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,19 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
CircleIcon,
|
||||
EditIcon,
|
||||
IconButton,
|
||||
Pane,
|
||||
RingIcon,
|
||||
Tooltip,
|
||||
} from "evergreen-ui";
|
||||
|
||||
import { IconButton } from "@app/components/IconButton.js";
|
||||
import {
|
||||
TabbedContent,
|
||||
TabType,
|
||||
} from "@components/layout/page/TabbedContent.js";
|
||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import {
|
||||
EllipsisHorizontalCircleIcon,
|
||||
PencilIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
import { ChannelChat } from "./ChannelChat.js";
|
||||
|
||||
export const MessagesPage = (): JSX.Element => {
|
||||
const { channels, setActivePage } = useDevice();
|
||||
|
||||
@@ -28,29 +24,32 @@ export const MessagesPage = (): JSX.Element => {
|
||||
: channel.config.index === 0
|
||||
? "Primary"
|
||||
: `Ch ${channel.config.index}`,
|
||||
icon: channel.messages.length ? RingIcon : CircleIcon,
|
||||
icon: channel.messages.length ? (
|
||||
<EllipsisHorizontalCircleIcon className="h-4" />
|
||||
) : (
|
||||
<XCircleIcon className="h-4" />
|
||||
),
|
||||
element: () => <ChannelChat channel={channel} />,
|
||||
disabled: channel.config.role === Protobuf.Channel_Role.DISABLED,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Pane display="flex" flexDirection="column" width="100%">
|
||||
<div className="flex flex-col w-full">
|
||||
<TabbedContent
|
||||
tabs={tabs}
|
||||
actions={[
|
||||
() => (
|
||||
<Tooltip content="Edit Channels">
|
||||
<IconButton
|
||||
icon={EditIcon}
|
||||
onClick={() => {
|
||||
setActivePage("channels");
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<PencilIcon className="h-4" />}
|
||||
onClick={() => {
|
||||
setActivePage("channels");
|
||||
}}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import type React from "react";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
import {
|
||||
AddLocationIcon,
|
||||
IconButton,
|
||||
majorScale,
|
||||
Pane,
|
||||
Popover,
|
||||
SendMessageIcon,
|
||||
TextInputField,
|
||||
Tooltip,
|
||||
} from "evergreen-ui";
|
||||
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import type { Channel } from "@core/stores/deviceStore.js";
|
||||
import { Message } from "@pages/Messages/Message.js";
|
||||
import { NewLocationMessage } from "@pages/Messages/NewLocationMessage.js";
|
||||
|
||||
export interface ChannelChatProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||
const { nodes, connection, ackMessage } = useDevice();
|
||||
const [currentMessage, setCurrentMessage] = useState("");
|
||||
|
||||
const sendMessage = (): void => {
|
||||
void connection?.sendText(
|
||||
currentMessage,
|
||||
undefined,
|
||||
true,
|
||||
channel.config.index,
|
||||
(id) => {
|
||||
ackMessage(channel.config.index, id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
);
|
||||
setCurrentMessage("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Pane display="flex" flexDirection="column" flexGrow={1}>
|
||||
<Pane display="flex" flexDirection="column" flexGrow={1}>
|
||||
{channel.messages.map((message, index) => (
|
||||
<Message
|
||||
key={index}
|
||||
message={message}
|
||||
lastMsgSameUser={
|
||||
index === 0
|
||||
? false
|
||||
: channel.messages[index - 1].packet.from ===
|
||||
message.packet.from
|
||||
}
|
||||
sender={
|
||||
nodes.find((node) => node.data.num === message.packet.from)?.data
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pane>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<form
|
||||
style={{ display: "flex", flexGrow: 1 }}
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}}
|
||||
>
|
||||
<Pane display="flex" flexGrow={1} gap={majorScale(1)}>
|
||||
<TextInputField
|
||||
marginTop="auto"
|
||||
minLength={2}
|
||||
width="100%"
|
||||
label=""
|
||||
placeholder="Enter Message"
|
||||
marginBottom={0}
|
||||
value={currentMessage}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentMessage(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Tooltip content="Send">
|
||||
<IconButton
|
||||
icon={SendMessageIcon}
|
||||
marginTop={majorScale(2)}
|
||||
width={majorScale(8)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Pane>
|
||||
</form>
|
||||
<Tooltip content="Send Location">
|
||||
<Popover content={<NewLocationMessage />}>
|
||||
<IconButton
|
||||
icon={AddLocationIcon}
|
||||
marginTop={majorScale(2)}
|
||||
width={majorScale(8)}
|
||||
/>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
CircleIcon,
|
||||
FullCircleIcon,
|
||||
majorScale,
|
||||
Pane,
|
||||
Small,
|
||||
Strong,
|
||||
Text,
|
||||
} from "evergreen-ui";
|
||||
|
||||
import type { AllMessageTypes } from "@app/core/stores/deviceStore.js";
|
||||
import { WaypointMessage } from "@app/pages/Messages/WaypointMessage.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export interface MessageProps {
|
||||
lastMsgSameUser: boolean;
|
||||
message: AllMessageTypes;
|
||||
sender?: Protobuf.NodeInfo;
|
||||
}
|
||||
|
||||
export const Message = ({
|
||||
lastMsgSameUser,
|
||||
message,
|
||||
sender,
|
||||
}: MessageProps): JSX.Element => {
|
||||
const { setPeerInfoOpen, setActivePeer } = useDevice();
|
||||
|
||||
const openPeer = (): void => {
|
||||
setActivePeer(message.packet.from);
|
||||
setPeerInfoOpen(true);
|
||||
};
|
||||
|
||||
return lastMsgSameUser ? (
|
||||
<Pane display="flex" marginLeft={majorScale(3)}>
|
||||
{message.ack ? (
|
||||
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
|
||||
) : (
|
||||
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<Text
|
||||
color={message.ack ? "#474d66" : "#9c9fab"}
|
||||
marginLeft={majorScale(2)}
|
||||
paddingLeft={majorScale(1)}
|
||||
borderLeft="3px solid #e6e6e6"
|
||||
>
|
||||
{message.text}
|
||||
</Text>
|
||||
)}
|
||||
</Pane>
|
||||
) : (
|
||||
<Pane marginX={majorScale(2)} gap={majorScale(1)} marginTop={majorScale(1)}>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<Pane onClick={openPeer} cursor="pointer" width={majorScale(3)}>
|
||||
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
||||
</Pane>
|
||||
<Strong onClick={openPeer} cursor="pointer" size={500}>
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</Strong>
|
||||
<Small>
|
||||
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Small>
|
||||
</Pane>
|
||||
<Pane display="flex" marginLeft={majorScale(1)}>
|
||||
{message.ack ? (
|
||||
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
|
||||
) : (
|
||||
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<Text
|
||||
color={message.ack ? "#474d66" : "#9c9fab"}
|
||||
marginLeft={majorScale(2)}
|
||||
paddingLeft={majorScale(1)}
|
||||
borderLeft="3px solid #e6e6e6"
|
||||
>
|
||||
{message.text}
|
||||
</Text>
|
||||
)}
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
majorScale,
|
||||
Pane,
|
||||
SelectField,
|
||||
TextInputField,
|
||||
} from "evergreen-ui";
|
||||
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
enum LocationType {
|
||||
MGRS,
|
||||
LatLng,
|
||||
DecimalDegrees,
|
||||
}
|
||||
|
||||
export const NewLocationMessage = (): JSX.Element => {
|
||||
const { connection } = useDevice();
|
||||
|
||||
return (
|
||||
<Pane width={240} margin={majorScale(2)}>
|
||||
<form
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<TextInputField label="Name" />
|
||||
<TextInputField label="Description" />
|
||||
<SelectField label="Type" value={LocationType.MGRS}>
|
||||
{renderOptions(LocationType)}
|
||||
</SelectField>
|
||||
<TextInputField label="Coordinates" />
|
||||
<Button
|
||||
width="100%"
|
||||
onClick={() => {
|
||||
void connection?.sendWaypoint(
|
||||
Protobuf.Waypoint.create({
|
||||
latitudeI: Math.floor(3.89103 * 1e7),
|
||||
longitudeI: Math.floor(105.87005 * 1e7),
|
||||
name: "TEST",
|
||||
description: "This is a description",
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</form>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
Heading,
|
||||
LocateIcon,
|
||||
majorScale,
|
||||
minorScale,
|
||||
Pane,
|
||||
Small,
|
||||
Text,
|
||||
} from "evergreen-ui";
|
||||
|
||||
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||
import { toMGRS } from "@core/utils/toMGRS.js";
|
||||
|
||||
export interface WaypointMessageProps {
|
||||
waypointID: number;
|
||||
}
|
||||
|
||||
export const WaypointMessage = ({
|
||||
waypointID,
|
||||
}: WaypointMessageProps): JSX.Element => {
|
||||
const { waypoints } = useDevice();
|
||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||
|
||||
return (
|
||||
<Pane
|
||||
marginLeft={majorScale(2)}
|
||||
paddingLeft={majorScale(1)}
|
||||
borderLeft="3px solid #e6e6e6"
|
||||
>
|
||||
<Pane
|
||||
gap={majorScale(1)}
|
||||
display="flex"
|
||||
borderRadius={majorScale(1)}
|
||||
elevation={1}
|
||||
padding={minorScale(1)}
|
||||
>
|
||||
<LocateIcon color="#474d66" marginY="auto" />
|
||||
<Pane>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<Heading>{waypoint?.name}</Heading>
|
||||
<Text color="orange">
|
||||
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
||||
</Text>
|
||||
</Pane>
|
||||
<Small>{waypoint?.description}</Small>
|
||||
</Pane>
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user