mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-11 01:02:48 +03:00
* bump backend and frontend dependencies
Bumps every outdated dependency that passed testing, with the code changes each one needs.
- peewee 4.5 and peewee_migrate 2.3: use SqliteDatabase in place of the removed SqliteExtDatabase, and fix types for peewee's bundled stubs
- mypy 2.4.0: remove unused type ignores and add the annotations mypy 2 needs
- pandas 3.0: convert the motion activity index with as_unit("s"), because pandas 3 infers second resolution for whole-second timestamps and the old // 10**9 returned 1
- transformers 5.19: load Jina tokenizers from the saved directory with trust_remote_code off and use CLIPImageProcessor, since v5 removed CLIPFeatureExtractor and remote code pulled in torch
- tensorflow-cpu 2.21: pin protobuf to 7.36 in the TensorRT amd64 image, because the old 3.20.3 pin was installed over TensorFlow and broke its import
- pydantic 2.13 and google-genai 2.29: regenerate the API spec
- ruff 0.16: set an explicit lint select, since 0.16 widened the default rules
- react-router-dom 7: set useTransitions={false} on BrowserRouter to keep v6 update timing, which useSearchEffect relies on
- typescript 6: drop baseUrl and add node types in tsconfig
- radix: update to the latest release and remove the slot patch, which upstream now includes
- apexcharts 7.8: guard the tooltip formatter against null values
- cryptography 50, joserfc 1.7, pywebpush 2.5, openai 3.26, aiohttp, uvicorn, onvif-zeep-async, scipy, framer-motion 14, react-day-picker 10 and other minor and patch bumps
* silence httpx2 request logging
* redownload an incomplete jina tokenizer
An interrupted tokenizer download left the Hugging Face cache directory without the saved tokenizer files, and since the download is skipped whenever that directory exists, every restart failed to load the tokenizer. The Jina constructors now remove a tokenizer directory that has no tokenizer_config.json so the download runs again.
350 lines
9.9 KiB
TypeScript
350 lines
9.9 KiB
TypeScript
import { useTheme } from "@/context/theme-provider";
|
|
import { generateColors } from "@/utils/colorUtil";
|
|
import { useCallback, useEffect, useMemo } from "react";
|
|
import ApexCharts from "apexcharts";
|
|
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, opts) {
|
|
const entry = opts ? series[opts.seriesIndex] : undefined;
|
|
if (entry) {
|
|
return `${getUnitSize(entry.usage)} (${(val ?? 0).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 (
|
|
<div className="text-xs text-primary-variant">
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t("quality.main", { ns: "components/player" })}
|
|
</span>{" "}
|
|
{getUnitSize(mainValue)}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t("quality.sub", { ns: "components/player" })}
|
|
</span>{" "}
|
|
{getUnitSize(subValue)}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
[t],
|
|
);
|
|
|
|
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.camera)}
|
|
</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>{t("storage.cameraStorage.camera")}</TableHead>
|
|
<TableHead>
|
|
<div className="flex flex-row items-center gap-1">
|
|
{t("storage.cameraStorage.storageUsed")}
|
|
{hasSubStorage && (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
className="focus:outline-none"
|
|
aria-label={t(
|
|
"storage.cameraStorage.subStream.information",
|
|
)}
|
|
>
|
|
<CiCircleAlert
|
|
className="size-5"
|
|
aria-label={t(
|
|
"storage.cameraStorage.subStream.information",
|
|
)}
|
|
/>
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80">
|
|
<div className="space-y-2">
|
|
{t("storage.cameraStorage.subStream.tips")}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead>
|
|
{t("storage.cameraStorage.percentageOfTotalUsed")}
|
|
</TableHead>
|
|
<TableHead>{t("storage.cameraStorage.bandwidth")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((row) => {
|
|
const isAggregate = row.name == "Unused" || row.name == "Other";
|
|
|
|
return (
|
|
<TableRow key={row.name}>
|
|
<TableCell className="flex flex-row items-center gap-2 font-medium smart-capitalize">
|
|
<div
|
|
className="size-3 rounded-md"
|
|
style={{ backgroundColor: row.color }}
|
|
></div>
|
|
{getItemTitle(row.name)}
|
|
{isAggregate && (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
className="focus:outline-none"
|
|
aria-label={t(
|
|
"storage.cameraStorage.unusedStorageInformation",
|
|
)}
|
|
>
|
|
<CiCircleAlert
|
|
className="size-5"
|
|
aria-label={t(
|
|
"storage.cameraStorage.unusedStorageInformation",
|
|
)}
|
|
/>
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80">
|
|
<div className="space-y-2">
|
|
{t("storage.cameraStorage.unused.tips")}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{getUnitSize(row.usage)}
|
|
{getStreamSplit(row, "usage")}
|
|
</TableCell>
|
|
<TableCell>
|
|
{((row.usage / totalStorage.total) * 100).toFixed(2)}%
|
|
</TableCell>
|
|
<TableCell>
|
|
{isAggregate ? "—" : `${getUnitSize(row.bandwidth)} / hour`}
|
|
{getStreamSplit(row, "bandwidth")}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|