diff --git a/README.md b/README.md index 7959ce4..95980c9 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,116 @@ -# React + TypeScript + Vite +# Mav Mobile UI -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This is a React-based web application built with Vite (react and typescript). -Currently, two official plugins are available: +## Getting started -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +### Prerequisites -## Expanding the ESLint configuration +- Node.js (v18 or higher recommended) +- Yarn (v1.22+) (https://yarnpkg.com/) -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +### Installation -```js -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```bash +git clone https://mavportal.com/TobaOjo/Mav-Mobile-UI.git +cd Mav-Mobile-UI +yarn install ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +### Running Locally + +```bash +yarn dev +``` + +The app will be available at `http://localhost:5173`. + +To run on locally on other devices + +```bash +yarn dev --host +``` + +The app will be available at the exposed addresses to access e.g. http://1xx.xxx.x.xxx:/Mobile + +## Tech Stack + +- **React** – UI library +- **Vite** – Build tool +- **Yarn** – Package manager + +## Configuration + +Create a `.env` file to access the Mav Mobile box in unit 5 for or for any environment-specific settings: + +```env +VITE_AGX_BOX_URL=http://1xx.xxx.xxx.xxx: +``` + +## Development + +### Linting & Formatting + +```bash +yarn lint +yarn format +``` + +### Testing + +(Currently not implemented – consider adding Jest or Vitest) + +## Deployment + +To build for production: +Navigate to the Mav-Mobile-UI folder + +```bash +cd Mav-Mobile-UI +``` + +**Delete** the local .env (Production will use its own domain) + +run + +```bash +yarn build +``` + +- Navigate to your Mav-Mobile-UI folder +- Select the Dist folder +- Compress to (ZIP) +- Log into box on Moba using Session > SSH and putting IP in Remote Host. +- Creds are mav:mav +- Drag and drop dist.zip into file explorer menu on left hand side (has to be named dist.zip exactly). +- Run command + +```bash +sudo ./integrate-web-ui.sh +``` + +Run + +```bash +sudo nano web-static/index.html +``` + +- add the following between the lines & ```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) + + + ``` + +- Run + +```bash +sudo reboot +``` + +It should come back up all working diff --git a/src/components/SettingForms/System/SystemConfigFields.tsx b/src/components/SettingForms/System/SystemConfigFields.tsx index cb40d86..e58709d 100644 --- a/src/components/SettingForms/System/SystemConfigFields.tsx +++ b/src/components/SettingForms/System/SystemConfigFields.tsx @@ -5,6 +5,8 @@ import { timezones } from "./timezones"; import SystemFileUpload from "./SystemFileUpload"; import type { SystemValues, SystemValuesErrors } from "../../../types/types"; import { useDNSSettings, useSystemConfig } from "../../../hooks/useSystemConfig"; +import { ValidateIPaddress } from "../../../utils/utils"; +import { toast } from "sonner"; const SystemConfigFields = () => { const { saveSystemSettings, systemSettingsData, saveSystemSettingsLoading } = useSystemConfig(); @@ -35,6 +37,14 @@ const SystemConfigFields = () => { if (!values.timeZone) errors.timeZone = "Required"; if (isNaN(interval) || interval <= 0) errors.sntpInterval = "Cannot be less than 0"; if (!values.sntpServer) errors.sntpServer = "Required"; + const invalidPrimary = ValidateIPaddress(values.serverPrimary); + const invalidSecondary = ValidateIPaddress(values.serverSecondary); + + if (invalidPrimary || invalidSecondary) { + toast.error(invalidPrimary || invalidSecondary, { + id: "invalid-ip", + }); + } return errors; }; @@ -52,7 +62,7 @@ const SystemConfigFields = () => { onSubmit={handleSubmit} validate={validateValues} enableReinitialize - validateOnChange + validateOnChange={false} validateOnBlur > {({ values, errors, touched, isSubmitting }) => ( diff --git a/src/components/SettingForms/System/Upload.ts b/src/components/SettingForms/System/Upload.ts index 1403722..39123e0 100644 --- a/src/components/SettingForms/System/Upload.ts +++ b/src/components/SettingForms/System/Upload.ts @@ -14,9 +14,6 @@ export async function sendBlobFileUpload({ file, opts }: BlobFileUpload): Promis if (!file) throw new Error("No file supplied"); if (!opts?.uploadUrl) throw new Error("No URL supplied"); - if (file?.type !== "text/csv") { - throw new Error("This file is not supported, please upload a CSV file."); - } const timeoutMs = opts?.timeoutMs ?? 30000; const fieldName = opts?.fieldName ?? "upload"; const fileName = opts?.overrideFileName ?? file?.name; diff --git a/src/components/SettingForms/WiFi&Modem/ModemCard.tsx b/src/components/SettingForms/WiFi&Modem/ModemCard.tsx index dcc2dfc..46cbb16 100644 --- a/src/components/SettingForms/WiFi&Modem/ModemCard.tsx +++ b/src/components/SettingForms/WiFi&Modem/ModemCard.tsx @@ -6,7 +6,6 @@ const ModemCard = () => { return ( - ); diff --git a/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx b/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx index 73a8748..723b392 100644 --- a/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx +++ b/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx @@ -6,6 +6,8 @@ import { useEffect, useState } from "react"; import ModemToggle from "./ModemToggle"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ValidateIPaddress } from "../../../utils/utils"; +import { toast, Toaster } from "sonner"; const ModemSettings = () => { const [showSettings, setShowSettings] = useState(false); @@ -16,6 +18,8 @@ const ModemSettings = () => { const username = modemQuery?.data?.propUsername.value; const password = modemQuery?.data?.propPassword?.value; const mode = modemQuery?.data?.propMode?.value; + const serverPrimary = modemQuery?.data?.propNameServerPrimary?.value; + const serverSecondary = modemQuery?.data?.propNameServerSecondary?.value; useEffect(() => { setShowSettings(mode === "AUTO"); @@ -26,9 +30,19 @@ const ModemSettings = () => { username: username ?? "", password: password ?? "", authenticationType: "PAP", + serverPrimary: serverPrimary ?? "", + serverSecondary: serverSecondary ?? "", }; const handleSubmit = async (values: ModemSettingsType) => { + const invalidPrimary = ValidateIPaddress(values.serverPrimary); + const invalidSecondary = ValidateIPaddress(values.serverSecondary); + if (invalidPrimary || invalidSecondary) { + toast.error(invalidPrimary || invalidSecondary, { + id: "invalid-ip", + }); + return; + } const modemConfig = { id: "ModemAndWifiManager-modem", fields: [ @@ -49,9 +63,24 @@ const ModemSettings = () => { property: "propMode", value: showSettings ? "AUTO" : "MANUAL", }, + { + property: "propNameServerPrimary", + value: values.serverPrimary, + }, + { + property: "propNameServerSecondary", + value: values.serverSecondary, + }, ], }; - await modemMutation.mutateAsync(modemConfig); + + const response = await modemMutation.mutateAsync(modemConfig); + + if (!response?.id) { + toast.success("Modem settings updated successfully", { + id: "modemSettings", + }); + } }; return ( @@ -107,9 +136,33 @@ const ModemSettings = () => { /> + + + + + + + + { )} + ); }; diff --git a/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx index 2443b2b..457482f 100644 --- a/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx +++ b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx @@ -5,6 +5,7 @@ import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem"; import { useState } from "react"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { toast, Toaster } from "sonner"; const WiFiSettingsForm = () => { const [showPwd, setShowPwd] = useState(false); @@ -19,6 +20,13 @@ const WiFiSettingsForm = () => { encryption: "WPA2", }; + const validatePassword = (password: string) => { + if (password.length < 8) { + toast.error("Password must be at least 8 characters long", { id: "password" }); + return "Password must be at least 8 characters long"; + } + }; + const handleSubmit = async (values: WifiSettingValues) => { const wifiConfig = { id: "ModemAndWifiManager-wifi", @@ -37,66 +45,71 @@ const WiFiSettingsForm = () => { await wifiMutation.mutateAsync(wifiConfig); }; return ( - - {({ isSubmitting }) => ( -
- - - - - - -
+ <> + + {({ isSubmitting }) => ( + + + - setShowPwd((s) => !s)} - icon={showPwd ? faEyeSlash : faEye} - /> -
-
- - - + + +
+ + + setShowPwd((s) => !s)} + icon={showPwd ? faEyeSlash : faEye} + /> +
+
+ + + + + + + + + + - - )} -
+ {isSubmitting || wifiMutation.isPending ? "Saving..." : " Save WiFi settings"} + + + )} + + + ); }; diff --git a/src/components/SightingModal/SightingModal.tsx b/src/components/SightingModal/SightingModal.tsx index f748c7b..f4c2d8f 100644 --- a/src/components/SightingModal/SightingModal.tsx +++ b/src/components/SightingModal/SightingModal.tsx @@ -160,12 +160,13 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
{sighting?.seenCount ?? "-"}
- {sighting?.make && ( -
-
Make
-
{sighting?.make ?? "-"}
-
- )} + {sighting?.make || + (sighting?.make.trim() && ( +
+
Make
+
{sighting?.make ?? "-"}
+
+ ))} {sighting?.model || (!sighting?.model.trim() && (
@@ -173,12 +174,13 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
{sighting?.model ?? "-"}
))} - {sighting?.color && ( -
-
Colour
-
{sighting?.color ?? "-"}
-
- )} + {sighting?.color || + (!sighting?.color.trim() && ( +
+
Colour
+
{sighting?.color ?? "-"}
+
+ ))}
Time
diff --git a/src/types/types.ts b/src/types/types.ts index fbf4f0e..9d5cfdf 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -403,6 +403,8 @@ export type ModemSettingsType = { username: string; password: string; authenticationType: string; + serverPrimary: string; + serverSecondary: string; }; export type HitKind = "NPED" | "HOTLIST"; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ed80a28..7cdc0a8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -184,3 +184,14 @@ export const reverseZoomMapping = (magnification: string) => { break; } }; + +export const ValidateIPaddress = (value: string | undefined) => { + if (!value) return; + + const regex = + /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/; + + if (!regex.test(value)) { + return "Invalid IP address format"; + } +};