+
);
};
diff --git a/src/features/dashboard/components/SystemStatusCard.tsx b/src/features/dashboard/components/SystemStatusCard.tsx
deleted file mode 100644
index b887e7e..0000000
--- a/src/features/dashboard/components/SystemStatusCard.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useInfoSocket } from "../../../hooks/useInfoWebSocket";
-import Card from "../../../ui/Card";
-import CardHeader from "../../../ui/CardHeader";
-
-const SystemStatusCard = () => {
- const { stats } = useInfoSocket();
-
- return (
-
-
- {stats ? (
- <>
- UTC: {stats["system-clock-utc"]}
- Local: {stats["system-clock-local"]}
- CPU: {stats["memory-cpu-status"]}
- Threads: {stats["thread-count"]}
- >
- ) : (
- Loading system status…
- )}
-
-
- );
-};
-
-export default SystemStatusCard;
diff --git a/src/features/dashboard/components/cameraStatus/CameraStatus.tsx b/src/features/dashboard/components/cameraStatus/CameraStatus.tsx
new file mode 100644
index 0000000..bfbdfb5
--- /dev/null
+++ b/src/features/dashboard/components/cameraStatus/CameraStatus.tsx
@@ -0,0 +1,50 @@
+import type { SystemHealthStatus } from "../../../../types/types";
+import Card from "../../../../ui/Card";
+import StatusIndicators from "../../../../ui/StatusIndicators";
+import { capitalize } from "../../../../utils/utils";
+import CameraStatusGridItem from "./CameraStatusGridItem";
+
+type CameraStatusProps = {
+ title: string;
+ category: SystemHealthStatus[];
+ isError?: boolean;
+};
+
+const CameraStatus = ({ title, category, isError }: CameraStatusProps) => {
+ const isAllGood = category && category.length > 0 && category.every((status) => status.tags.includes("RUNNING"));
+ // check if some are down
+ // check if all are down
+ //check if offline
+ return (
+
+
+
+ {isError ? (
+
+ ) : isAllGood ? (
+
+ ) : (
+
+ )}
+ {capitalize(title)}
+
+ {isError ? (
+
Error loading camera health.
+ ) : isAllGood ? (
+
All systems running
+ ) : (
+
Some systems down
+ )}
+
+ {category && category?.length <= 0 ? (
+ Loading Camera health...
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export default CameraStatus;
diff --git a/src/features/dashboard/components/cameraStatus/CameraStatusGridItem.tsx b/src/features/dashboard/components/cameraStatus/CameraStatusGridItem.tsx
new file mode 100644
index 0000000..a75ae3d
--- /dev/null
+++ b/src/features/dashboard/components/cameraStatus/CameraStatusGridItem.tsx
@@ -0,0 +1,38 @@
+import { useState } from "react";
+import type { SystemHealthStatus } from "../../../../types/types";
+import { capitalize } from "../../../../utils/utils";
+import SystemHealthModal from "../systemHealth/systemHealthModal/SystemHealthModal";
+
+type CameraStatusGridItemProps = {
+ title: string;
+ statusCategory: SystemHealthStatus[];
+};
+
+const CameraStatusGridItem = ({ title, statusCategory }: CameraStatusGridItemProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const isAllGood = statusCategory?.every((status) => status.tags.includes("RUNNING"));
+
+ const handleClick = () => {
+ setIsOpen(false);
+ };
+ return (
+ <>
+
setIsOpen(true)}
+ >
+
{capitalize(title)}
+
{isAllGood ? "Click to view module status" : "Some systems down"}
+
+
+ >
+ );
+};
+
+export default CameraStatusGridItem;
diff --git a/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx b/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx
new file mode 100644
index 0000000..9b3b833
--- /dev/null
+++ b/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx
@@ -0,0 +1,44 @@
+import { useState } from "react";
+import type { SystemHealthStatus } from "../../../../types/types";
+import StatusIndicators from "../../../../ui/StatusIndicators";
+import { capitalize } from "../../../../utils/utils";
+import SystemHealthModal from "../systemHealth/systemHealthModal/SystemHealthModal";
+
+type StatusGridItemProps = {
+ title: string;
+ statusCategory: SystemHealthStatus[];
+};
+
+const StatusGridItem = ({ title, statusCategory }: StatusGridItemProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const isAllGood =
+ statusCategory && statusCategory.length > 0 && statusCategory.every((status) => status.tags.includes("RUNNING"));
+
+ const handleClick = () => {
+ setIsOpen(false);
+ };
+
+ return (
+ <>
+
setIsOpen(true)}
+ >
+
+ {isAllGood ? : }
+ {capitalize(title)}
+
+
{isAllGood ? "All systems running" : "Some systems down"}
+
+
+ >
+ );
+};
+
+export default StatusGridItem;
diff --git a/src/features/dashboard/components/systemHealth/SystemHealth.tsx b/src/features/dashboard/components/systemHealth/SystemHealth.tsx
new file mode 100644
index 0000000..37f62db
--- /dev/null
+++ b/src/features/dashboard/components/systemHealth/SystemHealth.tsx
@@ -0,0 +1,60 @@
+import type { SystemHealthStatus } from "../../../../types/types";
+import StatusGridItem from "../statusGridItem/StatusGridItem";
+
+type SystemHealthProps = {
+ startTime: string;
+ uptime: string;
+ statuses: SystemHealthStatus[];
+ isLoading: boolean;
+ isError: boolean;
+ dateUpdatedAt?: number;
+};
+
+const SystemHealth = ({ startTime, uptime, statuses, isLoading, isError, dateUpdatedAt }: SystemHealthProps) => {
+ const updatedDate = dateUpdatedAt ? new Date(dateUpdatedAt).toLocaleString() : null;
+
+ const statusCategories = statuses?.reduce
>(
+ (acc, cur) => {
+ if (cur?.groupID === "ChannelA") acc?.channelA?.push(cur);
+ if (cur?.groupID === "ChannelB") acc?.channelB?.push(cur);
+ if (cur?.groupID === "ChannelC") acc?.channelC?.push(cur);
+ if (cur?.groupID === "Default") acc?.default?.push(cur);
+ return acc;
+ },
+ {
+ channelA: [],
+ channelB: [],
+ channelC: [],
+ default: [],
+ },
+ );
+
+ const categoryDefault = statusCategories?.default ?? [];
+
+ if (isError) {
+ return Error loading system health.;
+ }
+ if (isLoading) {
+ return Loading system health…;
+ }
+ return (
+
+
+
+
Start Time
{startTime}
+
+
+
Up Time
{uptime}
+
+
+
+
+
+
+ {`Last refeshed ${updatedDate}`}
+
+
+ );
+};
+
+export default SystemHealth;
diff --git a/src/features/dashboard/components/systemHealth/SystemHealthCard.tsx b/src/features/dashboard/components/systemHealth/SystemHealthCard.tsx
new file mode 100644
index 0000000..22cc9a0
--- /dev/null
+++ b/src/features/dashboard/components/systemHealth/SystemHealthCard.tsx
@@ -0,0 +1,42 @@
+import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons";
+import Card from "../../../../ui/Card";
+import CardHeader from "../../../../ui/CardHeader";
+
+import SystemHealth from "./SystemHealth";
+import type { SystemHealthStatus } from "../../../../types/types";
+
+type SystemOverviewProps = {
+ startTime: string;
+ uptime: string;
+ statuses: SystemHealthStatus[];
+ isLoading: boolean;
+ isError: boolean;
+ dateUpdatedAt: number;
+ refetch: () => void;
+};
+
+const SystemHealthCard = ({
+ startTime,
+ uptime,
+ statuses,
+ isLoading,
+ isError,
+ dateUpdatedAt,
+ refetch,
+}: SystemOverviewProps) => {
+ return (
+
+
+
+
+ );
+};
+
+export default SystemHealthCard;
diff --git a/src/features/dashboard/components/systemHealth/systemHealthModal/SystemHealthModal.tsx b/src/features/dashboard/components/systemHealth/systemHealthModal/SystemHealthModal.tsx
new file mode 100644
index 0000000..eeebe0d
--- /dev/null
+++ b/src/features/dashboard/components/systemHealth/systemHealthModal/SystemHealthModal.tsx
@@ -0,0 +1,48 @@
+import type { SystemHealthStatus } from "../../../../../types/types";
+import Badge from "../../../../../ui/Badge";
+import ModalComponent from "../../../../../ui/ModalComponent";
+import StatusIndicators from "../../../../../ui/StatusIndicators";
+import { capitalize } from "../../../../../utils/utils";
+
+type SystemHealthModalProps = {
+ isSystemHealthModalOpen: boolean;
+ handleClose: () => void;
+ statusCategory: SystemHealthStatus[];
+ title: string;
+ isAllGood: boolean;
+};
+
+const SystemHealthModal = ({
+ isSystemHealthModalOpen,
+ handleClose,
+ statusCategory,
+ title,
+ isAllGood,
+}: SystemHealthModalProps) => {
+ return (
+
+
+
+
+ {isAllGood ? : }
+ {capitalize(title)}
+
+
{isAllGood ? "All systems running" : "Some systems down"}
+
+
+
+ {statusCategory?.map((status: SystemHealthStatus) => (
+
+ {status.id}
+
+ ))}
+
+
+
+ );
+};
+
+export default SystemHealthModal;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/DownloadLogButton.tsx b/src/features/dashboard/components/systemStatus/StatusItems/DownloadLogButton.tsx
new file mode 100644
index 0000000..33e51be
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/DownloadLogButton.tsx
@@ -0,0 +1,50 @@
+import { faDownload } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useDownloadLogFiles } from "../../../hooks/useDownloadLogFiles";
+import { toast } from "sonner";
+
+const DownloadLogButton = () => {
+ const { downloadLogFilesQuery } = useDownloadLogFiles();
+ const isLoading = downloadLogFilesQuery?.isFetching;
+
+ const handleDownloadClick = async () => {
+ try {
+ const blob = await downloadLogFilesQuery?.refetch().then((res) => res.data);
+ if (!blob) {
+ throw new Error("No log file data received");
+ }
+ const url = window.URL.createObjectURL(new Blob([blob]));
+ const link = document.createElement("a");
+ if (!link) {
+ throw new Error("Failed to create download link");
+ } else {
+ link.href = url;
+ link.setAttribute("download", "FlexiAI-0.log");
+ document.body.appendChild(link);
+ link.click();
+ link.parentNode?.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ }
+ } catch (error: unknown) {
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
+ toast.error(errorMessage);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default DownloadLogButton;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/StatusItemCPU.tsx b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemCPU.tsx
new file mode 100644
index 0000000..3ae97f6
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemCPU.tsx
@@ -0,0 +1,23 @@
+import { faHardDrive } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+type StatusItemProps = {
+ statusInfoItem: string;
+ description: string;
+};
+
+const StatusItemCPU = ({ statusInfoItem, description }: StatusItemProps) => {
+ return (
+
+
+
+
+
+
{statusInfoItem}
+
+
{description}
+
+ );
+};
+
+export default StatusItemCPU;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/StatusItemLocal.tsx b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemLocal.tsx
new file mode 100644
index 0000000..f99cf40
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemLocal.tsx
@@ -0,0 +1,31 @@
+import { faClock } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+type StatusItemProps = {
+ statusInfoItem: string;
+ description: string;
+};
+
+const StatusItemLocal = ({ statusInfoItem, description }: StatusItemProps) => {
+ const humanReadable = (string: string) => {
+ if (description.toLowerCase().includes("local")) {
+ const text = string.slice(0, statusInfoItem.length - 5);
+ return text;
+ }
+ };
+
+ return (
+
+
+
+
+
+
{description.toLowerCase().includes("local") && humanReadable(statusInfoItem)}
+
+
+
{description}
+
+ );
+};
+
+export default StatusItemLocal;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/StatusItemThreads.tsx b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemThreads.tsx
new file mode 100644
index 0000000..69ce8c8
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemThreads.tsx
@@ -0,0 +1,24 @@
+import { faMicrochip } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+type StatusItemProps = {
+ statusInfoItem: string;
+ description: string;
+};
+
+const StatusItemThreads = ({ statusInfoItem, description }: StatusItemProps) => {
+ return (
+
+
+
+
+
+
{statusInfoItem}
+
+
+
{description}
+
+ );
+};
+
+export default StatusItemThreads;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/StatusItemUTC.tsx b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemUTC.tsx
new file mode 100644
index 0000000..b4adef8
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/StatusItemUTC.tsx
@@ -0,0 +1,32 @@
+import { faClock } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+type StatusItemProps = {
+ statusInfoItem: string;
+ description: string;
+};
+
+const StatusItemUTC = ({ statusInfoItem, description }: StatusItemProps) => {
+ const humanReadable = (string: string) => {
+ if (description.includes("UTC")) {
+ const text = string.slice(0, statusInfoItem.length - 3);
+ return text;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
{description.toLowerCase().includes("utc") && humanReadable(statusInfoItem)}
+
+
+
{description}
+
+ );
+};
+
+export default StatusItemUTC;
diff --git a/src/features/dashboard/components/systemStatus/StatusItems/StatusReads.tsx b/src/features/dashboard/components/systemStatus/StatusItems/StatusReads.tsx
new file mode 100644
index 0000000..287c18c
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/StatusItems/StatusReads.tsx
@@ -0,0 +1,48 @@
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChartSimple } from "@fortawesome/free-solid-svg-icons";
+
+type StatusReadsProps = {
+ reads: {
+ totalPending: number;
+ totalActive: number;
+ totalSent: number;
+ totalReceived: number;
+ totalLost: number;
+ sanityCheck: boolean;
+ sanityCheckFormula: string;
+ };
+ isReadsLoading?: boolean;
+};
+
+const StatusReads = ({ reads, isReadsLoading }: StatusReadsProps) => {
+ const totalPending = reads?.totalPending ?? 0;
+ const totalActive = reads?.totalActive ?? 0;
+ const totalSent = reads?.totalSent ?? 0;
+ const totalLost = reads?.totalLost ?? 0;
+ const totalReceived = reads?.totalReceived ?? 0;
+
+ if (isReadsLoading) {
+ return Loading reads…
;
+ }
+ return (
+
+
+
+
+ Pending: {totalPending} | Active:{" "}
+ {totalActive} | Lost: {totalLost}
+
+ Sent / Received: {totalSent} |{" "}
+ {totalReceived}
+
+
+ );
+};
+
+export default StatusReads;
diff --git a/src/features/dashboard/components/systemStatus/SystemStatusCard.tsx b/src/features/dashboard/components/systemStatus/SystemStatusCard.tsx
new file mode 100644
index 0000000..6260c0c
--- /dev/null
+++ b/src/features/dashboard/components/systemStatus/SystemStatusCard.tsx
@@ -0,0 +1,49 @@
+import { useEffect } from "react";
+import { useInfoSocket } from "../../../../app/context/WebSocketContext";
+import Card from "../../../../ui/Card";
+import CardHeader from "../../../../ui/CardHeader";
+import DownloadLogButton from "./StatusItems/DownloadLogButton";
+import StatusItemLocal from "./StatusItems/StatusItemLocal";
+import StatusItemUTC from "./StatusItems/StatusItemUTC";
+import StatusReads from "./StatusItems/StatusReads";
+import { useGetStore } from "../../hooks/useGetStore";
+
+const SystemStatusCard = () => {
+ const { data: stats } = useInfoSocket();
+ const { storeQuery } = useGetStore();
+
+ const reads = storeQuery?.data;
+ const isReadsLoading = storeQuery?.isFetching;
+ const isError = storeQuery?.isError || !storeQuery?.data;
+
+ useEffect(() => {
+ storeQuery.refetch();
+ }, [reads]);
+
+ if (isError) {
+ return (
+
+
+ Error loading system status.
+
+ );
+ }
+ return (
+
+
+ {stats ? (
+
+
+
+
+
+
+ ) : (
+ Loading system status…
+ )}
+
+
+ );
+};
+
+export default SystemStatusCard;
diff --git a/src/features/dashboard/hooks/useDownloadLogFiles.ts b/src/features/dashboard/hooks/useDownloadLogFiles.ts
new file mode 100644
index 0000000..75bdc15
--- /dev/null
+++ b/src/features/dashboard/hooks/useDownloadLogFiles.ts
@@ -0,0 +1,20 @@
+import { useQuery } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+
+const getDownloadLogFiles = async () => {
+ const response = await fetch(`${CAMBASE}/LogView/download?filename=FlexiAI-0.log`);
+ if (!response.ok) {
+ throw new Error("Failed to download log files");
+ }
+ return response.blob();
+};
+
+export const useDownloadLogFiles = () => {
+ const downloadLogFilesQuery = useQuery({
+ queryKey: ["downloadLogFiles"],
+ queryFn: getDownloadLogFiles,
+ enabled: false,
+ });
+
+ return { downloadLogFilesQuery };
+};
diff --git a/src/features/dashboard/hooks/useGetStore.ts b/src/features/dashboard/hooks/useGetStore.ts
new file mode 100644
index 0000000..887176f
--- /dev/null
+++ b/src/features/dashboard/hooks/useGetStore.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+
+const getStoreData = async () => {
+ const response = await fetch(`${CAMBASE}/Store0/diagnostics-json`);
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+export const useGetStore = () => {
+ const storeQuery = useQuery({
+ queryKey: ["storeData"],
+ queryFn: getStoreData,
+ // refetchInterval: 10 * 60 * 1000,
+ });
+ return { storeQuery };
+};
diff --git a/src/features/dashboard/hooks/useGetSystemHealth.ts b/src/features/dashboard/hooks/useGetSystemHealth.ts
new file mode 100644
index 0000000..130370f
--- /dev/null
+++ b/src/features/dashboard/hooks/useGetSystemHealth.ts
@@ -0,0 +1,17 @@
+import { useQuery } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+
+const fetchData = async () => {
+ const response = await fetch(`${CAMBASE}/api/system-health`);
+ if (!response.ok) throw new Error("Cannot get System overview");
+ return response.json();
+};
+
+export const useGetSystemHealth = () => {
+ const query = useQuery({
+ queryKey: ["fetchSystemData"],
+ queryFn: fetchData,
+ refetchInterval: 300000,
+ });
+ return { query };
+};
diff --git a/src/features/output/components/BearerTypeCard.tsx b/src/features/output/components/BearerTypeCard.tsx
new file mode 100644
index 0000000..f7144e2
--- /dev/null
+++ b/src/features/output/components/BearerTypeCard.tsx
@@ -0,0 +1,14 @@
+import Card from "../../../ui/Card";
+import CardHeader from "../../../ui/CardHeader";
+import BearerTypeFields from "./BearerTypeFields";
+
+const BearerTypeCard = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default BearerTypeCard;
diff --git a/src/features/output/components/BearerTypeFields.tsx b/src/features/output/components/BearerTypeFields.tsx
new file mode 100644
index 0000000..36f8e99
--- /dev/null
+++ b/src/features/output/components/BearerTypeFields.tsx
@@ -0,0 +1,34 @@
+import { Field, useFormikContext } from "formik";
+import type { FormTypes } from "../../../types/types";
+
+const BearerTypeFields = () => {
+ useFormikContext();
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BearerTypeFields;
diff --git a/src/features/output/components/ChannelCard.tsx b/src/features/output/components/ChannelCard.tsx
new file mode 100644
index 0000000..cf440fd
--- /dev/null
+++ b/src/features/output/components/ChannelCard.tsx
@@ -0,0 +1,32 @@
+import { useFormikContext } from "formik";
+import Card from "../../../ui/Card";
+import CardHeader from "../../../ui/CardHeader";
+import ChannelFields from "./ChannelFields";
+import type { FormTypes } from "../../../types/types";
+import { useGetBearerConfig } from "../hooks/useBearer";
+
+const ChannelCard = () => {
+ const { values, errors, touched, setFieldValue } = useFormikContext();
+ const { bearerQuery } = useGetBearerConfig(values?.format?.toLowerCase() || "json");
+ const outputData = bearerQuery?.data;
+ return (
+
+
+
+
+
+ );
+};
+
+export default ChannelCard;
diff --git a/src/features/output/components/ChannelFields.tsx b/src/features/output/components/ChannelFields.tsx
new file mode 100644
index 0000000..2b14bb9
--- /dev/null
+++ b/src/features/output/components/ChannelFields.tsx
@@ -0,0 +1,324 @@
+import { Field, FieldArray } from "formik";
+import type { FormTypes, InitialValuesFormErrors, OutputDataResponse } from "../../../types/types";
+import { useEffect, useMemo } from "react";
+import { useOptionalConstants } from "../hooks/useOptionalConstants";
+
+type ChannelFieldsProps = {
+ values: FormTypes;
+ errors: InitialValuesFormErrors;
+ touched: {
+ connectTimeoutSeconds?: boolean | undefined;
+ readTimeoutSeconds?: boolean | undefined;
+ };
+ outputData?: OutputDataResponse;
+ onSetFieldValue: (field: string, value: string, shouldValidate?: boolean | undefined) => void;
+};
+
+const ChannelFields = ({ errors, touched, values, outputData, onSetFieldValue }: ChannelFieldsProps) => {
+ const { optionalConstantsQuery } = useOptionalConstants(outputData?.id?.split("-")[1] || "");
+ const optionalConstants = optionalConstantsQuery?.data;
+
+ const channelFieldsObject = useMemo(() => {
+ return {
+ connectTimeoutSeconds: outputData?.propConnectTimeoutSeconds?.value || "5",
+ readTimeoutSeconds: outputData?.propReadTimeoutSeconds?.value || "15",
+ backOfficeURL: outputData?.propBackofficeURL?.value || "",
+ username: outputData?.propUsername?.value || "",
+ password: outputData?.propPassword?.value || "",
+ SCID: optionalConstants?.propSourceIdentifier?.value || "",
+ timestampSource: optionalConstants?.propTimeZoneType?.value || "UTC",
+ GPSFormat: optionalConstants?.propGpsFormat?.value || "Minutes",
+ FFID: optionalConstants?.propFeedIdentifier?.value || "",
+ };
+ }, [
+ optionalConstants?.propFeedIdentifier?.value,
+ optionalConstants?.propGpsFormat?.value,
+ optionalConstants?.propSourceIdentifier?.value,
+ optionalConstants?.propTimeZoneType?.value,
+ outputData?.propBackofficeURL?.value,
+ outputData?.propConnectTimeoutSeconds?.value,
+ outputData?.propPassword?.value,
+ outputData?.propReadTimeoutSeconds?.value,
+ outputData?.propUsername?.value,
+ ]);
+
+ useEffect(() => {
+ for (const [key, value] of Object.entries(channelFieldsObject)) {
+ onSetFieldValue(key, value);
+ }
+ }, [channelFieldsObject, onSetFieldValue, outputData]);
+
+ return (
+
+ {values.format.toLowerCase() !== "ftp" ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {values.format.toLowerCase() === "utmc" && (
+ <>
+
+
{values.format} Constants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {values.format?.toLowerCase() === "bof2" && (
+ <>
+
+
+
{values.format} Constants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{values.format} Lane ID Config
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
Custom Fields
+
+
+
+ {(arrayHelpers) => (
+ <>
+ {values?.customFields?.map((_, index) => {
+ // if (!field.value) return null;
+ return (
+
+
+
+
+ );
+ })}
+
+
+ {values?.customFields && values?.customFields?.length > 0 && (
+
+ )}
+
+ >
+ )}
+
+
+ >
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+export default ChannelFields;
diff --git a/src/features/output/components/OSDFieldToggle.tsx b/src/features/output/components/OSDFieldToggle.tsx
new file mode 100644
index 0000000..dadb66f
--- /dev/null
+++ b/src/features/output/components/OSDFieldToggle.tsx
@@ -0,0 +1,27 @@
+import { Field } from "formik";
+
+type OSDFieldToggleProps = {
+ value: string;
+ label: string;
+};
+
+const OSDFieldToggle = ({ value, label }: OSDFieldToggleProps) => {
+ const spacesWords = (label: string) => {
+ if (label.includes("VRM")) return label.replace("VRM", " VRM");
+ return label.replace(/([A-Z])/g, " $1").trim();
+ };
+
+ return (
+
+ );
+};
+
+export default OSDFieldToggle;
diff --git a/src/features/output/components/OSDFields.tsx b/src/features/output/components/OSDFields.tsx
new file mode 100644
index 0000000..c4a3928
--- /dev/null
+++ b/src/features/output/components/OSDFields.tsx
@@ -0,0 +1,72 @@
+import { Field, useFormikContext } from "formik";
+import { useOSDConfig } from "../hooks/useOSDConfig";
+import OSDFieldToggle from "./OSDFieldToggle";
+import type { OSDConfigFields } from "../../../types/types";
+import { toast } from "sonner";
+
+type OSDFieldsProps = {
+ isOSDLoading: boolean;
+};
+
+const OSDFields = ({ isOSDLoading }: OSDFieldsProps) => {
+ const { osdMutation } = useOSDConfig();
+ const { values } = useFormikContext();
+
+ const includeKeys = Object.keys(values as OSDConfigFields).filter((value) => value.includes("include"));
+
+ const handleSubmit = async (values: OSDConfigFields) => {
+ const result = await osdMutation.mutateAsync(values);
+ if (result?.id) {
+ toast.success("OSD Config updated successfully");
+ }
+ };
+
+ if (isOSDLoading) {
+ return Loading OSD Options...
;
+ }
+
+ return (
+
+
+
+
+ {includeKeys.map((key) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OSDFields;
diff --git a/src/features/output/components/OSDOptionsCard.tsx b/src/features/output/components/OSDOptionsCard.tsx
new file mode 100644
index 0000000..dc0f924
--- /dev/null
+++ b/src/features/output/components/OSDOptionsCard.tsx
@@ -0,0 +1,31 @@
+import Card from "../../../ui/Card";
+import CardHeader from "../../../ui/CardHeader";
+import OSDFields from "./OSDFields";
+import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
+import "react-tabs/style/react-tabs.css";
+
+type OSDOptionsCardProps = {
+ isOSDLoading: boolean;
+};
+
+const OSDOptionsCard = ({ isOSDLoading }: OSDOptionsCardProps) => {
+ return (
+
+
+
+
+ OSD Settings
+ payload Settings
+
+
+
+
+
+ payload settings
+
+
+
+ );
+};
+
+export default OSDOptionsCard;
diff --git a/src/features/output/components/OutputForms.tsx b/src/features/output/components/OutputForms.tsx
new file mode 100644
index 0000000..58faa66
--- /dev/null
+++ b/src/features/output/components/OutputForms.tsx
@@ -0,0 +1,161 @@
+import { Formik, Form } from "formik";
+import BearerTypeCard from "./BearerTypeCard";
+import ChannelCard from "./ChannelCard";
+import type { BearerTypeFields, FormTypes, OptionalBOF2Constants, OptionalUTMCConstants } from "../../../types/types";
+import { usePostBearerConfig } from "../hooks/useBearer";
+import { useDispatcherConfig } from "../hooks/useDispatcherConfig";
+import { useOptionalConstants } from "../hooks/useOptionalConstants";
+import { useCustomFields } from "../hooks/useCustomFields";
+import OSDOptionsCard from "./OSDOptionsCard";
+import { useOSDConfig } from "../hooks/useOSDConfig";
+
+const OutputForms = () => {
+ const { bearerMutation } = usePostBearerConfig();
+ const { dispatcherQuery, dispatcherMutation } = useDispatcherConfig();
+ const { customFieldsQuery, customFieldsMutation } = useCustomFields();
+ const { osdQuery } = useOSDConfig();
+
+ const isLoading = dispatcherQuery?.isLoading;
+ const isOSDLoading = osdQuery?.isLoading;
+
+ const includeVRM = osdQuery?.data?.propIncludeVRM?.value.toLowerCase() === "true";
+ const includeMotion = osdQuery?.data?.propIncludeMotion?.value.toLowerCase() === "true";
+ const includeTimeStamp = osdQuery?.data?.propIncludeTimestamp?.value.toLowerCase() === "true";
+ const includeCameraName = osdQuery?.data?.propIncludeCameraName?.value.toLowerCase() === "true";
+ const overlayPosition = osdQuery?.data?.propOverlayPosition?.value;
+ const OSDTimestampFormat = osdQuery?.data?.propTimestampFormat?.value;
+
+ const format = dispatcherQuery?.data?.propFormat?.value;
+ const { optionalConstantsQuery, optionalConstantsMutation } = useOptionalConstants(format?.toLowerCase());
+ const FFID = optionalConstantsQuery?.data?.propFeedIdentifier?.value;
+ const SCID = optionalConstantsQuery?.data?.propSourceIdentifier?.value;
+ const timestampSource = optionalConstantsQuery?.data?.propTimeZoneType?.value;
+ const gpsFormat = optionalConstantsQuery?.data?.propGpsFormat?.value;
+
+ const customFieldLabel1 = customFieldsQuery?.data?.propCustomFieldName1?.value;
+ const customFieldLabel2 = customFieldsQuery?.data?.propCustomFieldName2?.value;
+ const customFieldLabel3 = customFieldsQuery?.data?.propCustomFieldName3?.value;
+ const customFieldLabel4 = customFieldsQuery?.data?.propStringName4?.value;
+ const customFieldLabel5 = customFieldsQuery?.data?.propStringName5?.value;
+ const customFieldLabel6 = customFieldsQuery?.data?.propStringName6?.value;
+
+ const customFieldValues1 = customFieldsQuery?.data?.propCustomFieldValue1?.value;
+ const customFieldValues2 = customFieldsQuery?.data?.propCustomFieldValue2?.value;
+ const customFieldValues3 = customFieldsQuery?.data?.propCustomFieldValue3?.value;
+ const customFieldValues4 = customFieldsQuery?.data?.propStringValue4?.value;
+ const customFieldValues5 = customFieldsQuery?.data?.propStringValue5?.value;
+ const customFieldValues6 = customFieldsQuery?.data?.propStringValue6?.value;
+
+ const initialCustomFields = [
+ { label: customFieldLabel1 || "", value: customFieldValues1 || "" },
+ { label: customFieldLabel2 || "", value: customFieldValues2 || "" },
+ { label: customFieldLabel3 || "", value: customFieldValues3 || "" },
+ { label: customFieldLabel4 || "", value: customFieldValues4 || "" },
+ { label: customFieldLabel5 || "", value: customFieldValues5 || "" },
+ { label: customFieldLabel6 || "", value: customFieldValues6 || "" },
+ ].filter((field) => field.label && field.value);
+
+ const inititalValues: FormTypes = {
+ format: format ?? "JSON",
+ enabled: true,
+ backOfficeURL: "",
+ username: "",
+ password: "",
+ connectTimeoutSeconds: Number(5),
+ readTimeoutSeconds: Number(15),
+ overviewQuality: "HIGH",
+ cropSizeFactor: "3/4",
+
+ // optional constants
+ FFID: FFID ?? "",
+ SCID: SCID ?? "",
+ timestampSource: timestampSource ?? "UTC",
+ GPSFormat: gpsFormat ?? "Minutes",
+
+ //BOF2 - optional Lane IDs
+ laneId: "",
+ LID1: "",
+ LID2: "",
+
+ // ftp - fields
+
+ //custom fields
+ customFields: initialCustomFields,
+
+ // OSD Options
+ includeVRM: includeVRM ?? false,
+ includeMotion: includeMotion ?? false,
+ includeTimeStamp: includeTimeStamp ?? false,
+ includeCameraName: includeCameraName ?? false,
+ overlayPosition: overlayPosition ?? "Top",
+ OSDTimestampFormat: OSDTimestampFormat ?? "UTC",
+ };
+
+ const handleSubmit = async (values: FormTypes) => {
+ const bearerTypeFields = {
+ format: values.format,
+ enabled: values.enabled,
+ };
+
+ const bearerFields: BearerTypeFields = {
+ format: values.format,
+ enabled: values.enabled,
+ backOfficeURL: values.backOfficeURL,
+ username: values.username,
+ password: values.password,
+ connectTimeoutSeconds: values.connectTimeoutSeconds,
+ readTimeoutSeconds: values.readTimeoutSeconds,
+ overviewQuality: values.overviewQuality,
+ cropSizeFactor: values.cropSizeFactor,
+ };
+
+ const result = await dispatcherMutation.mutateAsync(bearerTypeFields);
+
+ if (result?.id) {
+ await bearerMutation.mutateAsync(bearerFields);
+
+ if (values.format === "BOF2") {
+ const optionalBOF2Fields: OptionalBOF2Constants = {
+ format: values.format,
+ FFID: values.FFID,
+ SCID: values.SCID,
+ timestampSource: values.timestampSource,
+ GPSFormat: values.GPSFormat,
+ };
+ await optionalConstantsMutation.mutateAsync(optionalBOF2Fields);
+ }
+ if (values.format === "UTMC") {
+ const optionalUTMCFields: OptionalUTMCConstants = {
+ format: values.format,
+ SCID: values.SCID,
+ timestampSource: values.timestampSource,
+ GPSFormat: values.GPSFormat,
+ };
+ await optionalConstantsMutation.mutateAsync(optionalUTMCFields);
+ }
+ }
+ if (values.customFields && values.customFields.length > 0) {
+ const customFields = [...values.customFields];
+
+ await customFieldsMutation.mutateAsync(customFields);
+ }
+ };
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default OutputForms;
diff --git a/src/features/output/hooks/useBearer.ts b/src/features/output/hooks/useBearer.ts
new file mode 100644
index 0000000..a0c9fd0
--- /dev/null
+++ b/src/features/output/hooks/useBearer.ts
@@ -0,0 +1,66 @@
+import { useMutation, useQuery } from "@tanstack/react-query";
+import type { BearerTypeFields } from "../../../types/types";
+import { CAMBASE } from "../../../utils/config";
+
+const fetchBearerConfig = async (bearerConfig: string) => {
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=Dispatcher0-${bearerConfig}`, {
+ method: "GET",
+ });
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+const postBearerConfig = async (config: BearerTypeFields) => {
+ const channelConfigPayload = {
+ id: `Dispatcher0-${config.format.toLowerCase()}`,
+ fields: [
+ {
+ property: "propBackofficeURL",
+ value: config.backOfficeURL,
+ },
+ {
+ property: "propConnectTimeoutSeconds",
+ value: config.connectTimeoutSeconds,
+ },
+ {
+ property: "propPassword",
+ value: config.password,
+ },
+ {
+ property: "propReadTimeoutSeconds",
+ value: config.readTimeoutSeconds,
+ },
+ {
+ property: "propUsername",
+ value: config.username,
+ },
+ ],
+ };
+ const response = await fetch(`${CAMBASE}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(channelConfigPayload),
+ });
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+export const usePostBearerConfig = () => {
+ const bearerMutation = useMutation({
+ mutationFn: (query: BearerTypeFields) => postBearerConfig(query),
+ mutationKey: ["outputs"],
+ });
+ return { bearerMutation };
+};
+
+export const useGetBearerConfig = (bearerConfig: string) => {
+ const bearerQuery = useQuery({
+ queryKey: ["outputs", bearerConfig],
+ queryFn: () => fetchBearerConfig(bearerConfig),
+ });
+
+ return { bearerQuery };
+};
diff --git a/src/features/output/hooks/useCustomFields.ts b/src/features/output/hooks/useCustomFields.ts
new file mode 100644
index 0000000..b281453
--- /dev/null
+++ b/src/features/output/hooks/useCustomFields.ts
@@ -0,0 +1,65 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+import type { CustomFieldConfig } from "../../../types/types";
+
+const fetchCustomFields = async () => {
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=SightingAmmend0-custom-fields`);
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+const postCustomFields = async (customFieldConfig: CustomFieldConfig[]) => {
+ const fields = [];
+
+ for (const customField of customFieldConfig) {
+ if (customField.value)
+ if (customFieldConfig.indexOf(customField) > 2) {
+ fields.push({
+ property: `propStringName${customFieldConfig.indexOf(customField) + 1}`,
+ value: customField.label,
+ });
+ fields.push({
+ property: `propStringValue${customFieldConfig.indexOf(customField) + 1}`,
+ value: customField.value,
+ });
+ } else {
+ fields.push(
+ {
+ property: `propCustomFieldName${customFieldConfig.indexOf(customField) + 1}`,
+ value: customField.label,
+ },
+ {
+ property: `propCustomFieldValue${customFieldConfig.indexOf(customField) + 1}`,
+ value: customField.value,
+ },
+ );
+ }
+ }
+ const customFieldConfigPayload = {
+ id: "SightingAmmend0-custom-fields",
+ fields,
+ };
+
+ const response = await fetch(`${CAMBASE}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(customFieldConfigPayload),
+ });
+ if (!response.ok) throw new Error("Network response was not ok");
+ return response.json();
+};
+
+export const useCustomFields = () => {
+ const customFieldsQuery = useQuery({
+ queryKey: ["customFields"],
+ queryFn: fetchCustomFields,
+ });
+
+ const customFieldsMutation = useMutation({
+ mutationKey: ["customFieldsMutation"],
+ mutationFn: postCustomFields,
+ });
+
+ return { customFieldsQuery, customFieldsMutation };
+};
diff --git a/src/features/output/hooks/useDispatcherConfig.ts b/src/features/output/hooks/useDispatcherConfig.ts
new file mode 100644
index 0000000..af9b82c
--- /dev/null
+++ b/src/features/output/hooks/useDispatcherConfig.ts
@@ -0,0 +1,48 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import type { DispatcherConfig } from "../../../types/types";
+import { CAMBASE } from "../../../utils/config";
+
+const getDispatcherConfig = async () => {
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=Dispatcher0`);
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+const postDispatcherConfig = async (config: DispatcherConfig) => {
+ const updateConfigPayload = {
+ id: "Dispatcher0",
+ fields: [
+ {
+ property: "propEnabled",
+ value: config.enabled,
+ },
+ {
+ property: "propFormat",
+ value: config.format,
+ },
+ ],
+ };
+ const response = await fetch(`${CAMBASE}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(updateConfigPayload),
+ });
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+export const useDispatcherConfig = () => {
+ const dispatcherQuery = useQuery({
+ queryKey: ["dispatcherConfig"],
+ queryFn: () => getDispatcherConfig(),
+ });
+
+ const dispatcherMutation = useMutation({
+ mutationKey: ["postDispatcherConfig"],
+ mutationFn: (config: DispatcherConfig) => postDispatcherConfig(config),
+ });
+ return { dispatcherQuery, dispatcherMutation };
+};
diff --git a/src/features/output/hooks/useOSDConfig.ts b/src/features/output/hooks/useOSDConfig.ts
new file mode 100644
index 0000000..7208a44
--- /dev/null
+++ b/src/features/output/hooks/useOSDConfig.ts
@@ -0,0 +1,52 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+import type { OSDConfigFields } from "../../../types/types";
+
+const fetchOSDConfig = async () => {
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=SightingAmmend0-overlay`);
+
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+const postOSDConfig = async (data: OSDConfigFields) => {
+ const fields = [
+ { property: "propIncludeVRM", value: data.includeVRM },
+ { property: "propIncludeMotion", value: data.includeMotion },
+ { property: "propIncludeTimestamp", value: data.includeTimeStamp },
+ { property: "propIncludeCameraName", value: data.includeCameraName },
+ { property: "propOverlayPosition", value: data.overlayPosition },
+ { property: "propTimestampFormat", value: data.OSDTimestampFormat },
+ ];
+
+ const osdConfigPayload = {
+ id: "SightingAmmend0-overlay",
+ fields: fields,
+ };
+
+ const response = await fetch(`${CAMBASE}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(osdConfigPayload),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to post OSD Config");
+ }
+ return response.json();
+};
+
+export const useOSDConfig = () => {
+ const osdQuery = useQuery({
+ queryKey: ["osdConfig"],
+ queryFn: fetchOSDConfig,
+ });
+
+ const osdMutation = useMutation({
+ mutationFn: postOSDConfig,
+ mutationKey: ["postOSDConfig"],
+ });
+
+ return { osdQuery, osdMutation };
+};
diff --git a/src/features/output/hooks/useOptionalConstants.ts b/src/features/output/hooks/useOptionalConstants.ts
new file mode 100644
index 0000000..12b92fa
--- /dev/null
+++ b/src/features/output/hooks/useOptionalConstants.ts
@@ -0,0 +1,63 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import type { OptionalBOF2Constants } from "../../../types/types";
+import { CAMBASE } from "../../../utils/config";
+
+const fetchOptionalConstants = async (format: string) => {
+ if (!format || format === "json") return null;
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=Dispatcher0-${format}-constants`);
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+const postOptionalConstants = async (config: OptionalBOF2Constants) => {
+ const fields = [
+ {
+ property: "propSourceIdentifier",
+ value: config?.SCID,
+ },
+ {
+ property: "propTimeZoneType",
+ value: config?.timestampSource,
+ },
+ {
+ property: "propGpsFormat",
+ value: config?.GPSFormat,
+ },
+ ];
+
+ if (config.FFID) {
+ fields.push({
+ property: "propFeedIdentifier",
+ value: config.FFID,
+ });
+ }
+ const updateConfigPayload = {
+ id: `Dispatcher0-${config.format?.toLowerCase()}-constants`,
+ fields: fields,
+ };
+
+ const response = await fetch(`${CAMBASE}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(updateConfigPayload),
+ });
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return response.json();
+};
+
+export const useOptionalConstants = (format: string) => {
+ const optionalConstantsQuery = useQuery({
+ queryKey: ["optionalConstants", format],
+ queryFn: () => fetchOptionalConstants(format),
+ enabled: !!format && format !== "json",
+ });
+
+ const optionalConstantsMutation = useMutation({
+ mutationKey: ["postOptionalConstants"],
+ mutationFn: postOptionalConstants,
+ });
+ return { optionalConstantsQuery, optionalConstantsMutation };
+};
diff --git a/src/features/settings/components/Settings.tsx b/src/features/settings/components/Settings.tsx
new file mode 100644
index 0000000..ee92572
--- /dev/null
+++ b/src/features/settings/components/Settings.tsx
@@ -0,0 +1,25 @@
+import { Tabs, Tab, TabList, TabPanel } from "react-tabs";
+import "react-tabs/style/react-tabs.css";
+import Card from "../../../ui/Card";
+import SystemConfig from "./SystemConfig";
+import CardHeader from "../../../ui/CardHeader";
+
+const Settings = () => {
+ return (
+
+
+
+ Systems
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/src/features/settings/components/SystemConfig.tsx b/src/features/settings/components/SystemConfig.tsx
new file mode 100644
index 0000000..4d064df
--- /dev/null
+++ b/src/features/settings/components/SystemConfig.tsx
@@ -0,0 +1,233 @@
+import { Formik, Form, Field, FieldArray } from "formik";
+import { useSystemSettings } from "../hooks/useSystemSettings";
+import type { NetworkConfig, SystemSettings } from "../../../types/types";
+import { toast } from "sonner";
+import { useGetNetworkConfig } from "../hooks/useGetNetworkConfig";
+
+const SystemConfig = () => {
+ const { systemSettingsQuery, systemSettingsMutation } = useSystemSettings();
+ const { networkConfigQuery, networkConfigMutation } = useGetNetworkConfig();
+
+ const isLoading = networkConfigMutation?.isPending || networkConfigMutation?.isPending;
+ const isGettingLoading = systemSettingsQuery?.isLoading || networkConfigQuery?.isLoading;
+ const timeZoneOptions = systemSettingsQuery?.data?.propLocalTimeZone?.accepted;
+ const timeZoneOpts = timeZoneOptions?.split(",").map((option: string) => option.trim().replace(/\[|\]/g, ""));
+ const timeSourceOptions = systemSettingsQuery?.data?.propTimeSource?.accepted;
+ const timeSourceOpts = timeSourceOptions?.split(",").map((option: string) => option.trim().replace(/\[|\]/g, ""));
+ const deviceName = systemSettingsQuery?.data?.propDeviceName?.value;
+ const timeZone = systemSettingsQuery?.data?.propLocalTimeZone?.value;
+ const SNTPServer = systemSettingsQuery?.data?.propSNTPServer?.value;
+ const SNTPInterval = systemSettingsQuery?.data?.propSNTPIntervalMinutes?.value;
+ const timeSource = systemSettingsQuery?.data?.propTimeSource?.value;
+ const primaryServer = networkConfigQuery?.data?.propNameServerPrimary?.value;
+ const secondaryServer = networkConfigQuery?.data?.propNameServerSecondary?.value;
+ const ipAddress = networkConfigQuery?.data?.propHost?.value;
+ const subnetMask = networkConfigQuery?.data?.propNetmask?.value;
+ const gateway = networkConfigQuery?.data?.propGateway?.value;
+
+ const initialValues = {
+ deviceName: deviceName ?? "",
+ timeZone: timeZone ?? "",
+ localTimeZone: timeZone ?? "",
+ SNTPServer: SNTPServer ?? "",
+ SNTPInterval: SNTPInterval ?? 60,
+ SNTPIntervalMinutes: SNTPInterval ?? 60,
+ primaryServer: primaryServer ?? "",
+ secondaryServer: secondaryServer ?? "",
+ timeSource: timeSource ?? "",
+ ipAddress: ipAddress ?? "",
+ subnetMask: subnetMask ?? "",
+ gateway: gateway ?? "",
+ customFields: [],
+ };
+
+ const handleSubmit = async (values: SystemSettings & NetworkConfig) => {
+ const result = await systemSettingsMutation.mutateAsync(values);
+ const networkResult = await networkConfigMutation.mutateAsync({
+ ipAddress: values.ipAddress,
+ subnetMask: values.subnetMask,
+ gateway: values.gateway,
+ primaryServer: values.primaryServer,
+ secondaryServer: values.secondaryServer,
+ });
+
+ if (result.id && networkResult.id) {
+ toast.success("System settings updated successfully");
+ } else {
+ toast.error("Failed to update system settings");
+ }
+ };
+
+ if (isGettingLoading) {
+ return Loading...
;
+ }
+
+ return (
+
+ {({ values }) => (
+
+ )}
+
+ );
+};
+
+export default SystemConfig;
diff --git a/src/features/settings/hooks/useGetNetworkConfig.ts b/src/features/settings/hooks/useGetNetworkConfig.ts
new file mode 100644
index 0000000..1750634
--- /dev/null
+++ b/src/features/settings/hooks/useGetNetworkConfig.ts
@@ -0,0 +1,49 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+import type { NetworkConfig } from "../../../types/types";
+
+const fetchNetworkConfig = async () => {
+ const response = await fetch(`${CAMBASE}/api/fetch-config?id=GLOBAL--NetworkConfig`);
+ if (!response.ok) throw new Error("Network response was not ok");
+ return response.json();
+};
+
+const postNetworkConfig = async (networkConfig: NetworkConfig) => {
+ const fields = [
+ { property: "propNetmask", value: networkConfig.subnetMask },
+ { property: "propHost", value: networkConfig.ipAddress },
+ { property: "propGateway", value: networkConfig.gateway },
+ ];
+
+ if (networkConfig.primaryServer !== undefined) {
+ fields.push({ property: "propNameServerPrimary", value: networkConfig.primaryServer });
+ }
+ if (networkConfig.secondaryServer !== undefined) {
+ fields.push({ property: "propNameServerSecondary", value: networkConfig.secondaryServer });
+ }
+ const networkConfigPayload = {
+ id: "GLOBAL--NetworkConfig",
+ fields,
+ };
+
+ const respones = await fetch(`${CAMBASE}/api/update-config?id=GLOBAL--NetworkConfig`, {
+ method: "POST",
+ body: JSON.stringify(networkConfigPayload),
+ });
+ if (!respones.ok) throw new Error("Network response was not ok");
+ return respones.json();
+};
+
+export const useGetNetworkConfig = () => {
+ const networkConfigQuery = useQuery({
+ queryKey: ["networkConfig"],
+ queryFn: fetchNetworkConfig,
+ });
+
+ const networkConfigMutation = useMutation({
+ mutationKey: ["networkConfigMutation"],
+ mutationFn: postNetworkConfig,
+ });
+
+ return { networkConfigQuery, networkConfigMutation };
+};
diff --git a/src/features/settings/hooks/useSystemSettings.ts b/src/features/settings/hooks/useSystemSettings.ts
new file mode 100644
index 0000000..46eb4e8
--- /dev/null
+++ b/src/features/settings/hooks/useSystemSettings.ts
@@ -0,0 +1,54 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { CAMBASE } from "../../../utils/config";
+import type { SystemSettings } from "../../../types/types";
+const camBase = import.meta.env.MODE !== "development" ? CAMBASE : "";
+
+const fetchSystemSettings = async () => {
+ const response = await fetch(`${camBase}/api/fetch-config?id=GLOBAL--Device`);
+ if (!response.ok) {
+ throw new Error("Failed to fetch system settings");
+ }
+ return response.json();
+};
+
+const postSystemSettings = async (settings: SystemSettings) => {
+ const systemSettingConfig = {
+ id: "GLOBAL--Device",
+ fields: [
+ { property: "propDeviceName", value: settings.deviceName },
+ { property: "propSNTPServer", value: settings.SNTPServer },
+ {
+ property: "propSNTPIntervalMinutes",
+ value: Number(settings.SNTPIntervalMinutes),
+ },
+ { property: "propLocalTimeZone", value: settings.localTimeZone },
+ { property: "propTimeSource", value: settings.timeSource },
+ ],
+ };
+
+ const response = await fetch(`${camBase}/api/update-config`, {
+ method: "POST",
+ body: JSON.stringify(systemSettingConfig),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error("Failed to update system settings");
+ }
+ return response.json();
+};
+
+export const useSystemSettings = () => {
+ const systemSettingsQuery = useQuery({
+ queryKey: ["systemSettings"],
+ queryFn: fetchSystemSettings,
+ });
+
+ const systemSettingsMutation = useMutation({
+ mutationKey: ["updateSystemSettings"],
+ mutationFn: postSystemSettings,
+ });
+
+ return { systemSettingsQuery, systemSettingsMutation };
+};
diff --git a/src/hooks/useBlackBoard.ts b/src/hooks/useBlackBoard.ts
new file mode 100644
index 0000000..638fcae
--- /dev/null
+++ b/src/hooks/useBlackBoard.ts
@@ -0,0 +1,35 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { CAMBASE } from "../utils/config";
+import type { BlackBoardOptions } from "../types/types";
+
+const fetchBlackBoardData = async () => {
+ const response = await fetch(`${CAMBASE}/api/blackboard`);
+ if (!response.ok) {
+ throw new Error("Failed to fetch blackboard data");
+ }
+ return response.json();
+};
+
+const viewBlackBoardData = async (options: BlackBoardOptions) => {
+ const response = await fetch(`${CAMBASE}/api/blackboard`, {
+ method: "POST",
+ body: JSON.stringify(options),
+ });
+ if (!response.ok) {
+ throw new Error("Failed to view blackboard data");
+ }
+ return response.json();
+};
+
+export const useBlackBoard = () => {
+ const blackboardQuery = useQuery({
+ queryKey: ["blackboardData"],
+ queryFn: fetchBlackBoardData,
+ });
+
+ const blackboardMutation = useMutation({
+ mutationKey: ["viewBlackBoardData"],
+ mutationFn: (options: BlackBoardOptions) => viewBlackBoardData(options),
+ });
+ return { blackboardQuery, blackboardMutation };
+};
diff --git a/src/index.css b/src/index.css
index 9951853..3cc880d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,3 +5,33 @@ body {
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
+
+/* Modal animations */
+.ReactModal__Overlay {
+ opacity: 0;
+ transition: opacity 200ms ease-in-out;
+}
+
+.ReactModal__Overlay--after-open {
+ opacity: 1;
+}
+
+.ReactModal__Overlay--before-close {
+ opacity: 0;
+}
+
+.ReactModal__Content {
+ transform: scale(0.9) translateY(-20px);
+ opacity: 0;
+ transition: all 200ms ease-in-out;
+}
+
+.ReactModal__Content--after-open {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+}
+
+.ReactModal__Content--before-close {
+ transform: scale(0.9) translateY(-20px);
+ opacity: 0;
+}
diff --git a/src/main.tsx b/src/main.tsx
index d0ee7d9..90b08ec 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,9 +4,12 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen"; // generated by plugin
import { AppProviders } from "./app/providers/AppProviders";
import "./index.css";
+import Modal from "react-modal";
const router = createRouter({ routeTree });
+Modal.setAppElement("#root");
+
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 6621494..5fff20b 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -2,12 +2,14 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import Header from "../ui/Header";
import Footer from "../ui/Footer";
+import { Toaster } from "sonner";
const RootLayout = () => (
<>
+
diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx
index 8ec01dd..4fbcf5b 100644
--- a/src/routes/baywatch.tsx
+++ b/src/routes/baywatch.tsx
@@ -1,9 +1,14 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute } from "@tanstack/react-router";
+import CameraGrid from "../features/cameras/components/CameraGrid";
-export const Route = createFileRoute('/baywatch')({
+export const Route = createFileRoute("/baywatch")({
component: RouteComponent,
-})
+});
function RouteComponent() {
- return Hello "/baywatch"!
+ return (
+
+
+
+ );
}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 8cd7e69..3f2d9cb 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -6,10 +6,5 @@ export const Route = createFileRoute("/")({
});
function HomePage() {
- return (
-
-
Dashboard
-
-
- );
+ return ;
}
diff --git a/src/routes/output.tsx b/src/routes/output.tsx
index 430d04d..f1be44e 100644
--- a/src/routes/output.tsx
+++ b/src/routes/output.tsx
@@ -1,9 +1,14 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute } from "@tanstack/react-router";
+import OutputForms from "../features/output/components/OutputForms";
-export const Route = createFileRoute('/output')({
+export const Route = createFileRoute("/output")({
component: RouteComponent,
-})
+});
function RouteComponent() {
- return Hello "/output"!
+ return (
+
+
+
+ );
}
diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx
index 41456d0..4c5c820 100644
--- a/src/routes/settings.tsx
+++ b/src/routes/settings.tsx
@@ -1,9 +1,14 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute } from "@tanstack/react-router";
+import Settings from "../features/settings/components/Settings";
-export const Route = createFileRoute('/settings')({
+export const Route = createFileRoute("/settings")({
component: RouteComponent,
-})
+});
function RouteComponent() {
- return Hello "/settings"!
+ return (
+
+
+
+ );
}
diff --git a/src/types/types.ts b/src/types/types.ts
index bc491f9..a6c1138 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -10,3 +10,231 @@ export type InfoBarData = {
"memory-cpu-status": string;
"thread-count": string;
};
+
+export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger";
+export type Region = {
+ name: string;
+ brushColour: string;
+};
+
+export type SystemHealthStatus = {
+ id: string;
+ tags: string[];
+ groupID: string;
+};
+
+export type StatusGroups = {
+ channelA: SystemHealthStatus[];
+ channelB: SystemHealthStatus[];
+ channelC: SystemHealthStatus[];
+ default: SystemHealthStatus[];
+};
+
+export type BearerTypeFields = {
+ format: string;
+ enabled: boolean;
+ backOfficeURL: string;
+ username: string;
+ password: string;
+ connectTimeoutSeconds: number;
+ readTimeoutSeconds: number;
+ overviewQuality: string;
+ cropSizeFactor: string;
+};
+
+export type OptionalConstants = {
+ FFID?: string;
+ SCID?: string;
+ timestampSource?: string;
+ GPSFormat?: string;
+};
+
+export type OptionalLaneIDs = {
+ laneId?: string;
+ LID1?: string;
+ LID2?: string;
+ LID3?: string;
+};
+
+export type CustomField = {
+ label: string;
+ value: string;
+};
+
+export type CustomFields = {
+ customFields?: CustomField[];
+};
+
+export type InitialValuesFormErrors = {
+ backOfficeURL?: string;
+ username?: string;
+ password?: string;
+ connectTimeoutSeconds?: string;
+ readTimeoutSeconds?: string;
+};
+
+export type OSDConfigFields = {
+ includeVRM: boolean;
+ includeMotion: boolean;
+ includeTimeStamp: boolean;
+ includeCameraName: boolean;
+ overlayPosition: "Top" | "Bottom" | "Left" | "Right";
+ OSDTimestampFormat: "UTC" | "LOCAL";
+};
+
+export type FormTypes = BearerTypeFields & OptionalConstants & OptionalLaneIDs & CustomFields & OSDConfigFields;
+type FieldProperty = {
+ datatype: string;
+ value: string;
+};
+export type OutputDataResponse = {
+ id: string;
+ configHash: string;
+} & Record;
+
+export type PaintedCell = {
+ colour: string;
+ region: Region;
+};
+
+export type DispatcherConfig = {
+ format: string;
+ enabled: boolean;
+};
+
+export type OptionalBOF2Constants = {
+ format?: string;
+ FFID?: string;
+ SCID?: string;
+ timestampSource?: string;
+ GPSFormat?: string;
+};
+
+export type OptionalUTMCConstants = {
+ format?: string;
+ SCID?: string;
+ timestampSource?: string;
+ GPSFormat?: string;
+};
+export type OptionalBOF2LaneIDs = {
+ laneId?: string;
+ LID1?: string;
+ LID2?: string;
+ LID3?: string;
+};
+
+export type CameraFeedState = {
+ cameraFeedID: "A" | "B" | "C";
+ paintedCells: {
+ A: Map;
+ B: Map;
+ C: Map;
+ };
+ regionsByCamera: {
+ A: Region[];
+ B: Region[];
+ C: Region[];
+ };
+ selectedRegionIndex: number;
+ modeByCamera: {
+ A: string;
+ B: string;
+ C: string;
+ };
+
+ tabIndex?: number;
+ zoomLevel: {
+ A: number;
+ B: number;
+ C: number;
+ };
+};
+
+export type CameraFeedAction =
+ | {
+ type: "SET_CAMERA_FEED";
+ payload: "A" | "B" | "C";
+ }
+ | {
+ type: "CHANGE_MODE";
+ payload: { cameraFeedID: "A" | "B" | "C"; mode: string };
+ }
+ | { type: "SET_SELECTED_REGION_INDEX"; payload: number }
+ | {
+ type: "SET_SELECTED_REGION_COLOUR";
+ payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; newColour: string };
+ }
+ | {
+ type: "ADD_NEW_REGION";
+ payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; brushColour: string };
+ }
+ | {
+ type: "REMOVE_REGION";
+ payload: { cameraFeedID: "A" | "B" | "C"; regionName: string };
+ }
+ | {
+ type: "RESET_PAINTED_CELLS";
+ payload: { cameraFeedID: "A" | "B" | "C"; paintedCells: Map };
+ }
+ | {
+ type: "SET_CAMERA_FEED_DATA";
+ cameraState: CameraFeedState;
+ }
+ | {
+ type: "RESET_CAMERA_FEED";
+ }
+ | {
+ type: "SET_ZOOM_LEVEL";
+ payload: { cameraFeedID: "A" | "B" | "C"; zoomLevel: number };
+ };
+
+export type DecodeReading = {
+ id: number;
+ vrm: string;
+ laneID: number;
+ seenCount: number;
+ firstSeenTime?: number;
+ lastSeenTime?: number;
+ duplicate?: true;
+ firstSeenTimeHumane: string;
+ lastSeenTimeHumane: string;
+};
+
+export type ColourData = {
+ id: string | number;
+ cells: number[][];
+};
+
+export type ColourDetectionPayload = {
+ cameraFeedID: "A" | "B" | "C";
+ regions: ColourData[];
+};
+
+export type SystemSettings = {
+ deviceName: string;
+ localTimeZone: string;
+ timeSource: string;
+ SNTPServer: string;
+ SNTPIntervalMinutes: number;
+};
+
+export type NetworkConfig = {
+ ipAddress: string;
+ subnetMask: string;
+ gateway: string;
+ primaryServer?: string;
+ secondaryServer?: string;
+};
+
+export type CustomFieldConfig = {
+ label: string;
+ value: string;
+};
+
+export type BlackBoardOptions = {
+ operation?: string;
+ path?: string;
+ value?: object | string | number | (string | number)[] | null;
+};
+
+export type CameraZoomConfig = { cameraFeedID: string; zoomLevel: number };
diff --git a/src/ui/Badge.tsx b/src/ui/Badge.tsx
new file mode 100644
index 0000000..7e088af
--- /dev/null
+++ b/src/ui/Badge.tsx
@@ -0,0 +1,24 @@
+import type { Icon, IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { capitalize } from "../utils/utils";
+
+type BadgeProps = {
+ icon?: Icon | IconDefinition;
+ text: string;
+};
+
+const Badge = ({ icon, text }: BadgeProps) => {
+ const lowerCaseWord = text.toLowerCase();
+ return (
+
+ {icon && }
+ {capitalize(lowerCaseWord)}
+
+ );
+};
+
+export default Badge;
diff --git a/src/ui/Card.tsx b/src/ui/Card.tsx
index bf2316c..2b814a9 100644
--- a/src/ui/Card.tsx
+++ b/src/ui/Card.tsx
@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
return (
diff --git a/src/ui/CardHeader.tsx b/src/ui/CardHeader.tsx
index 027ecae..c2acc45 100644
--- a/src/ui/CardHeader.tsx
+++ b/src/ui/CardHeader.tsx
@@ -1,18 +1,27 @@
import clsx from "clsx";
+import StatusIndicators from "./StatusIndicators";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import type { IconProp } from "@fortawesome/fontawesome-svg-core";
type CameraOverviewHeaderProps = {
title?: string;
+ status?: string;
+ refetch?: () => void;
+ icon?: IconProp;
};
-const CardHeader = ({ title }: CameraOverviewHeaderProps) => {
+const CardHeader = ({ title, status, icon, refetch }: CameraOverviewHeaderProps) => {
return (
-
- {/* {icon &&
} */}
-
{title}
+
+
+ {status && }
+ {title}
+
+ {icon && }
);
diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx
index c1934dd..296ad65 100644
--- a/src/ui/Header.tsx
+++ b/src/ui/Header.tsx
@@ -1,33 +1,76 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faGaugeHigh } from "@fortawesome/free-solid-svg-icons";
+import { useState } from "react";
import { Link } from "@tanstack/react-router";
import Logo from "/MAV.svg";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faBars } from "@fortawesome/free-solid-svg-icons";
const Header = () => {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const toggleMenu = () => {
+ setIsMenuOpen(!isMenuOpen);
+ };
return (
-
-
-
-

-
+