From 21782dddd2a6073069fa5b3fa557ffdb51bd23d7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 16 Dec 2025 15:10:48 -0700 Subject: [PATCH] Export filter UI (#21322) * Get started on export filters * implement basic filter * Implement filtering and adjust api * Improve filter handling * Improve navigation * Cleanup * handle scrolling --- frigate/api/export.py | 7 +- .../components/filter/ExportFilterGroup.tsx | 67 +++++++++++++++++++ web/src/types/export.ts | 10 +++ web/vite.config.ts | 2 +- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 web/src/components/filter/ExportFilterGroup.tsx diff --git a/frigate/api/export.py b/frigate/api/export.py index 812a1b4b2..c2cf66a34 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -62,7 +62,7 @@ router = APIRouter(tags=[Tags.export]) def get_exports( allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), export_case_id: Optional[str] = None, - camera: Optional[List[str]] = Query(default=None), + cameras: Optional[str] = Query(default="all"), start_date: Optional[float] = None, end_date: Optional[float] = None, ): @@ -74,8 +74,9 @@ def get_exports( else: query = query.where(Export.export_case == export_case_id) - if camera: - filtered_cameras = [c for c in camera if c in allowed_cameras] + if cameras and cameras != "all": + requested = set(cameras.split(",")) + filtered_cameras = list(requested.intersection(allowed_cameras)) if not filtered_cameras: return JSONResponse(content=[]) query = query.where(Export.camera << filtered_cameras) diff --git a/web/src/components/filter/ExportFilterGroup.tsx b/web/src/components/filter/ExportFilterGroup.tsx new file mode 100644 index 000000000..c5fe4f33c --- /dev/null +++ b/web/src/components/filter/ExportFilterGroup.tsx @@ -0,0 +1,67 @@ +import { cn } from "@/lib/utils"; +import { + DEFAULT_EXPORT_FILTERS, + ExportFilter, + ExportFilters, +} from "@/types/export"; +import { CamerasFilterButton } from "./CamerasFilterButton"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; +import { useMemo } from "react"; +import { FrigateConfig } from "@/types/frigateConfig"; +import useSWR from "swr"; + +type ExportFilterGroupProps = { + className: string; + filters?: ExportFilters[]; + filter?: ExportFilter; + onUpdateFilter: (filter: ExportFilter) => void; +}; +export default function ExportFilterGroup({ + className, + filter, + filters = DEFAULT_EXPORT_FILTERS, + onUpdateFilter, +}: ExportFilterGroupProps) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const allowedCameras = useAllowedCameras(); + + const filterValues = useMemo( + () => ({ + cameras: allowedCameras, + }), + [allowedCameras], + ); + + const groups = useMemo(() => { + if (!config) { + return []; + } + + return Object.entries(config.camera_groups).sort( + (a, b) => a[1].order - b[1].order, + ); + }, [config]); + + return ( +
+ {filters.includes("cameras") && ( + { + onUpdateFilter({ ...filter, cameras: newCameras }); + }} + /> + )} +
+ ); +} diff --git a/web/src/types/export.ts b/web/src/types/export.ts index 1184becf0..c606855f2 100644 --- a/web/src/types/export.ts +++ b/web/src/types/export.ts @@ -21,3 +21,13 @@ export type DeleteClipType = { file: string; exportName: string; }; + +// filtering + +const EXPORT_FILTERS = ["cameras"] as const; +export type ExportFilters = (typeof EXPORT_FILTERS)[number]; +export const DEFAULT_EXPORT_FILTERS: ExportFilters[] = ["cameras"]; + +export type ExportFilter = { + cameras?: string[]; +}; diff --git a/web/vite.config.ts b/web/vite.config.ts index cb1a580bf..148048995 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import monacoEditorPlugin from "vite-plugin-monaco-editor"; -const proxyHost = process.env.PROXY_HOST || "localhost:5000"; +const proxyHost = process.env.PROXY_HOST || "1ocalhost:5000"; // https://vitejs.dev/config/ export default defineConfig({