mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-29 04:04:16 -06:00
Replace individual storage graphs with combined graph (#13438)
* Replace individual storage graphs with combined graph * replace underscores with spaces * fix bar height
This commit is contained in:
parent
a8dcc87019
commit
6a0b5c3a3f
204
web/src/components/graph/CombinedStorageGraph.tsx
Normal file
204
web/src/components/graph/CombinedStorageGraph.tsx
Normal file
@ -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 (
|
||||||
|
<div className="flex w-full flex-col gap-2.5">
|
||||||
|
<div className="flex w-full items-center justify-between gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="text-xs text-primary">
|
||||||
|
{getUnitSize(totalStorage.used)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-primary">/</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{getUnitSize(totalStorage.total)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-5 overflow-hidden rounded-md">
|
||||||
|
<Chart type="bar" options={options} series={series} height="100%" />
|
||||||
|
</div>
|
||||||
|
<div className="custom-legend">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Camera</TableHead>
|
||||||
|
<TableHead>Storage Used</TableHead>
|
||||||
|
<TableHead>Percentage of Total Used</TableHead>
|
||||||
|
<TableHead>Bandwidth</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{series.map((item) => (
|
||||||
|
<TableRow key={item.name}>
|
||||||
|
<TableCell className="flex flex-row items-center gap-2 font-medium capitalize">
|
||||||
|
{" "}
|
||||||
|
<div
|
||||||
|
className="size-3 rounded-md"
|
||||||
|
style={{ backgroundColor: item.color }}
|
||||||
|
></div>
|
||||||
|
{item.name.replaceAll("_", " ")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getUnitSize(item.usage)}</TableCell>
|
||||||
|
<TableCell>{item.data[0].toFixed(2)}%</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.name === "Unused Free Space"
|
||||||
|
? "—"
|
||||||
|
: `${getUnitSize(item.bandwidth)} / hour`}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
36
web/src/utils/colorUtil.ts
Normal file
36
web/src/utils/colorUtil.ts
Normal file
@ -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)}`;
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
|
import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph";
|
||||||
import { StorageGraph } from "@/components/graph/StorageGraph";
|
import { StorageGraph } from "@/components/graph/StorageGraph";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { getUnitSize } from "@/utils/storageUtil";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -74,22 +74,12 @@ export default function StorageMetrics({
|
|||||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||||
Camera Storage
|
Camera Storage
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
<div className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
{Object.keys(cameraStorage).map((camera) => (
|
<CombinedStorageGraph
|
||||||
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
graphId={`single-storage`}
|
||||||
<div className="mb-5 flex flex-row items-center justify-between">
|
cameraStorage={cameraStorage}
|
||||||
<div className="capitalize">{camera.replaceAll("_", " ")}</div>
|
totalStorage={totalStorage}
|
||||||
<div className="text-xs text-muted-foreground">
|
/>
|
||||||
{getUnitSize(cameraStorage[camera].bandwidth)} / hour
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<StorageGraph
|
|
||||||
graphId={`${camera}-storage`}
|
|
||||||
used={cameraStorage[camera].usage}
|
|
||||||
total={totalStorage.used}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user