From fbc0da6016d8d65b023ab4ef38b49dcf59a40cf5 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Thu, 4 Apr 2024 10:09:19 -0500
Subject: [PATCH 1/4] Optimistic UI (#10825)
* debounce motion only button
* implement custom hook
* optimistic severity toggle
* optimistic reviewed switch
---
.../components/filter/ReviewFilterGroup.tsx | 21 +++++----
web/src/hooks/use-optimistic-state.ts | 43 +++++++++++++++++++
web/src/views/events/EventView.tsx | 16 ++++---
3 files changed, 67 insertions(+), 13 deletions(-)
create mode 100644 web/src/hooks/use-optimistic-state.ts
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx
index 4d2e1db8e..9cfe7ed5f 100644
--- a/web/src/components/filter/ReviewFilterGroup.tsx
+++ b/web/src/components/filter/ReviewFilterGroup.tsx
@@ -2,7 +2,7 @@ import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr";
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -29,6 +29,7 @@ import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
import MobileReviewSettingsDrawer, {
DrawerFeatures,
} from "../overlay/MobileReviewSettingsDrawer";
+import useOptimisticState from "@/hooks/use-optimistic-state";
const REVIEW_FILTERS = [
"cameras",
@@ -361,13 +362,19 @@ function ShowReviewFilter({
showReviewed,
setShowReviewed,
}: ShowReviewedFilterProps) {
+ const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState(
+ showReviewed,
+ setShowReviewed,
+ );
return (
<>
setShowReviewed(showReviewed == 0 ? 1 : 0)}
+ checked={showReviewedSwitch == 1}
+ onCheckedChange={() =>
+ setShowReviewedSwitch(showReviewedSwitch == 0 ? 1 : 0)
+ }
/>
@@ -242,7 +248,7 @@ export default function EventView({
Date: Thu, 4 Apr 2024 09:43:54 -0600
Subject: [PATCH 2/4] Fix exports (#10824)
* Avoid crash from opening motion time right now
* Cleanup export margins
* Fix mobile filter
* Fix export
* Improve spacing
---
web/src/components/overlay/ExportDialog.tsx | 10 +++++-----
.../components/overlay/MobileReviewSettingsDrawer.tsx | 5 +----
web/src/views/events/EventView.tsx | 2 +-
web/src/views/events/RecordingView.tsx | 8 ++++++--
4 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx
index a9d597f83..5475dd00c 100644
--- a/web/src/components/overlay/ExportDialog.tsx
+++ b/web/src/components/overlay/ExportDialog.tsx
@@ -216,11 +216,11 @@ export function ExportContent({
Export
-
+
>
)}
onSelectTime(value as ExportOption)}
>
{EXPORT_OPTIONS.map((opt) => {
@@ -254,13 +254,13 @@ export function ExportContent({
/>
)}
setName(e.target.value)}
/>
- {isDesktop && }
+ {isDesktop && }
@@ -371,7 +371,7 @@ function CustomTimeSelector({
return (
{
const cameraConfig = config.cameras[camera];
cameraConfig.objects.track.forEach((label) => {
- if (!ATTRIBUTES.includes(label)) {
- labels.add(label);
- }
+ labels.add(label);
});
if (cameraConfig.audio.enabled_in_config) {
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index c57223c68..8a12ec347 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -851,7 +851,7 @@ function MotionReview({
onClick={() =>
onOpenRecording({
camera: camera.name,
- startTime: currentTime,
+ startTime: Math.min(currentTime, Date.now() / 1000 - 10),
severity: "significant_motion",
})
}
diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx
index c6cc866e9..dbcc37752 100644
--- a/web/src/views/events/RecordingView.tsx
+++ b/web/src/views/events/RecordingView.tsx
@@ -142,7 +142,7 @@ export function RecordingView({
);
useEffect(() => {
- if (scrubbing) {
+ if (scrubbing || exportRange) {
if (
currentTime > currentTimeRange.before + 60 ||
currentTime < currentTimeRange.after - 60
@@ -157,6 +157,8 @@ export function RecordingView({
controller.scrubToTimestamp(currentTime);
});
}
+ // we only want to seek when current time updates
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
currentTime,
scrubbing,
@@ -486,7 +488,9 @@ function Timeline({
setExportRange({ after: exportStart, before: exportEnd });
}
- }, [exportRange, exportStart, exportEnd, setExportRange, setCurrentTime]);
+ // we only want to update when the export parts change
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [exportStart, exportEnd, setExportRange, setCurrentTime]);
return (
Date: Thu, 4 Apr 2024 10:46:19 -0500
Subject: [PATCH 3/4] optimistic ui for mobile buttons too (#10827)
---
web/src/components/filter/ReviewFilterGroup.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx
index 9cfe7ed5f..f5b09836b 100644
--- a/web/src/components/filter/ReviewFilterGroup.tsx
+++ b/web/src/components/filter/ReviewFilterGroup.tsx
@@ -385,10 +385,10 @@ function ShowReviewFilter({
className="block md:hidden"
size="sm"
variant="secondary"
- onClick={() => setShowReviewed(showReviewed == 0 ? 1 : 0)}
+ onClick={() => setShowReviewedSwitch(showReviewedSwitch == 0 ? 1 : 0)}
>
>
@@ -664,10 +664,10 @@ function ShowMotionOnlyButton({
From 42559fa55df87d64b261377f7221d1596cf8b2d7 Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Thu, 4 Apr 2024 10:24:23 -0600
Subject: [PATCH 4/4] Storage Graphs (#10826)
* Rename graph
* Use separate view for general metrics
* Get storage graph formatted
* Show camera storage usage
* Cleanup ticks
* Remove storage link
* Add icons and frigate logo
* Undo
* Use optimistic state for metrics toggle
* Use optimistic state and skeletons for loading
---
frigate/stats/emitter.py | 7 +-
web/src/App.tsx | 2 -
web/src/components/graph/SystemGraph.tsx | 121 ++++-
.../components/settings/GeneralSettings.tsx | 13 -
web/src/pages/Storage.tsx | 245 ----------
web/src/pages/System.tsx | 440 +----------------
web/src/views/system/GeneralMetrics.tsx | 441 ++++++++++++++++++
web/src/views/system/StorageMetrics.tsx | 92 ++++
8 files changed, 676 insertions(+), 685 deletions(-)
delete mode 100644 web/src/pages/Storage.tsx
create mode 100644 web/src/views/system/GeneralMetrics.tsx
create mode 100644 web/src/views/system/StorageMetrics.tsx
diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py
index 2c29548e9..a90be5271 100644
--- a/frigate/stats/emitter.py
+++ b/frigate/stats/emitter.py
@@ -16,7 +16,8 @@ from frigate.types import StatsTrackingTypes
logger = logging.getLogger(__name__)
-MAX_STATS_POINTS = 120
+MAX_STATS_POINTS = 80
+FREQUENCY_STATS_POINTS = 15
class StatsEmitter(threading.Thread):
@@ -70,9 +71,9 @@ class StatsEmitter(threading.Thread):
def run(self) -> None:
time.sleep(10)
for counter in itertools.cycle(
- range(int(self.config.mqtt.stats_interval / 10))
+ range(int(self.config.mqtt.stats_interval / FREQUENCY_STATS_POINTS))
):
- if self.stop_event.wait(10):
+ if self.stop_event.wait(FREQUENCY_STATS_POINTS):
break
logger.debug("Starting stats collection")
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 98385fc20..2f0853200 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -11,7 +11,6 @@ import { Suspense, lazy } from "react";
const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
const Export = lazy(() => import("@/pages/Export"));
-const Storage = lazy(() => import("@/pages/Storage"));
const SubmitPlus = lazy(() => import("@/pages/SubmitPlus"));
const ConfigEditor = lazy(() => import("@/pages/ConfigEditor"));
const System = lazy(() => import("@/pages/System"));
@@ -38,7 +37,6 @@ function App() {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx
index ec750dceb..a6ca219b0 100644
--- a/web/src/components/graph/SystemGraph.tsx
+++ b/web/src/components/graph/SystemGraph.tsx
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo } from "react";
import Chart from "react-apexcharts";
import useSWR from "swr";
-type SystemGraphProps = {
+type ThresholdBarGraphProps = {
graphId: string;
name: string;
unit: string;
@@ -13,14 +13,14 @@ type SystemGraphProps = {
updateTimes: number[];
data: ApexAxisChartSeries;
};
-export default function SystemGraph({
+export function ThresholdBarGraph({
graphId,
name,
unit,
threshold,
updateTimes,
data,
-}: SystemGraphProps) {
+}: ThresholdBarGraphProps) {
const { data: config } = useSWR("config", {
revalidateOnFocus: false,
});
@@ -87,8 +87,12 @@ export default function SystemGraph({
tooltip: {
theme: systemTheme || theme,
},
+ markers: {
+ size: 0,
+ },
xaxis: {
- tickAmount: 6,
+ tickAmount: 4,
+ tickPlacement: "on",
labels: {
formatter: formatTime,
},
@@ -104,7 +108,7 @@ export default function SystemGraph({
min: 0,
max: threshold.warning + 10,
},
- };
+ } as ApexCharts.ApexOptions;
}, [graphId, threshold, systemTheme, theme, formatTime]);
useEffect(() => {
@@ -124,3 +128,110 @@ export default function SystemGraph({
);
}
+
+const getUnitSize = (MB: number) => {
+ if (isNaN(MB) || MB < 0) return "Invalid number";
+ if (MB < 1024) return `${MB} MiB`;
+ if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`;
+
+ return `${(MB / 1048576).toFixed(2)} TiB`;
+};
+
+type StorageGraphProps = {
+ graphId: string;
+ used: number;
+ total: number;
+};
+export function StorageGraph({ graphId, used, total }: StorageGraphProps) {
+ const { theme, systemTheme } = useTheme();
+
+ const options = useMemo(() => {
+ return {
+ chart: {
+ id: graphId,
+ background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5",
+ selection: {
+ enabled: false,
+ },
+ toolbar: {
+ show: false,
+ },
+ zoom: {
+ enabled: false,
+ },
+ },
+ grid: {
+ show: false,
+ padding: {
+ bottom: -40,
+ top: -60,
+ left: -20,
+ right: 0,
+ },
+ },
+ legend: {
+ show: false,
+ },
+ dataLabels: {
+ enabled: false,
+ },
+ plotOptions: {
+ bar: {
+ horizontal: true,
+ },
+ },
+ tooltip: {
+ theme: systemTheme || theme,
+ },
+ xaxis: {
+ axisBorder: {
+ show: false,
+ },
+ axisTicks: {
+ show: false,
+ },
+ labels: {
+ show: false,
+ },
+ },
+ yaxis: {
+ show: false,
+ min: 0,
+ max: 100,
+ },
+ };
+ }, [graphId, systemTheme, theme]);
+
+ useEffect(() => {
+ ApexCharts.exec(graphId, "updateOptions", options, true, true);
+ }, [graphId, options]);
+
+ return (
+
+
+
+
+ {getUnitSize(used)}
+
+
/
+
+ {getUnitSize(total)}
+
+
+
+ {Math.round((used / total) * 100)}%
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/components/settings/GeneralSettings.tsx b/web/src/components/settings/GeneralSettings.tsx
index e225257f3..b13051ea4 100644
--- a/web/src/components/settings/GeneralSettings.tsx
+++ b/web/src/components/settings/GeneralSettings.tsx
@@ -1,7 +1,6 @@
import {
LuActivity,
LuGithub,
- LuHardDrive,
LuLifeBuoy,
LuList,
LuMoon,
@@ -138,18 +137,6 @@ export default function GeneralSettings({ className }: GeneralSettings) {
System
-
-
-