Compare commits
17 Commits
25a744bd8d
...
feature/ou
| Author | SHA1 | Date | |
|---|---|---|---|
| 16829ad5a5 | |||
| ec81392899 | |||
| 8c967b3ae2 | |||
| 27d2c6a1b9 | |||
| 31b6bd45f5 | |||
| c9dde6b992 | |||
| d15a69e6b9 | |||
| b18e11ca6a | |||
| 8dd20e409c | |||
| b084c3016d | |||
| c5fe6754c3 | |||
| 68711b9087 | |||
| 9deeda1bc3 | |||
| e984c74333 | |||
| 510f837cbc | |||
| 4f13ed104d | |||
| a21b7bb87e |
@@ -19,8 +19,14 @@
|
||||
"@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"
|
||||
"react-dom": "^19.2.0",
|
||||
"react-konva": "^19.2.0",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-use-websocket": "3.0.0",
|
||||
"sonner": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
23
src/app/context/WebSocketContext.ts
Normal file
23
src/app/context/WebSocketContext.ts
Normal file
@@ -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<WebSocketConextValue | null>(null);
|
||||
|
||||
const useWebSocketContext = () => {
|
||||
const ctx = useContext(WebsocketContext);
|
||||
if (!ctx) throw new Error("useWebSocketContext must be used inside <WebSocketConext.Provider>");
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const useInfoSocket = () => useWebSocketContext().info;
|
||||
@@ -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 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
return (
|
||||
<QueryProvider>
|
||||
<WebSocketProvider>{children}</WebSocketProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
};
|
||||
|
||||
38
src/app/providers/WebSocketProvider.tsx
Normal file
38
src/app/providers/WebSocketProvider.tsx
Normal file
@@ -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<InfoBarData | null>(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<WebSocketConextValue>(
|
||||
() => ({
|
||||
info: {
|
||||
data: systemData,
|
||||
readyState: infoSocket.readyState,
|
||||
sendJson: infoSocket.sendJsonMessage,
|
||||
},
|
||||
}),
|
||||
[infoSocket.readyState, infoSocket.sendJsonMessage, systemData],
|
||||
);
|
||||
|
||||
return <WebsocketContext.Provider value={value}>{children}</WebsocketContext.Provider>;
|
||||
};
|
||||
40
src/features/cameras/components/CameraGrid.tsx
Normal file
40
src/features/cameras/components/CameraGrid.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
|
||||
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<Region[]>([
|
||||
{ name: "Region 1", brushColour: "#ff0000" },
|
||||
{ name: "Region 2", brushColour: "#00ff00" },
|
||||
{ name: "Region 3", brushColour: "#0400ff" },
|
||||
]);
|
||||
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);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 grid-rows-2">
|
||||
<VideoFeedGridPainter regions={regions} selectedRegionIndex={selectedRegionIndex} mode={mode} />
|
||||
<CameraSettings
|
||||
regions={regions}
|
||||
selectedRegionIndex={selectedRegionIndex}
|
||||
onSelectRegion={setSelectedRegionIndex}
|
||||
onChangeRegionColour={updateRegionColour}
|
||||
mode={mode}
|
||||
onSelectMode={setMode}
|
||||
tabIndex={tabIndex}
|
||||
setTabIndex={setTabIndex}
|
||||
/>
|
||||
<PlatePatch />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraGrid;
|
||||
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
mode: string;
|
||||
onSelectMode: (mode: string) => void;
|
||||
setTabIndex: (tabIndex: number) => void;
|
||||
tabIndex: number;
|
||||
};
|
||||
|
||||
const CameraSettings = ({
|
||||
regions,
|
||||
selectedRegionIndex,
|
||||
onSelectRegion,
|
||||
onChangeRegionColour,
|
||||
mode,
|
||||
onSelectMode,
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
}: CameraSettingsProps) => {
|
||||
return (
|
||||
<Card className="p-4 max-h-screen col-span-3">
|
||||
<Tabs
|
||||
selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none rounded-sm mb-1"
|
||||
className="react-tabs"
|
||||
onSelect={(index) => setTabIndex(index)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab>Target Detection</Tab>
|
||||
<Tab>Camera 1</Tab>
|
||||
<Tab>Camera 2</Tab>
|
||||
<Tab>Camera 3</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<RegionSelector
|
||||
regions={regions}
|
||||
selectedRegionIndex={selectedRegionIndex}
|
||||
onSelectRegion={onSelectRegion}
|
||||
onChangeRegionColour={onChangeRegionColour}
|
||||
mode={mode}
|
||||
onSelectMode={onSelectMode}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Camera details {tabIndex}</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Camera details {tabIndex}</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Camera details {tabIndex}</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraSettings;
|
||||
@@ -0,0 +1,10 @@
|
||||
type ColourPickerProps = {
|
||||
colour: string;
|
||||
setColour: (colour: string) => void;
|
||||
};
|
||||
|
||||
const ColourPicker = ({ colour, setColour }: ColourPickerProps) => {
|
||||
return <input type="color" name="" id="" value={colour} onChange={(e) => setColour(e.target.value)} />;
|
||||
};
|
||||
|
||||
export default ColourPicker;
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
mode: string;
|
||||
onSelectMode: (mode: string) => void;
|
||||
};
|
||||
|
||||
const RegionSelector = ({
|
||||
regions,
|
||||
selectedRegionIndex,
|
||||
onSelectRegion,
|
||||
onChangeRegionColour,
|
||||
mode,
|
||||
onSelectMode,
|
||||
}: RegionSelectorProps) => {
|
||||
const handleChange = (e: { target: { value: string } }) => {
|
||||
onSelectMode(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-xl">Region Select</h2>
|
||||
</div>
|
||||
<div>
|
||||
{regions.map((region, idx) => (
|
||||
<div
|
||||
key={region.name}
|
||||
className="items-center p-4 border border-gray-700 bg-slate-700 rounded-xl m-4 w-[40%]"
|
||||
>
|
||||
<label style={{ marginRight: "0.5rem" }}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={selectedRegionIndex === idx}
|
||||
onChange={() => {
|
||||
onSelectMode("painter");
|
||||
onSelectRegion(idx);
|
||||
}}
|
||||
/>{" "}
|
||||
{region.name}
|
||||
</label>
|
||||
<ColourPicker colour={region.brushColour} setColour={(c: string) => onChangeRegionColour(idx, c)} />
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<h2 className="text-xl">Tools</h2>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="mode">
|
||||
<input id="mode" type="radio" onChange={handleChange} checked={mode === "painter"} value="painter" />
|
||||
Paint mode
|
||||
</label>
|
||||
|
||||
<label htmlFor="erase">
|
||||
<input type="radio" onChange={handleChange} checked={mode === "eraser"} value={"eraser"} />
|
||||
Erase mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionSelector;
|
||||
@@ -0,0 +1,7 @@
|
||||
import Card from "../../../../ui/Card";
|
||||
|
||||
const PlatePatch = () => {
|
||||
return <Card>PlatePatch</Card>;
|
||||
};
|
||||
|
||||
export default PlatePatch;
|
||||
153
src/features/cameras/components/Video/VideoFeedGridPainter.tsx
Normal file
153
src/features/cameras/components/Video/VideoFeedGridPainter.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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 type { Region } from "../../../../types/types";
|
||||
import Card from "../../../../ui/Card";
|
||||
|
||||
const rows = 40;
|
||||
const cols = 40;
|
||||
const size = 20;
|
||||
const gap = 0;
|
||||
|
||||
type VideoFeedGridPainterProps = {
|
||||
regions: Region[];
|
||||
selectedRegionIndex: number;
|
||||
mode: string;
|
||||
};
|
||||
|
||||
type PaintedCell = {
|
||||
colour: string;
|
||||
};
|
||||
|
||||
const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode }: VideoFeedGridPainterProps) => {
|
||||
const { latestBitmapRef, isloading } = useCreateVideoSnapshot();
|
||||
const [stageSize, setStageSize] = useState({ width: 740, height: 460 });
|
||||
const isDrawingRef = useRef(false);
|
||||
const paintedCellsRef = useRef<Map<string, PaintedCell>>(new Map());
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const paintLayerRef = useRef<any>(null);
|
||||
|
||||
const draw = (bmp: RefObject<ImageBitmap | null>): ImageBitmap | null => {
|
||||
if (!bmp || !bmp.current) {
|
||||
return null;
|
||||
}
|
||||
const image = bmp.current;
|
||||
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));
|
||||
|
||||
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
|
||||
|
||||
const activeRegion = regions[selectedRegionIndex];
|
||||
if (!activeRegion) return;
|
||||
|
||||
const key = `${row}-${col}`;
|
||||
const currentColour = regions[selectedRegionIndex].brushColour;
|
||||
|
||||
const map = paintedCellsRef.current;
|
||||
const existing = map.get(key);
|
||||
|
||||
if (mode === "eraser") {
|
||||
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<MouseEvent>) => {
|
||||
if (!regions[selectedRegionIndex]) return;
|
||||
isDrawingRef.current = true;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const handleStageMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!isDrawingRef.current) return;
|
||||
if (!regions[selectedRegionIndex]) return;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const handleStageMouseUp = () => {
|
||||
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 (
|
||||
<Card className="row-span-1 col-span-2 rounded-lg p-4 w-full">
|
||||
<span className="text-slate-500">Loading Video feed…</span>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<div className="row-span-1 col-span-2 rounded-lg">
|
||||
<Stage
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
onMouseDown={handleStageMouseDown}
|
||||
onMouseMove={handleStageMouseMove}
|
||||
onMouseUp={handleStageMouseUp}
|
||||
onMouseLeave={handleStageMouseUp}
|
||||
>
|
||||
<Layer>
|
||||
<Image image={image} width={stageSize.width} height={stageSize.height} classname={"rounded-lg"} />
|
||||
</Layer>
|
||||
|
||||
<Layer ref={paintLayerRef} opacity={0.6}>
|
||||
<Shape
|
||||
sceneFunc={(ctx, shape) => {
|
||||
const cells = paintedCellsRef.current;
|
||||
cells.forEach((cell, 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 = cell.colour;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
ctx.fillStrokeShape(shape);
|
||||
}}
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
/>
|
||||
</Layer>
|
||||
</Stage>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoFeedGridPainter;
|
||||
22
src/features/cameras/hooks/useGetVideoFeed.ts
Normal file
22
src/features/cameras/hooks/useGetVideoFeed.ts
Normal file
@@ -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 };
|
||||
};
|
||||
27
src/features/cameras/hooks/useGetvideoSnapshots.ts
Normal file
27
src/features/cameras/hooks/useGetvideoSnapshots.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useGetVideoFeed } from "./useGetVideoFeed";
|
||||
|
||||
export const useCreateVideoSnapshot = () => {
|
||||
const latestBitmapRef = useRef<ImageBitmap | null>(null);
|
||||
const { videoQuery } = useGetVideoFeed();
|
||||
|
||||
const snapShot = videoQuery?.data;
|
||||
const isloading = videoQuery.isPending;
|
||||
|
||||
useEffect(() => {
|
||||
async function createBitmap() {
|
||||
if (!snapShot) return;
|
||||
|
||||
try {
|
||||
const bitmap = await createImageBitmap(snapShot);
|
||||
if (!bitmap) return;
|
||||
latestBitmapRef.current = bitmap;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
createBitmap();
|
||||
}, [snapShot]);
|
||||
|
||||
return { latestBitmapRef, isloading };
|
||||
};
|
||||
@@ -9,7 +9,7 @@ type CameraStatusProps = {
|
||||
|
||||
const CameraStatus = ({ title, status, description }: CameraStatusProps) => {
|
||||
return (
|
||||
<Card className="p-4 w-[40%]">
|
||||
<Card className="p-4">
|
||||
<CardHeader title={title} status={status} />
|
||||
<p className=" text-gray-500">{description}</p>
|
||||
</Card>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import CameraStatus from "./CameraStatus";
|
||||
import SystemOverview from "./SystemOverview";
|
||||
import SystemStatusCard from "./SystemStatusCard";
|
||||
|
||||
const DashboardGrid = () => {
|
||||
return (
|
||||
<div className="grid grid-rows-2">
|
||||
<div className="grid grid-cols-1 md:grid-rows-2 md:grid-cols-2">
|
||||
<SystemStatusCard />
|
||||
<CameraStatus title="Camera 1" status={"bg-red-500"} description={"Camera not responding"} />
|
||||
<CameraStatus title="Camera 2" status={"bg-gray-500"} description={"Camera Offline"} />
|
||||
<SystemOverview />
|
||||
<div className="grid grid-cols-1 md:col-span-2 md:grid-cols-3">
|
||||
<CameraStatus title="Camera 1" status={"bg-red-500"} description={"Camera not responding"} />
|
||||
<CameraStatus title="Camera 2" status={"bg-gray-500"} description={"Camera Offline"} />
|
||||
<CameraStatus title="Camera 3" status={"bg-gray-500"} description={"Camera Offline"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className="p-3 border border-gray-700 rounded-lg hover:bg-[#233241]">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="font-bold text-xl bg-slate-700 p-1 px-2 rounded-md">
|
||||
<FontAwesomeIcon icon={faHardDrive} />
|
||||
</span>
|
||||
<p className="text-lg">{statusInfoItem}</p>
|
||||
</div>
|
||||
<p className="text-slate-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusItemCPU;
|
||||
@@ -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 (
|
||||
<div className="p-3 border border-gray-700 rounded-lg hover:bg-[#233241]">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="font-bold text-xl bg-slate-700 p-1 px-2 rounded-md">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
</span>
|
||||
<p className="text-lg">{description.toLowerCase().includes("local") && humanReadable(statusInfoItem)}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-slate-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusItemLocal;
|
||||
@@ -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 (
|
||||
<div className="p-3 border border-gray-700 rounded-lg hover:bg-[#233241]">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="font-bold text-xl bg-slate-700 p-1 px-2 rounded-md">
|
||||
<FontAwesomeIcon icon={faMicrochip} />
|
||||
</span>
|
||||
<p className="text-lg">{statusInfoItem}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-slate-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusItemThreads;
|
||||
@@ -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 (
|
||||
<div className="p-3 border border-gray-700 rounded-lg hover:bg-[#233241]">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="font-bold text-xl bg-slate-700 p-1 px-2 rounded-md">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
</span>
|
||||
|
||||
<p className="text-lg">{description.toLowerCase().includes("utc") && humanReadable(statusInfoItem)}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-slate-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusItemUTC;
|
||||
38
src/features/dashboard/components/SystemHealth.tsx
Normal file
38
src/features/dashboard/components/SystemHealth.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import Badge from "../../../ui/Badge";
|
||||
|
||||
type SystemHealthProps = {
|
||||
startTime: string;
|
||||
uptime: string;
|
||||
statuses: SystemHealthStatus[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const SystemHealth = ({ startTime, uptime, statuses, isLoading }: SystemHealthProps) => {
|
||||
if (isLoading) {
|
||||
return <span className="text-slate-500">Loading system health…</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-100 md:h-70">
|
||||
<div className="p-2 border-b border-gray-600 grid grid-cols-2 justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg">Start Time</h3> <span className="text-slate-300">{startTime}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg">Up Time</h3> <span className="text-slate-300">{uptime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{statuses?.map((status: SystemHealthStatus) => (
|
||||
<div className="border border-gray-700 p-4 rounded-md m-2 flex justify-between">
|
||||
<span>{status.id}</span> <Badge text={status.tags[0]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemHealth;
|
||||
22
src/features/dashboard/components/SystemOverview.tsx
Normal file
22
src/features/dashboard/components/SystemOverview.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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 (
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Health" refetch={query?.refetch} icon={faArrowsRotate} />
|
||||
<SystemHealth startTime={startTime} uptime={uptime} statuses={statuses} isLoading={isLoading} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemOverview;
|
||||
@@ -1,20 +1,24 @@
|
||||
import { useInfoSocket } from "../../../hooks/useInfoWebSocket";
|
||||
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 { stats } = useInfoSocket();
|
||||
const { data: stats } = useInfoSocket();
|
||||
|
||||
return (
|
||||
<Card className="p-4 w-[40%]">
|
||||
<CardHeader title="Overview" />
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Status" />
|
||||
{stats ? (
|
||||
<>
|
||||
<div>UTC: {stats["system-clock-utc"]}</div>
|
||||
<div>Local: {stats["system-clock-local"]}</div>
|
||||
<span>CPU: {stats["memory-cpu-status"]}</span>
|
||||
<span>Threads: {stats["thread-count"]}</span>
|
||||
</>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-4 col-span-2">
|
||||
<StatusItemUTC statusInfoItem={stats["system-clock-utc"]} description={"UTC Time"} />
|
||||
<StatusItemLocal statusInfoItem={stats["system-clock-local"]} description={"Local Time"} />
|
||||
<StatusItemCPU statusInfoItem={stats["memory-cpu-status"]} description={"CPU"} />
|
||||
<StatusItemThreads statusInfoItem={stats["thread-count"]} description={"Threads"} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">Loading system status…</span>
|
||||
)}
|
||||
|
||||
15
src/features/dashboard/hooks/useGetSystemHealth.ts
Normal file
15
src/features/dashboard/hooks/useGetSystemHealth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const fetchData = async () => {
|
||||
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();
|
||||
};
|
||||
|
||||
export const useGetSystemHealth = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ["fetchSystemData"],
|
||||
queryFn: fetchData,
|
||||
});
|
||||
return { query };
|
||||
};
|
||||
14
src/features/output/components/BearerTypeCard.tsx
Normal file
14
src/features/output/components/BearerTypeCard.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Card from "../../../ui/Card";
|
||||
import CardHeader from "../../../ui/CardHeader";
|
||||
import BearerTypeFields from "./BearerTypeFields";
|
||||
|
||||
const BearerTypeCard = () => {
|
||||
return (
|
||||
<Card className="p-4 h-50">
|
||||
<CardHeader title={"Bearer Type"} />
|
||||
<BearerTypeFields />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BearerTypeCard;
|
||||
32
src/features/output/components/BearerTypeFields.tsx
Normal file
32
src/features/output/components/BearerTypeFields.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Field } from "formik";
|
||||
|
||||
const BearerTypeFields = () => {
|
||||
return (
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="format" className="text-xl">
|
||||
Format
|
||||
</label>
|
||||
<Field
|
||||
as="select"
|
||||
name="format"
|
||||
id="format"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option key={"JSON"} value={"JSON"}>
|
||||
JSON
|
||||
</option>
|
||||
<option key={"BOF2"} value={"BOF2"}>
|
||||
BOF2
|
||||
</option>
|
||||
<option key={"UTMC"} value={"UTMC"}>
|
||||
UTMC
|
||||
</option>
|
||||
<option key={"FTP"} value={"FTP"}>
|
||||
FTP
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BearerTypeFields;
|
||||
24
src/features/output/components/ChannelCard.tsx
Normal file
24
src/features/output/components/ChannelCard.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
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, errors, touched } = useFormikContext<FormTypes>();
|
||||
|
||||
return (
|
||||
<Card className="p-4 h-150 md:h-full">
|
||||
<CardHeader title={`Channel (${values?.format})`} />
|
||||
<ChannelFields errors={errors} touched={touched} values={values} />
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full md:w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5"
|
||||
>
|
||||
{"Save Changes"}
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelCard;
|
||||
230
src/features/output/components/ChannelFields.tsx
Normal file
230
src/features/output/components/ChannelFields.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Field } from "formik";
|
||||
import type { FormTypes, InitialValuesFormErrors } from "../../../types/types";
|
||||
|
||||
type ChannelFieldsProps = {
|
||||
values: FormTypes;
|
||||
errors: InitialValuesFormErrors;
|
||||
touched: {
|
||||
connectTimeoutSeconds?: boolean | undefined;
|
||||
readTimeoutSeconds?: boolean | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const ChannelFields = ({ errors, touched, values }: ChannelFieldsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="backoffice" className="block mb-2 font-medium">
|
||||
Back Office URL
|
||||
</label>
|
||||
<Field
|
||||
name={"backOfficeURL"}
|
||||
type="text"
|
||||
id="backoffice"
|
||||
placeholder="https://www.backoffice.com"
|
||||
className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="username" className="block mb-2 font-medium">
|
||||
Username
|
||||
</label>
|
||||
<Field
|
||||
name={"username"}
|
||||
type="text"
|
||||
id="username"
|
||||
placeholder="Back office username"
|
||||
className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="password">Password</label>
|
||||
<Field
|
||||
name={"password"}
|
||||
type={"password"}
|
||||
id="password"
|
||||
placeholder="Back office password"
|
||||
className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="readTimeoutSeconds">Read Timeout Seconds</label>
|
||||
<Field
|
||||
name={"readTimeoutSeconds"}
|
||||
type="number"
|
||||
id="readTimeoutSeconds"
|
||||
placeholder="https://example.com"
|
||||
className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="connectTimeoutSeconds">Connect Timeout Seconds</label>
|
||||
<Field
|
||||
name={"connectTimeoutSeconds"}
|
||||
type="number"
|
||||
id="connectTimeoutSeconds"
|
||||
className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="overviewQuality">Overview quality and scale</label>
|
||||
<Field
|
||||
name={"overviewQuality"}
|
||||
as="select"
|
||||
id="overviewQuality"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"HIGH"}>High</option>
|
||||
<option value={"MEDIUM"}>Medium</option>
|
||||
<option value={"LOW"}>Low</option>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="cropSizeFactor">Crop Size Factor</label>
|
||||
<Field
|
||||
name={"cropSizeFactor"}
|
||||
as="select"
|
||||
id="cropSizeFactor"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"FULL"}>Full</option>
|
||||
<option value={"3/4"}>3/4</option>
|
||||
<option value={"1/2"}>1/2</option>
|
||||
<option value={"1/4"}>1/4</option>
|
||||
</Field>
|
||||
</div>
|
||||
{values.format.toLowerCase() === "utmc" && (
|
||||
<>
|
||||
<div className="border-b border-gray-500 my-3">
|
||||
<h2 className="font-bold">{values.format} Constants</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="SCID">Source ID / Camera ID</label>
|
||||
<Field
|
||||
name={"SCID"}
|
||||
type="text"
|
||||
id="SCID"
|
||||
placeholder="DEF345"
|
||||
className={`p-1.5 border ${
|
||||
errors.readTimeoutSeconds && touched.readTimeoutSeconds ? "border-red-500" : "border-gray-400 "
|
||||
} rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="timestampSource">Timestamp Source</label>
|
||||
<Field
|
||||
name={"timestampSource"}
|
||||
as="select"
|
||||
id="timestampSource"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"UTC"}>UTC</option>
|
||||
<option value={"local"}>Local</option>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="GPSFormat">GPS Format</label>
|
||||
<Field
|
||||
name={"GPSFormat"}
|
||||
as="select"
|
||||
id="GPSFormat"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"Minutes"}>Minutes</option>
|
||||
<option value={"Decimal Degrees"}>Decimal degrees</option>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{values.format?.toLowerCase() === "bof2" && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="border-b border-gray-500 my-3">
|
||||
<h2 className="font-bold">{values.format} Constants</h2>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="FFID">Feed ID / Force ID</label>
|
||||
<Field
|
||||
name={"FFID"}
|
||||
type="text"
|
||||
id="FFID"
|
||||
placeholder="ABC123"
|
||||
className={`p-1.5 border ${
|
||||
errors.readTimeoutSeconds && touched.readTimeoutSeconds ? "border-red-500" : "border-gray-400 "
|
||||
} rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="SCID">Source ID / Camera ID</label>
|
||||
<Field
|
||||
name={"SCID"}
|
||||
type="text"
|
||||
id="SCID"
|
||||
placeholder="DEF345"
|
||||
className={`p-1.5 border ${
|
||||
errors.readTimeoutSeconds && touched.readTimeoutSeconds ? "border-red-500" : "border-gray-400 "
|
||||
} rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="timestampSource">Timestamp Source</label>
|
||||
<Field
|
||||
name={"timestampSource"}
|
||||
as="select"
|
||||
id="timestampSource"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"UTC"}>UTC</option>
|
||||
<option value={"local"}>Local</option>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="GPSFormat">GPS Format</label>
|
||||
<Field
|
||||
name={"GPSFormat"}
|
||||
as="select"
|
||||
id="GPSFormat"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
|
||||
>
|
||||
<option value={"Minutes"}>Minutes</option>
|
||||
<option value={"Decimal Degrees"}>Decimal degrees</option>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="border-b border-gray-500 my-3">
|
||||
<h2 className="font-bold">{values.format} Lane ID Config</h2>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="LID1">Lane ID 1 (Camera A)</label>
|
||||
<Field
|
||||
name={"LID1"}
|
||||
type="text"
|
||||
id="LID1"
|
||||
placeholder="10"
|
||||
className={`p-1.5 border ${
|
||||
errors.readTimeoutSeconds && touched.readTimeoutSeconds ? "border-red-500" : "border-gray-400 "
|
||||
} rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label htmlFor="LID2">Lane ID 2 (Camera B)</label>
|
||||
<Field
|
||||
name={"LID2"}
|
||||
type="text"
|
||||
id="LID2"
|
||||
placeholder="20"
|
||||
className={`p-1.5 border ${
|
||||
errors.readTimeoutSeconds && touched.readTimeoutSeconds ? "border-red-500" : "border-gray-400 "
|
||||
} rounded-lg w-full md:w-60`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelFields;
|
||||
44
src/features/output/components/Output.tsx
Normal file
44
src/features/output/components/Output.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Formik, Form } from "formik";
|
||||
import BearerTypeCard from "./BearerTypeCard";
|
||||
import ChannelCard from "./ChannelCard";
|
||||
import type { FormTypes } from "../../../types/types";
|
||||
|
||||
const Output = () => {
|
||||
const handleSubmit = (values: FormTypes) => {
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
const inititalValues: FormTypes = {
|
||||
format: "JSON",
|
||||
enabled: true,
|
||||
backOfficeURL: "",
|
||||
username: "",
|
||||
password: "",
|
||||
connectTimeoutSeconds: Number(5),
|
||||
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 (
|
||||
<Formik initialValues={inititalValues} onSubmit={handleSubmit}>
|
||||
<Form className="grid grid-cols-1 md:grid-cols-2">
|
||||
<BearerTypeCard />
|
||||
<ChannelCard />
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default Output;
|
||||
@@ -1,9 +1,16 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import CameraGrid from "../features/cameras/components/CameraGrid";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
export const Route = createFileRoute('/baywatch')({
|
||||
export const Route = createFileRoute("/baywatch")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/baywatch"!</div>
|
||||
return (
|
||||
<div>
|
||||
<CameraGrid />
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export const Route = createFileRoute("/")({
|
||||
function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold"> Dashboard</h2>
|
||||
<DashboardGrid />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 <div>Hello "/output"!</div>
|
||||
return (
|
||||
<div>
|
||||
<Output />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,3 +12,48 @@ export type InfoBarData = {
|
||||
};
|
||||
|
||||
export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger";
|
||||
export type Region = {
|
||||
name: string;
|
||||
brushColour: string;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
24
src/ui/Badge.tsx
Normal file
24
src/ui/Badge.tsx
Normal file
@@ -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 (
|
||||
<span
|
||||
className={`text-sm font-medium inline-flex items-center px-2 py-0.5 rounded-md me-2
|
||||
border-2 space-x-2
|
||||
${text.toLowerCase() === "running" ? "bg-green-800 text-green-300 border-green-900" : "bg-red-800 text-red-300 border-red-900"} `}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} />}
|
||||
<span>{capitalize(lowerCaseWord)}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
@@ -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 (
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 mb-6 relative justify-between",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* {icon && <FontAwesomeIcon icon={icon} className="size-4" />} */}
|
||||
{status && <StatusIndicators status={status} />}
|
||||
<h2 className="text-xl">{title}</h2>
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<h2 className="flex flex-row text-xl items-center">
|
||||
{status && <StatusIndicators status={status} />}
|
||||
{title}
|
||||
</h2>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="size-4" onClick={refetch} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ const Header = () => {
|
||||
</Link>
|
||||
|
||||
<Link to="/baywatch" className="[&.active]:font-bold">
|
||||
Baywatch
|
||||
Cameras
|
||||
</Link>
|
||||
<Link to="/output" className="[&.active]:font-bold">
|
||||
Output
|
||||
|
||||
3
src/utils/utils.ts
Normal file
3
src/utils/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function capitalize(s?: string) {
|
||||
return s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
||||
}
|
||||
141
yarn.lock
141
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"
|
||||
@@ -1011,6 +1018,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"
|
||||
@@ -1283,7 +1300,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==
|
||||
@@ -1341,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"
|
||||
@@ -1591,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"
|
||||
@@ -1669,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"
|
||||
@@ -1726,12 +1769,19 @@ 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"
|
||||
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==
|
||||
@@ -1775,6 +1825,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"
|
||||
@@ -1864,11 +1919,28 @@ 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"
|
||||
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"
|
||||
@@ -1940,6 +2012,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"
|
||||
@@ -2027,6 +2104,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"
|
||||
@@ -2044,11 +2130,51 @@ react-dom@^19.2.0:
|
||||
dependencies:
|
||||
scheduler "^0.27.0"
|
||||
|
||||
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==
|
||||
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
@@ -2125,7 +2251,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==
|
||||
@@ -2162,6 +2288,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"
|
||||
@@ -2204,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==
|
||||
@@ -2229,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==
|
||||
|
||||
Reference in New Issue
Block a user