diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx
index 5fafb6a7d..36f90ce71 100644
--- a/web/src/components/graph/SystemGraph.tsx
+++ b/web/src/components/graph/SystemGraph.tsx
@@ -125,6 +125,108 @@ export function ThresholdBarGraph({
);
}
-export function StorageGraph() {
-
+const getUnitSize = (MB: number) => {
+ if (isNaN(MB) || MB < 0) return "Invalid number";
+ if (MB < 1024) return `${MB} MiB`;
+ if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`;
+
+ return `${(MB / 1048576).toFixed(2)} TiB`;
+};
+
+type StorageGraphProps = {
+ graphId: string;
+ used: number;
+ total: number;
+ data: ApexAxisChartSeries;
+};
+export function StorageGraph({
+ graphId,
+ used,
+ total,
+ data,
+}: StorageGraphProps) {
+ const { theme, systemTheme } = useTheme();
+
+ const options = useMemo(() => {
+ return {
+ chart: {
+ id: graphId,
+ background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5",
+ selection: {
+ enabled: false,
+ },
+ toolbar: {
+ show: false,
+ },
+ zoom: {
+ enabled: false,
+ },
+ },
+ grid: {
+ show: false,
+ padding: {
+ bottom: -40,
+ top: -60,
+ left: -20,
+ right: 0,
+ },
+ },
+ legend: {
+ show: false,
+ },
+ dataLabels: {
+ enabled: false,
+ },
+ plotOptions: {
+ bar: {
+ horizontal: true,
+ },
+ },
+ tooltip: {
+ theme: systemTheme || theme,
+ },
+ xaxis: {
+ axisBorder: {
+ show: false,
+ },
+ axisTicks: {
+ show: false,
+ },
+ labels: {
+ show: false,
+ },
+ },
+ yaxis: {
+ show: false,
+ min: 0,
+ max: 100,
+ },
+ };
+ }, [graphId, systemTheme, theme]);
+
+ useEffect(() => {
+ ApexCharts.exec(graphId, "updateOptions", options, true, true);
+ }, [graphId, options]);
+
+ return (
+
+
+
+
+ {getUnitSize(used)}
+
+
/
+
+ {getUnitSize(total)}
+
+
+
+ {Math.round((used / total) * 100)}%
+
+
+
+
+
+
+ );
}
diff --git a/web/src/pages/System.tsx b/web/src/pages/System.tsx
index 5fe6a7e16..af81543be 100644
--- a/web/src/pages/System.tsx
+++ b/web/src/pages/System.tsx
@@ -5,6 +5,7 @@ import TimeAgo from "@/components/dynamic/TimeAgo";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { isDesktop } from "react-device-detect";
import GeneralMetrics from "@/views/system/GeneralMetrics";
+import StorageMetrics from "@/views/system/StorageMetrics";
const metrics = ["general", "storage", "cameras"] as const;
type SystemMetric = (typeof metrics)[number];
@@ -70,6 +71,12 @@ function System() {
setLastUpdated={setLastUpdated}
/>
)}
+ {page == "storage" && (
+
+ )}
);
}
diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx
new file mode 100644
index 000000000..d428aba4f
--- /dev/null
+++ b/web/src/views/system/StorageMetrics.tsx
@@ -0,0 +1,59 @@
+import { StorageGraph } from "@/components/graph/SystemGraph";
+import { useMemo } from "react";
+import useSWR from "swr";
+
+type CameraStorage = {
+ [key: string]: {
+ bandwidth: number;
+ usage: number;
+ usage_percent: number;
+ };
+};
+
+type StorageMetricsProps = {
+ lastUpdated: number;
+ setLastUpdated: (last: number) => void;
+};
+export default function StorageMetrics({
+ lastUpdated,
+ setLastUpdated,
+}: StorageMetricsProps) {
+ const { data: storage } = useSWR("recordings/storage");
+
+ const totalStorage = useMemo(() => {
+ if (!storage) {
+ return undefined;
+ }
+
+ const totalStorage = {
+ total: 0,
+ };
+
+ Object.values(storage).forEach((cam) => (totalStorage.total += cam.usage));
+
+ return totalStorage;
+ }, [storage]);
+
+ if (!totalStorage) {
+ return;
+ }
+
+ return (
+
+
+ General Storage
+
+
+
+ );
+}