diff --git a/package.json b/package.json index ccb357a1..f4d3e411 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "@heroicons/react": "^2.0.13", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^2.9.10", - "@meshtastic/meshtasticjs": "^0.7.2", + "@meshtastic/meshtasticjs": "^0.7.3", "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.8", "base64-js": "^1.5.1", "chart.js": "^4.0.1", - "chartjs-adapter-date-fns": "^2.0.1", + "chartjs-adapter-date-fns": "^3.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "date-fns": "^2.29.3", @@ -56,9 +56,9 @@ }, "devDependencies": { "@tailwindcss/forms": "^0.5.3", - "@types/chrome": "^0.0.203", + "@types/chrome": "^0.0.204", "@types/geodesy": "^2.2.3", - "@types/node": "^18.11.12", + "@types/node": "^18.11.13", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/w3c-web-serial": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56d4a6bd..2e705ace 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,13 +6,13 @@ specifiers: '@heroicons/react': ^2.0.13 '@hookform/error-message': ^2.0.1 '@hookform/resolvers': ^2.9.10 - '@meshtastic/meshtasticjs': ^0.7.2 + '@meshtastic/meshtasticjs': ^0.7.3 '@tailwindcss/forms': ^0.5.3 '@tailwindcss/line-clamp': ^0.4.2 '@tailwindcss/typography': ^0.5.8 - '@types/chrome': ^0.0.203 + '@types/chrome': ^0.0.204 '@types/geodesy': ^2.2.3 - '@types/node': ^18.11.12 + '@types/node': ^18.11.13 '@types/react': ^18.0.26 '@types/react-dom': ^18.0.9 '@types/w3c-web-serial': ^1.0.3 @@ -23,7 +23,7 @@ specifiers: autoprefixer: ^10.4.13 base64-js: ^1.5.1 chart.js: ^4.0.1 - chartjs-adapter-date-fns: ^2.0.1 + chartjs-adapter-date-fns: ^3.0.0 class-transformer: ^0.5.1 class-validator: ^0.14.0 date-fns: ^2.29.3 @@ -69,12 +69,12 @@ dependencies: '@heroicons/react': 2.0.13_react@18.2.0 '@hookform/error-message': 2.0.1_nrnvpvixg5xdweezd67llqjwze '@hookform/resolvers': 2.9.10_react-hook-form@7.40.0 - '@meshtastic/meshtasticjs': 0.7.2 + '@meshtastic/meshtasticjs': 0.7.3 '@tailwindcss/line-clamp': 0.4.2_tailwindcss@3.2.4 '@tailwindcss/typography': 0.5.8_tailwindcss@3.2.4 base64-js: 1.5.1 chart.js: 4.0.1 - chartjs-adapter-date-fns: 2.0.1_chart.js@4.0.1 + chartjs-adapter-date-fns: 3.0.0_thp3sedjxvmiqxrdsmekdnimom class-transformer: 0.5.1 class-validator: 0.14.0 date-fns: 2.29.3 @@ -98,9 +98,9 @@ dependencies: devDependencies: '@tailwindcss/forms': 0.5.3_tailwindcss@3.2.4 - '@types/chrome': 0.0.203 + '@types/chrome': 0.0.204 '@types/geodesy': 2.2.3 - '@types/node': 18.11.12 + '@types/node': 18.11.13 '@types/react': 18.0.26 '@types/react-dom': 18.0.9 '@types/w3c-web-serial': 1.0.3 @@ -125,7 +125,7 @@ devDependencies: tslib: 2.4.1 typescript: 4.9.4 unimported: 1.23.0 - vite: 4.0.0_@types+node@18.11.12 + vite: 4.0.0_@types+node@18.11.13 vite-plugin-environment: 1.1.3_vite@4.0.0 packages: @@ -785,8 +785,8 @@ packages: engines: {node: '>=6.0.0'} dev: false - /@meshtastic/meshtasticjs/0.7.2: - resolution: {integrity: sha512-RJCJLtlGUn7To+I4MnxuSDxwWK/MgAAfbfq+Naib6fve2VQEBu2CPF2Iy0A+wAHxR1ygOKcS8VbyS2D1FAQhcw==} + /@meshtastic/meshtasticjs/0.7.3: + resolution: {integrity: sha512-9NvWvTQSqCRNhYrE7FnhW9/bVSmnZ9LYGy04P+8IwhQzlYduhnaIb5vDKgLSURl5YhpnHlWOQFJVXs8tuZv2CQ==} dependencies: '@protobuf-ts/runtime': 2.8.2 glob: 8.0.3 @@ -887,8 +887,8 @@ packages: resolution: {integrity: sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg==} dev: false - /@types/chrome/0.0.203: - resolution: {integrity: sha512-JlQNebwpBETVc8U1Rr2inDFuOTtn0lahRAhnddx1dd0S5RrLAFJEEsyIu7AXI14mkCgSunksnuLGioH8kvBqRA==} + /@types/chrome/0.0.204: + resolution: {integrity: sha512-EvnHfxMHUWP5EAlRMK66uIEUiy36t72vg5RwmzQv9tdIl2ZmAp92NwvmEZJKpbRnIMTEc2BmSmtrFiEISUJ0Sw==} dependencies: '@types/filesystem': 0.0.32 '@types/har-format': 1.2.10 @@ -946,8 +946,8 @@ packages: '@types/pbf': 3.0.2 dev: false - /@types/node/18.11.12: - resolution: {integrity: sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg==} + /@types/node/18.11.13: + resolution: {integrity: sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w==} dev: true /@types/normalize-package-data/2.4.1: @@ -1153,7 +1153,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.5 magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.0.0_@types+node@18.11.12 + vite: 4.0.0_@types+node@18.11.13 transitivePeerDependencies: - supports-color dev: true @@ -1435,12 +1435,14 @@ packages: engines: {pnpm: ^7.0.0} dev: false - /chartjs-adapter-date-fns/2.0.1_chart.js@4.0.1: - resolution: {integrity: sha512-v3WV9rdnQ05ce3A0ZCjzUekJCAbfm6+3HqSoeY2BIkdMYZoYr/4T+ril1tZyDl869lz6xdNVMXejUFT9YKpw4A==} + /chartjs-adapter-date-fns/3.0.0_thp3sedjxvmiqxrdsmekdnimom: + resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} peerDependencies: chart.js: '>=2.8.0' + date-fns: '>=2.0.0' dependencies: chart.js: 4.0.1 + date-fns: 2.29.3 dev: false /chokidar/3.5.3: @@ -3758,8 +3760,8 @@ packages: yargs: 17.6.2 dev: true - /rollup/3.7.1: - resolution: {integrity: sha512-ek6+FORvI79VQTNlIYtXpIrGEPRlYSNZO+5EcmaozKkRL5L6KLvGDUbM5E+bd6jnHW9fgcK0DKTdWjIsEmNb4g==} + /rollup/3.7.2: + resolution: {integrity: sha512-orqIX5zkHyHKVsIBl8J5a2tnVikOAMte0DgOLViyW6McYuj45FG+cQPrXILhaifBSmy0D0hKbHg2RbgzFJcwTg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -4288,10 +4290,10 @@ packages: peerDependencies: vite: '>= 2.7' dependencies: - vite: 4.0.0_@types+node@18.11.12 + vite: 4.0.0_@types+node@18.11.13 dev: true - /vite/4.0.0_@types+node@18.11.12: + /vite/4.0.0_@types+node@18.11.13: resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -4316,11 +4318,11 @@ packages: terser: optional: true dependencies: - '@types/node': 18.11.12 + '@types/node': 18.11.13 esbuild: 0.16.4 postcss: 8.4.19 resolve: 1.22.1 - rollup: 3.7.1 + rollup: 3.7.2 optionalDependencies: fsevents: 2.3.2 dev: true diff --git a/src/components/CommandPalette/Index.tsx b/src/components/CommandPalette/Index.tsx index f1090214..14f6e8de 100644 --- a/src/components/CommandPalette/Index.tsx +++ b/src/components/CommandPalette/Index.tsx @@ -40,6 +40,7 @@ import { MapIcon, MoonIcon, PlusIcon, + PowerIcon, QrCodeIcon, Square3Stack3DIcon, TrashIcon, @@ -61,9 +62,16 @@ export interface Command { export const CommandPalette = (): JSX.Element => { const [query, setQuery] = useState(""); - const [open, setOpen] = useState(false); + const { commandPaletteOpen, setCommandPaletteOpen } = useAppStore(); + // const [open, setOpen] = useState(false); - const { setQRDialogOpen, setActivePage, connection } = useDevice(); + const { + setQRDialogOpen, + setShutdownDialogOpen, + setRebootDialogOpen, + setActivePage, + connection + } = useDevice(); const { setSelectedDevice, removeDevice, selectedDevice } = useAppStore(); const groups: Group[] = [ @@ -181,6 +189,20 @@ export const CommandPalette = (): JSX.Element => { setSelectedDevice(0); removeDevice(selectedDevice ?? 0); } + }, + { + name: "Schedule Shutdown", + icon: PowerIcon, + action() { + setShutdownDialogOpen(true); + } + }, + { + name: "Schedule Reboot", + icon: ArrowPathIcon, + action() { + setRebootDialogOpen(true); + } } ] }, @@ -222,7 +244,7 @@ export const CommandPalette = (): JSX.Element => { const handleKeydown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); - setOpen(true); + setCommandPaletteOpen(true); } }; @@ -249,12 +271,16 @@ export const CommandPalette = (): JSX.Element => { return ( setQuery("")} appear > - + @@ -262,7 +288,7 @@ export const CommandPalette = (): JSX.Element => { if (typeof input === "string") { setQuery(input); } else if (input.action) { - setOpen(false); + setCommandPaletteOpen(false); input.action(); } }} diff --git a/src/components/DeviceSelector.tsx b/src/components/DeviceSelector.tsx index 55c0e9de..e6e7f3de 100644 --- a/src/components/DeviceSelector.tsx +++ b/src/components/DeviceSelector.tsx @@ -4,7 +4,7 @@ import { useAppStore } from "@app/core/stores/appStore.js"; import { useDeviceStore } from "@app/core/stores/deviceStore.js"; import { Mono } from "@components/generic/Mono.js"; import { Hashicon } from "@emeraldpay/hashicon-react"; -import { CommandLineIcon, PlusIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; export const DeviceSelector = (): JSX.Element => { const { getDevices } = useDeviceStore(); @@ -43,14 +43,7 @@ export const DeviceSelector = (): JSX.Element => { - -
- - - Ctrl+ - K - -
+ ); }; diff --git a/src/components/Dialog/DialogManager.tsx b/src/components/Dialog/DialogManager.tsx index ac118639..97971442 100644 --- a/src/components/Dialog/DialogManager.tsx +++ b/src/components/Dialog/DialogManager.tsx @@ -3,8 +3,20 @@ import type React from "react"; import { useDevice } from "@app/core/providers/useDevice.js"; import { QRDialog } from "@components/Dialog/QRDialog.js"; +import { RebootDialog } from "./RebootDialog.js"; +import { ShutdownDialog } from "./ShutdownDialog.js"; + export const DialogManager = (): JSX.Element => { - const { channels, config, QRDialogOpen, setQRDialogOpen } = useDevice(); + const { + channels, + config, + QRDialogOpen, + setQRDialogOpen, + shutdownDialogOpen, + setShutdownDialogOpen, + rebootDialogOpen, + setRebootDialogOpen + } = useDevice(); return ( <> { channels={channels.map((ch) => ch.config)} loraConfig={config.lora} /> + { + setShutdownDialogOpen(false); + }} + /> + { + setRebootDialogOpen(false); + }} + /> ); }; diff --git a/src/components/Dialog/ImportDialog.tsx b/src/components/Dialog/ImportDialog.tsx deleted file mode 100644 index cfd6b821..00000000 --- a/src/components/Dialog/ImportDialog.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import type React from "react"; -import { useEffect, useState } from "react"; - -import { fromByteArray } from "base64-js"; -import { toast } from "react-hot-toast"; -import { QRCode } from "react-qrcode-logo"; - -import { Checkbox } from "@components/form/Checkbox.js"; -import { IconButton } from "@components/form/IconButton.js"; -import { Input } from "@components/form/Input.js"; -import { Dialog } from "@headlessui/react"; -import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { Protobuf } from "@meshtastic/meshtasticjs"; - -export interface ImportDialogProps { - isOpen: boolean; - close: () => void; - loraConfig?: Protobuf.Config_LoRaConfig; - channels: Protobuf.Channel[]; -} - -export const ImportDialog = ({ - isOpen, - close, - loraConfig, - channels -}: ImportDialogProps): JSX.Element => { - const [selectedChannels, setSelectedChannels] = useState([0]); - const [QRCodeURL, setQRCodeURL] = useState(""); - - useEffect(() => { - const channelsToEncode = channels - .filter((channel) => selectedChannels.includes(channel.index)) - .map((channel) => channel.settings) - .filter((ch): ch is Protobuf.ChannelSettings => !!ch); - const encoded = Protobuf.ChannelSet.toBinary({ - loraConfig, - settings: channelsToEncode - }); - const base64 = fromByteArray(encoded) - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); - - setQRCodeURL(`https://meshtastic.org/e/#${base64}`); - }, [channels, selectedChannels, loraConfig]); - - return ( - - - ); -}; diff --git a/src/components/Dialog/QRDialog.tsx b/src/components/Dialog/QRDialog.tsx index 7d004474..3246e094 100644 --- a/src/components/Dialog/QRDialog.tsx +++ b/src/components/Dialog/QRDialog.tsx @@ -6,10 +6,9 @@ import { toast } from "react-hot-toast"; import { QRCode } from "react-qrcode-logo"; import { Checkbox } from "@components/form/Checkbox.js"; -import { IconButton } from "@components/form/IconButton.js"; import { Input } from "@components/form/Input.js"; -import { Dialog } from "@headlessui/react"; -import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { Dialog } from "@components/generic/Dialog.js"; +import { ClipboardIcon } from "@heroicons/react/24/outline"; import { Protobuf } from "@meshtastic/meshtasticjs"; export interface QRDialogProps { @@ -48,79 +47,57 @@ export const QRDialog = ({ }, [channels, selectedChannels, loraConfig]); return ( - - ); diff --git a/src/components/Dialog/RebootDialog.tsx b/src/components/Dialog/RebootDialog.tsx new file mode 100644 index 00000000..9d965d8c --- /dev/null +++ b/src/components/Dialog/RebootDialog.tsx @@ -0,0 +1,68 @@ +import type React from "react"; +import { useState } from "react"; + +import { useDevice } from "@app/core/providers/useDevice.js"; +import { Dialog } from "@components/generic/Dialog.js"; +import { ArrowPathIcon, ClockIcon } from "@heroicons/react/24/outline"; + +import { Button } from "../form/Button.js"; +import { Input } from "../form/Input.js"; + +export interface RebootDialogProps { + isOpen: boolean; + close: () => void; +} + +export const RebootDialog = ({ + isOpen, + close +}: RebootDialogProps): JSX.Element => { + const { connection, setRebootDialogOpen } = useDevice(); + + const [time, setTime] = useState(5); + + return ( + +
+ setTime(parseInt(e.target.value))} + action={{ + icon: , + action() { + connection?.reboot({ + time: time * 60, + callback: async () => { + setRebootDialogOpen(false); + await Promise.resolve(); + } + }); + } + }} + /> + +
+
+ ); +}; diff --git a/src/components/Dialog/ShutdownDialog.tsx b/src/components/Dialog/ShutdownDialog.tsx new file mode 100644 index 00000000..bda010d3 --- /dev/null +++ b/src/components/Dialog/ShutdownDialog.tsx @@ -0,0 +1,68 @@ +import type React from "react"; +import { useState } from "react"; + +import { useDevice } from "@app/core/providers/useDevice.js"; +import { Dialog } from "@components/generic/Dialog.js"; +import { ClockIcon, PowerIcon } from "@heroicons/react/24/outline"; + +import { Button } from "../form/Button.js"; +import { Input } from "../form/Input.js"; + +export interface ShutdownDialogProps { + isOpen: boolean; + close: () => void; +} + +export const ShutdownDialog = ({ + isOpen, + close +}: ShutdownDialogProps): JSX.Element => { + const { connection, setShutdownDialogOpen } = useDevice(); + + const [time, setTime] = useState(5); + + return ( + +
+ setTime(parseInt(e.target.value))} + action={{ + icon: , + action() { + connection?.shutdown({ + time: time * 60, + callback: async () => { + setShutdownDialogOpen(false); + await Promise.resolve(); + } + }); + } + }} + /> + +
+
+ ); +}; diff --git a/src/components/PageComponents/Map/MapControlls.tsx b/src/components/PageComponents/Map/MapControlls.tsx index 4869f7ed..c0ce0540 100644 --- a/src/components/PageComponents/Map/MapControlls.tsx +++ b/src/components/PageComponents/Map/MapControlls.tsx @@ -10,8 +10,8 @@ export const MapControlls = (): JSX.Element => { const { current: map } = useMap(); return ( -
-
+
+
{ diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d9413969..37398350 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -8,12 +8,16 @@ import { PeersWidget } from "@components/Widgets/PeersWidget.js"; import { PositionWidget } from "@components/Widgets/PositionWidget.js"; import { useAppStore } from "@core/stores/appStore.js"; import { useDeviceStore } from "@core/stores/deviceStore.js"; +import { CommandLineIcon } from "@heroicons/react/24/outline"; import { Types } from "@meshtastic/meshtasticjs"; +import { Input } from "./form/Input.js"; + export const Sidebar = (): JSX.Element => { const { removeDevice } = useDeviceStore(); const { connection, hardware, nodes, status, currentMetrics } = useDevice(); - const { selectedDevice, setSelectedDevice } = useAppStore(); + const { selectedDevice, setSelectedDevice, setCommandPaletteOpen } = + useAppStore(); const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum); return ( @@ -35,7 +39,7 @@ export const Sidebar = (): JSX.Element => { }} /> -
+
{ myNode?.data.position?.longitudeI )} /> +
+ setCommandPaletteOpen(true)} + action={{ + icon: , + action() { + setCommandPaletteOpen(true); + } + }} + /> +
); diff --git a/src/components/form/Button.tsx b/src/components/form/Button.tsx index 16b822ff..8cb567aa 100644 --- a/src/components/form/Button.tsx +++ b/src/components/form/Button.tsx @@ -15,6 +15,7 @@ export const Button = ({ iconAfter, children, disabled, + className, ...rest }: ButtonProps): JSX.Element => { return ( @@ -22,7 +23,7 @@ export const Button = ({ className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${ variant === "primary" ? "bg-orange-600 text-white shadow-sm hover:bg-orange-700" - : "bg-orange-200 text-orange-700 hover:bg-orange-200" + : "bg-orange-200 text-orange-700 hover:bg-orange-300" } ${ size === "sm" ? "h-8 text-sm" @@ -33,7 +34,7 @@ export const Button = ({ disabled ? "cursor-not-allowed bg-gray-400 hover:bg-gray-400 focus:ring-gray-500" : "" - }`} + } ${className}`} disabled={disabled} {...rest} > diff --git a/src/components/form/IconButton.tsx b/src/components/form/IconButton.tsx index 1678b23e..aaa00bc6 100644 --- a/src/components/form/IconButton.tsx +++ b/src/components/form/IconButton.tsx @@ -21,7 +21,7 @@ export const IconButton = ({ className={`flex rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${ variant === "primary" ? "bg-orange-600 text-white shadow-sm hover:bg-orange-700" - : "bg-orange-100 text-orange-700 hover:bg-orange-200" + : "bg-orange-200 text-orange-700 hover:bg-orange-300" } ${ size === "sm" ? "h-8 w-8" : size === "md" ? "h-10 w-10" : "h-12 w-12" } ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""} ${ diff --git a/src/components/form/Input.tsx b/src/components/form/Input.tsx index b029a463..a44b5eb8 100644 --- a/src/components/form/Input.tsx +++ b/src/components/form/Input.tsx @@ -56,7 +56,7 @@ export const Input = forwardRef(function Input( diff --git a/src/components/generic/Dialog.tsx b/src/components/generic/Dialog.tsx new file mode 100644 index 00000000..d0481716 --- /dev/null +++ b/src/components/generic/Dialog.tsx @@ -0,0 +1,48 @@ +import type React from "react"; + +import { IconButton } from "@components/form/IconButton.js"; +import { Dialog as DialogUI } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; + +export interface DialogProps { + title: string; + description: string; + isOpen: boolean; + close: () => void; + children: React.ReactNode; +} + +export const Dialog = ({ + title, + description, + isOpen, + close, + children +}: DialogProps): JSX.Element => { + return ( + +