From 97818ca8d9ea87bb16962c61cbd282f327404583 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 7 Jan 2026 16:18:14 +0000 Subject: [PATCH 1/2] - Add SystemOverview component and related hooks; - update Dashboard layout and introduce new UI components --- src/components/ui/Badge.tsx | 20 ++++++ src/components/ui/StatusIndicator.tsx | 9 +++ src/features/dashboard/Dashboard.tsx | 11 ++-- .../SystemOverview/PlatesProcessed.tsx | 36 +++++++++++ .../SystemOverview/SystemOverview.tsx | 43 +++++++++++++ .../SystemOverview/SystemStatusContent.tsx | 62 +++++++++++++++++++ .../systemStatusModal/SystemStatusModal.tsx | 38 ++++++++++++ .../components/videoFeed/VideoFeed.tsx | 2 +- src/features/dashboard/hooks/useGetStore.ts | 16 +++++ .../dashboard/hooks/useGetSystemHealth.ts | 21 +++++++ src/utils/types.ts | 22 +++++++ src/utils/utils.ts | 4 ++ 12 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/StatusIndicator.tsx create mode 100644 src/features/dashboard/SystemOverview/PlatesProcessed.tsx create mode 100644 src/features/dashboard/SystemOverview/SystemOverview.tsx create mode 100644 src/features/dashboard/SystemOverview/SystemStatusContent.tsx create mode 100644 src/features/dashboard/SystemOverview/systemStatusModal/SystemStatusModal.tsx create mode 100644 src/features/dashboard/hooks/useGetStore.ts create mode 100644 src/features/dashboard/hooks/useGetSystemHealth.ts diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..d94cbb2 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,20 @@ +import { capitalize } from "../../utils/utils"; + +type BadgeProps = { + text: string; +}; + +const Badge = ({ text }: BadgeProps) => { + const lowerCaseWord = text.toLowerCase(); + return ( + + {capitalize(lowerCaseWord)} + + ); +}; + +export default Badge; diff --git a/src/components/ui/StatusIndicator.tsx b/src/components/ui/StatusIndicator.tsx new file mode 100644 index 0000000..a0a6bc4 --- /dev/null +++ b/src/components/ui/StatusIndicator.tsx @@ -0,0 +1,9 @@ +import clsx from "clsx"; + +type StatusIndicatorProps = { status: string }; + +const StatusIndicator = ({ status }: StatusIndicatorProps) => { + return ; +}; + +export default StatusIndicator; diff --git a/src/features/dashboard/Dashboard.tsx b/src/features/dashboard/Dashboard.tsx index 8a1de53..5e56d78 100644 --- a/src/features/dashboard/Dashboard.tsx +++ b/src/features/dashboard/Dashboard.tsx @@ -3,6 +3,7 @@ import SightingStack from "./components/sightingStack/SightingStack"; import VideoFeed from "./components/videoFeed/VideoFeed"; import { useSightingList } from "./hooks/useSightingList"; import { useCameraSettingsContext } from "../../app/context/CameraSettingsContext"; +import SystemOverview from "./SystemOverview/SystemOverview"; const Dashboard = () => { const { sightingList, isLoading } = useSightingList(); @@ -13,13 +14,15 @@ const Dashboard = () => { return (
-
system overview
-
-
+ +
+
- +
+ +
); diff --git a/src/features/dashboard/SystemOverview/PlatesProcessed.tsx b/src/features/dashboard/SystemOverview/PlatesProcessed.tsx new file mode 100644 index 0000000..83866c0 --- /dev/null +++ b/src/features/dashboard/SystemOverview/PlatesProcessed.tsx @@ -0,0 +1,36 @@ +import type { StoreData } from "../../../utils/types"; + +type PlatesProcessedProps = { + platesProcessed: StoreData; +}; + +const PlatesProcessed = ({ platesProcessed }: PlatesProcessedProps) => { + const totalPending = platesProcessed?.totalPending || 0; + const totalActive = platesProcessed?.totalActive || 0; + const totalFailed = platesProcessed?.totalLost || 0; + const toastReceived = platesProcessed?.totalReceived || 0; + + return ( +
+
+

Total Active

+ {totalActive} +
+
+

Total Received

+ {toastReceived} +
+
+

Total Pending

+ {totalPending} +
+ +
+

Total Failed

+ {totalFailed} +
+
+ ); +}; + +export default PlatesProcessed; diff --git a/src/features/dashboard/SystemOverview/SystemOverview.tsx b/src/features/dashboard/SystemOverview/SystemOverview.tsx new file mode 100644 index 0000000..63d1e0f --- /dev/null +++ b/src/features/dashboard/SystemOverview/SystemOverview.tsx @@ -0,0 +1,43 @@ +import Card from "../../../components/ui/Card"; +import { useGetStore } from "../hooks/useGetStore"; + +import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; +import PlatesProcessed from "./PlatesProcessed"; +import SystemStatusContent from "./SystemStatusContent"; + +const SystemOverview = () => { + const { systemHealthQuery } = useGetSystemHealth(); + const { storeQuery } = useGetStore(); + + const platesProcessed = storeQuery?.data || {}; + const upTime = systemHealthQuery?.data?.UptimeHumane || 0; + const startTime = systemHealthQuery?.data?.StartTimeHumane || ""; + const cameraStatus = systemHealthQuery?.data?.Status || []; + const isError = systemHealthQuery?.isError || false; + + if (systemHealthQuery.isLoading) { + return
Loading system overview...
; + } + return ( +
+
+ + + + +

Active Sightings

+
+ +

Reads

+ +
+ +

Up Time

{upTime} +

Start Time

{startTime} +
+
+
+ ); +}; + +export default SystemOverview; diff --git a/src/features/dashboard/SystemOverview/SystemStatusContent.tsx b/src/features/dashboard/SystemOverview/SystemStatusContent.tsx new file mode 100644 index 0000000..0209098 --- /dev/null +++ b/src/features/dashboard/SystemOverview/SystemStatusContent.tsx @@ -0,0 +1,62 @@ +import StatusIndicator from "../../../components/ui/StatusIndicator"; +import type { CameraStatus } from "../../../utils/types"; +import { useState } from "react"; +import SystemStatusModal from "./systemStatusModal/SystemStatusModal"; + +type SystemStatusContentProps = { + status?: CameraStatus[]; + isError: boolean; +}; + +const SystemStatusContent = ({ status, isError }: SystemStatusContentProps) => { + const [isSystemModalOpen, setIsSystemModalOpen] = useState(false); + const isAllCamerasOperational = status?.every((camera) => { + const allowedTags = ["RUNNING", "VIDEO-PLAYING", "CAMERA-CONTROLLER-READY"]; + return camera.tags.every((tag) => allowedTags.includes(tag)); + }); + const openModal = () => setIsSystemModalOpen(true); + + return ( + <> +
+
+ {isError ? ( + + ) : isAllCamerasOperational ? ( + + ) : ( + + )} +

System Status

+
+ + {isError ? ( +

Some systems are experiencing issues.

+ ) : ( + <> + {isAllCamerasOperational ? ( +

All systems are operational.

+ ) : ( +

Some systems have issues.

+ )} + {!isError && !isAllCamerasOperational && ( + + Click to view more details. + + )} + + )} +
+ + setIsSystemModalOpen(false)} + status={status} + isError={isError} + /> + + ); +}; + +export default SystemStatusContent; diff --git a/src/features/dashboard/SystemOverview/systemStatusModal/SystemStatusModal.tsx b/src/features/dashboard/SystemOverview/systemStatusModal/SystemStatusModal.tsx new file mode 100644 index 0000000..bb085c0 --- /dev/null +++ b/src/features/dashboard/SystemOverview/systemStatusModal/SystemStatusModal.tsx @@ -0,0 +1,38 @@ +import CardHeader from "../../../../components/CardHeader"; +import Badge from "../../../../components/ui/Badge"; +import ModalComponent from "../../../../components/ui/ModalComponent"; +import type { CameraStatus } from "../../../../utils/types"; + +type SystemStatusModalProps = { + title: string; + isOpen: boolean; + close: () => void; + status?: CameraStatus[]; + isError: boolean; +}; + +const SystemStatusModal = ({ isOpen, close, status, title }: SystemStatusModalProps) => { + return ( + + +
+
    + {status?.map((camera) => ( +
  • +
    +

    {camera.id}

    +
    + {camera.tags.map((tag) => ( + + ))} +
    +
    +
  • + ))} +
+
+
+ ); +}; + +export default SystemStatusModal; diff --git a/src/features/dashboard/components/videoFeed/VideoFeed.tsx b/src/features/dashboard/components/videoFeed/VideoFeed.tsx index dadc359..aaa44b7 100644 --- a/src/features/dashboard/components/videoFeed/VideoFeed.tsx +++ b/src/features/dashboard/components/videoFeed/VideoFeed.tsx @@ -33,7 +33,7 @@ const VideoFeed = ({ mostRecentSighting, isLoading, size, modeSetting, isModal = useEffect(() => { const updateSize = () => { - const width = window.innerWidth * 0.48; + const width = window.innerWidth * 0.57; const height = (width * 2) / 3; dispatch({ type: "SET_IMAGE_SIZE", payload: { width, height } }); }; diff --git a/src/features/dashboard/hooks/useGetStore.ts b/src/features/dashboard/hooks/useGetStore.ts new file mode 100644 index 0000000..496bcd5 --- /dev/null +++ b/src/features/dashboard/hooks/useGetStore.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { cambase } from "../../../app/config"; +const fetchStore = 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: fetchStore, + refetchInterval: 5000, + }); + return { storeQuery }; +}; diff --git a/src/features/dashboard/hooks/useGetSystemHealth.ts b/src/features/dashboard/hooks/useGetSystemHealth.ts new file mode 100644 index 0000000..fb9c475 --- /dev/null +++ b/src/features/dashboard/hooks/useGetSystemHealth.ts @@ -0,0 +1,21 @@ +import { useQuery } from "@tanstack/react-query"; +import { cambase } from "../../../app/config"; + +const fetchSystemHealth = async () => { + const response = await fetch(`${cambase}/api/system-health`); + if (!response.ok) throw new Error("Network response was not ok"); + return response.json(); +}; + +export const useGetSystemHealth = () => { + const systemHealthQuery = useQuery({ + queryKey: ["systemHealth"], + queryFn: fetchSystemHealth, + refetchInterval: 60000, + refetchOnWindowFocus: false, + retry: false, + staleTime: 30000, + }); + + return { systemHealthQuery }; +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 7b6ed6f..9e5c09f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -65,3 +65,25 @@ export type CameraSettingsAction = type: "SET_IMAGE_SIZE"; payload: { width: number; height: number }; }; + +export type CameraStatus = { + id: string; + groupID: string; + tags: string[]; +}; + +export type SystemHealth = { + UptimeHumane: string; + StartTimeHumane: string; + Status: CameraStatus[]; +}; + +export type StoreData = { + totalPending: number; + totalActive: number; + totalSent: number; + totalReceived: number; + totalLost: number; + sanityCheck: boolean; + sanityCheckFormula: string; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6fa3103..8c14d9a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -22,3 +22,7 @@ export const timeAgo = (timestampmili: number | null) => { return `${diffHours === 1 ? "1 hour" : diffHours + " hours"} ago`; } }; + +export function capitalize(s?: string) { + return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; +} From 8d44444c4d19ff12bebf96ce5292d9983a2854a0 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 9 Jan 2026 08:50:03 +0000 Subject: [PATCH 2/2] - Refactored Dashboard layout - enhanced PlatesProcessed component display --- src/features/dashboard/Dashboard.tsx | 2 +- .../SystemOverview/PlatesProcessed.tsx | 37 +++++++++++-------- .../SystemOverview/SystemOverview.tsx | 1 - .../SightingModalContent.tsx | 4 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/features/dashboard/Dashboard.tsx b/src/features/dashboard/Dashboard.tsx index 5e56d78..843a8dc 100644 --- a/src/features/dashboard/Dashboard.tsx +++ b/src/features/dashboard/Dashboard.tsx @@ -15,7 +15,7 @@ const Dashboard = () => { return (
-
+
diff --git a/src/features/dashboard/SystemOverview/PlatesProcessed.tsx b/src/features/dashboard/SystemOverview/PlatesProcessed.tsx index 83866c0..5d4b5d6 100644 --- a/src/features/dashboard/SystemOverview/PlatesProcessed.tsx +++ b/src/features/dashboard/SystemOverview/PlatesProcessed.tsx @@ -11,25 +11,30 @@ const PlatesProcessed = ({ platesProcessed }: PlatesProcessedProps) => { const toastReceived = platesProcessed?.totalReceived || 0; return ( -
-
-

Total Active

- {totalActive} -
-
-

Total Received

- {toastReceived} -
-
-

Total Pending

- {totalPending} + <> +
+

Reads

+
+
+

Total Active

+ {totalActive} +
+
+

Total Received

+ {toastReceived} +
+
+

Total Pending

+ {totalPending} +
-
-

Total Failed

- {totalFailed} +
+

Total Failed

+ {totalFailed} +
-
+ ); }; diff --git a/src/features/dashboard/SystemOverview/SystemOverview.tsx b/src/features/dashboard/SystemOverview/SystemOverview.tsx index 63d1e0f..907b000 100644 --- a/src/features/dashboard/SystemOverview/SystemOverview.tsx +++ b/src/features/dashboard/SystemOverview/SystemOverview.tsx @@ -28,7 +28,6 @@ const SystemOverview = () => {

Active Sightings

-

Reads

diff --git a/src/features/dashboard/components/sightingStack/sightingItemModal/SightingModalContent.tsx b/src/features/dashboard/components/sightingStack/sightingItemModal/SightingModalContent.tsx index a33013c..45acd45 100644 --- a/src/features/dashboard/components/sightingStack/sightingItemModal/SightingModalContent.tsx +++ b/src/features/dashboard/components/sightingStack/sightingItemModal/SightingModalContent.tsx @@ -51,9 +51,7 @@ const SightingModalContent = ({ sighting }: SightingModalContentProps) => {
- ) : ( -

No sighting data available.

- )} + ) : null}
); };