From 25a744bd8d70bb3945c7237e3eb69540501dc855 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 21 Nov 2025 08:55:23 +0000 Subject: [PATCH 01/47] addind dashboard features will come back to finish --- index.html | 4 ++-- .../dashboard/components/CameraStatus.tsx | 19 +++++++++++++++++++ .../dashboard/components/DashboardGrid.tsx | 5 ++++- .../dashboard/components/SystemStatusCard.tsx | 2 +- src/types/types.ts | 2 ++ src/ui/CardHeader.tsx | 8 ++++++-- src/ui/StatusIndicators.tsx | 9 +++++++++ 7 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/ui/StatusIndicators.tsx diff --git a/index.html b/index.html index fd0bb43..aec7947 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - MAV | BayiQ + BayIQ
diff --git a/src/features/dashboard/components/CameraStatus.tsx b/src/features/dashboard/components/CameraStatus.tsx index e69de29..47cb2d6 100644 --- a/src/features/dashboard/components/CameraStatus.tsx +++ b/src/features/dashboard/components/CameraStatus.tsx @@ -0,0 +1,19 @@ +import Card from "../../../ui/Card"; +import CardHeader from "../../../ui/CardHeader"; + +type CameraStatusProps = { + title: string; + status?: string; + description: string; +}; + +const CameraStatus = ({ title, status, description }: CameraStatusProps) => { + return ( + + +

{description}

+
+ ); +}; + +export default CameraStatus; diff --git a/src/features/dashboard/components/DashboardGrid.tsx b/src/features/dashboard/components/DashboardGrid.tsx index 8cf481c..3e52ec0 100644 --- a/src/features/dashboard/components/DashboardGrid.tsx +++ b/src/features/dashboard/components/DashboardGrid.tsx @@ -1,9 +1,12 @@ +import CameraStatus from "./CameraStatus"; import SystemStatusCard from "./SystemStatusCard"; const DashboardGrid = () => { return ( -
+
+ +
); }; diff --git a/src/features/dashboard/components/SystemStatusCard.tsx b/src/features/dashboard/components/SystemStatusCard.tsx index b887e7e..7d842e9 100644 --- a/src/features/dashboard/components/SystemStatusCard.tsx +++ b/src/features/dashboard/components/SystemStatusCard.tsx @@ -11,7 +11,7 @@ const SystemStatusCard = () => { {stats ? ( <>
UTC: {stats["system-clock-utc"]}
- Local: {stats["system-clock-local"]} +
Local: {stats["system-clock-local"]}
CPU: {stats["memory-cpu-status"]} Threads: {stats["thread-count"]} diff --git a/src/types/types.ts b/src/types/types.ts index bc491f9..3c59478 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -10,3 +10,5 @@ export type InfoBarData = { "memory-cpu-status": string; "thread-count": string; }; + +export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger"; diff --git a/src/ui/CardHeader.tsx b/src/ui/CardHeader.tsx index 027ecae..25da205 100644 --- a/src/ui/CardHeader.tsx +++ b/src/ui/CardHeader.tsx @@ -1,17 +1,21 @@ import clsx from "clsx"; +import StatusIndicators from "./StatusIndicators"; type CameraOverviewHeaderProps = { title?: string; + status?: string; }; -const CardHeader = ({ title }: CameraOverviewHeaderProps) => { +const CardHeader = ({ title, status }: CameraOverviewHeaderProps) => { + console.log(status); return (
-
+
{/* {icon && } */} + {status && }

{title}

diff --git a/src/ui/StatusIndicators.tsx b/src/ui/StatusIndicators.tsx new file mode 100644 index 0000000..5d4d529 --- /dev/null +++ b/src/ui/StatusIndicators.tsx @@ -0,0 +1,9 @@ +import clsx from "clsx"; + +type StatusIndicatorsProps = { status: string }; + +const StatusIndicators = ({ status }: StatusIndicatorsProps) => { + return ; +}; + +export default StatusIndicators; From a21b7bb87e77edad915af7f7e567f7a9f87630ff Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 21 Nov 2025 10:12:42 +0000 Subject: [PATCH 02/47] - added Konvas lib - added feed from proof of concept --- package.json | 4 +- .../cameras/components/CameraGrid.tsx | 11 +++ .../cameras/components/VideoFeedCard.tsx | 13 ++++ .../components/VideoFeedGridPainter.tsx | 75 +++++++++++++++++++ src/features/cameras/hooks/useGetVideoFeed.ts | 22 ++++++ .../cameras/hooks/useGetvideoSnapshots.ts | 25 +++++++ src/routes/baywatch.tsx | 14 +++- src/ui/Header.tsx | 2 +- yarn.lock | 41 +++++++++- 9 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/features/cameras/components/CameraGrid.tsx create mode 100644 src/features/cameras/components/VideoFeedCard.tsx create mode 100644 src/features/cameras/components/VideoFeedGridPainter.tsx create mode 100644 src/features/cameras/hooks/useGetVideoFeed.ts create mode 100644 src/features/cameras/hooks/useGetvideoSnapshots.ts diff --git a/package.json b/package.json index cb4c39e..31317c7 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "@tanstack/react-router": "^1.136.18", "@tanstack/react-router-devtools": "^1.136.18", "clsx": "^2.1.1", + "konva": "^10.0.11", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-konva": "^19.2.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx new file mode 100644 index 0000000..eb5c2b3 --- /dev/null +++ b/src/features/cameras/components/CameraGrid.tsx @@ -0,0 +1,11 @@ +import VideoFeedCard from "./VideoFeedCard"; + +const CameraGrid = () => { + return ( +
+ +
+ ); +}; + +export default CameraGrid; diff --git a/src/features/cameras/components/VideoFeedCard.tsx b/src/features/cameras/components/VideoFeedCard.tsx new file mode 100644 index 0000000..2d5dc8c --- /dev/null +++ b/src/features/cameras/components/VideoFeedCard.tsx @@ -0,0 +1,13 @@ +import Card from "../../../ui/Card"; + +import VideoFeedGridPainter from "./VideoFeedGridPainter"; + +const VideoFeedCard = () => { + return ( + + + + ); +}; + +export default VideoFeedCard; diff --git a/src/features/cameras/components/VideoFeedGridPainter.tsx b/src/features/cameras/components/VideoFeedGridPainter.tsx new file mode 100644 index 0000000..3ecdba0 --- /dev/null +++ b/src/features/cameras/components/VideoFeedGridPainter.tsx @@ -0,0 +1,75 @@ +import { useRef, type RefObject } from "react"; +import { Stage, Layer, Image, Rect } from "react-konva"; +import { useCreateVideoSnapshot } from "../hooks/useGetvideoSnapshots"; + +const VideoFeedGridPainter = () => { + const { latestBitmapRef } = useCreateVideoSnapshot(); + const isDrawing = useRef(false); + + const rows = 100; + const cols = 100; + const size = 10; + const gap = 0; + + const squares = []; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + squares.push( + , + ); + } + } + + const getCoords = (e) => { + isDrawing.current = true; + }; + + const handleMouseMove = (e) => { + if (!isDrawing.current) { + return; + } + const pos = e.target.getStage().getPointerPosition(); + console.log(pos); + }; + + const handleMouseUp = () => { + isDrawing.current = false; + }; + + const draw = (bmp: RefObject) => { + if (!bmp || !bmp.current) { + return; + } else { + const frame = bmp.current; + return frame; + } + }; + + return ( + + + + + {squares} + + ); +}; + +export default VideoFeedGridPainter; diff --git a/src/features/cameras/hooks/useGetVideoFeed.ts b/src/features/cameras/hooks/useGetVideoFeed.ts new file mode 100644 index 0000000..bedbaeb --- /dev/null +++ b/src/features/cameras/hooks/useGetVideoFeed.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +const getfeed = async () => { + const response = await fetch(`http://100.115.148.59/TargetDetectionColour-preview`, { + signal: AbortSignal.timeout(300000), + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +}; + +export const useGetVideoFeed = () => { + const videoQuery = useQuery({ + queryKey: ["getfeed"], + queryFn: getfeed, + refetchInterval: 500, + }); + + return { videoQuery }; +}; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts new file mode 100644 index 0000000..68a6946 --- /dev/null +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef } from "react"; +import { useGetVideoFeed } from "./useGetVideoFeed"; + +export const useCreateVideoSnapshot = () => { + const latestBitmapRef = useRef(null); + const { videoQuery } = useGetVideoFeed(); + + const snapShot = videoQuery?.data; + + useEffect(() => { + async function createBitmap() { + if (!snapShot) return; + + try { + const bitmap = await createImageBitmap(snapShot); + latestBitmapRef.current = bitmap; + } catch (error) { + console.log(error); + } + } + createBitmap(); + }, [snapShot]); + + return { latestBitmapRef }; +}; diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx index 8ec01dd..9271b97 100644 --- a/src/routes/baywatch.tsx +++ b/src/routes/baywatch.tsx @@ -1,9 +1,15 @@ -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 ( +
+

Cameras

+ +
+ ); } diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index c1934dd..31d2d2a 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -18,7 +18,7 @@ const Header = () => { - Baywatch + Cameras Output diff --git a/yarn.lock b/yarn.lock index f9e9fcd..2c31fb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,6 +1011,16 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== +"@types/react-reconciler@^0.28.9": + version "0.28.9" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b" + integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg== + +"@types/react-reconciler@^0.32.2": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz#eb4b346f367f29f07628032934d30a4f3f9eaba7" + integrity sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA== + "@types/react@^19.2.5": version "19.2.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.6.tgz#d27db1ff45012d53980f5589fda925278e1249ca" @@ -1726,6 +1736,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +its-fine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-2.0.0.tgz#a90b18a3ee4c211a1fb6faac2abcc2b682ce1f21" + integrity sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng== + dependencies: + "@types/react-reconciler" "^0.28.9" + jiti@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" @@ -1775,6 +1792,11 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +konva@^10.0.11: + version "10.0.11" + resolved "https://registry.yarnpkg.com/konva/-/konva-10.0.11.tgz#f63d3422625d13513094b646a2f09359d644542a" + integrity sha512-h0O6YNrwdgfb76kzkiMIqkyufUxE6GPcNwJZhYalnZ5qnYBEuxSRk62fQwtJqygGP5hdZKzJs2ea1ivVP+zetw== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -2044,6 +2066,23 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" +react-konva@^19.2.0: + version "19.2.0" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-19.2.0.tgz#b4cc5d73cd6d642569e4df36a0139996c3dcf8e6" + integrity sha512-Ofifq/rdNvff50+Lj8x86WSfoeQDvdysOlsXMMrpD2uWmDxUPrEYSRLt27iCfdovQZL6xinKRpX9VaL9xDwXDQ== + dependencies: + "@types/react-reconciler" "^0.32.2" + its-fine "^2.0.0" + react-reconciler "0.33.0" + scheduler "0.27.0" + +react-reconciler@0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.33.0.tgz#9dd20208d45baa5b0b4701781f858236657f15e1" + integrity sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA== + dependencies: + scheduler "^0.27.0" + react-refresh@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" @@ -2125,7 +2164,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -scheduler@^0.27.0: +scheduler@0.27.0, scheduler@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== From 4f13ed104d879fe17f949bd61d65e3570c206acb Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 21 Nov 2025 11:41:42 +0000 Subject: [PATCH 03/47] - added new websocket implementation --- package.json | 3 +- src/app/context/WebSocketContext.ts | 23 +++++++++++ src/app/providers/AppProviders.tsx | 10 +++-- src/app/providers/WebSocketProvider.tsx | 38 +++++++++++++++++++ .../dashboard/components/SystemStatusCard.tsx | 4 +- yarn.lock | 5 +++ 6 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/app/context/WebSocketContext.ts create mode 100644 src/app/providers/WebSocketProvider.tsx diff --git a/package.json b/package.json index cb4c39e..11627c2 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "@tanstack/react-router-devtools": "^1.136.18", "clsx": "^2.1.1", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-use-websocket": "3.0.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/app/context/WebSocketContext.ts b/src/app/context/WebSocketContext.ts new file mode 100644 index 0000000..65799c0 --- /dev/null +++ b/src/app/context/WebSocketContext.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from "react"; +import { ReadyState } from "react-use-websocket"; +import type { InfoBarData } from "../../types/types"; + +type InfoSocketState = { + data: InfoBarData | null; + readyState: ReadyState; + sendJson: (msg: unknown) => void; +}; + +export type WebSocketConextValue = { + info: InfoSocketState; +}; + +export const WebsocketContext = createContext(null); + +const useWebSocketContext = () => { + const ctx = useContext(WebsocketContext); + if (!ctx) throw new Error("useWebSocketContext must be used inside "); + return ctx; +}; + +export const useInfoSocket = () => useWebSocketContext().info; diff --git a/src/app/providers/AppProviders.tsx b/src/app/providers/AppProviders.tsx index a07b51a..7a73155 100644 --- a/src/app/providers/AppProviders.tsx +++ b/src/app/providers/AppProviders.tsx @@ -1,7 +1,11 @@ -import { QueryClientProvider } from "@tanstack/react-query"; -import { queryClient } from "../config/queryClient"; import type { PropsWithChildren } from "react"; +import { QueryProvider } from "./QueryProviders"; +import { WebSocketProvider } from "./WebSocketProvider"; export const AppProviders = ({ children }: PropsWithChildren) => { - return {children}; + return ( + + {children} + + ); }; diff --git a/src/app/providers/WebSocketProvider.tsx b/src/app/providers/WebSocketProvider.tsx new file mode 100644 index 0000000..42ff194 --- /dev/null +++ b/src/app/providers/WebSocketProvider.tsx @@ -0,0 +1,38 @@ +import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { WebsocketContext, type WebSocketConextValue } from "../context/WebSocketContext"; +import useWebSocket from "react-use-websocket"; +import { wsConfig } from "../config/wsconfig"; +import type { InfoBarData } from "../../types/types"; + +type WebSocketProviderProps = { + children: ReactNode; +}; + +export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { + const [systemData, setSystemData] = useState(null); + const infoSocket = useWebSocket(wsConfig.infoBar, { share: true, shouldReconnect: () => true }); + + useEffect(() => { + async function parseData() { + if (infoSocket.lastMessage) { + const text = await infoSocket.lastMessage.data.text(); + const data = JSON.parse(text); + setSystemData(data); + } + } + parseData(); + }, [infoSocket.lastMessage]); + + const value = useMemo( + () => ({ + info: { + data: systemData, + readyState: infoSocket.readyState, + sendJson: infoSocket.sendJsonMessage, + }, + }), + [infoSocket.readyState, infoSocket.sendJsonMessage, systemData], + ); + + return {children}; +}; diff --git a/src/features/dashboard/components/SystemStatusCard.tsx b/src/features/dashboard/components/SystemStatusCard.tsx index b887e7e..e7d6c09 100644 --- a/src/features/dashboard/components/SystemStatusCard.tsx +++ b/src/features/dashboard/components/SystemStatusCard.tsx @@ -1,9 +1,9 @@ -import { useInfoSocket } from "../../../hooks/useInfoWebSocket"; +import { useInfoSocket } from "../../../app/context/WebSocketContext"; import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; const SystemStatusCard = () => { - const { stats } = useInfoSocket(); + const { data: stats } = useInfoSocket(); return ( diff --git a/yarn.lock b/yarn.lock index f9e9fcd..1101e46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2049,6 +2049,11 @@ react-refresh@^0.18.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== +react-use-websocket@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-3.0.0.tgz#754cb8eea76f55d31c5676d4abe3e573bc2cea04" + integrity sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw== + react@^19.2.0: version "19.2.0" resolved "https://registry.yarnpkg.com/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5" From 68711b908725c090287e3e3bd148ef2b9f3f35e0 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 21 Nov 2025 16:01:34 +0000 Subject: [PATCH 04/47] - started painer on video feed will finish --- package.json | 1 + .../cameras/components/CameraGrid.tsx | 23 ++- .../cameras/components/CameraSettings.tsx | 43 +++++ .../cameras/components/ColourPicker.tsx | 11 ++ .../cameras/components/RegionSelector.tsx | 37 +++++ .../cameras/components/VideoFeedCard.tsx | 13 -- .../components/VideoFeedGridPainter.tsx | 150 +++++++++++------- .../cameras/hooks/useGetvideoSnapshots.ts | 1 + src/types/types.ts | 5 + yarn.lock | 38 ++++- 10 files changed, 245 insertions(+), 77 deletions(-) create mode 100644 src/features/cameras/components/CameraSettings.tsx create mode 100644 src/features/cameras/components/ColourPicker.tsx create mode 100644 src/features/cameras/components/RegionSelector.tsx delete mode 100644 src/features/cameras/components/VideoFeedCard.tsx diff --git a/package.json b/package.json index 7afb985..4f25338 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-konva": "^19.2.0", + "react-tabs": "^6.1.0", "react-use-websocket": "3.0.0" }, "devDependencies": { diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index eb5c2b3..c1f0033 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -1,9 +1,28 @@ -import VideoFeedCard from "./VideoFeedCard"; +import { useState } from "react"; + +import VideoFeedGridPainter from "./VideoFeedGridPainter"; +import CameraSettings from "./CameraSettings"; +import type { Region } from "../../../types/types"; const CameraGrid = () => { + const [regions, setRegions] = useState([ + { name: "Region 1", brushColour: "#ff0000" }, + { name: "Region 2", brushColour: "#00ff00" }, + ]); + const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); + + const updateRegionColour = (index: number, newColour: string) => { + setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); + }; return (
- + +
); }; diff --git a/src/features/cameras/components/CameraSettings.tsx b/src/features/cameras/components/CameraSettings.tsx new file mode 100644 index 0000000..751dd8c --- /dev/null +++ b/src/features/cameras/components/CameraSettings.tsx @@ -0,0 +1,43 @@ +import Card from "../../../ui/Card"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import "react-tabs/style/react-tabs.css"; +import RegionSelector from "./RegionSelector"; +import type { Region } from "../../../types/types"; + +type CameraSettingsProps = { + regions: Region[]; + selectedRegionIndex: number; + onSelectRegion: (index: number) => void; + onChangeRegionColour: (index: number, colour: string) => void; +}; + +const CameraSettings = ({ + regions, + selectedRegionIndex, + onSelectRegion, + onChangeRegionColour, +}: CameraSettingsProps) => { + return ( + + + + Target Detection + Camera 1 + + + + + +
Camera details
+
+
+
+ ); +}; + +export default CameraSettings; diff --git a/src/features/cameras/components/ColourPicker.tsx b/src/features/cameras/components/ColourPicker.tsx new file mode 100644 index 0000000..2180e09 --- /dev/null +++ b/src/features/cameras/components/ColourPicker.tsx @@ -0,0 +1,11 @@ +type ColourPickerProps = { + colour: string; + setColour: (colour: string) => void; +}; + +const ColourPicker = ({ colour, setColour }: ColourPickerProps) => { + console.log(colour); + return setColour(e.target.value)} />; +}; + +export default ColourPicker; diff --git a/src/features/cameras/components/RegionSelector.tsx b/src/features/cameras/components/RegionSelector.tsx new file mode 100644 index 0000000..55276ad --- /dev/null +++ b/src/features/cameras/components/RegionSelector.tsx @@ -0,0 +1,37 @@ +import ColourPicker from "./colourPicker"; +import type { Region } from "../../../types/types"; + +type RegionSelectorProps = { + regions: Region[]; + selectedRegionIndex: number; + onSelectRegion: (index: number) => void; + onChangeRegionColour: (index: number, colour: string) => void; +}; + +const RegionSelector = ({ + regions, + selectedRegionIndex, + onSelectRegion, + onChangeRegionColour, +}: RegionSelectorProps) => { + return ( +
+
+

Region Select

+
+
+ {regions.map((region, idx) => ( +
+ + onChangeRegionColour(idx, c)} /> +
+ ))} +
+
+ ); +}; + +export default RegionSelector; diff --git a/src/features/cameras/components/VideoFeedCard.tsx b/src/features/cameras/components/VideoFeedCard.tsx deleted file mode 100644 index 2d5dc8c..0000000 --- a/src/features/cameras/components/VideoFeedCard.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Card from "../../../ui/Card"; - -import VideoFeedGridPainter from "./VideoFeedGridPainter"; - -const VideoFeedCard = () => { - return ( - - - - ); -}; - -export default VideoFeedCard; diff --git a/src/features/cameras/components/VideoFeedGridPainter.tsx b/src/features/cameras/components/VideoFeedGridPainter.tsx index 3ecdba0..3511a68 100644 --- a/src/features/cameras/components/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/VideoFeedGridPainter.tsx @@ -1,74 +1,104 @@ import { useRef, type RefObject } from "react"; -import { Stage, Layer, Image, Rect } from "react-konva"; +import { Stage, Layer, Image, Shape } from "react-konva"; +import type { KonvaEventObject } from "konva/lib/Node"; import { useCreateVideoSnapshot } from "../hooks/useGetvideoSnapshots"; +import Card from "../../../ui/Card"; +import type { Region } from "../../../types/types"; -const VideoFeedGridPainter = () => { +const rows = 40; +const cols = 40; +const size = 20; +const gap = 0; + +type VideoFeedGridPainterProps = { + regions: Region[]; + selectedRegionIndex: number; +}; + +const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPainterProps) => { const { latestBitmapRef } = useCreateVideoSnapshot(); - const isDrawing = useRef(false); - const rows = 100; - const cols = 100; - const size = 10; - const gap = 0; + const isDrawingRef = useRef(false); + const paintedCellsRef = useRef>(new Set()); - const squares = []; - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - squares.push( - , - ); - } - } - - const getCoords = (e) => { - isDrawing.current = true; - }; - - const handleMouseMove = (e) => { - if (!isDrawing.current) { - return; - } - const pos = e.target.getStage().getPointerPosition(); - console.log(pos); - }; - - const handleMouseUp = () => { - isDrawing.current = false; - }; + const paintLayerRef = useRef(null); const draw = (bmp: RefObject) => { - if (!bmp || !bmp.current) { - return; - } else { - const frame = bmp.current; - return frame; - } + if (!bmp || !bmp.current) return null; + return bmp.current; + }; + + const paintCell = (x: number, y: number) => { + const col = Math.floor(x / (size + gap)); + const row = Math.floor(y / (size + gap)); + + if (row < 0 || row >= rows || col < 0 || col >= cols) return; + + const key = `${row}-${col}`; + const set = paintedCellsRef.current; + if (set.has(key)) return; + + set.add(key); + + paintLayerRef.current?.batchDraw(); + }; + + const handleStageMouseDown = (e: KonvaEventObject) => { + if (!selectedRegionIndex) return; + isDrawingRef.current = true; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) paintCell(pos.x, pos.y); + }; + + const handleStageMouseMove = (e: KonvaEventObject) => { + if (!isDrawingRef.current) return; + if (!selectedRegionIndex) return; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) paintCell(pos.x, pos.y); + }; + + const handleStageMouseUp = () => { + isDrawingRef.current = false; }; return ( - - - - - {squares} - + + + + + + + + { + const cells = paintedCellsRef.current; + cells.forEach((key) => { + const [rowStr, colStr] = key.split("-"); + const row = Number(rowStr); + const col = Number(colStr); + + const x = col * (size + gap); + const y = row * (size + gap); + + ctx.beginPath(); + ctx.rect(x, y, size, size); + ctx.fillStyle = regions[selectedRegionIndex]?.brushColour; + ctx.fill(); + }); + + ctx.fillStrokeShape(shape); + }} + /> + + + ); }; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts index 68a6946..c9f0e66 100644 --- a/src/features/cameras/hooks/useGetvideoSnapshots.ts +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -13,6 +13,7 @@ export const useCreateVideoSnapshot = () => { try { const bitmap = await createImageBitmap(snapShot); + if (!bitmap) return; latestBitmapRef.current = bitmap; } catch (error) { console.log(error); diff --git a/src/types/types.ts b/src/types/types.ts index bc491f9..e838896 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -10,3 +10,8 @@ export type InfoBarData = { "memory-cpu-status": string; "thread-count": string; }; + +export type Region = { + name: string; + brushColour: string; +}; diff --git a/yarn.lock b/yarn.lock index 3ccfe29..992702f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1293,7 +1293,7 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" -clsx@^2.1.1: +clsx@^2.0.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -1748,7 +1748,7 @@ jiti@^2.6.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1891,6 +1891,13 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -1962,6 +1969,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -2049,6 +2061,15 @@ prettier@^3.5.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== +prop-types@^15.5.0: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -2066,6 +2087,11 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-konva@^19.2.0: version "19.2.0" resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-19.2.0.tgz#b4cc5d73cd6d642569e4df36a0139996c3dcf8e6" @@ -2088,6 +2114,14 @@ react-refresh@^0.18.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== +react-tabs@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.1.0.tgz#a1fc9d9b8db4c6e7bb327a1b6783bc51a1c457a1" + integrity sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ== + dependencies: + clsx "^2.0.0" + prop-types "^15.5.0" + react-use-websocket@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-3.0.0.tgz#754cb8eea76f55d31c5676d4abe3e573bc2cea04" From c5fe6754c359b85a786807143a91069fec220a02 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Sun, 23 Nov 2025 22:36:08 +0000 Subject: [PATCH 05/47] fixed colour painting and eraser --- package.json | 3 +- .../cameras/components/CameraGrid.tsx | 6 ++- .../cameras/components/CameraSettings.tsx | 8 ++- .../cameras/components/ColourPicker.tsx | 1 - .../cameras/components/RegionSelector.tsx | 14 ++++- .../components/VideoFeedGridPainter.tsx | 52 +++++++++++++------ src/routes/baywatch.tsx | 2 + yarn.lock | 5 ++ 8 files changed, 71 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 4f25338..be15300 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-dom": "^19.2.0", "react-konva": "^19.2.0", "react-tabs": "^6.1.0", - "react-use-websocket": "3.0.0" + "react-use-websocket": "3.0.0", + "sonner": "^2.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index c1f0033..7cb2353 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -8,20 +8,24 @@ const CameraGrid = () => { const [regions, setRegions] = useState([ { name: "Region 1", brushColour: "#ff0000" }, { name: "Region 2", brushColour: "#00ff00" }, + { name: "Region 3", brushColour: "#0400ff" }, ]); const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); + const [isErasing, setErasing] = useState(false); const updateRegionColour = (index: number, newColour: string) => { setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); }; return (
- +
); diff --git a/src/features/cameras/components/CameraSettings.tsx b/src/features/cameras/components/CameraSettings.tsx index 751dd8c..e0669cd 100644 --- a/src/features/cameras/components/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings.tsx @@ -9,6 +9,8 @@ type CameraSettingsProps = { selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; + isErasing: boolean; + onSelectErasing: (isErasing: boolean) => void; }; const CameraSettings = ({ @@ -16,9 +18,11 @@ const CameraSettings = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, + isErasing, + onSelectErasing, }: CameraSettingsProps) => { return ( - + Target Detection @@ -30,6 +34,8 @@ const CameraSettings = ({ selectedRegionIndex={selectedRegionIndex} onSelectRegion={onSelectRegion} onChangeRegionColour={onChangeRegionColour} + isErasing={isErasing} + onSelectErasing={onSelectErasing} /> diff --git a/src/features/cameras/components/ColourPicker.tsx b/src/features/cameras/components/ColourPicker.tsx index 2180e09..f097cc4 100644 --- a/src/features/cameras/components/ColourPicker.tsx +++ b/src/features/cameras/components/ColourPicker.tsx @@ -4,7 +4,6 @@ type ColourPickerProps = { }; const ColourPicker = ({ colour, setColour }: ColourPickerProps) => { - console.log(colour); return setColour(e.target.value)} />; }; diff --git a/src/features/cameras/components/RegionSelector.tsx b/src/features/cameras/components/RegionSelector.tsx index 55276ad..04d9e06 100644 --- a/src/features/cameras/components/RegionSelector.tsx +++ b/src/features/cameras/components/RegionSelector.tsx @@ -1,4 +1,4 @@ -import ColourPicker from "./colourPicker"; +import ColourPicker from "./ColourPicker"; import type { Region } from "../../../types/types"; type RegionSelectorProps = { @@ -6,6 +6,8 @@ type RegionSelectorProps = { selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; + isErasing: boolean; + onSelectErasing: (isErasing: boolean) => void; }; const RegionSelector = ({ @@ -13,7 +15,13 @@ const RegionSelector = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, + isErasing, + onSelectErasing, }: RegionSelectorProps) => { + const handleChange = () => { + onSelectErasing(!isErasing); + }; + return (
@@ -29,6 +37,10 @@ const RegionSelector = ({ onChangeRegionColour(idx, c)} />
))} +
); diff --git a/src/features/cameras/components/VideoFeedGridPainter.tsx b/src/features/cameras/components/VideoFeedGridPainter.tsx index 3511a68..960cdc2 100644 --- a/src/features/cameras/components/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/VideoFeedGridPainter.tsx @@ -13,19 +13,26 @@ const gap = 0; type VideoFeedGridPainterProps = { regions: Region[]; selectedRegionIndex: number; + isErasing: boolean; }; -const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPainterProps) => { +type PaintedCell = { + colour: string; +}; + +const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: VideoFeedGridPainterProps) => { const { latestBitmapRef } = useCreateVideoSnapshot(); - const isDrawingRef = useRef(false); - const paintedCellsRef = useRef>(new Set()); - + const paintedCellsRef = useRef>(new Map()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); - const draw = (bmp: RefObject) => { - if (!bmp || !bmp.current) return null; - return bmp.current; + const draw = (bmp: RefObject): ImageBitmap | null => { + if (!bmp || !bmp.current) { + return null; + } + const image = bmp.current; + return image; }; const paintCell = (x: number, y: number) => { @@ -34,17 +41,32 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai if (row < 0 || row >= rows || col < 0 || col >= cols) return; - const key = `${row}-${col}`; - const set = paintedCellsRef.current; - if (set.has(key)) return; + const activeRegion = regions[selectedRegionIndex]; + if (!activeRegion) return; - set.add(key); + const key = `${row}-${col}`; + const currentColour = regions[selectedRegionIndex].brushColour; + + const map = paintedCellsRef.current; + const existing = map.get(key); + + if (isErasing) { + if (map.has(key)) { + map.delete(key); + paintLayerRef.current?.batchDraw(); + } + return; + } + + if (existing && existing.colour === currentColour) return; + + map.set(key, { colour: currentColour }); paintLayerRef.current?.batchDraw(); }; const handleStageMouseDown = (e: KonvaEventObject) => { - if (!selectedRegionIndex) return; + if (!regions[selectedRegionIndex]) return; isDrawingRef.current = true; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); @@ -52,7 +74,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai const handleStageMouseMove = (e: KonvaEventObject) => { if (!isDrawingRef.current) return; - if (!selectedRegionIndex) return; + if (!regions[selectedRegionIndex]) return; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; @@ -79,7 +101,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai { const cells = paintedCellsRef.current; - cells.forEach((key) => { + cells.forEach((cell, key) => { const [rowStr, colStr] = key.split("-"); const row = Number(rowStr); const col = Number(colStr); @@ -89,7 +111,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai ctx.beginPath(); ctx.rect(x, y, size, size); - ctx.fillStyle = regions[selectedRegionIndex]?.brushColour; + ctx.fillStyle = cell.colour; ctx.fill(); }); diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx index 9271b97..f7401ab 100644 --- a/src/routes/baywatch.tsx +++ b/src/routes/baywatch.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import CameraGrid from "../features/cameras/components/CameraGrid"; +import { Toaster } from "sonner"; export const Route = createFileRoute("/baywatch")({ component: RouteComponent, @@ -10,6 +11,7 @@ function RouteComponent() {

Cameras

+
); } diff --git a/yarn.lock b/yarn.lock index 992702f..b567169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2240,6 +2240,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +sonner@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6" + integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" From b084c3016d9faaec8e1e38923061911a42dd4fc6 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 12:19:51 +0000 Subject: [PATCH 06/47] - improved grid painting and included resizing --- .../cameras/components/CameraGrid.tsx | 8 ++-- .../{ => CameraSettings}/CameraSettings.tsx | 24 ++++++++-- .../{ => CameraSettings}/ColourPicker.tsx | 0 .../{ => CameraSettings}/RegionSelector.tsx | 2 +- .../components/PlatePatch/PlatePatch.tsx | 7 +++ .../{ => Video}/VideoFeedGridPainter.tsx | 46 +++++++++++++++---- .../cameras/hooks/useGetvideoSnapshots.ts | 3 +- 7 files changed, 70 insertions(+), 20 deletions(-) rename src/features/cameras/components/{ => CameraSettings}/CameraSettings.tsx (61%) rename src/features/cameras/components/{ => CameraSettings}/ColourPicker.tsx (100%) rename src/features/cameras/components/{ => CameraSettings}/RegionSelector.tsx (95%) create mode 100644 src/features/cameras/components/PlatePatch/PlatePatch.tsx rename src/features/cameras/components/{ => Video}/VideoFeedGridPainter.tsx (69%) diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index 7cb2353..6482fa9 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; -import VideoFeedGridPainter from "./VideoFeedGridPainter"; -import CameraSettings from "./CameraSettings"; +import VideoFeedGridPainter from "./Video/VideoFeedGridPainter"; +import CameraSettings from "./CameraSettings/CameraSettings"; import type { Region } from "../../../types/types"; +import PlatePatch from "./PlatePatch/PlatePatch"; const CameraGrid = () => { const [regions, setRegions] = useState([ @@ -17,7 +18,7 @@ const CameraGrid = () => { setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); }; return ( -
+
{ isErasing={isErasing} onSelectErasing={setErasing} /> +
); }; diff --git a/src/features/cameras/components/CameraSettings.tsx b/src/features/cameras/components/CameraSettings/CameraSettings.tsx similarity index 61% rename from src/features/cameras/components/CameraSettings.tsx rename to src/features/cameras/components/CameraSettings/CameraSettings.tsx index e0669cd..88f383b 100644 --- a/src/features/cameras/components/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings/CameraSettings.tsx @@ -1,8 +1,9 @@ -import Card from "../../../ui/Card"; +import Card from "../../../../ui/Card"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import RegionSelector from "./RegionSelector"; -import type { Region } from "../../../types/types"; +import type { Region } from "../../../../types/types"; +import { useState } from "react"; type CameraSettingsProps = { regions: Region[]; @@ -21,12 +22,19 @@ const CameraSettings = ({ isErasing, onSelectErasing, }: CameraSettingsProps) => { + const [tabIndex, setTabIndex] = useState(0); return ( - - + + setTabIndex(index)} + > Target Detection Camera 1 + Camera 2 + Camera 3 -
Camera details
+
Camera details {tabIndex}
+
+ +
Camera details {tabIndex}
+
+ +
Camera details {tabIndex}
diff --git a/src/features/cameras/components/ColourPicker.tsx b/src/features/cameras/components/CameraSettings/ColourPicker.tsx similarity index 100% rename from src/features/cameras/components/ColourPicker.tsx rename to src/features/cameras/components/CameraSettings/ColourPicker.tsx diff --git a/src/features/cameras/components/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx similarity index 95% rename from src/features/cameras/components/RegionSelector.tsx rename to src/features/cameras/components/CameraSettings/RegionSelector.tsx index 04d9e06..b8a1688 100644 --- a/src/features/cameras/components/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -1,5 +1,5 @@ import ColourPicker from "./ColourPicker"; -import type { Region } from "../../../types/types"; +import type { Region } from "../../../../types/types"; type RegionSelectorProps = { regions: Region[]; diff --git a/src/features/cameras/components/PlatePatch/PlatePatch.tsx b/src/features/cameras/components/PlatePatch/PlatePatch.tsx new file mode 100644 index 0000000..6a652f2 --- /dev/null +++ b/src/features/cameras/components/PlatePatch/PlatePatch.tsx @@ -0,0 +1,7 @@ +import Card from "../../../../ui/Card"; + +const PlatePatch = () => { + return PlatePatch; +}; + +export default PlatePatch; diff --git a/src/features/cameras/components/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx similarity index 69% rename from src/features/cameras/components/VideoFeedGridPainter.tsx rename to src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 960cdc2..ce706e1 100644 --- a/src/features/cameras/components/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -1,9 +1,9 @@ -import { useRef, type RefObject } from "react"; +import { useEffect, useRef, useState, type RefObject } from "react"; import { Stage, Layer, Image, Shape } from "react-konva"; import type { KonvaEventObject } from "konva/lib/Node"; -import { useCreateVideoSnapshot } from "../hooks/useGetvideoSnapshots"; -import Card from "../../../ui/Card"; -import type { Region } from "../../../types/types"; +import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; +import type { Region } from "../../../../types/types"; +import Card from "../../../../ui/Card"; const rows = 40; const cols = 40; @@ -21,7 +21,8 @@ type PaintedCell = { }; const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: VideoFeedGridPainterProps) => { - const { latestBitmapRef } = useCreateVideoSnapshot(); + const { latestBitmapRef, isloading } = useCreateVideoSnapshot(); + const [stageSize, setStageSize] = useState({ width: 740, height: 460 }); const isDrawingRef = useRef(false); const paintedCellsRef = useRef>(new Map()); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -35,6 +36,8 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: Video return image; }; + const image = draw(latestBitmapRef); + const paintCell = (x: number, y: number) => { const col = Math.floor(x / (size + gap)); const row = Math.floor(y / (size + gap)); @@ -83,18 +86,39 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: Video isDrawingRef.current = false; }; + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + + const aspectRatio = 740 / 460; + const newWidth = width * 0.36; + const newHeight = newWidth / aspectRatio; + setStageSize({ width: newWidth, height: newHeight }); + }; + handleResize(); + window.addEventListener("resize", handleResize); + + return () => window.removeEventListener("resize", handleResize); + }, []); + + if (image === null || isloading) + return ( + + Loading Video feed… + + ); return ( - +
- + @@ -117,10 +141,12 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: Video ctx.fillStrokeShape(shape); }} + width={stageSize.width} + height={stageSize.height} /> - +
); }; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts index c9f0e66..4c9679c 100644 --- a/src/features/cameras/hooks/useGetvideoSnapshots.ts +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -6,6 +6,7 @@ export const useCreateVideoSnapshot = () => { const { videoQuery } = useGetVideoFeed(); const snapShot = videoQuery?.data; + const isloading = videoQuery.isPending; useEffect(() => { async function createBitmap() { @@ -22,5 +23,5 @@ export const useCreateVideoSnapshot = () => { createBitmap(); }, [snapShot]); - return { latestBitmapRef }; + return { latestBitmapRef, isloading }; }; From 8dd20e409c915b60746cd6220845fe2866b0b61d Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 13:29:00 +0000 Subject: [PATCH 07/47] - minor improvements to painter --- .../cameras/components/CameraGrid.tsx | 14 ++++-- .../CameraSettings/CameraSettings.tsx | 18 ++++---- .../CameraSettings/RegionSelector.tsx | 46 +++++++++++++------ .../components/Video/VideoFeedGridPainter.tsx | 6 +-- 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index 6482fa9..27a5b0c 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -12,21 +12,25 @@ const CameraGrid = () => { { name: "Region 3", brushColour: "#0400ff" }, ]); const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); - const [isErasing, setErasing] = useState(false); - + const [mode, setMode] = useState(""); + const [tabIndex, setTabIndex] = useState(0); + console.log(tabIndex); const updateRegionColour = (index: number, newColour: string) => { setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); }; + console.log(mode); return (
- +
diff --git a/src/features/cameras/components/CameraSettings/CameraSettings.tsx b/src/features/cameras/components/CameraSettings/CameraSettings.tsx index 88f383b..b0ad443 100644 --- a/src/features/cameras/components/CameraSettings/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings/CameraSettings.tsx @@ -3,15 +3,16 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import RegionSelector from "./RegionSelector"; import type { Region } from "../../../../types/types"; -import { useState } from "react"; type CameraSettingsProps = { regions: Region[]; selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; - isErasing: boolean; - onSelectErasing: (isErasing: boolean) => void; + mode: string; + onSelectMode: (mode: string) => void; + setTabIndex: (tabIndex: number) => void; + tabIndex: number; }; const CameraSettings = ({ @@ -19,10 +20,11 @@ const CameraSettings = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, - isErasing, - onSelectErasing, + mode, + onSelectMode, + tabIndex, + setTabIndex, }: CameraSettingsProps) => { - const [tabIndex, setTabIndex] = useState(0); return ( diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index b8a1688..f03d132 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -6,8 +6,8 @@ type RegionSelectorProps = { selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; - isErasing: boolean; - onSelectErasing: (isErasing: boolean) => void; + mode: string; + onSelectMode: (mode: string) => void; }; const RegionSelector = ({ @@ -15,32 +15,52 @@ const RegionSelector = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, - isErasing, - onSelectErasing, + mode, + onSelectMode, }: RegionSelectorProps) => { - const handleChange = () => { - onSelectErasing(!isErasing); + const handleChange = (e: { target: { value: string } }) => { + onSelectMode(e.target.value); }; return (
-

Region Select

+

Region Select

{regions.map((region, idx) => ( -
+
onChangeRegionColour(idx, c)} />
))} - +
+

Tools

+
+
+ + + +
); diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index ce706e1..4ddebdb 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -13,14 +13,14 @@ const gap = 0; type VideoFeedGridPainterProps = { regions: Region[]; selectedRegionIndex: number; - isErasing: boolean; + mode: string; }; type PaintedCell = { colour: string; }; -const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: VideoFeedGridPainterProps) => { +const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode }: VideoFeedGridPainterProps) => { const { latestBitmapRef, isloading } = useCreateVideoSnapshot(); const [stageSize, setStageSize] = useState({ width: 740, height: 460 }); const isDrawingRef = useRef(false); @@ -53,7 +53,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: Video const map = paintedCellsRef.current; const existing = map.get(key); - if (isErasing) { + if (mode === "eraser") { if (map.has(key)) { map.delete(key); paintLayerRef.current?.batchDraw(); From c9dde6b992562400f2195166b9f6dd79708e677f Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 14:54:05 +0000 Subject: [PATCH 08/47] - improved the system status ui --- .../dashboard/components/DashboardGrid.tsx | 4 ++- .../components/StatusItems/StatusItemCPU.tsx | 23 +++++++++++++ .../StatusItems/StatusItemLocal.tsx | 31 ++++++++++++++++++ .../StatusItems/StatusItemThreads.tsx | 24 ++++++++++++++ .../components/StatusItems/StatusItemUTC.tsx | 32 +++++++++++++++++++ .../dashboard/components/SystemOverview.tsx | 12 +++++++ .../dashboard/components/SystemStatusCard.tsx | 20 +++++++----- .../dashboard/hooks/useGetSystemHealth.ts | 15 +++++++++ 8 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 src/features/dashboard/components/StatusItems/StatusItemCPU.tsx create mode 100644 src/features/dashboard/components/StatusItems/StatusItemLocal.tsx create mode 100644 src/features/dashboard/components/StatusItems/StatusItemThreads.tsx create mode 100644 src/features/dashboard/components/StatusItems/StatusItemUTC.tsx create mode 100644 src/features/dashboard/components/SystemOverview.tsx create mode 100644 src/features/dashboard/hooks/useGetSystemHealth.ts diff --git a/src/features/dashboard/components/DashboardGrid.tsx b/src/features/dashboard/components/DashboardGrid.tsx index 3e52ec0..b76ea25 100644 --- a/src/features/dashboard/components/DashboardGrid.tsx +++ b/src/features/dashboard/components/DashboardGrid.tsx @@ -1,10 +1,12 @@ import CameraStatus from "./CameraStatus"; +import SystemOverview from "./SystemOverview"; import SystemStatusCard from "./SystemStatusCard"; const DashboardGrid = () => { return ( -
+
+
diff --git a/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx b/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx new file mode 100644 index 0000000..937e5c4 --- /dev/null +++ b/src/features/dashboard/components/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/StatusItems/StatusItemLocal.tsx b/src/features/dashboard/components/StatusItems/StatusItemLocal.tsx new file mode 100644 index 0000000..77dd4ba --- /dev/null +++ b/src/features/dashboard/components/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/StatusItems/StatusItemThreads.tsx b/src/features/dashboard/components/StatusItems/StatusItemThreads.tsx new file mode 100644 index 0000000..01e9239 --- /dev/null +++ b/src/features/dashboard/components/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/StatusItems/StatusItemUTC.tsx b/src/features/dashboard/components/StatusItems/StatusItemUTC.tsx new file mode 100644 index 0000000..0b7a5d0 --- /dev/null +++ b/src/features/dashboard/components/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/SystemOverview.tsx b/src/features/dashboard/components/SystemOverview.tsx new file mode 100644 index 0000000..de6e89b --- /dev/null +++ b/src/features/dashboard/components/SystemOverview.tsx @@ -0,0 +1,12 @@ +import Card from "../../../ui/Card"; +import CardHeader from "../../../ui/CardHeader"; + +const SystemOverview = () => { + return ( + + + + ); +}; + +export default SystemOverview; diff --git a/src/features/dashboard/components/SystemStatusCard.tsx b/src/features/dashboard/components/SystemStatusCard.tsx index d2ee725..e3307b4 100644 --- a/src/features/dashboard/components/SystemStatusCard.tsx +++ b/src/features/dashboard/components/SystemStatusCard.tsx @@ -1,20 +1,24 @@ import { useInfoSocket } from "../../../app/context/WebSocketContext"; import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; +import StatusItemCPU from "./StatusItems/StatusItemCPU"; +import StatusItemLocal from "./StatusItems/StatusItemLocal"; +import StatusItemThreads from "./StatusItems/StatusItemThreads"; +import StatusItemUTC from "./StatusItems/StatusItemUTC"; const SystemStatusCard = () => { const { data: 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… )} diff --git a/src/features/dashboard/hooks/useGetSystemHealth.ts b/src/features/dashboard/hooks/useGetSystemHealth.ts new file mode 100644 index 0000000..2c4bb3f --- /dev/null +++ b/src/features/dashboard/hooks/useGetSystemHealth.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; + +const fetchData = async () => { + const response = await fetch(`http://192.168.202.121/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, + }); + return { query }; +}; From 31b6bd45f571fb77f88b426a21e88abc962c3a5a Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 16:17:27 +0000 Subject: [PATCH 09/47] - improved Dashboard ui --- .../dashboard/components/CameraStatus.tsx | 2 +- .../dashboard/components/DashboardGrid.tsx | 7 ++-- .../components/StatusItems/StatusItemCPU.tsx | 2 +- .../StatusItems/StatusItemLocal.tsx | 2 +- .../StatusItems/StatusItemThreads.tsx | 2 +- .../components/StatusItems/StatusItemUTC.tsx | 2 +- .../dashboard/components/SystemHealth.tsx | 34 +++++++++++++++++++ .../dashboard/components/SystemOverview.tsx | 4 ++- src/routes/baywatch.tsx | 1 - src/routes/index.tsx | 1 - src/types/types.ts | 5 +++ src/ui/Badge.tsx | 24 +++++++++++++ src/utils/utils.ts | 3 ++ 13 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 src/features/dashboard/components/SystemHealth.tsx create mode 100644 src/ui/Badge.tsx create mode 100644 src/utils/utils.ts diff --git a/src/features/dashboard/components/CameraStatus.tsx b/src/features/dashboard/components/CameraStatus.tsx index 47cb2d6..9c28274 100644 --- a/src/features/dashboard/components/CameraStatus.tsx +++ b/src/features/dashboard/components/CameraStatus.tsx @@ -9,7 +9,7 @@ type CameraStatusProps = { const CameraStatus = ({ title, status, description }: CameraStatusProps) => { return ( - +

{description}

diff --git a/src/features/dashboard/components/DashboardGrid.tsx b/src/features/dashboard/components/DashboardGrid.tsx index b76ea25..b430106 100644 --- a/src/features/dashboard/components/DashboardGrid.tsx +++ b/src/features/dashboard/components/DashboardGrid.tsx @@ -7,8 +7,11 @@ const DashboardGrid = () => {
- - +
+ + + +
); }; diff --git a/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx b/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx index 937e5c4..3ae97f6 100644 --- a/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx +++ b/src/features/dashboard/components/StatusItems/StatusItemCPU.tsx @@ -8,7 +8,7 @@ type StatusItemProps = { const StatusItemCPU = ({ statusInfoItem, description }: StatusItemProps) => { return ( -
+
diff --git a/src/features/dashboard/components/StatusItems/StatusItemLocal.tsx b/src/features/dashboard/components/StatusItems/StatusItemLocal.tsx index 77dd4ba..f99cf40 100644 --- a/src/features/dashboard/components/StatusItems/StatusItemLocal.tsx +++ b/src/features/dashboard/components/StatusItems/StatusItemLocal.tsx @@ -15,7 +15,7 @@ const StatusItemLocal = ({ statusInfoItem, description }: StatusItemProps) => { }; return ( -
+
diff --git a/src/features/dashboard/components/StatusItems/StatusItemThreads.tsx b/src/features/dashboard/components/StatusItems/StatusItemThreads.tsx index 01e9239..69ce8c8 100644 --- a/src/features/dashboard/components/StatusItems/StatusItemThreads.tsx +++ b/src/features/dashboard/components/StatusItems/StatusItemThreads.tsx @@ -8,7 +8,7 @@ type StatusItemProps = { const StatusItemThreads = ({ statusInfoItem, description }: StatusItemProps) => { return ( -
+
diff --git a/src/features/dashboard/components/StatusItems/StatusItemUTC.tsx b/src/features/dashboard/components/StatusItems/StatusItemUTC.tsx index 0b7a5d0..b4adef8 100644 --- a/src/features/dashboard/components/StatusItems/StatusItemUTC.tsx +++ b/src/features/dashboard/components/StatusItems/StatusItemUTC.tsx @@ -15,7 +15,7 @@ const StatusItemUTC = ({ statusInfoItem, description }: StatusItemProps) => { }; return ( -
+
diff --git a/src/features/dashboard/components/SystemHealth.tsx b/src/features/dashboard/components/SystemHealth.tsx new file mode 100644 index 0000000..c93d731 --- /dev/null +++ b/src/features/dashboard/components/SystemHealth.tsx @@ -0,0 +1,34 @@ +import type { SystemHealthStatus } from "../../../types/types"; +import Badge from "../../../ui/Badge"; +import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; + +const SystemHealth = () => { + const { query } = useGetSystemHealth(); + + const startTime = query?.data?.StartTimeHumane; + const uptime = query?.data?.UptimeHumane; + const statuses = query?.data?.Status; + + return ( +
+
+
+

Start Time

{startTime} +
+
+

Up Time

{uptime} +
+
+ +
+ {statuses?.map((status: SystemHealthStatus) => ( +
+ {status.id} +
+ ))} +
+
+ ); +}; + +export default SystemHealth; diff --git a/src/features/dashboard/components/SystemOverview.tsx b/src/features/dashboard/components/SystemOverview.tsx index de6e89b..05d1f20 100644 --- a/src/features/dashboard/components/SystemOverview.tsx +++ b/src/features/dashboard/components/SystemOverview.tsx @@ -1,10 +1,12 @@ import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; +import SystemHealth from "./SystemHealth"; const SystemOverview = () => { return ( - + + ); }; diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx index f7401ab..0cb9718 100644 --- a/src/routes/baywatch.tsx +++ b/src/routes/baywatch.tsx @@ -9,7 +9,6 @@ export const Route = createFileRoute("/baywatch")({ function RouteComponent() { return (
-

Cameras

diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 8cd7e69..301f601 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -8,7 +8,6 @@ export const Route = createFileRoute("/")({ function HomePage() { return (
-

Dashboard

); diff --git a/src/types/types.ts b/src/types/types.ts index 127fad1..b130f22 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -16,3 +16,8 @@ export type Region = { name: string; brushColour: string; }; + +export type SystemHealthStatus = { + id: string; + tags: string[]; +}; 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/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..23c5527 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,3 @@ +export function capitalize(s?: string) { + return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; +} From 27d2c6a1b98925a89e84ef9ead9f136dd12d78bc Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 20:32:52 +0000 Subject: [PATCH 10/47] - improved ui for dashboard - added refetch fn --- .../dashboard/components/SystemHealth.tsx | 16 ++++++++++------ .../dashboard/components/SystemOverview.tsx | 12 ++++++++++-- .../dashboard/hooks/useGetSystemHealth.ts | 2 +- src/ui/CardHeader.tsx | 17 +++++++++++------ 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/components/SystemHealth.tsx b/src/features/dashboard/components/SystemHealth.tsx index c93d731..6f1e12b 100644 --- a/src/features/dashboard/components/SystemHealth.tsx +++ b/src/features/dashboard/components/SystemHealth.tsx @@ -1,13 +1,17 @@ import type { SystemHealthStatus } from "../../../types/types"; import Badge from "../../../ui/Badge"; -import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; -const SystemHealth = () => { - const { query } = useGetSystemHealth(); +type SystemHealthProps = { + startTime: string; + uptime: string; + statuses: SystemHealthStatus[]; + isLoading: boolean; +}; - const startTime = query?.data?.StartTimeHumane; - const uptime = query?.data?.UptimeHumane; - const statuses = query?.data?.Status; +const SystemHealth = ({ startTime, uptime, statuses, isLoading }: SystemHealthProps) => { + if (isLoading) { + return Loading system health…; + } return (
diff --git a/src/features/dashboard/components/SystemOverview.tsx b/src/features/dashboard/components/SystemOverview.tsx index 05d1f20..2a6559d 100644 --- a/src/features/dashboard/components/SystemOverview.tsx +++ b/src/features/dashboard/components/SystemOverview.tsx @@ -1,12 +1,20 @@ +import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons"; import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; +import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; import SystemHealth from "./SystemHealth"; const SystemOverview = () => { + const { query } = useGetSystemHealth(); + + const startTime = query?.data?.StartTimeHumane; + const uptime = query?.data?.UptimeHumane; + const statuses = query?.data?.Status; + const isLoading = query?.isLoading; return ( - - + + ); }; diff --git a/src/features/dashboard/hooks/useGetSystemHealth.ts b/src/features/dashboard/hooks/useGetSystemHealth.ts index 2c4bb3f..d9599d6 100644 --- a/src/features/dashboard/hooks/useGetSystemHealth.ts +++ b/src/features/dashboard/hooks/useGetSystemHealth.ts @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; const fetchData = async () => { - const response = await fetch(`http://192.168.202.121/api/system-health`); + const response = await fetch(`http://100.115.148.59/api/system-health`); if (!response.ok) throw new Error("Cannot get System overview"); return response.json(); }; diff --git a/src/ui/CardHeader.tsx b/src/ui/CardHeader.tsx index 25da205..3d6c3b2 100644 --- a/src/ui/CardHeader.tsx +++ b/src/ui/CardHeader.tsx @@ -1,22 +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, status }: CameraOverviewHeaderProps) => { - console.log(status); +const CardHeader = ({ title, status, icon, refetch }: CameraOverviewHeaderProps) => { return (
-
- {/* {icon && } */} - {status && } -

{title}

+
+

+ {status && } + {title} +

+ {icon && }
); From ec813928994c5623a46502e88dfed1b25850e636 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 24 Nov 2025 22:07:08 +0000 Subject: [PATCH 11/47] started adding to the output form --- package.json | 1 + .../output/components/BearerTypeCard.tsx | 14 ++++ .../output/components/BearerTypeFields.tsx | 30 +++++++++ .../output/components/ChannelCard.tsx | 17 +++++ .../output/components/ChannelFields.tsx | 67 +++++++++++++++++++ src/features/output/components/Output.tsx | 32 +++++++++ src/routes/output.tsx | 13 ++-- yarn.lock | 54 ++++++++++++++- 8 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 src/features/output/components/BearerTypeCard.tsx create mode 100644 src/features/output/components/BearerTypeFields.tsx create mode 100644 src/features/output/components/ChannelCard.tsx create mode 100644 src/features/output/components/ChannelFields.tsx create mode 100644 src/features/output/components/Output.tsx diff --git a/package.json b/package.json index be15300..f1f0622 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tanstack/react-router": "^1.136.18", "@tanstack/react-router-devtools": "^1.136.18", "clsx": "^2.1.1", + "formik": "^2.4.9", "konva": "^10.0.11", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/src/features/output/components/BearerTypeCard.tsx b/src/features/output/components/BearerTypeCard.tsx new file mode 100644 index 0000000..23f9b51 --- /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..a4d0a31 --- /dev/null +++ b/src/features/output/components/BearerTypeFields.tsx @@ -0,0 +1,30 @@ +import { Field } from "formik"; + +const BearerTypeFields = () => { + 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..1468c4d --- /dev/null +++ b/src/features/output/components/ChannelCard.tsx @@ -0,0 +1,17 @@ +import { useFormikContext } from "formik"; +import Card from "../../../ui/Card"; +import CardHeader from "../../../ui/CardHeader"; +import ChannelFields from "./ChannelFields"; + +const ChannelCard = () => { + const { values } = useFormikContext(); + console.log(values); + 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..88e9adc --- /dev/null +++ b/src/features/output/components/ChannelFields.tsx @@ -0,0 +1,67 @@ +import { Field } from "formik"; + +type ChannelFieldsProps = { + format: string; +}; + +const ChannelFields = ({ format }: ChannelFieldsProps) => { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +}; + +export default ChannelFields; diff --git a/src/features/output/components/Output.tsx b/src/features/output/components/Output.tsx new file mode 100644 index 0000000..ece7d10 --- /dev/null +++ b/src/features/output/components/Output.tsx @@ -0,0 +1,32 @@ +import { Formik, Form } from "formik"; +import BearerTypeCard from "./BearerTypeCard"; +import ChannelCard from "./ChannelCard"; + +const Output = () => { + const handleSubmit = (values) => { + console.log(values); + }; + + const inititalValues = { + format: "JSON", + enabled: true, + backOfficeURL: "", + username: "", + password: "", + connectTimeoutSeconds: Number(5), + readTimeoutSeconds: Number(15), + overviewQuality: "HIGH", + cropSizeFactor: "3/4", + }; + + return ( + +
+ + + +
+ ); +}; + +export default Output; diff --git a/src/routes/output.tsx b/src/routes/output.tsx index 430d04d..10b16e8 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 Output from "../features/output/components/Output"; -export const Route = createFileRoute('/output')({ +export const Route = createFileRoute("/output")({ component: RouteComponent, -}) +}); function RouteComponent() { - return
Hello "/output"!
+ return ( +
+ +
+ ); } diff --git a/yarn.lock b/yarn.lock index b567169..ceebd90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -994,6 +994,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.7" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" + integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== + dependencies: + hoist-non-react-statics "^3.3.0" + "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1351,6 +1358,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + detect-libc@^2.0.3: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" @@ -1601,6 +1613,20 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +formik@^2.4.9: + version "2.4.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.9.tgz#7e5b81e9c9e215d0ce2ac8fed808cf7fba0cd204" + integrity sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + fraction.js@^5.3.4: version "5.3.4" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" @@ -1679,6 +1705,13 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -1886,11 +1919,21 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2087,7 +2130,12 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" -react-is@^16.13.1: +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2287,7 +2335,7 @@ tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tiny-warning@^1.0.3: +tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -2312,7 +2360,7 @@ ts-api-utils@^2.1.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.0.1, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.4.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 16829ad5a5be70a6eb2a184f4fbb4294e840ac95 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 25 Nov 2025 10:07:35 +0000 Subject: [PATCH 12/47] - created forms for JSON, BOF2, UTMC and FTP --- .../output/components/BearerTypeCard.tsx | 2 +- .../output/components/BearerTypeFields.tsx | 8 +- .../output/components/ChannelCard.tsx | 15 +- .../output/components/ChannelFields.tsx | 167 +++++++++++++++++- src/features/output/components/Output.tsx | 18 +- src/types/types.ts | 36 ++++ 6 files changed, 233 insertions(+), 13 deletions(-) diff --git a/src/features/output/components/BearerTypeCard.tsx b/src/features/output/components/BearerTypeCard.tsx index 23f9b51..f7144e2 100644 --- a/src/features/output/components/BearerTypeCard.tsx +++ b/src/features/output/components/BearerTypeCard.tsx @@ -4,7 +4,7 @@ import BearerTypeFields from "./BearerTypeFields"; const BearerTypeCard = () => { return ( - + diff --git a/src/features/output/components/BearerTypeFields.tsx b/src/features/output/components/BearerTypeFields.tsx index a4d0a31..dccbd26 100644 --- a/src/features/output/components/BearerTypeFields.tsx +++ b/src/features/output/components/BearerTypeFields.tsx @@ -2,8 +2,10 @@ import { Field } from "formik"; const BearerTypeFields = () => { return ( - <> - +
+ { FTP - +
); }; diff --git a/src/features/output/components/ChannelCard.tsx b/src/features/output/components/ChannelCard.tsx index 1468c4d..1378161 100644 --- a/src/features/output/components/ChannelCard.tsx +++ b/src/features/output/components/ChannelCard.tsx @@ -2,14 +2,21 @@ import { useFormikContext } from "formik"; import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; import ChannelFields from "./ChannelFields"; +import type { FormTypes } from "../../../types/types"; const ChannelCard = () => { - const { values } = useFormikContext(); - console.log(values); + const { values, errors, touched } = useFormikContext(); + return ( - + - + + ); }; diff --git a/src/features/output/components/ChannelFields.tsx b/src/features/output/components/ChannelFields.tsx index 88e9adc..231a9c6 100644 --- a/src/features/output/components/ChannelFields.tsx +++ b/src/features/output/components/ChannelFields.tsx @@ -1,10 +1,16 @@ import { Field } from "formik"; +import type { FormTypes, InitialValuesFormErrors } from "../../../types/types"; type ChannelFieldsProps = { - format: string; + values: FormTypes; + errors: InitialValuesFormErrors; + touched: { + connectTimeoutSeconds?: boolean | undefined; + readTimeoutSeconds?: boolean | undefined; + }; }; -const ChannelFields = ({ format }: ChannelFieldsProps) => { +const ChannelFields = ({ errors, touched, values }: ChannelFieldsProps) => { return (
@@ -60,6 +66,163 @@ const ChannelFields = ({ format }: ChannelFieldsProps) => { className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`} />
+
+ + + + + + +
+
+ + + + + + + +
+ {values.format.toLowerCase() === "utmc" && ( + <> +
+

{values.format} Constants

+
+ +
+ + +
+
+ + + + + +
+
+ + + + + +
+ + )} + {values.format?.toLowerCase() === "bof2" && ( + <> +
+
+

{values.format} Constants

+
+
+ + +
+
+ + +
+
+ + + + + +
+
+ + + + + +
+
+
+
+

{values.format} Lane ID Config

+
+
+ + +
+
+ + +
+
+ + )}
); }; diff --git a/src/features/output/components/Output.tsx b/src/features/output/components/Output.tsx index ece7d10..5c6854b 100644 --- a/src/features/output/components/Output.tsx +++ b/src/features/output/components/Output.tsx @@ -1,13 +1,14 @@ import { Formik, Form } from "formik"; import BearerTypeCard from "./BearerTypeCard"; import ChannelCard from "./ChannelCard"; +import type { FormTypes } from "../../../types/types"; const Output = () => { - const handleSubmit = (values) => { + const handleSubmit = (values: FormTypes) => { console.log(values); }; - const inititalValues = { + const inititalValues: FormTypes = { format: "JSON", enabled: true, backOfficeURL: "", @@ -17,11 +18,22 @@ const Output = () => { readTimeoutSeconds: Number(15), overviewQuality: "HIGH", cropSizeFactor: "3/4", + + // Bof2 -optional constants + FFID: "", + SCID: "", + timestampSource: "UTC", + GPSFormat: "Minutes", + + //BOF2 - optional Lane IDs + laneId: "", + LID1: "", + LID2: "", }; return ( -
+ diff --git a/src/types/types.ts b/src/types/types.ts index b130f22..a768370 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -21,3 +21,39 @@ export type SystemHealthStatus = { id: string; tags: string[]; }; + +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 InitialValuesFormErrors = { + backOfficeURL?: string; + username?: string; + password?: string; + connectTimeoutSeconds?: string; + readTimeoutSeconds?: string; +}; + +export type FormTypes = BearerTypeFields & OptionalConstants & OptionalLaneIDs; From 18124924f7a48c4dfb31ffd66414380d04206165 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 25 Nov 2025 14:57:18 +0000 Subject: [PATCH 13/47] - improved ui for region selector and camera settings --- .../cameras/components/CameraGrid.tsx | 20 ++- .../CameraSettings/CameraSettings.tsx | 8 +- .../CameraSettings/ColourPicker.tsx | 11 +- .../CameraSettings/RegionSelector.tsx | 127 +++++++++++++----- .../components/PlatePatch/PlatePatch.tsx | 2 +- .../components/Video/VideoFeedGridPainter.tsx | 19 ++- src/types/types.ts | 4 + 7 files changed, 133 insertions(+), 58 deletions(-) diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index 27a5b0c..c661c6d 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -1,8 +1,8 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import VideoFeedGridPainter from "./Video/VideoFeedGridPainter"; import CameraSettings from "./CameraSettings/CameraSettings"; -import type { Region } from "../../../types/types"; +import type { PaintedCell, Region } from "../../../types/types"; import PlatePatch from "./PlatePatch/PlatePatch"; const CameraGrid = () => { @@ -14,14 +14,21 @@ const CameraGrid = () => { const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); const [mode, setMode] = useState(""); const [tabIndex, setTabIndex] = useState(0); - console.log(tabIndex); + const updateRegionColour = (index: number, newColour: string) => { setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); }; - console.log(mode); + + const paintedCellsRef = useRef>(new Map()); + return ( -
- +
+ { onSelectMode={setMode} tabIndex={tabIndex} setTabIndex={setTabIndex} + paintedCells={paintedCellsRef} />
diff --git a/src/features/cameras/components/CameraSettings/CameraSettings.tsx b/src/features/cameras/components/CameraSettings/CameraSettings.tsx index b0ad443..5daf525 100644 --- a/src/features/cameras/components/CameraSettings/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings/CameraSettings.tsx @@ -2,7 +2,8 @@ import Card from "../../../../ui/Card"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import RegionSelector from "./RegionSelector"; -import type { Region } from "../../../../types/types"; +import type { PaintedCell, Region } from "../../../../types/types"; +import type { RefObject } from "react"; type CameraSettingsProps = { regions: Region[]; @@ -13,6 +14,7 @@ type CameraSettingsProps = { onSelectMode: (mode: string) => void; setTabIndex: (tabIndex: number) => void; tabIndex: number; + paintedCells: RefObject>; }; const CameraSettings = ({ @@ -24,9 +26,10 @@ const CameraSettings = ({ onSelectMode, tabIndex, setTabIndex, + paintedCells, }: CameraSettingsProps) => { return ( - + diff --git a/src/features/cameras/components/CameraSettings/ColourPicker.tsx b/src/features/cameras/components/CameraSettings/ColourPicker.tsx index f097cc4..6f44248 100644 --- a/src/features/cameras/components/CameraSettings/ColourPicker.tsx +++ b/src/features/cameras/components/CameraSettings/ColourPicker.tsx @@ -4,7 +4,16 @@ type ColourPickerProps = { }; const ColourPicker = ({ colour, setColour }: ColourPickerProps) => { - return setColour(e.target.value)} />; + return ( + setColour(e.target.value)} + className="h-8 w-8 p-0 rounded-md border border-slate-500 cursor-pointer" + /> + ); }; export default ColourPicker; diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index f03d132..d432669 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -1,5 +1,6 @@ import ColourPicker from "./ColourPicker"; -import type { Region } from "../../../../types/types"; +import type { PaintedCell, Region } from "../../../../types/types"; +import type { RefObject } from "react"; type RegionSelectorProps = { regions: Region[]; @@ -8,6 +9,7 @@ type RegionSelectorProps = { onChangeRegionColour: (index: number, colour: string) => void; mode: string; onSelectMode: (mode: string) => void; + paintedCells: RefObject>; }; const RegionSelector = ({ @@ -17,49 +19,100 @@ const RegionSelector = ({ onChangeRegionColour, mode, onSelectMode, + paintedCells, }: RegionSelectorProps) => { const handleChange = (e: { target: { value: string } }) => { onSelectMode(e.target.value); }; - return ( -
-
-

Region Select

-
-
- {regions.map((region, idx) => ( -
- - onChangeRegionColour(idx, c)} /> -
- ))} -
-

Tools

-
-
- + const handleResetClick = () => { + const map = paintedCells.current; + map.clear(); + }; -
-
- {statuses?.map((status: SystemHealthStatus) => ( -
- {status.id} -
+
+ {statusCategoryArray?.map((status) => ( + ))}
diff --git a/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx b/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx new file mode 100644 index 0000000..05318b5 --- /dev/null +++ b/src/features/dashboard/components/statusGridItem/StatusGridItem.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import type { SystemHealthStatus } from "../../../../types/types"; +import StatusIndicators from "../../../../ui/StatusIndicators"; +import { capitalize } from "../../../../utils/utils"; +import SystemHealthModal from "../systemHealthModal/SystemHealthModal"; + +type StatusGridItemProps = { + title: string; + statusCategory: SystemHealthStatus[]; +}; + +const StatusGridItem = ({ title, statusCategory }: StatusGridItemProps) => { + const [isOpen, setIsOpen] = useState(false); + const isAllGood = 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/systemHealthModal/SystemHealthModal.tsx b/src/features/dashboard/components/systemHealthModal/SystemHealthModal.tsx new file mode 100644 index 0000000..fb43cb2 --- /dev/null +++ b/src/features/dashboard/components/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/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/types/types.ts b/src/types/types.ts index d59e1ad..814decb 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -20,6 +20,14 @@ export type Region = { export type SystemHealthStatus = { id: string; tags: string[]; + groupID: string; +}; + +export type StatusGroups = { + channelA: SystemHealthStatus[]; + channelB: SystemHealthStatus[]; + channelC: SystemHealthStatus[]; + default: SystemHealthStatus[]; }; export type BearerTypeFields = { diff --git a/src/ui/ModalComponent.tsx b/src/ui/ModalComponent.tsx new file mode 100644 index 0000000..6a1684d --- /dev/null +++ b/src/ui/ModalComponent.tsx @@ -0,0 +1,22 @@ +import Modal from "react-modal"; + +type ModalComponentProps = { + isModalOpen: boolean; + children: React.ReactNode; + close: () => void; +}; + +const ModalComponent = ({ isModalOpen, children, close }: ModalComponentProps) => { + return ( + + {children} + + ); +}; + +export default ModalComponent; diff --git a/yarn.lock b/yarn.lock index ceebd90..612afcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1018,6 +1018,13 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== +"@types/react-modal@^3.16.3": + version "3.16.3" + resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.16.3.tgz#250f32c07f1de28e2bcf9c3e84b56adaa6897013" + integrity sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg== + dependencies: + "@types/react" "*" + "@types/react-reconciler@^0.28.9": version "0.28.9" resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b" @@ -1028,6 +1035,13 @@ resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz#eb4b346f367f29f07628032934d30a4f3f9eaba7" integrity sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA== +"@types/react@*": + version "19.2.7" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" + integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== + dependencies: + csstype "^3.2.2" + "@types/react@^19.2.5": version "19.2.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.6.tgz#d27db1ff45012d53980f5589fda925278e1249ca" @@ -1540,6 +1554,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1934,7 +1953,7 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2104,7 +2123,7 @@ prettier@^3.5.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== -prop-types@^15.5.0: +prop-types@^15.5.0, prop-types@^15.7.2: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2150,6 +2169,21 @@ react-konva@^19.2.0: react-reconciler "0.33.0" scheduler "0.27.0" +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-modal@^3.16.3: + version "3.16.3" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.3.tgz#c412d41915782e3c261253435d01468e2439b11b" + integrity sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw== + dependencies: + exenv "^1.2.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-reconciler@0.33.0: version "0.33.0" resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.33.0.tgz#9dd20208d45baa5b0b4701781f858236657f15e1" @@ -2446,6 +2480,13 @@ vite@^7.1.7, vite@^7.2.4: optionalDependencies: fsevents "~2.3.3" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + webpack-virtual-modules@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" From 6cb2e88b3b2f3a24ea0df69899d506beacb56ba8 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 1 Dec 2025 09:33:29 +0000 Subject: [PATCH 28/47] - added new dashboard items - added camera module statuses --- .../dashboard/components/CameraStatus.tsx | 30 +++++++++--- .../components/CameraStatusGridItem.tsx | 38 +++++++++++++++ .../dashboard/components/DashboardGrid.tsx | 47 +++++++++++++++++-- .../dashboard/components/SystemHealth.tsx | 13 ++--- .../dashboard/components/SystemOverview.tsx | 32 ++++++++----- src/utils/utils.ts | 8 ++++ 6 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 src/features/dashboard/components/CameraStatusGridItem.tsx diff --git a/src/features/dashboard/components/CameraStatus.tsx b/src/features/dashboard/components/CameraStatus.tsx index 9c28274..18ecac7 100644 --- a/src/features/dashboard/components/CameraStatus.tsx +++ b/src/features/dashboard/components/CameraStatus.tsx @@ -1,17 +1,35 @@ +import type { SystemHealthStatus } from "../../../types/types"; import Card from "../../../ui/Card"; -import CardHeader from "../../../ui/CardHeader"; +import StatusIndicators from "../../../ui/StatusIndicators"; +import { capitalize } from "../../../utils/utils"; +import CameraStatusGridItem from "./CameraStatusGridItem"; type CameraStatusProps = { title: string; - status?: string; - description: string; + category: SystemHealthStatus[]; }; -const CameraStatus = ({ title, status, description }: CameraStatusProps) => { +const CameraStatus = ({ title, category }: CameraStatusProps) => { + const isAllGood = category?.every((status) => status.tags.includes("RUNNING")); + // check if some are down + // check if all are down + //check if offline return ( - -

{description}

+
+

+ {isAllGood ? : } + {capitalize(title)} +

+

{isAllGood ? "All systems running" : "Some systems down"}

+
+ {category && category?.length <= 0 ? ( +

Loading Camera health...

+ ) : ( +
+ +
+ )}
); }; diff --git a/src/features/dashboard/components/CameraStatusGridItem.tsx b/src/features/dashboard/components/CameraStatusGridItem.tsx new file mode 100644 index 0000000..8217dc8 --- /dev/null +++ b/src/features/dashboard/components/CameraStatusGridItem.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import type { SystemHealthStatus } from "../../../types/types"; +import { capitalize } from "../../../utils/utils"; +import SystemHealthModal from "./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/DashboardGrid.tsx b/src/features/dashboard/components/DashboardGrid.tsx index b430106..1d75e5b 100644 --- a/src/features/dashboard/components/DashboardGrid.tsx +++ b/src/features/dashboard/components/DashboardGrid.tsx @@ -1,16 +1,55 @@ +import type { SystemHealthStatus } from "../../../types/types"; +import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; import CameraStatus from "./CameraStatus"; import SystemOverview from "./SystemOverview"; import SystemStatusCard from "./SystemStatusCard"; const DashboardGrid = () => { + const { query } = useGetSystemHealth(); + const startTime = query?.data?.StartTimeHumane; + const uptime = query?.data?.UptimeHumane; + const statuses: SystemHealthStatus[] = query?.data?.Status; + const isLoading = query?.isLoading; + const isError = query?.isError; + const dateUpdatedAt = query?.dataUpdatedAt; + const refetch = query?.refetch; + + 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 categoryA = statusCategories?.channelA ?? []; + const categoryB = statusCategories?.channelB ?? []; + const categoryC = statusCategories?.channelC ?? []; + return (
- +
- - - + + +
); diff --git a/src/features/dashboard/components/SystemHealth.tsx b/src/features/dashboard/components/SystemHealth.tsx index 4055e7a..1471947 100644 --- a/src/features/dashboard/components/SystemHealth.tsx +++ b/src/features/dashboard/components/SystemHealth.tsx @@ -29,12 +29,7 @@ const SystemHealth = ({ startTime, uptime, statuses, isLoading, isError, dateUpd }, ); - const convertObjtoArray = (obj: Record) => { - if (!obj) return; - const statusCategoryArray = Object.entries(obj); - return statusCategoryArray; - }; - const statusCategoryArray = convertObjtoArray(statusCategories); + const categoryDefault = statusCategories?.default ?? []; if (isError) { return Error loading system health.; @@ -52,10 +47,8 @@ const SystemHealth = ({ startTime, uptime, statuses, isLoading, isError, dateUpd

Up Time

{uptime}
-
- {statusCategoryArray?.map((status) => ( - - ))} +
+
{`Last refeshed ${updatedDate}`} diff --git a/src/features/dashboard/components/SystemOverview.tsx b/src/features/dashboard/components/SystemOverview.tsx index b440e22..418070e 100644 --- a/src/features/dashboard/components/SystemOverview.tsx +++ b/src/features/dashboard/components/SystemOverview.tsx @@ -1,22 +1,32 @@ import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons"; import Card from "../../../ui/Card"; import CardHeader from "../../../ui/CardHeader"; -import { useGetSystemHealth } from "../hooks/useGetSystemHealth"; + import SystemHealth from "./SystemHealth"; +import type { SystemHealthStatus } from "../../../types/types"; -const SystemOverview = () => { - const { query } = useGetSystemHealth(); - - const startTime = query?.data?.StartTimeHumane; - const uptime = query?.data?.UptimeHumane; - const statuses = query?.data?.Status; - const isLoading = query?.isLoading; - const isError = query?.isError; - const dateUpdatedAt = query?.dataUpdatedAt; +type SystemOverviewProps = { + startTime: string; + uptime: string; + statuses: SystemHealthStatus[]; + isLoading: boolean; + isError: boolean; + dateUpdatedAt: number; + refetch: () => void; +}; +const SystemOverview = ({ + startTime, + uptime, + statuses, + isLoading, + isError, + dateUpdatedAt, + refetch, +}: SystemOverviewProps) => { return ( - + ) => { + if (!obj) return; + const statusCategoryArray = Object.entries(obj); + return statusCategoryArray; +}; From ce79591de0932b2f13de2adf3a5c098b0e7bc9fe Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 1 Dec 2025 14:36:25 +0000 Subject: [PATCH 29/47] - enhancedcamera settings and region painter. - Can send to the back end --- src/app/reducers/cameraFeedReducer.ts | 13 +++- .../CameraSettings/CameraSettings.tsx | 2 +- .../CameraSettings/ColourPicker.tsx | 1 + .../CameraSettings/RegionSelector.tsx | 69 +++++++++++++++++-- .../components/Video/VideoFeedGridPainter.tsx | 31 +++++---- .../cameras/hooks/useColourDetection.ts | 24 +++++++ src/types/types.ts | 11 +++ src/utils/utils.ts | 9 +++ 8 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 src/features/cameras/hooks/useColourDetection.ts diff --git a/src/app/reducers/cameraFeedReducer.ts b/src/app/reducers/cameraFeedReducer.ts index 5a2b4f6..fa40d6a 100644 --- a/src/app/reducers/cameraFeedReducer.ts +++ b/src/app/reducers/cameraFeedReducer.ts @@ -12,12 +12,23 @@ export const initialState: CameraFeedState = { { name: "Region 1", brushColour: "#ff0000" }, { name: "Region 2", brushColour: "#00ff00" }, { name: "Region 3", brushColour: "#0400ff" }, + { name: "Region 4", brushColour: "#ffff00" }, + { name: "Region 5", brushColour: "#fc35db" }, ], B: [ { name: "Region 1", brushColour: "#ff0000" }, { name: "Region 2", brushColour: "#00ff00" }, + { name: "Region 3", brushColour: "#0400ff" }, + { name: "Region 4", brushColour: "#ffff00" }, + { name: "Region 5", brushColour: "#fc35db" }, + ], + C: [ + { name: "Region 1", brushColour: "#ff0000" }, + { name: "Region 2", brushColour: "#00ff00" }, + { name: "Region 3", brushColour: "#0400ff" }, + { name: "Region 4", brushColour: "#ffff00" }, + { name: "Region 5", brushColour: "#fc35db" }, ], - C: [{ name: "Region 1", brushColour: "#ff0000" }], }, selectedRegionIndex: 0, diff --git a/src/features/cameras/components/CameraSettings/CameraSettings.tsx b/src/features/cameras/components/CameraSettings/CameraSettings.tsx index b36f5f7..e0be2f8 100644 --- a/src/features/cameras/components/CameraSettings/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings/CameraSettings.tsx @@ -10,7 +10,7 @@ type CameraSettingsProps = { const CameraSettings = ({ tabIndex, setTabIndex }: CameraSettingsProps) => { return ( - + { name="" id="" value={colour} + disabled onChange={(e) => setColour(e.target.value)} className="h-8 w-8 p-0 rounded-md border border-slate-500 cursor-pointer" /> diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index 6918f2f..135511d 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -1,6 +1,7 @@ -import type { PaintedCell, Region } from "../../../../types/types"; +import type { ColourData, PaintedCell, Region } from "../../../../types/types"; import ColourPicker from "./ColourPicker"; import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; +import { useColourDectection } from "../../hooks/useColourDetection"; type RegionSelectorProps = { regions: Region[]; @@ -10,7 +11,10 @@ type RegionSelectorProps = { }; const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: RegionSelectorProps) => { - const { dispatch } = useCameraFeedContext(); + const { colourMutation } = useColourDectection(); + const { state, dispatch } = useCameraFeedContext(); + const paintedCells = state.paintedCells[cameraFeedID]; + const handleChange = (e: { target: { value: string } }) => { dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } }); }; @@ -54,6 +58,53 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re }); }; + const handleSaveclick = () => { + const regions: ColourData[] = []; + const test = Array.from(paintedCells.entries()); + const region1 = test.filter(([, cell]) => cell.region.name === "Region 1"); + const region2 = test.filter(([, cell]) => cell.region.name === "Region 2"); + const region3 = test.filter(([, cell]) => cell.region.name === "Region 3"); + const region4 = test.filter(([, cell]) => cell.region.name === "Region 4"); + const region5 = test.filter(([, cell]) => cell.region.name === "Region 5"); + const region1Data = { + id: 1, + cells: region1.map(([key]) => [parseInt(key.split("-")[1]), parseInt(key.split("-")[0])]), + }; + const region2Data = { + id: 2, + cells: region2.map(([key]) => [parseInt(key.split("-")[1]), parseInt(key.split("-")[0])]), + }; + const region3Data = { + id: 3, + cells: region3.map(([key]) => [parseInt(key.split("-")[1]), parseInt(key.split("-")[0])]), + }; + const region4Data = { + id: 4, + cells: region4.map(([key]) => [parseInt(key.split("-")[1]), parseInt(key.split("-")[0])]), + }; + const region5Data = { + id: 5, + cells: region5.map(([key]) => [parseInt(key.split("-")[1]), parseInt(key.split("-")[0])]), + }; + if (region1Data.cells.length > 0) { + regions.push(region1Data); + } + if (region2Data.cells.length > 0) { + regions.push(region2Data); + } + if (region3Data.cells.length > 0) { + regions.push(region3Data); + } + if (region4Data.cells.length > 0) { + regions.push(region4Data); + } + if (region5Data.cells.length > 0) { + regions.push(region5Data); + } + + colourMutation.mutate({ cameraFeedID, regions: regions }); + }; + return (
@@ -103,7 +154,7 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re ); })}
- -
@@ -139,6 +190,12 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re

Actions

+ + + + ); +}; + +export default SystemConfig; 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/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 = () => ( <>
+