import { useTheme } from "@/context/theme-provider"; import { generateColors } from "@/utils/colorUtil"; import { useCallback, useEffect, useMemo } from "react"; import Chart from "react-apexcharts"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Popover, PopoverContent, 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 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; totalStorage: TotalStorage; }; export function CombinedStorageGraph({ graphId, cameraStorage, totalStorage, }: CombinedStorageGraphProps) { const { t } = useTranslation(["views/system", "components/player"]); const { theme, systemTheme } = useTheme(); const isDark = (systemTheme || theme) == "dark"; const rows: StorageRow[] = useMemo(() => { const entities = Object.keys(cameraStorage); const colors = generateColors(entities.length); 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]); 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: isDark ? "#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, isDark, systemTheme, theme, series]); useEffect(() => { ApexCharts.exec(graphId, "updateOptions", options, true, true); }, [graphId, options]); // convenience const getItemTitle = useCallback( (name: string) => { if (name == "Unused") { return t("storage.cameraStorage.unused.title"); } else if (name == "Other") { return t("label.other", { ns: "common" }); } else { return name.replaceAll("_", " "); } }, [t], ); const getStreamSplit = useCallback( (row: StorageRow, field: keyof StreamStorage) => { const mainValue = row.streams?.main?.[field]; const subValue = row.streams?.sub?.[field]; // omit the split entirely when either side is unknown, rather than // showing a zero that would read as "this stream costs nothing" if (mainValue == null || subValue == null) { return null; } return (
{t("quality.main", { ns: "components/player" })} {" "} {getUnitSize(mainValue)}
{t("quality.sub", { ns: "components/player" })} {" "} {getUnitSize(subValue)}
); }, [t], ); return (
{getUnitSize(totalStorage.camera)}
/
{getUnitSize(totalStorage.total)}
{t("storage.cameraStorage.camera")}
{t("storage.cameraStorage.storageUsed")} {hasSubStorage && (
{t("storage.cameraStorage.subStream.tips")}
)}
{t("storage.cameraStorage.percentageOfTotalUsed")} {t("storage.cameraStorage.bandwidth")}
{rows.map((row) => { const isAggregate = row.name == "Unused" || row.name == "Other"; return (
{getItemTitle(row.name)} {isAggregate && (
{t("storage.cameraStorage.unused.tips")}
)}
{getUnitSize(row.usage)} {getStreamSplit(row, "usage")} {((row.usage / totalStorage.total) * 100).toFixed(2)}% {isAggregate ? "—" : `${getUnitSize(row.bandwidth)} / hour`} {getStreamSplit(row, "bandwidth")}
); })}
); }