Compare commits
10 Commits
enhancemen
...
enhancemen
| Author | SHA1 | Date | |
|---|---|---|---|
| 173b1d0e51 | |||
| 9b35deaf12 | |||
| 59bcb3c45b | |||
| 10590e5658 | |||
| f7964d4fc0 | |||
| 2a4afc7eae | |||
| 1810fc04b5 | |||
| d9594546a0 | |||
| dbadc7388c | |||
| 748be931ed |
@@ -9,7 +9,7 @@ const CameraGrid = () => {
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 md:grid-rows-5 max-h-screen">
|
||||
<div className="flex flex-col gap-4 p-4 md:grid md:grid-cols-5 md:grid-rows-5 md:max-h-screen md:gap-0 md:p-0">
|
||||
<VideoFeedGridPainter />
|
||||
<CameraSettings tabIndex={tabIndex} setTabIndex={setTabIndex} />
|
||||
<PlatePatch />
|
||||
|
||||
@@ -10,7 +10,7 @@ type CameraSettingsProps = {
|
||||
|
||||
const CameraSettings = ({ tabIndex, setTabIndex }: CameraSettingsProps) => {
|
||||
return (
|
||||
<Card className="p-4 col-span-2 row-span-5 col-start-3 md:col-span-3 md:row-span-5 overflow-auto">
|
||||
<Card className="p-4 w-full overflow-auto md:col-span-2 md:row-span-5 md:col-start-4 md:row-start-1">
|
||||
<Tabs
|
||||
selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none rounded-sm mb-1"
|
||||
className="react-tabs"
|
||||
|
||||
@@ -19,14 +19,6 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re
|
||||
dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } });
|
||||
};
|
||||
|
||||
const handleAddRegionClick = () => {
|
||||
const regionName = `Region ${regions.length + 1}`;
|
||||
dispatch({
|
||||
type: "ADD_NEW_REGION",
|
||||
payload: { cameraFeedID: cameraFeedID, regionName: regionName, brushColour: "#ffffff" },
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetRegion = () => {
|
||||
dispatch({
|
||||
type: "RESET_PAINTED_CELLS",
|
||||
@@ -34,13 +26,6 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveClick = () => {
|
||||
dispatch({
|
||||
type: "REMOVE_REGION",
|
||||
payload: { cameraFeedID: cameraFeedID, regionName: regions[selectedRegionIndex].name },
|
||||
});
|
||||
};
|
||||
|
||||
const handleModeChange = (newMode: string) => {
|
||||
dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: newMode } });
|
||||
};
|
||||
@@ -177,14 +162,6 @@ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: Re
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<div className=" mx-auto flex flex-row gap-4 mt-4">
|
||||
<button className="border border-blue-900 bg-blue-700 px-4 py-1 rounded-md" onClick={handleAddRegionClick}>
|
||||
Add Region
|
||||
</button>
|
||||
<button className="border border-red-900 bg-red-700 px-4 py-1 rounded-md" onClick={handleRemoveClick}>
|
||||
Remove Region
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2 border border-gray-600 rounded-lg flex flex-col md:col-span-2 h-50">
|
||||
|
||||
@@ -6,7 +6,7 @@ import SightingExitTable from "./SightingExitTable";
|
||||
|
||||
const PlatePatch = () => {
|
||||
return (
|
||||
<Card className="md:row-start-4 md:col-span-2 p-4 h-[190%]">
|
||||
<Card className="p-4 w-full md:w-[95%] md:row-start-4 md:col-span-3 md:h-[190%]">
|
||||
<CardHeader title="Entry / Exit" />
|
||||
<Tabs>
|
||||
<TabList>
|
||||
|
||||
@@ -93,7 +93,7 @@ const VideoFeedGridPainter = () => {
|
||||
const width = window.innerWidth;
|
||||
|
||||
const aspectRatio = BACKEND_WIDTH / BACKEND_HEIGHT;
|
||||
const newWidth = width * 0.39;
|
||||
const newWidth = width * 0.55;
|
||||
const newHeight = newWidth / aspectRatio;
|
||||
setStageSize({ width: newWidth, height: newHeight });
|
||||
};
|
||||
@@ -107,7 +107,7 @@ const VideoFeedGridPainter = () => {
|
||||
if (image === null || isloading) return <span className="text-slate-500">Loading Video feed…</span>;
|
||||
return (
|
||||
<div
|
||||
className={`mt-4.5 row-span-1 col-span-2 ${mode === "painter" ? "hover:cursor-crosshair" : ""} ${
|
||||
className={`w-full md:row-span-3 md:col-span-3 ${mode === "painter" ? "hover:cursor-crosshair" : ""} ${
|
||||
mode === "eraser" ? "hover:cursor-pointer" : ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import Card from "../../../ui/Card";
|
||||
import StatusIndicators from "../../../ui/StatusIndicators";
|
||||
import { capitalize } from "../../../utils/utils";
|
||||
import CameraStatusGridItem from "./CameraStatusGridItem";
|
||||
|
||||
type CameraStatusProps = {
|
||||
title: string;
|
||||
category: SystemHealthStatus[];
|
||||
};
|
||||
|
||||
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 (
|
||||
<Card className="p-4">
|
||||
<div className="border-b border-gray-600">
|
||||
<h3 className="text-lg flex flex-row items-center">
|
||||
{isAllGood ? <StatusIndicators status={"bg-green-500"} /> : <StatusIndicators status={"bg-amber-500"} />}
|
||||
{capitalize(title)}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-300">{isAllGood ? "All systems running" : "Some systems down"}</p>
|
||||
</div>
|
||||
{category && category?.length <= 0 ? (
|
||||
<p className=" text-gray-500">Loading Camera health...</p>
|
||||
) : (
|
||||
<div>
|
||||
<CameraStatusGridItem title={title} statusCategory={category} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraStatus;
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import { useGetSystemHealth } from "../hooks/useGetSystemHealth";
|
||||
import CameraStatus from "./CameraStatus";
|
||||
import SystemOverview from "./SystemOverview";
|
||||
import SystemStatusCard from "./SystemStatusCard";
|
||||
import CameraStatus from "./cameraStatus/CameraStatus";
|
||||
import SystemHealthCard from "./systemHealth/SystemHealthCard";
|
||||
import SystemStatusCard from "./systemStatus/SystemStatusCard";
|
||||
|
||||
const DashboardGrid = () => {
|
||||
const { query } = useGetSystemHealth();
|
||||
@@ -37,7 +37,7 @@ const DashboardGrid = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-rows-2 md:grid-cols-2">
|
||||
<SystemStatusCard />
|
||||
<SystemOverview
|
||||
<SystemHealthCard
|
||||
startTime={startTime}
|
||||
uptime={uptime}
|
||||
statuses={statuses}
|
||||
@@ -47,9 +47,9 @@ const DashboardGrid = () => {
|
||||
refetch={refetch}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:col-span-2 md:grid-cols-3">
|
||||
<CameraStatus title="Camera A" category={categoryA} />
|
||||
<CameraStatus title="Camera B" category={categoryB} />
|
||||
<CameraStatus title="Camera C" category={categoryC} />
|
||||
<CameraStatus title="Camera A" category={categoryA} isError={isError} />
|
||||
<CameraStatus title="Camera B" category={categoryB} isError={isError} />
|
||||
<CameraStatus title="Camera C" category={categoryC} isError={isError} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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 (
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Status" />
|
||||
{stats ? (
|
||||
<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>
|
||||
)}
|
||||
<div className="text-sm flex gap-4"></div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatusCard;
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { SystemHealthStatus } from "../../../../types/types";
|
||||
import Card from "../../../../ui/Card";
|
||||
import StatusIndicators from "../../../../ui/StatusIndicators";
|
||||
import { capitalize } from "../../../../utils/utils";
|
||||
import CameraStatusGridItem from "./CameraStatusGridItem";
|
||||
|
||||
type CameraStatusProps = {
|
||||
title: string;
|
||||
category: SystemHealthStatus[];
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
const CameraStatus = ({ title, category, isError }: CameraStatusProps) => {
|
||||
const isAllGood = category && category.length > 0 && category.every((status) => status.tags.includes("RUNNING"));
|
||||
// check if some are down
|
||||
// check if all are down
|
||||
//check if offline
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="border-b border-gray-600">
|
||||
<h3 className="text-lg flex flex-row items-center">
|
||||
{isError ? (
|
||||
<StatusIndicators status={"bg-red-500"} />
|
||||
) : isAllGood ? (
|
||||
<StatusIndicators status={"bg-green-500"} />
|
||||
) : (
|
||||
<StatusIndicators status={"bg-amber-500"} />
|
||||
)}
|
||||
{capitalize(title)}
|
||||
</h3>
|
||||
{isError ? (
|
||||
<p className="text-sm text-red-500">Error loading camera health.</p>
|
||||
) : isAllGood ? (
|
||||
<p className="text-sm text-green-500">All systems running</p>
|
||||
) : (
|
||||
<p className="text-sm text-amber-500">Some systems down</p>
|
||||
)}
|
||||
</div>
|
||||
{category && category?.length <= 0 ? (
|
||||
<p className=" text-gray-500">Loading Camera health...</p>
|
||||
) : (
|
||||
<div>
|
||||
<CameraStatusGridItem title={title} statusCategory={category} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraStatus;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import { capitalize } from "../../../utils/utils";
|
||||
import SystemHealthModal from "./systemHealthModal/SystemHealthModal";
|
||||
import type { SystemHealthStatus } from "../../../../types/types";
|
||||
import { capitalize } from "../../../../utils/utils";
|
||||
import SystemHealthModal from "../systemHealth/systemHealthModal/SystemHealthModal";
|
||||
|
||||
type CameraStatusGridItemProps = {
|
||||
title: string;
|
||||
@@ -2,7 +2,7 @@ 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";
|
||||
import SystemHealthModal from "../systemHealth/systemHealthModal/SystemHealthModal";
|
||||
|
||||
type StatusGridItemProps = {
|
||||
title: string;
|
||||
@@ -11,7 +11,8 @@ type StatusGridItemProps = {
|
||||
|
||||
const StatusGridItem = ({ title, statusCategory }: StatusGridItemProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isAllGood = statusCategory.every((status) => status.tags.includes("RUNNING"));
|
||||
const isAllGood =
|
||||
statusCategory && statusCategory.length > 0 && statusCategory.every((status) => status.tags.includes("RUNNING"));
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import StatusGridItem from "./statusGridItem/StatusGridItem";
|
||||
import type { SystemHealthStatus } from "../../../../types/types";
|
||||
import StatusGridItem from "../statusGridItem/StatusGridItem";
|
||||
|
||||
type SystemHealthProps = {
|
||||
startTime: string;
|
||||
@@ -38,7 +38,7 @@ const SystemHealth = ({ startTime, uptime, statuses, isLoading, isError, dateUpd
|
||||
return <span className="text-slate-500">Loading system health…</span>;
|
||||
}
|
||||
return (
|
||||
<div className="h-100 md:h-75 overflow-y-auto flex flex-col gap-4">
|
||||
<div className="relative h-100 md:h-75 overflow-y-auto flex flex-col gap-4">
|
||||
<div className="p-2 border-b border-gray-600 grid grid-cols-2 justify-between">
|
||||
<div className="flex flex-col border border-gray-600 p-4 rounded-lg mr-4 hover:bg-[#233241]">
|
||||
<h3 className="text-lg">Start Time</h3> <span className="text-slate-300">{startTime}</span>
|
||||
@@ -50,7 +50,7 @@ const SystemHealth = ({ startTime, uptime, statuses, isLoading, isError, dateUpd
|
||||
<div className="overflow-auto gap-4">
|
||||
<StatusGridItem title={"Modules"} statusCategory={categoryDefault} />
|
||||
</div>
|
||||
<div className="border-t border-gray-500">
|
||||
<div className="absolute bottom-0 left-0 border-t border-gray-500 w-full">
|
||||
<small className="italic text-gray-400 ">{`Last refeshed ${updatedDate}`}</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import Card from "../../../ui/Card";
|
||||
import CardHeader from "../../../ui/CardHeader";
|
||||
import Card from "../../../../ui/Card";
|
||||
import CardHeader from "../../../../ui/CardHeader";
|
||||
|
||||
import SystemHealth from "./SystemHealth";
|
||||
import type { SystemHealthStatus } from "../../../types/types";
|
||||
import type { SystemHealthStatus } from "../../../../types/types";
|
||||
|
||||
type SystemOverviewProps = {
|
||||
startTime: string;
|
||||
@@ -15,7 +15,7 @@ type SystemOverviewProps = {
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
const SystemOverview = ({
|
||||
const SystemHealthCard = ({
|
||||
startTime,
|
||||
uptime,
|
||||
statuses,
|
||||
@@ -39,4 +39,4 @@ const SystemOverview = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemOverview;
|
||||
export default SystemHealthCard;
|
||||
@@ -1,8 +1,8 @@
|
||||
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";
|
||||
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;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useDownloadLogFiles } from "../../../hooks/useDownloadLogFiles";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const DownloadLogButton = () => {
|
||||
const { downloadLogFilesQuery } = useDownloadLogFiles();
|
||||
const isLoading = downloadLogFilesQuery?.isFetching;
|
||||
|
||||
const handleDownloadClick = async () => {
|
||||
try {
|
||||
const blob = await downloadLogFilesQuery?.refetch().then((res) => res.data);
|
||||
if (!blob) {
|
||||
throw new Error("No log file data received");
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([blob]));
|
||||
const link = document.createElement("a");
|
||||
if (!link) {
|
||||
throw new Error("Failed to create download link");
|
||||
} else {
|
||||
link.href = url;
|
||||
link.setAttribute("download", "FlexiAI-0.log");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="p-3 border border-gray-700 rounded-lg hover:bg-[#233241] hover:cursor-pointer"
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
<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={faDownload} />
|
||||
</span>
|
||||
<p className="text-lg">{"Download Log Files"}</p>
|
||||
</div>
|
||||
<p className="text-slate-400 italic text-start">{isLoading ? "Downloading..." : "View logs"}</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadLogButton;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChartSimple } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type StatusReadsProps = {
|
||||
reads: {
|
||||
totalPending: number;
|
||||
totalActive: number;
|
||||
totalSent: number;
|
||||
totalReceived: number;
|
||||
totalLost: number;
|
||||
sanityCheck: boolean;
|
||||
sanityCheckFormula: string;
|
||||
};
|
||||
isReadsLoading?: boolean;
|
||||
};
|
||||
|
||||
const StatusReads = ({ reads, isReadsLoading }: StatusReadsProps) => {
|
||||
const totalPending = reads?.totalPending ?? 0;
|
||||
const totalActive = reads?.totalActive ?? 0;
|
||||
const totalSent = reads?.totalSent ?? 0;
|
||||
const totalLost = reads?.totalLost ?? 0;
|
||||
const totalReceived = reads?.totalReceived ?? 0;
|
||||
|
||||
if (isReadsLoading) {
|
||||
return <p className="text-slate-400">Loading reads…</p>;
|
||||
}
|
||||
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={faChartSimple} />
|
||||
</span>
|
||||
|
||||
<p className="text-lg">Reads</p>
|
||||
</div>
|
||||
|
||||
<div className="text-slate-400 mt-1">
|
||||
Pending: <span className="text-yellow-500">{totalPending}</span> | Active:{" "}
|
||||
<span className="text-cyan-500">{totalActive}</span> | Lost: <span className="text-red-500">{totalLost}</span>
|
||||
<br />
|
||||
Sent / Received: <span className="text-blue-500">{totalSent}</span> |{" "}
|
||||
<span className="text-green-500">{totalReceived}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusReads;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from "react";
|
||||
import { useInfoSocket } from "../../../../app/context/WebSocketContext";
|
||||
import Card from "../../../../ui/Card";
|
||||
import CardHeader from "../../../../ui/CardHeader";
|
||||
import DownloadLogButton from "./StatusItems/DownloadLogButton";
|
||||
import StatusItemLocal from "./StatusItems/StatusItemLocal";
|
||||
import StatusItemUTC from "./StatusItems/StatusItemUTC";
|
||||
import StatusReads from "./StatusItems/StatusReads";
|
||||
import { useGetStore } from "../../hooks/useGetStore";
|
||||
|
||||
const SystemStatusCard = () => {
|
||||
const { data: stats } = useInfoSocket();
|
||||
const { storeQuery } = useGetStore();
|
||||
|
||||
const reads = storeQuery?.data;
|
||||
const isReadsLoading = storeQuery?.isFetching;
|
||||
const isError = storeQuery?.isError || !storeQuery?.data;
|
||||
|
||||
useEffect(() => {
|
||||
storeQuery.refetch();
|
||||
}, [reads]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Status" />
|
||||
<span className="text-red-500">Error loading system status.</span>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Status" />
|
||||
{stats ? (
|
||||
<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"} />
|
||||
<DownloadLogButton />
|
||||
<StatusReads reads={reads} isReadsLoading={isReadsLoading} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">Loading system status…</span>
|
||||
)}
|
||||
<div className="text-sm flex gap-4"></div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatusCard;
|
||||
20
src/features/dashboard/hooks/useDownloadLogFiles.ts
Normal file
20
src/features/dashboard/hooks/useDownloadLogFiles.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CAMBASE } from "../../../utils/config";
|
||||
|
||||
const getDownloadLogFiles = async () => {
|
||||
const response = await fetch(`${CAMBASE}/LogView/download?filename=FlexiAI-0.log`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to download log files");
|
||||
}
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
export const useDownloadLogFiles = () => {
|
||||
const downloadLogFilesQuery = useQuery({
|
||||
queryKey: ["downloadLogFiles"],
|
||||
queryFn: getDownloadLogFiles,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
return { downloadLogFilesQuery };
|
||||
};
|
||||
19
src/features/dashboard/hooks/useGetStore.ts
Normal file
19
src/features/dashboard/hooks/useGetStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CAMBASE } from "../../../utils/config";
|
||||
|
||||
const getStoreData = async () => {
|
||||
const response = await fetch(`${CAMBASE}/Store0/diagnostics-json`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const useGetStore = () => {
|
||||
const storeQuery = useQuery({
|
||||
queryKey: ["storeData"],
|
||||
queryFn: getStoreData,
|
||||
// refetchInterval: 10 * 60 * 1000,
|
||||
});
|
||||
return { storeQuery };
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export const useGetSystemHealth = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ["fetchSystemData"],
|
||||
queryFn: fetchData,
|
||||
refetchInterval: 300000,
|
||||
});
|
||||
return { query };
|
||||
};
|
||||
|
||||
25
src/features/settings/components/Settings.tsx
Normal file
25
src/features/settings/components/Settings.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Tabs, Tab, TabList, TabPanel } from "react-tabs";
|
||||
import "react-tabs/style/react-tabs.css";
|
||||
import Card from "../../../ui/Card";
|
||||
import SystemConfig from "./SystemConfig";
|
||||
import CardHeader from "../../../ui/CardHeader";
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<div>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Systems</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<Card className="p-4">
|
||||
<CardHeader title="System Configuration" />
|
||||
<SystemConfig />
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
133
src/features/settings/components/SystemConfig.tsx
Normal file
133
src/features/settings/components/SystemConfig.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import { useSystemSettings } from "../hooks/useSystemSettings";
|
||||
import type { SystemSettings } from "../../../types/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const SystemConfig = () => {
|
||||
const { systemSettingsQuery, systemSettingsMutation } = useSystemSettings();
|
||||
|
||||
const timeZoneOptions = systemSettingsQuery?.data?.propLocalTimeZone?.accepted;
|
||||
const timeZoneOpts = timeZoneOptions?.split(",").map((option: string) => option.trim().replace(/\[|\]/g, ""));
|
||||
const timeSourceOptions = systemSettingsQuery?.data?.propTimeSource?.accepted;
|
||||
const timeSourceOpts = timeSourceOptions?.split(",").map((option: string) => option.trim().replace(/\[|\]/g, ""));
|
||||
const deviceName = systemSettingsQuery?.data?.propDeviceName?.value;
|
||||
const timeZone = systemSettingsQuery?.data?.propLocalTimeZone?.value;
|
||||
const SNTPServer = systemSettingsQuery?.data?.propSNTPServer?.value;
|
||||
const SNTPInterval = systemSettingsQuery?.data?.propSNTPIntervalMinutes?.value;
|
||||
const timeSource = systemSettingsQuery?.data?.propTimeSource?.value;
|
||||
// const primaryServer = systemSettingsQuery?.data?.propPrimaryDNSServer?.value;
|
||||
// const secondaryServer = systemSettingsQuery?.data?.propSecondaryDNSServer?.value;
|
||||
|
||||
const initialValues = {
|
||||
deviceName: deviceName ?? "",
|
||||
timeZone: timeZone ?? "",
|
||||
localTimeZone: timeZone ?? "",
|
||||
SNTPServer: SNTPServer ?? "",
|
||||
SNTPInterval: SNTPInterval ?? 60,
|
||||
SNTPIntervalMinutes: SNTPInterval ?? 60,
|
||||
primaryServer: "",
|
||||
secondaryServer: "",
|
||||
timeSource: timeSource ?? "",
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: SystemSettings) => {
|
||||
const result = await systemSettingsMutation.mutateAsync(values);
|
||||
console.log(result);
|
||||
if (result.id) {
|
||||
toast.success("System settings updated successfully");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit} enableReinitialize>
|
||||
<Form>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="deviceName">Device Name</label>
|
||||
<Field
|
||||
name="deviceName"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter device name"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="timeZone">Timezone</label>
|
||||
<Field
|
||||
name="timeZone"
|
||||
as="select"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs bg-[#253445] "
|
||||
autoComplete="off"
|
||||
>
|
||||
{timeZoneOpts?.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="timeSource">Time Source</label>
|
||||
<Field
|
||||
name="timeSource"
|
||||
as="select"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs bg-[#253445] "
|
||||
autoComplete="off"
|
||||
>
|
||||
{timeSourceOpts?.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="SNTPServer">SNTP Server</label>
|
||||
<Field
|
||||
name="SNTPServer"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter SNTP server"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="SNTPInterval">SNTP Interval</label>
|
||||
<Field
|
||||
name="SNTPInterval"
|
||||
type="number"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter SNTP interval"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="primaryServer">Primary DNS Server</label>
|
||||
<Field
|
||||
name="primaryServer"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter primary DNS server"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<label htmlFor="secondaryServer">Secondary DNS Server</label>
|
||||
<Field
|
||||
name="secondaryServer"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter secondary DNS server"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="px-4 py-2 bg-green-700 text-white rounded-lg">
|
||||
Save Settings
|
||||
</button>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemConfig;
|
||||
54
src/features/settings/hooks/useSystemSettings.ts
Normal file
54
src/features/settings/hooks/useSystemSettings.ts
Normal file
@@ -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 };
|
||||
};
|
||||
@@ -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 = () => (
|
||||
<>
|
||||
<Header />
|
||||
<main className="p-4 min-h-screen">
|
||||
<Outlet />
|
||||
<Toaster />
|
||||
</main>
|
||||
<Footer />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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,7 +9,6 @@ function RouteComponent() {
|
||||
return (
|
||||
<div>
|
||||
<CameraGrid />
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,5 @@ export const Route = createFileRoute("/")({
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<DashboardGrid />
|
||||
</div>
|
||||
);
|
||||
return <DashboardGrid />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Settings from "../features/settings/components/Settings";
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/settings"!</div>
|
||||
return (
|
||||
<div>
|
||||
<Settings />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,3 +175,13 @@ export type ColourDetectionPayload = {
|
||||
cameraFeedID: "A" | "B" | "C";
|
||||
regions: ColourData[];
|
||||
};
|
||||
|
||||
export type SystemSettings = {
|
||||
deviceName: string;
|
||||
localTimeZone: string;
|
||||
timeSource: string;
|
||||
SNTPServer: string;
|
||||
SNTPIntervalMinutes: number;
|
||||
primaryServer?: string;
|
||||
secondaryServer?: string;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ const ModalComponent = ({ isModalOpen, children, close }: ModalComponentProps) =
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={close}
|
||||
className="bg-[#1e2a38] p-6 rounded-lg shadow-lg w-[95%] mt-[2%] md:w-[40%] z-100 overflow-y-auto border border-gray-600"
|
||||
className="bg-[#1e2a38] p-6 rounded-lg shadow-lg w-[95%] mt-[2%] md:w-[40%] z-100 overflow-y-auto border border-gray-600 max-h-[90%]"
|
||||
overlayClassName="fixed inset-0 bg-[#1e2a38]/70 flex justify-center items-start z-100"
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -13,4 +13,12 @@ export default defineConfig({
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://100.115.125.56",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user