diff --git a/src/components/SettingForms/BearerType/BearerTypeFields.tsx b/src/components/SettingForms/BearerType/BearerTypeFields.tsx index db276b4..b06dde3 100644 --- a/src/components/SettingForms/BearerType/BearerTypeFields.tsx +++ b/src/components/SettingForms/BearerType/BearerTypeFields.tsx @@ -1,32 +1,75 @@ -import { Field, useFormikContext } from "formik"; +import { Field, Form, Formik } from "formik"; import FormToggle from "../components/FormToggle"; +import { useCameraOutput } from "../../../hooks/useCameraOutput"; +import { cleanArray } from "../../../utils/utils"; +import FormGroup from "../components/FormGroup"; +import type { BearerTypeFieldType } from "../../../types/types"; export const ValuesComponent = () => { return null; }; const BearerTypeFields = () => { - useFormikContext(); + const { dispatcherQuery, dispatcherMutation } = useCameraOutput(); + + const format = dispatcherQuery?.data?.propFormat?.value; + const rawOptions = dispatcherQuery?.data?.propFormat?.accepted; + const enabled = dispatcherQuery?.data?.propEnabled?.value; + const verbose = dispatcherQuery?.data?.propVerbose?.value; + const options = cleanArray(rawOptions); + + const initialValues: BearerTypeFieldType = { + format: format ?? "JSON", + enabled: enabled === "true", + verbose: verbose === "true", + }; + + const handleSubmit = async (values: BearerTypeFieldType) => { + await dispatcherMutation.mutateAsync(values); + }; return ( -
-
- - - - - -
-
- - -
-
+ + {({ isSubmitting }) => ( +
+
+ + + + {options?.map((option: string) => ( + + ))} + + + +
+ + +
+
+ +
+
+ )} +
); }; diff --git a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx index 7ed45a5..387eec0 100644 --- a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx +++ b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx @@ -1,72 +1,185 @@ -import { Field, useFormikContext } from "formik"; +import { Field, Form, Formik, useFormikContext } from "formik"; import FormGroup from "../components/FormGroup"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useCameraOutput } from "../../../hooks/useCameraOutput"; +import type { + InitialValuesForm, + InitialValuesFormErrors, +} from "../../../types/types"; +import { toast } from "sonner"; const ChannelFields = () => { - useFormikContext(); const [showPwd, setShowPwd] = useState(false); + const { backOfficeQuery, backOfficeMutation } = useCameraOutput(); + + const backOfficeURL = backOfficeQuery?.data?.propBackofficeURL?.value; + const username = backOfficeQuery?.data?.propUsername?.value; + const password = backOfficeQuery?.data?.propPassword?.value; + const connectTimeoutSeconds = + backOfficeQuery?.data?.propConnectTimeoutSeconds?.value; + const readTimeoutSeconds = + backOfficeQuery?.data?.propReadTimeoutSeconds?.value; + + const initialValues: InitialValuesForm = { + backOfficeURL: backOfficeURL ?? "", + username: username ?? "", + password: password ?? "", + connectTimeoutSeconds: Number(connectTimeoutSeconds), + readTimeoutSeconds: Number(readTimeoutSeconds), + }; + + const handleSubmit = async (values: InitialValuesForm) => { + await backOfficeMutation.mutateAsync(values); + }; + + const ValidationToastOnce = () => { + const { submitCount, isValid } = useFormikContext(); + useEffect(() => { + if (submitCount > 0 && !isValid) { + toast.error("Check fields are filled in"); + } + }, [submitCount, isValid]); + return null; + }; + + const validateValues = ( + values: InitialValuesForm + ): InitialValuesFormErrors => { + const errors: InitialValuesFormErrors = {}; + + const url = values.backOfficeURL?.trim(); + const username = values.username?.trim(); + const password = values.password?.trim(); + + if (!url) { + errors.backOfficeURL = "Required"; + } + + if (!username) errors.username = "Required"; + if (!password) errors.password = "Required"; + + const read = Number(values.readTimeoutSeconds); + if (!Number.isFinite(read)) { + errors.readTimeoutSeconds = "Must be a number"; + } else if (read < 0) { + errors.readTimeoutSeconds = "Must be ≥ 0"; + } + + const connect = Number(values.connectTimeoutSeconds); + if (!Number.isFinite(connect)) { + errors.connectTimeoutSeconds = "Must be a number"; + } else if (connect < 0) { + errors.connectTimeoutSeconds = "Must be ≥ 0"; + } + + return errors; + }; + return ( -
- - - - - - - - - - - - setShowPwd((s) => !s)} - icon={showPwd ? faEyeSlash : faEye} - /> - - - - - - - - - -
+ + {({ errors, touched, isSubmitting }) => ( +
+
+ + + + + + + + + + + + + setShowPwd((s) => !s)} + icon={showPwd ? faEyeSlash : faEye} + /> + + + + + + + + + +
+ + + + )} +
); }; diff --git a/src/components/SettingForms/OverviewText/OverviewTextFields.tsx b/src/components/SettingForms/OverviewText/OverviewTextFields.tsx index e6944bc..d7a83ef 100644 --- a/src/components/SettingForms/OverviewText/OverviewTextFields.tsx +++ b/src/components/SettingForms/OverviewText/OverviewTextFields.tsx @@ -1,10 +1,8 @@ -import { Field, useFormikContext } from "formik"; +import { Field } from "formik"; import FormGroup from "../components/FormGroup"; import FormToggle from "../components/FormToggle"; const OverviewTextFields = () => { - useFormikContext(); - return (
diff --git a/src/components/SettingForms/SettingForms/SettingForms.tsx b/src/components/SettingForms/SettingForms/SettingForms.tsx index c0a31b9..632d076 100644 --- a/src/components/SettingForms/SettingForms/SettingForms.tsx +++ b/src/components/SettingForms/SettingForms/SettingForms.tsx @@ -1,83 +1,12 @@ -import { Formik, Form } from "formik"; import BearerTypeCard from "../BearerType/BearerTypeCard"; import ChannelCard from "../Channel1-JSON/ChannelCard"; -import type { InitialValuesForm } from "../../../types/types"; -import { useState } from "react"; -import AdvancedToggle from "../../UI/AdvancedToggle"; -import OverviewTextCard from "../OverviewText/OverviewTextCard"; -import SightingDataCard from "../SightingData/SightingDataCard"; const SettingForms = () => { - const [advancedToggle, setAdvancedToggle] = useState(false); - - const initialValues = { - format: "JSON", - enabled: false, - verbose: false, - backOfficeURL: "", - username: "", - password: "", - connectTimeoutSeconds: 0, - readTimeoutSeconds: 0, - overviewQuality: "high", - overviewImageScaleFactor: "full", - overviewType: "Plate Overview", - invertMotion: false, - maxPlateValueLength: 0, - vrmToTransit: "plain VRM ASCII (default)", - staticReadAction: "Use Lane Direction", - noRegionAction: "send", - countryCodeType: "IBAN 2 Character code (default)", - filterMinConfidence: 0, - filterMaxConfidence: 100, - overviewQualityOverride: 0, - sightingDataEnabled: false, - sighthingDataVerbose: false, - includeVRM: false, - includeMotion: false, - includeTimestamp: false, - timestampFormat: "UTC", - includeCameraName: false, - customFieldA: "", - customFieldB: "", - customFieldC: "", - customFieldD: "", - overlayPosition: "Top", - }; - - const handleSubmit = (values: InitialValuesForm) => { - alert(JSON.stringify(values)); - }; - return ( - -
-
- - -
- - {advancedToggle && ( - <> -
- -
-
- -
- - )} - - -
+
+ + +
); }; diff --git a/src/components/SettingForms/SightingData/SightingDataFields.tsx b/src/components/SettingForms/SightingData/SightingDataFields.tsx index 262d694..0259714 100644 --- a/src/components/SettingForms/SightingData/SightingDataFields.tsx +++ b/src/components/SettingForms/SightingData/SightingDataFields.tsx @@ -1,10 +1,8 @@ -import { Field, useFormikContext } from "formik"; +import { Field } from "formik"; import FormGroup from "../components/FormGroup"; import FormToggle from "../components/FormToggle"; const SightingDataFields = () => { - useFormikContext(); - return (
diff --git a/src/hooks/useCameraOutput.ts b/src/hooks/useCameraOutput.ts new file mode 100644 index 0000000..9c7f681 --- /dev/null +++ b/src/hooks/useCameraOutput.ts @@ -0,0 +1,128 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; +import { useEffect } from "react"; +import { toast } from "sonner"; +import type { BearerTypeFieldType, InitialValuesForm } from "../types/types"; + +const getDispatcherConfig = async () => { + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher`); + if (!response.ok) throw new Error("Cannot get dispatcher configuration"); + return response.json(); +}; + +const updateDispatcherConfig = async (data: BearerTypeFieldType) => { + const updateConfigPayload = { + id: "Dispatcher", + fields: [ + { + property: "propEnabled", + value: data.enabled, + }, + // Todo: figure out how to add verbose conditionally + { + property: "propFormat", + value: data.format, + }, + ], + }; + const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, { + method: "POST", + body: JSON.stringify(updateConfigPayload), + }); + if (!response.ok) throw new Error("Cannot update dispatcher configuration"); + return response.json(); +}; + +const getBackOfficeConfig = async () => { + const response = await fetch( + `${CAM_BASE}/api/fetch-config?id=Dispatcher-json` + ); + if (!response.ok) throw new Error("Cannot get Back Office configuration"); + return response.json(); +}; + +const updateBackOfficeConfig = async (data: InitialValuesForm) => { + const updateConfigPayload = { + id: "Dispatcher-json", + fields: [ + { + property: "propBackofficeURL", + value: data.backOfficeURL, + }, + { + property: "propConnectTimeoutSeconds", + value: data.connectTimeoutSeconds, + }, + { + property: "propPassword", + value: data.password, + }, + { + property: "propReadTimeoutSeconds", + value: data.readTimeoutSeconds, + }, + { + property: "propUsername", + value: data.username, + }, + ], + }; + const response = await fetch( + `${CAM_BASE}/api/update-config?id=Dispatcher-json`, + { + method: "POST", + body: JSON.stringify(updateConfigPayload), + } + ); + if (!response.ok) throw new Error("Cannot update Back Office configuration"); + return response.json(); +}; + +export const useCameraOutput = () => { + const dispatcherQuery = useQuery({ + queryKey: ["dispatcher"], + queryFn: getDispatcherConfig, + }); + + const backOfficeQuery = useQuery({ + queryKey: ["backoffice"], + queryFn: getBackOfficeConfig, + }); + + const dispatcherMutation = useMutation({ + mutationFn: updateDispatcherConfig, + mutationKey: ["dispatcherUpdate"], + onError: (error) => toast.error(error.message), + onSuccess: (data) => { + if (data) { + toast.success("Settings successfully updated"); + } + }, + }); + + const backOfficeMutation = useMutation({ + mutationKey: ["backOfficeUpdate"], + mutationFn: updateBackOfficeConfig, + onError: (error) => toast.error(error.message), + onSuccess: (data) => { + if (data) { + toast.success("Settings successfully updated"); + } + }, + }); + + useEffect(() => { + if (dispatcherQuery.isError) toast.error(dispatcherQuery.error.message); + }, [dispatcherQuery?.error?.message, dispatcherQuery.isError]); + + useEffect(() => { + if (backOfficeQuery.isError) toast.error(backOfficeQuery.error.message); + }, [backOfficeQuery?.error?.message, backOfficeQuery.isError]); + + return { + dispatcherQuery, + dispatcherMutation, + backOfficeQuery, + backOfficeMutation, + }; +}; diff --git a/src/types/types.ts b/src/types/types.ts index bb1361e..6169124 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -49,9 +49,6 @@ export type BearerTypeFieldType = { }; export type InitialValuesForm = { - format: string; - enabled: boolean; - verbose: boolean; backOfficeURL: string; username: string; password: string; @@ -59,6 +56,14 @@ export type InitialValuesForm = { readTimeoutSeconds: number; }; +export type InitialValuesFormErrors = { + backOfficeURL?: string; + username?: string; + password?: string; + connectTimeoutSeconds?: string; + readTimeoutSeconds?: string; +}; + export type NPEDFieldType = { frontId: string; username: string | undefined; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ebd8391..1c1b257 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -19,6 +19,17 @@ const randomChars = () => { return letter; }; +export function cleanArray(str: string) { + const toArr = str?.split(","); + + const cleaned = toArr?.map((el: string) => { + const test = el.replace(/[^0-9a-z]/gi, ""); + + return test; + }); + return cleaned; +} + export function parseRTSPUrl(url: string) { const regex = /rtsp:\/\/([^:]+):([^@]+)@([^:/]+):?(\d+)?(\/.*)?/; const match = url?.match(regex);