diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index 84014aaffe..2aaa15cd76 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -148,6 +148,10 @@ "unused": { "title": "Unused", "tips": "This value may not accurately represent the free space available to Frigate if you have other files stored on your drive beyond Frigate's recordings. Frigate does not track storage usage outside of its recordings." + }, + "subStream": { + "information": "Sub Stream Storage Information", + "tips": "Cameras that record a sub stream show their Original and Low quality usage separately, using the same names as the quality options in recording playback. Low quality recordings stay on disk until their retention period expires, so this breakdown can still appear for a camera after sub stream recording has been turned off." } } }, diff --git a/web/src/components/graph/CombinedStorageGraph.tsx b/web/src/components/graph/CombinedStorageGraph.tsx index 4279e73c2e..c1ce711944 100644 --- a/web/src/components/graph/CombinedStorageGraph.tsx +++ b/web/src/components/graph/CombinedStorageGraph.tsx @@ -16,24 +16,28 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { getUnitSize } from "@/utils/storageUtil"; +import { CameraStorage, StreamStorage } from "@/types/stats"; import { CiCircleAlert } from "react-icons/ci"; import { useTranslation } from "react-i18next"; -type CameraStorage = { - [key: string]: { - bandwidth: number; - usage: number; - usage_percent: number; - }; -}; - type TotalStorage = { used: number; camera: number; total: number; }; +type StorageRow = { + name: string; + usage: number; + bandwidth: number; + color: string; + streams?: { + main?: StreamStorage; + sub?: StreamStorage; + }; +}; + type CombinedStorageGraphProps = { graphId: string; cameraStorage: CameraStorage; @@ -44,46 +48,59 @@ export function CombinedStorageGraph({ cameraStorage, totalStorage, }: CombinedStorageGraphProps) { - const { t } = useTranslation(["views/system"]); + const { t } = useTranslation(["views/system", "components/player"]); const { theme, systemTheme } = useTheme(); + const isDark = (systemTheme || theme) == "dark"; - const entities = Object.keys(cameraStorage); - const colors = generateColors(entities.length); + const rows: StorageRow[] = useMemo(() => { + 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 - })); + return [ + ...entities.map((entity, index) => ({ + name: entity, + usage: cameraStorage[entity].usage ?? 0, + bandwidth: cameraStorage[entity].bandwidth, + color: colors[index], + streams: cameraStorage[entity].streams, + })), + { + name: "Other", + usage: totalStorage.used - totalStorage.camera, + bandwidth: 0, + color: isDark ? "#606060" : "#D5D5D5", + }, + { + name: "Unused", + usage: totalStorage.total - totalStorage.used, + bandwidth: 0, + color: isDark ? "#404040" : "#E5E5E5", + }, + ]; + }, [cameraStorage, totalStorage, isDark]); - // Add the unused percentage to the series - series.push({ - name: "Other", - data: [ - ((totalStorage.used - totalStorage.camera) / totalStorage.total) * 100, - ], - usage: totalStorage.used - totalStorage.camera, - bandwidth: 0, - color: (systemTheme || theme) == "dark" ? "#606060" : "#D5D5D5", - }); - series.push({ - name: "Unused", - data: [ - ((totalStorage.total - totalStorage.used) / totalStorage.total) * 100, - ], - usage: totalStorage.total - totalStorage.used, - bandwidth: 0, - color: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", - }); + const series = useMemo( + () => + rows.map((row) => ({ + name: row.name, + data: [(row.usage / totalStorage.total) * 100], + usage: row.usage, + color: row.color, + })), + [rows, totalStorage.total], + ); + + const hasSubStorage = useMemo( + () => rows.some((row) => row.streams?.sub), + [rows], + ); const options = useMemo(() => { return { chart: { id: graphId, - background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + background: isDark ? "#404040" : "#E5E5E5", selection: { enabled: false, }, @@ -164,7 +181,7 @@ export function CombinedStorageGraph({ max: 100, }, } as ApexCharts.ApexOptions; - }, [graphId, systemTheme, theme, series]); + }, [graphId, isDark, systemTheme, theme, series]); useEffect(() => { ApexCharts.exec(graphId, "updateOptions", options, true, true); @@ -185,6 +202,34 @@ export function CombinedStorageGraph({ [t], ); + const getStreamSplit = useCallback( + (row: StorageRow, field: keyof StreamStorage) => { + const sub = row.streams?.sub; + + if (!sub) { + return null; + } + + return ( +