mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-25 10:20:32 -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 { 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({
|
||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||
Camera Storage
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
{Object.keys(cameraStorage).map((camera) => (
|
||||
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5 flex flex-row items-center justify-between">
|
||||
<div className="capitalize">{camera.replaceAll("_", " ")}</div>
|
||||
<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 className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<CombinedStorageGraph
|
||||
graphId={`single-storage`}
|
||||
cameraStorage={cameraStorage}
|
||||
totalStorage={totalStorage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user