From 3bf2a496e1b32fcbdaabe2ff048575c6c8431bc0 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 28 Feb 2024 15:23:56 -0700 Subject: [PATCH] Fix linter and fix lint issues (#10141) --- web/.eslintrc.cjs | 11 +- web/package-lock.json | 2 +- web/package.json | 2 +- web/postcss.config.js | 2 +- web/src/api/baseUrl.ts | 8 +- web/src/api/ws.tsx | 34 +++--- web/src/components/Statusbar.tsx | 2 +- .../camera/AutoUpdatingCameraImage.tsx | 12 +- web/src/components/camera/CameraImage.tsx | 2 +- .../components/camera/DebugCameraImage.tsx | 10 +- .../components/camera/ResizingCameraImage.tsx | 15 ++- web/src/components/dynamic/NewReviewData.tsx | 13 ++- web/src/components/dynamic/TimeAgo.tsx | 4 +- .../components/filter/ReviewFilterGroup.tsx | 14 +-- .../image/AnimatedEventThumbnail.tsx | 4 +- web/src/components/navigation/Bottombar.tsx | 26 ++--- .../overlay/TimelineDataOverlay.tsx | 26 ++--- .../components/player/DynamicVideoPlayer.tsx | 41 ++++--- web/src/components/player/JSMpegPlayer.tsx | 9 +- web/src/components/player/LivePlayer.tsx | 6 +- web/src/components/player/MsePlayer.tsx | 35 ++++-- .../player/PreviewThumbnailPlayer.tsx | 34 ++++-- web/src/components/player/VideoPlayer.tsx | 14 ++- web/src/components/player/WebRTCPlayer.tsx | 10 +- .../timeline/EventReviewTimeline.tsx | 23 +++- web/src/components/timeline/EventSegment.tsx | 24 ++-- web/src/context/theme-provider.tsx | 2 + web/src/env.ts | 2 +- web/src/hooks/resize-observer.ts | 4 +- web/src/hooks/use-api-filter.ts | 3 + web/src/hooks/use-camera-activity.ts | 6 +- web/src/hooks/use-camera-live-mode.ts | 8 +- web/src/hooks/use-event-utils.ts | 20 ++-- web/src/hooks/use-handle-dragging.ts | 20 ++-- web/src/hooks/use-keyboard-listener.tsx | 8 +- web/src/hooks/use-overlay-state.tsx | 4 +- web/src/hooks/use-persistence.ts | 4 +- web/src/hooks/use-segment-utils.ts | 24 ++-- web/src/lib/formatTimeAgo.ts | 40 +++---- web/src/lib/utils.ts | 6 +- web/src/main.tsx | 12 +- web/src/pages/ConfigEditor.tsx | 14 +-- web/src/pages/Events.tsx | 30 +++-- web/src/pages/Export.tsx | 14 +-- web/src/pages/Live.tsx | 8 +- web/src/pages/Logs.tsx | 2 +- web/src/pages/Storage.tsx | 10 +- web/src/pages/UIPlayground.tsx | 21 +--- web/src/pages/site-navigation.ts | 7 +- web/src/types/filter.ts | 4 +- web/src/types/frigateConfig.ts | 4 +- web/src/types/live.ts | 2 +- web/src/types/playback.ts | 5 +- web/src/types/preview.ts | 14 +-- web/src/types/record.ts | 6 +- web/src/types/stats.ts | 104 +++++++++--------- web/src/types/timeline.ts | 56 +++++----- web/src/types/ws.ts | 2 +- web/src/utils/dateUtil.ts | 8 +- web/src/utils/timelineUtil.tsx | 2 +- web/src/views/events/DesktopRecordingView.tsx | 14 ++- web/src/views/events/EventView.tsx | 17 +-- web/vite.config.ts | 55 ++++----- 63 files changed, 527 insertions(+), 418 deletions(-) diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index f22e1f8b6..deba5f544 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -1,12 +1,13 @@ module.exports = { root: true, - env: { browser: true, es2021: true, "vitest-globals/env": true }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", - "plugin:prettier", + "plugin:vitest-globals/recommended", + "plugin:prettier/recommended", ], + env: { browser: true, es2021: true, "vitest-globals/env": true }, ignorePatterns: ["dist", ".eslintrc.cjs"], parser: "@typescript-eslint/parser", parserOptions: { @@ -21,9 +22,11 @@ module.exports = { version: 27, }, }, - ignorePatterns: ["*.d.ts"], - plugins: ["react-refresh"], + ignorePatterns: ["*.d.ts", "/src/components/ui/*"], + plugins: ["react-hooks", "react-refresh"], rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, diff --git a/web/package-lock.json b/web/package-lock.json index 5b279adc3..8a59b62f8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -78,7 +78,7 @@ "@vitejs/plugin-react-swc": "^3.6.0", "@vitest/coverage-v8": "^1.0.0", "autoprefixer": "^10.4.16", - "eslint": "^8.53.0", + "eslint": "^8.55.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.6.0", "eslint-plugin-prettier": "^5.0.1", diff --git a/web/package.json b/web/package.json index 8eae592c4..01716f940 100644 --- a/web/package.json +++ b/web/package.json @@ -83,7 +83,7 @@ "@vitejs/plugin-react-swc": "^3.6.0", "@vitest/coverage-v8": "^1.0.0", "autoprefixer": "^10.4.16", - "eslint": "^8.53.0", + "eslint": "^8.55.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.6.0", "eslint-plugin-prettier": "^5.0.1", diff --git a/web/postcss.config.js b/web/postcss.config.js index 2e7af2b7f..2aa7205d4 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/web/src/api/baseUrl.ts b/web/src/api/baseUrl.ts index a47355e97..fb7faa62f 100644 --- a/web/src/api/baseUrl.ts +++ b/web/src/api/baseUrl.ts @@ -1,7 +1,7 @@ declare global { - interface Window { - baseUrl?: any; - } + interface Window { + baseUrl?: string; } +} -export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`; \ No newline at end of file +export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || "/"}`; diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 5c01e0341..a6638a94d 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -9,12 +9,12 @@ import { createContainer } from "react-tracked"; type Update = { topic: string; - payload: any; + payload: unknown; retain: boolean; }; type WsState = { - [topic: string]: any; + [topic: string]: unknown; }; type useValueReturn = [WsState, (update: Update) => void]; @@ -47,6 +47,8 @@ function useValue(): useValueReturn { }); setWsState({ ...wsState, ...cameraStates }); + // we only want this to run initially when the config is loaded + // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); // ws handler @@ -72,7 +74,7 @@ function useValue(): useValueReturn { }); } }, - [readyState, sendJsonMessage] + [readyState, sendJsonMessage], ); return [wsState, setState]; @@ -91,14 +93,14 @@ export function useWs(watchTopic: string, publishTopic: string) { const value = { payload: state[watchTopic] || null }; const send = useCallback( - (payload: any, retain = false) => { + (payload: unknown, retain = false) => { sendJsonMessage({ topic: publishTopic || watchTopic, payload, retain, }); }, - [sendJsonMessage, watchTopic, publishTopic] + [sendJsonMessage, watchTopic, publishTopic], ); return { value, send }; @@ -112,7 +114,7 @@ export function useDetectState(camera: string): { value: { payload }, send, } = useWs(`${camera}/detect/state`, `${camera}/detect/set`); - return { payload, send }; + return { payload: payload as ToggleableSetting, send }; } export function useRecordingsState(camera: string): { @@ -123,7 +125,7 @@ export function useRecordingsState(camera: string): { value: { payload }, send, } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`); - return { payload, send }; + return { payload: payload as ToggleableSetting, send }; } export function useSnapshotsState(camera: string): { @@ -134,7 +136,7 @@ export function useSnapshotsState(camera: string): { value: { payload }, send, } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`); - return { payload, send }; + return { payload: payload as ToggleableSetting, send }; } export function useAudioState(camera: string): { @@ -145,7 +147,7 @@ export function useAudioState(camera: string): { value: { payload }, send, } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); - return { payload, send }; + return { payload: payload as ToggleableSetting, send }; } export function usePtzCommand(camera: string): { @@ -156,7 +158,7 @@ export function usePtzCommand(camera: string): { value: { payload }, send, } = useWs(`${camera}/ptz`, `${camera}/ptz`); - return { payload, send }; + return { payload: payload as string, send }; } export function useRestart(): { @@ -167,40 +169,40 @@ export function useRestart(): { value: { payload }, send, } = useWs("restart", "restart"); - return { payload, send }; + return { payload: payload as string, send }; } export function useFrigateEvents(): { payload: FrigateEvent } { const { value: { payload }, } = useWs("events", ""); - return { payload: JSON.parse(payload) }; + return { payload: JSON.parse(payload as string) }; } export function useFrigateReviews(): { payload: FrigateReview } { const { value: { payload }, } = useWs("reviews", ""); - return { payload: JSON.parse(payload) }; + return { payload: JSON.parse(payload as string) }; } export function useFrigateStats(): { payload: FrigateStats } { const { value: { payload }, } = useWs("stats", ""); - return { payload: JSON.parse(payload) }; + return { payload: JSON.parse(payload as string) }; } export function useMotionActivity(camera: string): { payload: string } { const { value: { payload }, } = useWs(`${camera}/motion`, ""); - return { payload }; + return { payload: payload as string }; } export function useAudioActivity(camera: string): { payload: number } { const { value: { payload }, } = useWs(`${camera}/audio/rms`, ""); - return { payload }; + return { payload: payload as number }; } diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index 63f9ed891..186b919a7 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; -export default function Statusbar({}) { +export default function Statusbar() { const { data: initialStats } = useSWR("stats", { revalidateOnFocus: false, }); diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx index d51768be7..2f2005d9c 100644 --- a/web/src/components/camera/AutoUpdatingCameraImage.tsx +++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -3,7 +3,7 @@ import CameraImage from "./CameraImage"; type AutoUpdatingCameraImageProps = { camera: string; - searchParams?: {}; + searchParams?: URLSearchParams; showFps?: boolean; className?: string; reloadInterval?: number; @@ -13,7 +13,7 @@ const MIN_LOAD_TIMEOUT_MS = 200; export default function AutoUpdatingCameraImage({ camera, - searchParams = "", + searchParams = undefined, showFps = true, className, reloadInterval = MIN_LOAD_TIMEOUT_MS, @@ -35,6 +35,8 @@ export default function AutoUpdatingCameraImage({ setTimeoutId(undefined); } }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps }, [reloadInterval]); const handleLoad = useCallback(() => { @@ -53,9 +55,11 @@ export default function AutoUpdatingCameraImage({ () => { setKey(Date.now()); }, - loadTime > reloadInterval ? 1 : reloadInterval - ) + loadTime > reloadInterval ? 1 : reloadInterval, + ), ); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps }, [key, setFps]); return ( diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index df08a2ea8..b4c32e014 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -7,7 +7,7 @@ type CameraImageProps = { className?: string; camera: string; onload?: () => void; - searchParams?: {}; + searchParams?: string; }; export default function CameraImage({ diff --git a/web/src/components/camera/DebugCameraImage.tsx b/web/src/components/camera/DebugCameraImage.tsx index 34588aa06..a92ce7a51 100644 --- a/web/src/components/camera/DebugCameraImage.tsx +++ b/web/src/components/camera/DebugCameraImage.tsx @@ -24,25 +24,25 @@ export default function DebugCameraImage({ const [showSettings, setShowSettings] = useState(false); const [options, setOptions] = usePersistence( `${cameraConfig?.name}-feed`, - emptyObject + emptyObject, ); const handleSetOption = useCallback( (id: string, value: boolean) => { const newOptions = { ...options, [id]: value }; setOptions(newOptions); }, - [options] + [options, setOptions], ); const searchParams = useMemo( () => new URLSearchParams( Object.keys(options || {}).reduce((memo, key) => { - //@ts-ignore we know this is correct + //@ts-expect-error we know this is correct memo.push([key, options[key] === true ? "1" : "0"]); return memo; - }, []) + }, []), ), - [options] + [options], ); const handleToggleSettings = useCallback(() => { setShowSettings(!showSettings); diff --git a/web/src/components/camera/ResizingCameraImage.tsx b/web/src/components/camera/ResizingCameraImage.tsx index 18ffabcfb..d098b46d5 100644 --- a/web/src/components/camera/ResizingCameraImage.tsx +++ b/web/src/components/camera/ResizingCameraImage.tsx @@ -8,7 +8,7 @@ type CameraImageProps = { className?: string; camera: string; onload?: (event: Event) => void; - searchParams?: {}; + searchParams?: string; stretch?: boolean; // stretch to fit width fitAspect?: number; // shrink to fit height }; @@ -58,10 +58,17 @@ export default function CameraImage({ } return 100; - }, [availableWidth, aspectRatio, height, stretch]); + }, [ + availableWidth, + aspectRatio, + containerHeight, + fitAspect, + height, + stretch, + ]); const scaledWidth = useMemo( () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), - [scaledHeight, aspectRatio, scrollBarWidth] + [scaledHeight, aspectRatio, scrollBarWidth], ); const img = useMemo(() => new Image(), []); @@ -74,7 +81,7 @@ export default function CameraImage({ } onload && onload(event); }, - [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef] + [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef], ); useEffect(() => { diff --git a/web/src/components/dynamic/NewReviewData.tsx b/web/src/components/dynamic/NewReviewData.tsx index 2a743a924..b1b094e08 100644 --- a/web/src/components/dynamic/NewReviewData.tsx +++ b/web/src/components/dynamic/NewReviewData.tsx @@ -2,7 +2,7 @@ import { useFrigateReviews } from "@/api/ws"; import { ReviewSeverity } from "@/types/review"; import { Button } from "../ui/button"; import { LuRefreshCcw } from "react-icons/lu"; -import { MutableRefObject, useEffect, useState } from "react"; +import { MutableRefObject, useEffect, useMemo, useState } from "react"; type NewReviewDataProps = { className: string; @@ -18,7 +18,8 @@ export default function NewReviewData({ }: NewReviewDataProps) { const { payload: review } = useFrigateReviews(); - const [reviewId, setReviewId] = useState(""); + const startCheckTs = useMemo(() => Date.now() / 1000, []); + const [reviewTs, setReviewTs] = useState(startCheckTs); const [hasUpdate, setHasUpdate] = useState(false); useEffect(() => { @@ -27,15 +28,15 @@ export default function NewReviewData({ } if (review.type == "end" && review.review.severity == severity) { - setReviewId(review.review.id); + setReviewTs(review.review.start_time); } - }, [review]); + }, [review, severity]); useEffect(() => { - if (reviewId != "") { + if (reviewTs > startCheckTs) { setHasUpdate(true); } - }, [reviewId]); + }, [startCheckTs, reviewTs]); return (
diff --git a/web/src/components/dynamic/TimeAgo.tsx b/web/src/components/dynamic/TimeAgo.tsx index df2b70285..a9993db8a 100644 --- a/web/src/components/dynamic/TimeAgo.tsx +++ b/web/src/components/dynamic/TimeAgo.tsx @@ -91,7 +91,7 @@ const TimeAgo: FunctionComponent = ({ } else { return 3600000; // refresh every hour } - }, [currentTime, manualRefreshInterval]); + }, [currentTime, manualRefreshInterval, time]); useEffect(() => { const intervalId: NodeJS.Timeout = setInterval(() => { @@ -102,7 +102,7 @@ const TimeAgo: FunctionComponent = ({ const timeAgoValue = useMemo( () => timeAgo({ time, currentTime, ...rest }), - [currentTime, rest] + [currentTime, rest, time], ); return {timeAgoValue}; diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 1eb169c59..0da125795 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -54,7 +54,7 @@ export default function ReviewFilterGroup({ cameras: Object.keys(config?.cameras || {}), labels: Object.values(allLabels || {}), }), - [config, allLabels] + [config, allLabels], ); // handle updating filters @@ -67,7 +67,7 @@ export default function ReviewFilterGroup({ before: day == undefined ? undefined : getEndOfDayTimestamp(day), }); }, - [onUpdateFilter] + [filter, onUpdateFilter], ); return ( @@ -111,7 +111,7 @@ function CamerasFilterButton({ updateCameraFilter, }: CameraFilterButtonProps) { const [currentCameras, setCurrentCameras] = useState( - selectedCameras + selectedCameras, ); return ( @@ -200,7 +200,7 @@ function CalendarFilterButton({ }, []); const selectedDate = useFormattedTimestamp( day == undefined ? 0 : day?.getTime() / 1000, - "%b %-d" + "%b %-d", ); return ( @@ -273,7 +273,7 @@ function GeneralFilterButton({ diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 839c0c544..87b9f53db 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,5 +1,6 @@ import useApiFilter from "@/hooks/use-api-filter"; import useOverlayState from "@/hooks/use-overlay-state"; +import { Preview } from "@/types/preview"; import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import DesktopRecordingView from "@/views/events/DesktopRecordingView"; import EventView from "@/views/events/EventView"; @@ -24,6 +25,8 @@ export default function Events() { const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { setSize(1); setReviewFilter(newFilter); + // we don't want this updating + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // review paging @@ -41,9 +44,9 @@ export default function Events() { before: Math.floor(reviewSearchParams["before"]), after: Math.floor(reviewSearchParams["after"]), }; - }, [reviewSearchParams]); + }, [last24Hours, reviewSearchParams]); - const reviewSegmentFetcher = useCallback((key: any) => { + const reviewSegmentFetcher = useCallback((key: Array | string) => { const [path, params] = Array.isArray(key) ? key : [key, undefined]; return axios.get(path, { params }).then((res) => res.data); }, []); @@ -74,7 +77,7 @@ export default function Events() { }; return ["review", params]; }, - [reviewSearchParams, last24Hours] + [reviewSearchParams, last24Hours], ); const { @@ -90,7 +93,7 @@ export default function Events() { const isDone = useMemo( () => (reviewPages?.at(-1)?.length ?? 0) < API_LIMIT, - [reviewPages] + [reviewPages], ); const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); @@ -103,7 +106,7 @@ export default function Events() { if ( !reviewPages || reviewPages.length == 0 || - reviewPages.at(-1)!!.length == 0 + reviewPages.at(-1)?.length == 0 ) { return undefined; } @@ -111,7 +114,7 @@ export default function Events() { const startDate = new Date(); startDate.setMinutes(0, 0, 0); - const endDate = new Date(reviewPages.at(-1)!!.at(-1)!!.end_time); + const endDate = new Date(reviewPages.at(-1)?.at(-1)?.end_time || 0); endDate.setHours(0, 0, 0, 0); return { start: startDate.getTime() / 1000, @@ -122,7 +125,7 @@ export default function Events() { previewTimes ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` : null, - { revalidateOnFocus: false } + { revalidateOnFocus: false }, ); // review status @@ -156,11 +159,11 @@ export default function Events() { return newData; }, - { revalidate: false, populateCache: true } + { revalidate: false, populateCache: true }, ); } }, - [updateSegments] + [updateSegments], ); // selected items @@ -176,7 +179,7 @@ export default function Events() { const allReviews = reviewPages.flat(); const selectedReview = allReviews.find( - (item) => item.id == selectedReviewId + (item) => item.id == selectedReviewId, ); if (!selectedReview) { @@ -186,12 +189,15 @@ export default function Events() { return { selected: selectedReview, cameraSegments: allReviews.filter( - (seg) => seg.camera == selectedReview.camera + (seg) => seg.camera == selectedReview.camera, ), cameraPreviews: allPreviews?.filter( - (seg) => seg.camera == selectedReview.camera + (seg) => seg.camera == selectedReview.camera, ), }; + + // previews will not update after item is selected + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedReviewId, reviewPages]); if (selectedData) { diff --git a/web/src/pages/Export.tsx b/web/src/pages/Export.tsx index 878e1a8aa..07abfd268 100644 --- a/web/src/pages/Export.tsx +++ b/web/src/pages/Export.tsx @@ -51,7 +51,7 @@ function Export() { const { data: config } = useSWR("config"); const { data: exports, mutate } = useSWR( "exports/", - (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data) + (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data), ); // Export States @@ -96,7 +96,7 @@ function Export() { parseInt(startHour), parseInt(startMin), parseInt(startSec), - 0 + 0, ); const start = startDate.getTime() / 1000; const endDate = new Date((date.to || date.from).getTime()); @@ -117,7 +117,7 @@ function Export() { if (response.status == 200) { toast.success( "Successfully started export. View the file in the /exports folder.", - { position: "top-center" } + { position: "top-center" }, ); } @@ -127,7 +127,7 @@ function Export() { if (error.response?.data?.message) { toast.error( `Failed to start export: ${error.response.data.message}`, - { position: "top-center" } + { position: "top-center" }, ); } else { toast.error(`Failed to start export: ${error.message}`, { @@ -148,7 +148,7 @@ function Export() { mutate(); } }); - }, [deleteClip]); + }, [deleteClip, mutate]); return (
@@ -156,7 +156,7 @@ function Export() { setDeleteClip(undefined)} + onOpenChange={() => setDeleteClip(undefined)} > @@ -176,7 +176,7 @@ function Export() { setSelectedClip(undefined)} + onOpenChange={() => setSelectedClip(undefined)} > diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index 951edd633..8e3d58193 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -20,7 +20,7 @@ function Live() { const [layout, setLayout] = usePersistence<"grid" | "list">( "live-layout", - isDesktop ? "grid" : "list" + isDesktop ? "grid" : "list", ); // recent events @@ -40,7 +40,7 @@ function Live() { updateEvents(); return; } - }, [eventUpdate]); + }, [eventUpdate, updateEvents]); const events = useMemo(() => { if (!allEvents) { @@ -76,7 +76,7 @@ function Live() { return () => { removeEventListener("visibilitychange", visibilityListener); }; - }, []); + }, [visibilityListener]); return (
@@ -123,7 +123,7 @@ function Live() { > {cameras.map((camera) => { let grow; - let aspectRatio = camera.detect.width / camera.detect.height; + const aspectRatio = camera.detect.width / camera.detect.height; if (aspectRatio > 2) { grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`; } else if (aspectRatio < 1) { diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 591fe30f9..c09f71ab0 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -57,7 +57,7 @@ function Logs() { // no op } }, - [setEndVisible] + [setEndVisible], ); return ( diff --git a/web/src/pages/Storage.tsx b/web/src/pages/Storage.tsx index b3e99cf18..4f4c62b4e 100644 --- a/web/src/pages/Storage.tsx +++ b/web/src/pages/Storage.tsx @@ -47,7 +47,7 @@ function Storage() { service["storage"]["/media/frigate/recordings"]["total"] != service["storage"]["/media/frigate/clips"]["total"] ); - }, service); + }, [service]); const getUnitSize = (MB: number) => { if (isNaN(MB) || MB < 0) return "Invalid number"; @@ -106,12 +106,12 @@ function Storage() { {getUnitSize( - service["storage"]["/media/frigate/recordings"]["used"] + service["storage"]["/media/frigate/recordings"]["used"], )} {getUnitSize( - service["storage"]["/media/frigate/recordings"]["total"] + service["storage"]["/media/frigate/recordings"]["total"], )} @@ -120,12 +120,12 @@ function Storage() { Snapshots {getUnitSize( - service["storage"]["/media/frigate/clips"]["used"] + service["storage"]["/media/frigate/clips"]["used"], )} {getUnitSize( - service["storage"]["/media/frigate/clips"]["total"] + service["storage"]["/media/frigate/clips"]["total"], )} diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 3fc94ac56..003787b9c 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -2,7 +2,6 @@ import { useMemo, useRef, useState } from "react"; import Heading from "@/components/ui/heading"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; -import { Event } from "@/types/event"; import ActivityIndicator from "@/components/ui/activity-indicator"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import { ReviewData, ReviewSegment, ReviewSeverity } from "@/types/review"; @@ -84,19 +83,9 @@ function UIPlayground() { const contentRef = useRef(null); const [mockEvents, setMockEvents] = useState([]); const [handlebarTime, setHandlebarTime] = useState( - Math.floor(Date.now() / 1000) - 15 * 60 + Math.floor(Date.now() / 1000) - 15 * 60, ); - const recentTimestamp = useMemo(() => { - const now = new Date(); - now.setMinutes(now.getMinutes() - 240); - return now.getTime() / 1000; - }, []); - const { data: events } = useSWR([ - "events", - { limit: 10, after: recentTimestamp }, - ]); - useMemo(() => { const initialEvents = Array.from({ length: 50 }, generateRandomEvent); setMockEvents(initialEvents); @@ -108,16 +97,16 @@ function UIPlayground() { return Math.min(...mockEvents.map((event) => event.start_time)); } return Math.floor(Date.now() / 1000); // Default to current time if no events - }, [events]); + }, [mockEvents]); const minimapEndTime = useMemo(() => { if (mockEvents && mockEvents.length > 0) { return Math.max( - ...mockEvents.map((event) => event.end_time ?? event.start_time) + ...mockEvents.map((event) => event.end_time ?? event.start_time), ); } return Math.floor(Date.now() / 1000); // Default to current time if no events - }, [events]); + }, [mockEvents]); const [zoomLevel, setZoomLevel] = useState(0); const [zoomSettings, setZoomSettings] = useState({ @@ -134,7 +123,7 @@ function UIPlayground() { function handleZoomIn() { const nextZoomLevel = Math.min( possibleZoomLevels.length - 1, - zoomLevel + 1 + zoomLevel + 1, ); setZoomLevel(nextZoomLevel); setZoomSettings(possibleZoomLevels[nextZoomLevel]); diff --git a/web/src/pages/site-navigation.ts b/web/src/pages/site-navigation.ts index fab2bbb3c..0af342e8f 100644 --- a/web/src/pages/site-navigation.ts +++ b/web/src/pages/site-navigation.ts @@ -1,9 +1,4 @@ -import { - LuConstruction, - LuFileUp, - LuFlag, - LuVideo, -} from "react-icons/lu"; +import { LuConstruction, LuFileUp, LuFlag, LuVideo } from "react-icons/lu"; export const navbarLinks = [ { diff --git a/web/src/types/filter.ts b/web/src/types/filter.ts index e1c6c6cfc..722057fa1 100644 --- a/web/src/types/filter.ts +++ b/web/src/types/filter.ts @@ -1 +1,3 @@ -type FilterType = { [searchKey: string]: any }; +// allow any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type FilterType = { [searchKey: string]: any }; diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 5149264d8..a72e7f240 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -199,7 +199,7 @@ export interface CameraConfig { coordinates: string; filters: Record; inertia: number; - objects: any[]; + objects: string[]; }; }; } @@ -383,7 +383,7 @@ export interface FrigateConfig { }; telemetry: { - network_interfaces: any[]; + network_interfaces: string[]; stats: { amd_gpu_stats: boolean; intel_gpu_stats: boolean; diff --git a/web/src/types/live.ts b/web/src/types/live.ts index 65911ac3d..7e413894f 100644 --- a/web/src/types/live.ts +++ b/web/src/types/live.ts @@ -1 +1 @@ -export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug"; \ No newline at end of file +export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug"; diff --git a/web/src/types/playback.ts b/web/src/types/playback.ts index 07867b0f9..f787ba2db 100644 --- a/web/src/types/playback.ts +++ b/web/src/types/playback.ts @@ -1,4 +1,7 @@ -type DynamicPlayback = { +import { Preview } from "./preview"; +import { Recording } from "./record"; + +export type DynamicPlayback = { recordings: Recording[]; playbackUri: string; preview: Preview | undefined; diff --git a/web/src/types/preview.ts b/web/src/types/preview.ts index 7b1eb8e7d..e9bd12185 100644 --- a/web/src/types/preview.ts +++ b/web/src/types/preview.ts @@ -1,7 +1,7 @@ -type Preview = { - camera: string; - src: string; - type: string; - start: number; - end: number; - }; \ No newline at end of file +export type Preview = { + camera: string; + src: string; + type: string; + start: number; + end: number; +}; diff --git a/web/src/types/record.ts b/web/src/types/record.ts index a6979aea8..4522c1814 100644 --- a/web/src/types/record.ts +++ b/web/src/types/record.ts @@ -1,4 +1,4 @@ -type Recording = { +export type Recording = { id: string; camera: string; start_time: number; @@ -11,7 +11,7 @@ type Recording = { dBFS: number; }; -type RecordingSegment = { +export type RecordingSegment = { id: string; start_time: number; end_time: number; @@ -21,7 +21,7 @@ type RecordingSegment = { duration: number; }; -type RecordingActivity = { +export type RecordingActivity = { [hour: number]: RecordingSegmentActivity[]; }; diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index 0ed34de14..1ae1199c0 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -1,60 +1,60 @@ export interface FrigateStats { - cameras: { [camera_name: string]: CameraStats }; - cpu_usages: { [pid: string]: CpuStats }; - detectors: { [detectorKey: string]: DetectorStats }; - gpu_usages?: { [gpuKey: string]: GpuStats }; - processes: { [processKey: string]: ExtraProcessStats }; - service: ServiceStats; - detection_fps: number; - } + cameras: { [camera_name: string]: CameraStats }; + cpu_usages: { [pid: string]: CpuStats }; + detectors: { [detectorKey: string]: DetectorStats }; + gpu_usages?: { [gpuKey: string]: GpuStats }; + processes: { [processKey: string]: ExtraProcessStats }; + service: ServiceStats; + detection_fps: number; +} - export type CameraStats = { - audio_dBFPS: number; - audio_rms: number; - camera_fps: number; - capture_pid: number; - detection_enabled: number; - detection_fps: number; - ffmpeg_pid: number; - pid: number; - process_fps: number; - skipped_fps: number; - }; +export type CameraStats = { + audio_dBFPS: number; + audio_rms: number; + camera_fps: number; + capture_pid: number; + detection_enabled: number; + detection_fps: number; + ffmpeg_pid: number; + pid: number; + process_fps: number; + skipped_fps: number; +}; - export type CpuStats = { - cmdline: string; - cpu: string; - cpu_average: string; - mem: string; - }; +export type CpuStats = { + cmdline: string; + cpu: string; + cpu_average: string; + mem: string; +}; - export type DetectorStats = { - detection_start: number; - inference_speed: number; - pid: number; - }; +export type DetectorStats = { + detection_start: number; + inference_speed: number; + pid: number; +}; - export type ExtraProcessStats = { - pid: number; - }; +export type ExtraProcessStats = { + pid: number; +}; - export type GpuStats = { - gpu: string; - mem: string; - }; +export type GpuStats = { + gpu: string; + mem: string; +}; - export type ServiceStats = { - last_updated: number; - storage: { [path: string]: StorageStats }; - temperatures: { [apex: string]: number }; - update: number; - latest_version: string; - version: string; - }; +export type ServiceStats = { + last_updated: number; + storage: { [path: string]: StorageStats }; + temperatures: { [apex: string]: number }; + update: number; + latest_version: string; + version: string; +}; - export type StorageStats = { - free: number; - total: number; - used: number; - mount_type: string; - }; \ No newline at end of file +export type StorageStats = { + free: number; + total: number; + used: number; + mount_type: string; +}; diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index 3698a6ebe..86b364686 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -1,31 +1,33 @@ type Timeline = { + camera: string; + timestamp: number; + data: { camera: string; - timestamp: number; - data: { - camera: string; - label: string; - sub_label: string; - box?: [number, number, number, number]; - region: [number, number, number, number]; - attribute: string; - zones: string[]; - }; - class_type: - | "visible" - | "gone" - | "entered_zone" - | "attribute" - | "active" - | "stationary" - | "heard" - | "external"; - source_id: string; - source: string; + label: string; + sub_label: string; + box?: [number, number, number, number]; + region: [number, number, number, number]; + attribute: string; + zones: string[]; }; + class_type: + | "visible" + | "gone" + | "entered_zone" + | "attribute" + | "active" + | "stationary" + | "heard" + | "external"; + source_id: string; + source: string; +}; - type HourlyTimeline = { - start: number; - end: number; - count: number; - hours: { [key: string]: Timeline[] }; - }; \ No newline at end of file +// may be used in the future, keep for now for reference +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type HourlyTimeline = { + start: number; + end: number; + count: number; + hours: { [key: string]: Timeline[] }; +}; diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts index a8e454c93..8e86604b3 100644 --- a/web/src/types/ws.ts +++ b/web/src/types/ws.ts @@ -40,4 +40,4 @@ export interface FrigateEvent { after: FrigateObjectState; } -export type ToggleableSetting = "ON" | "OFF" +export type ToggleableSetting = "ON" | "OFF"; diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index 244dcb586..4a3f7158e 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -132,7 +132,7 @@ export const formatUnixTimestampToDateTime = ( date_style?: "full" | "long" | "medium" | "short"; time_style?: "full" | "long" | "medium" | "short"; strftime_fmt?: string; - } + }, ): string => { const { timezone, time_format, date_style, time_style, strftime_fmt } = config; @@ -187,7 +187,7 @@ export const formatUnixTimestampToDateTime = ( return `${date.toLocaleDateString( locale, - dateOptions + dateOptions, )} ${date.toLocaleTimeString(locale, timeOptions)}`; } @@ -213,7 +213,7 @@ interface DurationToken { */ export const getDurationFromTimestamps = ( start_time: number, - end_time: number | null + end_time: number | null, ): string => { if (isNaN(start_time)) { return "Invalid start time"; @@ -259,7 +259,7 @@ const getUTCOffset = (date: Date, timezone: string): number => { // Otherwise, calculate offset using provided timezone const utcDate = new Date( - date.getTime() - date.getTimezoneOffset() * 60 * 1000 + date.getTime() - date.getTimezoneOffset() * 60 * 1000, ); // locale of en-CA is required for proper locale format let iso = utcDate diff --git a/web/src/utils/timelineUtil.tsx b/web/src/utils/timelineUtil.tsx index d380bbb51..4bb6816a8 100644 --- a/web/src/utils/timelineUtil.tsx +++ b/web/src/utils/timelineUtil.tsx @@ -102,7 +102,7 @@ export function getTimelineItemDescription(timelineItem: Timeline) { ) { title = `${timelineItem.data.attribute.replaceAll( "_", - " " + " ", )} detected for ${label}`; } else { title = `${ diff --git a/web/src/views/events/DesktopRecordingView.tsx b/web/src/views/events/DesktopRecordingView.tsx index 8fafd86a8..3ee05f729 100644 --- a/web/src/views/events/DesktopRecordingView.tsx +++ b/web/src/views/events/DesktopRecordingView.tsx @@ -3,6 +3,7 @@ import DynamicVideoPlayer, { } from "@/components/player/DynamicVideoPlayer"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import { Button } from "@/components/ui/button"; +import { Preview } from "@/types/preview"; import { ReviewSegment } from "@/types/review"; import { getChunkedTimeRange } from "@/utils/timelineUtil"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -31,7 +32,7 @@ export default function DesktopRecordingView({ const timeRange = useMemo( () => getChunkedTimeRange(selectedReview.start_time), - [] + [selectedReview], ); const [selectedRangeIdx, setSelectedRangeIdx] = useState( timeRange.ranges.findIndex((chunk) => { @@ -39,7 +40,7 @@ export default function DesktopRecordingView({ chunk.start <= selectedReview.start_time && chunk.end >= selectedReview.start_time ); - }) + }), ); // move to next clip @@ -55,13 +56,13 @@ export default function DesktopRecordingView({ setSelectedRangeIdx(selectedRangeIdx - 1); } }); - }, [playerReady, selectedRangeIdx]); + }, [playerReady, selectedRangeIdx, timeRange]); // scrubbing and timeline state const [scrubbing, setScrubbing] = useState(false); const [currentTime, setCurrentTime] = useState( - selectedReview?.start_time || Date.now() / 1000 + selectedReview?.start_time || Date.now() / 1000, ); useEffect(() => { @@ -74,6 +75,9 @@ export default function DesktopRecordingView({ if (!scrubbing) { controllerRef.current?.seekToTimestamp(currentTime, true); } + + // we only want to seek when user stops scrubbing + // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrubbing]); return ( @@ -100,7 +104,7 @@ export default function DesktopRecordingView({ controllerRef.current?.seekToTimestamp( selectedReview.start_time, - true + true, ); }} /> diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index f678987c0..e5199b4a4 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -7,6 +7,7 @@ import ActivityIndicator from "@/components/ui/activity-indicator"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useEventUtils } from "@/hooks/use-event-utils"; import { FrigateConfig } from "@/types/frigateConfig"; +import { Preview } from "@/types/preview"; import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; @@ -84,7 +85,7 @@ export default function EventView({ const { alignStartDateToTimeline } = useEventUtils( reviewItems.all, - segmentDuration + segmentDuration, ); const currentItems = useMemo(() => { @@ -103,6 +104,8 @@ export default function EventView({ } return contentRef.current.scrollHeight > contentRef.current.clientHeight; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps }, [contentRef.current?.scrollHeight, severity]); // review interaction @@ -123,7 +126,7 @@ export default function EventView({ // no op } }, - [isValidating, reachedEnd] + [isValidating, reachedEnd, loadNextPage], ); const [minimap, setMinimap] = useState([]); @@ -148,7 +151,7 @@ export default function EventView({ setMinimap([...visibleTimestamps]); }); }, - { root: contentRef.current, threshold: isDesktop ? 0.1 : 0.5 } + { root: contentRef.current, threshold: isDesktop ? 0.1 : 0.5 }, ); return () => { @@ -167,7 +170,7 @@ export default function EventView({ // no op } }, - [minimapObserver] + [minimapObserver], ); const minimapBounds = useMemo(() => { const data = { @@ -177,7 +180,7 @@ export default function EventView({ const list = minimap.sort(); if (list.length > 0) { - data.end = parseFloat(list.at(-1)!!); + data.end = parseFloat(list.at(-1) || "0"); data.start = parseFloat(list[0]); } @@ -260,12 +263,12 @@ export default function EventView({ currentItems.map((value, segIdx) => { const lastRow = segIdx == reviewItems[severity].length - 1; const relevantPreview = Object.values( - relevantPreviews || [] + relevantPreviews || [], ).find( (preview) => preview.camera == value.camera && preview.start < value.start_time && - preview.end > value.end_time + preview.end > value.end_time, ); return ( diff --git a/web/vite.config.ts b/web/vite.config.ts index a97dbd014..5afefa331 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,45 +1,45 @@ /// -import path from "path" -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' -import monacoEditorPlugin from 'vite-plugin-monaco-editor'; +import path from "path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import monacoEditorPlugin from "vite-plugin-monaco-editor"; // https://vitejs.dev/config/ export default defineConfig({ define: { - 'import.meta.vitest': 'undefined', + "import.meta.vitest": "undefined", }, server: { proxy: { - '/api': { - target: 'http://localhost:5000', + "/api": { + target: "http://localhost:5000", ws: true, }, - '/vod': { - target: 'http://localhost:5000' + "/vod": { + target: "http://localhost:5000", }, - '/clips': { - target: 'http://localhost:5000' + "/clips": { + target: "http://localhost:5000", }, - '/exports': { - target: 'http://localhost:5000' + "/exports": { + target: "http://localhost:5000", }, - '/ws': { - target: 'ws://localhost:5000', + "/ws": { + target: "ws://localhost:5000", ws: true, }, - '/live': { - target: 'ws://localhost:5000', + "/live": { + target: "ws://localhost:5000", changeOrigin: true, ws: true, }, - } + }, }, plugins: [ react(), monacoEditorPlugin.default({ - customWorkers: [{ label: 'yaml', entry: 'monaco-yaml/yaml.worker' }], - languageWorkers: ['editorWorkerService'], // we don't use any of the default languages + customWorkers: [{ label: "yaml", entry: "monaco-yaml/yaml.worker" }], + languageWorkers: ["editorWorkerService"], // we don't use any of the default languages }), ], resolve: { @@ -48,17 +48,20 @@ export default defineConfig({ }, }, test: { - environment: 'jsdom', + environment: "jsdom", alias: { - 'testing-library': path.resolve(__dirname, './__test__/testing-library.js'), + "testing-library": path.resolve( + __dirname, + "./__test__/testing-library.js", + ), }, - setupFiles: ['./__test__/test-setup.ts'], - includeSource: ['src/**/*.{js,jsx,ts,tsx}'], + setupFiles: ["./__test__/test-setup.ts"], + includeSource: ["src/**/*.{js,jsx,ts,tsx}"], coverage: { - reporter: ['text-summary', 'text'], + reporter: ["text-summary", "text"], }, mockReset: true, restoreMocks: true, globals: true, }, -}) +});