diff --git a/web/src/components/graph/CombinedStorageGraph.tsx b/web/src/components/graph/CombinedStorageGraph.tsx new file mode 100644 index 0000000000..1a72f8c888 --- /dev/null +++ b/web/src/components/graph/CombinedStorageGraph.tsx @@ -0,0 +1,204 @@ +import { useTheme } from "@/context/theme-provider"; +import { generateColors } from "@/utils/colorUtil"; +import { useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { getUnitSize } from "@/utils/storageUtil"; + +type CameraStorage = { + [key: string]: { + bandwidth: number; + usage: number; + usage_percent: number; + }; +}; + +type TotalStorage = { + used: number; + total: number; +}; + +type CombinedStorageGraphProps = { + graphId: string; + cameraStorage: CameraStorage; + totalStorage: TotalStorage; +}; +export function CombinedStorageGraph({ + graphId, + cameraStorage, + totalStorage, +}: CombinedStorageGraphProps) { + const { theme, systemTheme } = useTheme(); + + const entities = Object.keys(cameraStorage); + const colors = generateColors(entities.length); + + const series = entities.map((entity, index) => ({ + name: entity, + data: [(cameraStorage[entity].usage / totalStorage.total) * 100], + usage: cameraStorage[entity].usage, + bandwidth: cameraStorage[entity].bandwidth, + color: colors[index], // Assign the corresponding color + })); + + // Add the unused percentage to the series + series.push({ + name: "Unused Free Space", + data: [ + ((totalStorage.total - totalStorage.used) / totalStorage.total) * 100, + ], + usage: totalStorage.total - totalStorage.used, + bandwidth: 0, + color: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + }); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + stacked: true, + stackType: "100%", + }, + grid: { + show: false, + padding: { + bottom: -45, + top: -40, + left: -20, + right: -20, + }, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + plotOptions: { + bar: { + horizontal: true, + }, + }, + states: { + active: { + filter: { + type: "none", + }, + }, + hover: { + filter: { + type: "none", + }, + }, + }, + tooltip: { + enabled: false, + x: { + show: false, + }, + y: { + formatter: function (val, { seriesIndex }) { + if (series[seriesIndex]) { + const usage = series[seriesIndex].usage; + return `${getUnitSize(usage)} (${val.toFixed(2)}%)`; + } + }, + }, + theme: systemTheme || theme, + }, + xaxis: { + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + labels: { + formatter: function (val) { + return val + "%"; + }, + }, + min: 0, + max: 100, + }, + yaxis: { + show: false, + min: 0, + max: 100, + }, + } as ApexCharts.ApexOptions; + }, [graphId, systemTheme, theme, series]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + return ( +
+
+
+
+ {getUnitSize(totalStorage.used)} +
+
/
+
+ {getUnitSize(totalStorage.total)} +
+
+
+
+ +
+
+ + + + Camera + Storage Used + Percentage of Total Used + Bandwidth + + + + {series.map((item) => ( + + + {" "} +
+ {item.name.replaceAll("_", " ")} +
+ {getUnitSize(item.usage)} + {item.data[0].toFixed(2)}% + + {item.name === "Unused Free Space" + ? "—" + : `${getUnitSize(item.bandwidth)} / hour`} + +
+ ))} +
+
+
+
+ ); +} diff --git a/web/src/utils/colorUtil.ts b/web/src/utils/colorUtil.ts new file mode 100644 index 0000000000..c87cbf7cee --- /dev/null +++ b/web/src/utils/colorUtil.ts @@ -0,0 +1,36 @@ +// Utility function to generate colors based on a predefined palette with slight variations +export const generateColors = (numColors: number) => { + const palette = [ + "#008FFB", + "#00E396", + "#FEB019", + "#FF4560", + "#775DD0", + "#3F51B5", + "#03A9F4", + "#4CAF50", + "#F9CE1D", + "#FF9800", + ]; + + const colors = [...palette]; // Start with the predefined palette + + for (let i = palette.length; i < numColors; i++) { + const baseColor = palette[i % palette.length]; + // Modify the base color slightly by adjusting the brightness for additional colors + const factor = 1 + Math.floor(i / palette.length) * 0.1; + const modifiedColor = adjustColorBrightness(baseColor, factor); + colors.push(modifiedColor); + } + + return colors.slice(0, numColors); +}; + +const adjustColorBrightness = (color: string, factor: number) => { + const rgb = parseInt(color.slice(1), 16); + const r = Math.min(255, Math.floor(((rgb >> 16) & 0xff) * factor)); + const g = Math.min(255, Math.floor(((rgb >> 8) & 0xff) * factor)); + const b = Math.min(255, Math.floor((rgb & 0xff) * factor)); + + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +}; diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx index 8db327f1d9..8bfa8b51fd 100644 --- a/web/src/views/system/StorageMetrics.tsx +++ b/web/src/views/system/StorageMetrics.tsx @@ -1,6 +1,6 @@ +import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph"; import { StorageGraph } from "@/components/graph/StorageGraph"; import { FrigateStats } from "@/types/stats"; -import { getUnitSize } from "@/utils/storageUtil"; import { useMemo } from "react"; import useSWR from "swr"; @@ -74,22 +74,12 @@ export default function StorageMetrics({
Camera Storage
-
- {Object.keys(cameraStorage).map((camera) => ( -
-
-
{camera.replaceAll("_", " ")}
-
- {getUnitSize(cameraStorage[camera].bandwidth)} / hour -
-
- -
- ))} +
+
);