Update frontend to React 19 (#22275)

* remove unused RecoilRoot and fix implicit ref callback

Remove the vestigial recoil dependency (zero consumers) and convert
the implicit-return ref callback in SearchView to block form to
prevent React 19 interpreting it as a cleanup function.

* replace react-transition-group with framer-motion in Chip

Replace CSSTransition with framer-motion AnimatePresence + motion.div
for React 19 compatibility (react-transition-group uses findDOMNode).
framer-motion is already a project dependency.

* migrate react-grid-layout v1 to v2

- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)

* upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch

- vaul: ^0.9.1 → ^1.1.2
- next-themes: ^0.3.0 → ^0.4.6
- framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support)
- react-zoom-pan-pinch: 3.4.4 → latest

* upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5

Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5

* upgrade typescript to 5.9.3

* modernize Context.Provider to React 19 shorthand

Replace <Context.Provider value={...}> with <Context value={...}>
across all project-owned context providers. External library contexts
(react-icons IconContext, radix TooltipPrimitive) left unchanged.

* add runtime patches for React 19 compatibility

- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
  to prevent infinite render loops from unstable ref callbacks
  https://github.com/radix-ui/primitives/issues/3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
  SlotClone instead of inline composeRefs to prevent re-render cycles
  https://github.com/radix-ui/primitives/pull/3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
  cause "Maximum update depth exceeded" with React 19 auto-batching
  https://github.com/facebook/react/issues/27613
- Add npm overrides to ensure single hoisted copies of compose-refs
  and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency

* formatting

* use availableWidth instead of useContainerWidth for grid layout

The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.

* remove unused carousel component and fix React 19 peer deps

Remove embla-carousel-react and its unused Carousel UI component.
Upgrade sonner v1 → v2 for native React 19 support. Remove
@types/react-icons stub (react-icons bundles its own types).
These changes eliminate all peer dependency conflicts, so
npm install works without --legacy-peer-deps.

* fix React 19 infinite re-render loop on live dashboard

The "Maximum update depth exceeded" error was caused by two issues:

1. useDeferredStreamMetadata returned a new `{}` default on every render
   when SWR data was undefined, creating an unstable reference that
   triggered the useEffect in useCameraLiveMode on every render cycle.
   Fixed by using a stable module-level EMPTY_METADATA constant.

2. useResizeObserver's rest parameter `...refs` created a new array on
   every render, causing its useEffect to re-run and re-observe elements
   continuously. Fixed by stabilizing refs with useRef and only
   reconnecting the observer when actual DOM elements change.
This commit is contained in:
Josh Hawkins 2026-03-05 08:42:38 -06:00 committed by GitHub
parent b2118382cb
commit 2782931c72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 2513 additions and 2828 deletions

4527
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite --host",
"postinstall": "patch-package",
"build": "tsc && vite build --base=/BASE_PATH/",
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .",
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
@ -32,7 +33,7 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
@ -50,8 +51,7 @@
"copy-to-clipboard": "^3.3.3",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.2.0",
"embla-carousel-react": "^8.2.0",
"framer-motion": "^11.5.4",
"framer-motion": "^12.35.0",
"hls.js": "^1.5.20",
"i18next": "^24.2.0",
"i18next-http-backend": "^3.0.1",
@ -61,30 +61,28 @@
"lodash": "^4.17.23",
"lucide-react": "^0.477.0",
"monaco-yaml": "^5.3.1",
"next-themes": "^0.3.0",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
"react": "^18.3.1",
"react": "^19.2.4",
"react-apexcharts": "^1.4.1",
"react-day-picker": "^9.7.0",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-dom": "^19.2.4",
"react-dropzone": "^14.3.8",
"react-grid-layout": "^1.5.0",
"react-grid-layout": "^2.2.2",
"react-hook-form": "^7.52.1",
"react-i18next": "^15.2.0",
"react-icons": "^5.5.0",
"react-konva": "^18.2.10",
"react-router-dom": "^6.30.3",
"react-konva": "^19.2.3",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"react-router-dom": "^6.30.3",
"react-swipeable": "^7.0.2",
"react-tracked": "^2.0.1",
"react-transition-group": "^4.4.5",
"react-use-websocket": "^4.8.1",
"react-zoom-pan-pinch": "3.4.4",
"recoil": "^0.7.7",
"react-zoom-pan-pinch": "^3.7.0",
"remark-gfm": "^4.0.0",
"scroll-into-view-if-needed": "^3.1.0",
"sonner": "^1.5.0",
"sonner": "^2.0.7",
"sort-by": "^1.2.0",
"strftime": "^0.10.3",
"swr": "^2.3.2",
@ -92,7 +90,7 @@
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.2.0",
"vaul": "^0.9.1",
"vaul": "^1.1.2",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^3.23.8"
},
@ -101,11 +99,8 @@
"@testing-library/jest-dom": "^6.6.2",
"@types/lodash": "^4.17.12",
"@types/node": "^20.14.10",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",
"@types/react-icons": "^3.0.0",
"@types/react-transition-group": "^4.4.10",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/strftime": "^0.9.8",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
@ -116,19 +111,26 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.2.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.8",
"eslint-plugin-vitest-globals": "^1.5.0",
"fake-indexeddb": "^6.0.0",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^24.1.1",
"monaco-editor": "^0.52.0",
"msw": "^2.3.5",
"patch-package": "^8.0.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.9",
"typescript": "^5.8.2",
"typescript": "^5.9.3",
"vite": "^6.4.1",
"vitest": "^3.0.7"
},
"overrides": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-slot": "1.2.4"
}
}

View File

@ -0,0 +1,75 @@
diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.js b/node_modules/@radix-ui/react-compose-refs/dist/index.js
index 5ba7a95..65aa7be 100644
--- a/node_modules/@radix-ui/react-compose-refs/dist/index.js
+++ b/node_modules/@radix-ui/react-compose-refs/dist/index.js
@@ -69,6 +69,31 @@ function composeRefs(...refs) {
};
}
function useComposedRefs(...refs) {
- return React.useCallback(composeRefs(...refs), refs);
+ const refsRef = React.useRef(refs);
+ React.useLayoutEffect(() => {
+ refsRef.current = refs;
+ });
+ return React.useCallback((node) => {
+ let hasCleanup = false;
+ const cleanups = refsRef.current.map((ref) => {
+ const cleanup = setRef(ref, node);
+ if (!hasCleanup && typeof cleanup === "function") {
+ hasCleanup = true;
+ }
+ return cleanup;
+ });
+ if (hasCleanup) {
+ return () => {
+ for (let i = 0; i < cleanups.length; i++) {
+ const cleanup = cleanups[i];
+ if (typeof cleanup === "function") {
+ cleanup();
+ } else {
+ setRef(refsRef.current[i], null);
+ }
+ }
+ };
+ }
+ }, []);
}
//# sourceMappingURL=index.js.map
diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
index 7dd9172..d1b53a5 100644
--- a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
+++ b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
@@ -32,7 +32,32 @@ function composeRefs(...refs) {
};
}
function useComposedRefs(...refs) {
- return React.useCallback(composeRefs(...refs), refs);
+ const refsRef = React.useRef(refs);
+ React.useLayoutEffect(() => {
+ refsRef.current = refs;
+ });
+ return React.useCallback((node) => {
+ let hasCleanup = false;
+ const cleanups = refsRef.current.map((ref) => {
+ const cleanup = setRef(ref, node);
+ if (!hasCleanup && typeof cleanup === "function") {
+ hasCleanup = true;
+ }
+ return cleanup;
+ });
+ if (hasCleanup) {
+ return () => {
+ for (let i = 0; i < cleanups.length; i++) {
+ const cleanup = cleanups[i];
+ if (typeof cleanup === "function") {
+ cleanup();
+ } else {
+ setRef(refsRef.current[i], null);
+ }
+ }
+ };
+ }
+ }, []);
}
export {
composeRefs,

View File

@ -0,0 +1,46 @@
diff --git a/node_modules/@radix-ui/react-slot/dist/index.js b/node_modules/@radix-ui/react-slot/dist/index.js
index 3691205..3b62ea8 100644
--- a/node_modules/@radix-ui/react-slot/dist/index.js
+++ b/node_modules/@radix-ui/react-slot/dist/index.js
@@ -85,11 +85,12 @@ function createSlotClone(ownerName) {
if (isLazyComponent(children) && typeof use === "function") {
children = use(children._payload);
}
+ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null;
+ const composedRef = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, childrenRef);
if (React.isValidElement(children)) {
- const childrenRef = getElementRef(children);
const props2 = mergeProps(slotProps, children.props);
if (children.type !== React.Fragment) {
- props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef;
+ props2.ref = forwardedRef ? composedRef : childrenRef;
}
return React.cloneElement(children, props2);
}
diff --git a/node_modules/@radix-ui/react-slot/dist/index.mjs b/node_modules/@radix-ui/react-slot/dist/index.mjs
index d7ea374..a990150 100644
--- a/node_modules/@radix-ui/react-slot/dist/index.mjs
+++ b/node_modules/@radix-ui/react-slot/dist/index.mjs
@@ -1,6 +1,6 @@
// src/slot.tsx
import * as React from "react";
-import { composeRefs } from "@radix-ui/react-compose-refs";
+import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs";
import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
var use = React[" use ".trim().toString()];
@@ -45,11 +45,12 @@ function createSlotClone(ownerName) {
if (isLazyComponent(children) && typeof use === "function") {
children = use(children._payload);
}
+ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null;
+ const composedRef = useComposedRefs(forwardedRef, childrenRef);
if (React.isValidElement(children)) {
- const childrenRef = getElementRef(children);
const props2 = mergeProps(slotProps, children.props);
if (children.type !== React.Fragment) {
- props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
+ props2.ref = forwardedRef ? composedRef : childrenRef;
}
return React.cloneElement(children, props2);
}

View File

@ -0,0 +1,23 @@
diff --git a/node_modules/react-use-websocket/dist/lib/use-websocket.js b/node_modules/react-use-websocket/dist/lib/use-websocket.js
index f01db48..b30aff2 100644
--- a/node_modules/react-use-websocket/dist/lib/use-websocket.js
+++ b/node_modules/react-use-websocket/dist/lib/use-websocket.js
@@ -139,15 +139,15 @@ var useWebSocket = function (url, options, connect) {
}
protectedSetLastMessage = function (message) {
if (!expectClose_1) {
- (0, react_dom_1.flushSync)(function () { return setLastMessage(message); });
+ setLastMessage(message);
}
};
protectedSetReadyState = function (state) {
if (!expectClose_1) {
- (0, react_dom_1.flushSync)(function () { return setReadyState(function (prev) {
+ setReadyState(function (prev) {
var _a;
return (__assign(__assign({}, prev), (convertedUrl.current && (_a = {}, _a[convertedUrl.current] = state, _a))));
- }); });
+ });
}
};
if (createOrJoin_1) {

View File

@ -114,10 +114,17 @@ interface PropertyElement {
content: React.ReactElement;
}
/** Shape of the props that RJSF injects into each property element. */
interface RjsfElementProps {
schema?: { type?: string | string[] };
uiSchema?: Record<string, unknown> & {
"ui:widget"?: string;
"ui:options"?: Record<string, unknown>;
};
}
function isObjectLikeElement(item: PropertyElement) {
const fieldSchema = item.content.props?.schema as
| { type?: string | string[] }
| undefined;
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
return fieldSchema?.type === "object";
}
@ -163,16 +170,21 @@ function GridLayoutObjectFieldTemplate(
// Override the properties rendering with grid layout
const isHiddenProp = (prop: (typeof properties)[number]) =>
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
"hidden";
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
// Separate regular and advanced properties
const advancedProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
(p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced === true,
);
const regularProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
(p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced !== true,
);
const hasModifiedAdvanced = advancedProps.some((prop) =>
isPathModified([...fieldPath, prop.name]),

View File

@ -448,6 +448,9 @@ export function FieldTemplate(props: FieldTemplateProps) {
);
};
const errorsProps = errors?.props as { errors?: unknown[] } | undefined;
const hasFieldErrors = !!errors && (errorsProps?.errors?.length ?? 0) > 0;
const renderStandardLabel = () => {
if (!shouldRenderStandardLabel) {
return null;
@ -459,7 +462,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
className={cn(
"text-sm font-medium",
isModified && "text-danger",
errors && errors.props?.errors?.length > 0 && "text-destructive",
hasFieldErrors && "text-destructive",
)}
>
{finalLabel}
@ -497,7 +500,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
className={cn(
"text-sm font-medium",
isModified && "text-danger",
errors && errors.props?.errors?.length > 0 && "text-destructive",
hasFieldErrors && "text-destructive",
)}
>
{finalLabel}

View File

@ -1,6 +1,7 @@
// Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields
// Renders simple nullable types as single inputs instead of dropdowns
import type { JSX } from "react";
import {
MultiSchemaFieldTemplateProps,
StrictRJSFSchema,

View File

@ -25,6 +25,15 @@ import {
import get from "lodash/get";
import { AddPropertyButton, AdvancedCollapsible } from "../components";
/** Shape of the props that RJSF injects into each property element. */
interface RjsfElementProps {
schema?: { type?: string | string[] };
uiSchema?: Record<string, unknown> & {
"ui:widget"?: string;
"ui:options"?: Record<string, unknown>;
};
}
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const {
title,
@ -182,16 +191,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
uiSchema?.["ui:options"]?.disableNestedCard === true;
const isHiddenProp = (prop: (typeof properties)[number]) =>
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
"hidden";
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
// Check for advanced section grouping
const advancedProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
(p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced === true,
);
const regularProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
(p) =>
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
?.advanced !== true,
);
const hasModifiedAdvanced = advancedProps.some((prop) =>
checkSubtreeModified([...fieldPath, prop.name]),
@ -333,9 +347,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const ungrouped = items.filter((item) => !grouped.has(item.name));
const isObjectLikeField = (item: (typeof properties)[number]) => {
const fieldSchema = item.content.props.schema as
| { type?: string | string[] }
| undefined;
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
return fieldSchema?.type === "object";
};

View File

@ -1,4 +1,5 @@
import { t } from "i18next";
import type { JSX } from "react";
import { FunctionComponent, useEffect, useMemo, useState } from "react";
interface IProp {

View File

@ -1,8 +1,8 @@
import { cn } from "@/lib/utils";
import { LogSeverity } from "@/types/log";
import { ReactNode, useMemo, useRef } from "react";
import { ReactNode, useMemo } from "react";
import { isIOS } from "react-device-detect";
import { CSSTransition } from "react-transition-group";
import { AnimatePresence, motion } from "framer-motion";
type ChipProps = {
className?: string;
@ -17,23 +17,14 @@ export default function Chip({
in: inProp = true,
onClick,
}: ChipProps) {
const nodeRef = useRef(null);
return (
<CSSTransition
in={inProp}
nodeRef={nodeRef}
timeout={500}
classNames={{
enter: "opacity-0",
enterActive: "opacity-100 transition-opacity duration-500 ease-in-out",
exit: "opacity-100",
exitActive: "opacity-0 transition-opacity duration-500 ease-in-out",
}}
unmountOnExit
>
<div
ref={nodeRef}
<AnimatePresence>
{inProp && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
className={cn(
"flex items-center rounded-2xl px-2 py-1.5",
className,
@ -48,8 +39,9 @@ export default function Chip({
}}
>
{children}
</div>
</CSSTransition>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@ -55,9 +55,9 @@ export function MobilePage({
});
return (
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
<MobilePageContext value={{ open, onOpenChange: setOpen }}>
{children}
</MobilePageContext.Provider>
</MobilePageContext>
);
}
@ -102,7 +102,7 @@ export function MobilePagePortal({
type MobilePageContentProps = {
children: React.ReactNode;
className?: string;
scrollerRef?: React.RefObject<HTMLDivElement>;
scrollerRef?: React.RefObject<HTMLDivElement | null>;
};
export function MobilePageContent({

View File

@ -10,7 +10,7 @@ import Konva from "konva";
import { useResizeObserver } from "@/hooks/resize-observer";
type DebugDrawingLayerProps = {
containerRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement | null>;
cameraWidth: number;
cameraHeight: number;
};

View File

@ -17,7 +17,7 @@ type ObjectPathProps = {
color?: number[];
width?: number;
pointRadius?: number;
imgRef: React.RefObject<HTMLImageElement>;
imgRef: React.RefObject<HTMLImageElement | null>;
onPointClick?: (index: number) => void;
visible?: boolean;
};

View File

@ -22,6 +22,7 @@ import {
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import { isMobile } from "react-device-detect";
import type { JSX } from "react";
import { useRef } from "react";
type PlatformAwareDialogProps = {

View File

@ -91,7 +91,7 @@ export default function HlsVideoPlayer({
// playback
const hlsRef = useRef<Hls>();
const hlsRef = useRef<Hls>(undefined);
const [useHlsCompat, setUseHlsCompat] = useState(false);
const [loadedMetadata, setLoadedMetadata] = useState(false);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();

View File

@ -51,10 +51,10 @@ export default function WebRtcPlayer({
// camera states
const pcRef = useRef<RTCPeerConnection | undefined>();
const pcRef = useRef<RTCPeerConnection | undefined>(undefined);
const videoRef = useRef<HTMLVideoElement | null>(null);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>();
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>(undefined);
const PeerConnection = useCallback(
async (media: string) => {

View File

@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil";
import { usePolygonStates } from "@/hooks/use-polygon-states";
type PolygonCanvasProps = {
containerRef: RefObject<HTMLDivElement>;
containerRef: RefObject<HTMLDivElement | null>;
camera: string;
width: number;
height: number;

View File

@ -18,7 +18,7 @@ import Konva from "konva";
import { Vector2d } from "konva/lib/types";
type PolygonDrawerProps = {
stageRef: RefObject<Konva.Stage>;
stageRef: RefObject<Konva.Stage | null>;
points: number[][];
distances: number[];
isActive: boolean;

View File

@ -37,8 +37,8 @@ export type EventReviewTimelineProps = {
events: ReviewSegment[];
visibleTimestamps?: number[];
severityType: ReviewSeverity;
timelineRef?: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement>;
timelineRef?: RefObject<HTMLDivElement | null>;
contentRef: RefObject<HTMLDivElement | null>;
onHandlebarDraggingChange?: (isDragging: boolean) => void;
isZooming: boolean;
zoomDirection: TimelineZoomDirection;

View File

@ -28,7 +28,7 @@ type EventSegmentProps = {
minimapStartTime?: number;
minimapEndTime?: number;
severityType: ReviewSeverity;
contentRef: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
dense: boolean;

View File

@ -41,8 +41,8 @@ export type MotionReviewTimelineProps = {
events: ReviewSegment[];
motion_events: MotionData[];
noRecordingRanges?: RecordingSegment[];
contentRef: RefObject<HTMLDivElement>;
timelineRef?: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement | null>;
timelineRef?: RefObject<HTMLDivElement | null>;
onHandlebarDraggingChange?: (isDragging: boolean) => void;
dense?: boolean;
isZooming: boolean;

View File

@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { TooltipPortal } from "@radix-ui/react-tooltip";
export type ReviewTimelineProps = {
timelineRef: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement>;
timelineRef: RefObject<HTMLDivElement | null>;
contentRef: RefObject<HTMLDivElement | null>;
segmentDuration: number;
timelineDuration: number;
timelineStartAligned: number;

View File

@ -14,7 +14,7 @@ import {
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
export type SummaryTimelineProps = {
reviewTimelineRef: React.RefObject<HTMLDivElement>;
reviewTimelineRef: React.RefObject<HTMLDivElement | null>;
timelineStart: number;
timelineEnd: number;
segmentDuration: number;

View File

@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment";
import { ReviewSegment, ReviewSeverity } from "@/types/review";
type VirtualizedEventSegmentsProps = {
timelineRef: React.RefObject<HTMLDivElement>;
timelineRef: React.RefObject<HTMLDivElement | null>;
segments: number[];
events: ReviewSegment[];
segmentDuration: number;
@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = {
minimapStartTime?: number;
minimapEndTime?: number;
severityType: ReviewSeverity;
contentRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean;
alignStartDateToTimeline: (timestamp: number) => number;

View File

@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment";
import { ReviewSegment, MotionData } from "@/types/review";
type VirtualizedMotionSegmentsProps = {
timelineRef: React.RefObject<HTMLDivElement>;
timelineRef: React.RefObject<HTMLDivElement | null>;
segments: number[];
events: ReviewSegment[];
motion_events: MotionData[];
@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = {
showMinimap: boolean;
minimapStartTime?: number;
minimapEndTime?: number;
contentRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement | null>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean;
motionOnly: boolean;

View File

@ -1,3 +1,4 @@
import type { JSX } from "react";
import { useState, useEffect, useRef } from "react";
import { Button } from "./button";
import { Calendar } from "./calendar";
@ -124,8 +125,8 @@ export function DateRangePicker({
);
// Refs to store the values of range and rangeCompare when the date picker is opened
const openedRangeRef = useRef<DateRange | undefined>();
const openedRangeCompareRef = useRef<DateRange | undefined>();
const openedRangeRef = useRef<DateRange | undefined>(undefined);
const openedRangeCompareRef = useRef<DateRange | undefined>(undefined);
const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
undefined,

View File

@ -1,265 +0,0 @@
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref,
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
});
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { t } = useTranslation(["views/explore"]);
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
aria-label={t("trackingDetails.carousel.previous")}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">{t("trackingDetails.carousel.previous")}</span>
</Button>
);
});
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { t } = useTranslation(["views/explore"]);
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
aria-label={t("trackingDetails.carousel.next")}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">{t("trackingDetails.carousel.next")}</span>
</Button>
);
});
CarouselNext.displayName = "CarouselNext";
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

@ -33,9 +33,9 @@ const FormField = <
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<FormFieldContext value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
</FormFieldContext>
);
};
@ -77,9 +77,9 @@ const FormItem = React.forwardRef<
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<FormItemContext value={{ id }}>
<div ref={ref} className={cn("space-y-1", className)} {...props} />
</FormItemContext.Provider>
</FormItemContext>
);
});
FormItem.displayName = "FormItem";

View File

@ -1,10 +1,10 @@
import { ForwardedRef, forwardRef } from "react";
import { IconType } from "react-icons";
interface IconWrapperProps {
interface IconWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
icon: IconType;
className?: string;
[key: string]: any;
disabled?: boolean;
}
const IconWrapper = forwardRef(

View File

@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef<
);
return (
<SidebarContext.Provider value={contextValue}>
<SidebarContext value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
@ -161,7 +161,7 @@ const SidebarProvider = React.forwardRef<
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
</SidebarContext>
);
},
);

View File

@ -22,9 +22,9 @@ const ToggleGroup = React.forwardRef<
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
<ToggleGroupContext value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupContext>
</ToggleGroupPrimitive.Root>
))

View File

@ -102,9 +102,5 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
axios.get("/logout", { withCredentials: true });
};
return (
<AuthContext.Provider value={{ auth, login, logout }}>
{children}
</AuthContext.Provider>
);
return <AuthContext value={{ auth, login, logout }}>{children}</AuthContext>;
}

View File

@ -125,11 +125,7 @@ export function DetailStreamProvider({
isDetailMode,
};
return (
<DetailStreamContext.Provider value={value}>
{children}
</DetailStreamContext.Provider>
);
return <DetailStreamContext value={value}>{children}</DetailStreamContext>;
}
// eslint-disable-next-line react-refresh/only-export-components

View File

@ -77,9 +77,9 @@ export function LanguageProvider({
};
return (
<LanguageProviderContext.Provider {...props} value={value}>
<LanguageProviderContext {...props} value={value}>
{children}
</LanguageProviderContext.Provider>
</LanguageProviderContext>
);
}

View File

@ -1,6 +1,5 @@
import { ReactNode } from "react";
import { ThemeProvider } from "@/context/theme-provider";
import { RecoilRoot } from "recoil";
import { ApiProvider } from "@/api";
import { IconContext } from "react-icons";
import { TooltipProvider } from "@/components/ui/tooltip";
@ -15,7 +14,6 @@ type TProvidersProps = {
function providers({ children }: TProvidersProps) {
return (
<RecoilRoot>
<AuthProvider>
<ApiProvider>
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
@ -33,7 +31,6 @@ function providers({ children }: TProvidersProps) {
</ThemeProvider>
</ApiProvider>
</AuthProvider>
</RecoilRoot>
);
}

View File

@ -107,10 +107,10 @@ export function StatusBarMessagesProvider({
}, []);
return (
<StatusBarMessagesContext.Provider
<StatusBarMessagesContext
value={{ messages, addMessage, removeMessage, clearMessages }}
>
{children}
</StatusBarMessagesContext.Provider>
</StatusBarMessagesContext>
);
}

View File

@ -44,7 +44,7 @@ export function StreamingSettingsProvider({
}, [allGroupsStreamingSettings, setPersistedGroupStreamingSettings]);
return (
<StreamingSettingsContext.Provider
<StreamingSettingsContext
value={{
allGroupsStreamingSettings,
setAllGroupsStreamingSettings,
@ -52,7 +52,7 @@ export function StreamingSettingsProvider({
}}
>
{children}
</StreamingSettingsContext.Provider>
</StreamingSettingsContext>
);
}

View File

@ -124,9 +124,9 @@ export function ThemeProvider({
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
<ThemeProviderContext {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
</ThemeProviderContext>
);
}

View File

@ -31,25 +31,24 @@ export function useResizeObserver(...refs: RefType[]) {
[],
);
// Resolve refs to actual DOM elements for use as stable effect dependencies.
// Rest params create a new array each call, but the underlying elements are
// stable DOM nodes, so spreading them into the dep array avoids re-running
// the effect on every render.
const elements = refs.map((ref) =>
ref instanceof Window ? document.body : ref.current,
);
useEffect(() => {
refs.forEach((ref) => {
if (ref instanceof Window) {
resizeObserver.observe(document.body);
} else if (ref.current) {
resizeObserver.observe(ref.current);
}
elements.forEach((el) => {
if (el) resizeObserver.observe(el);
});
return () => {
refs.forEach((ref) => {
if (ref instanceof Window) {
resizeObserver.unobserve(document.body);
} else if (ref.current) {
resizeObserver.unobserve(ref.current);
}
});
resizeObserver.disconnect();
};
}, [refs, resizeObserver]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...elements, resizeObserver]);
if (dimensions.length == refs.length) {
return dimensions;

View File

@ -5,6 +5,7 @@ import { LiveStreamMetadata } from "@/types/live";
const FETCH_TIMEOUT_MS = 10000;
const DEFER_DELAY_MS = 2000;
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
/**
* Hook that fetches go2rtc stream metadata with deferred loading.
@ -77,7 +78,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
return metadata;
}, []);
const { data: metadata = {} } = useSWR<{
const { data: metadata = EMPTY_METADATA } = useSWR<{
[key: string]: LiveStreamMetadata;
}>(swrKey, fetcher, {
revalidateOnFocus: false,

View File

@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
import useUserInteraction from "./use-user-interaction";
type DraggableElementProps = {
contentRef: React.RefObject<HTMLElement>;
timelineRef: React.RefObject<HTMLDivElement>;
segmentsRef: React.RefObject<HTMLDivElement>;
draggableElementRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLElement | null>;
timelineRef: React.RefObject<HTMLDivElement | null>;
segmentsRef: React.RefObject<HTMLDivElement | null>;
draggableElementRef: React.RefObject<HTMLDivElement | null>;
segmentDuration: number;
showDraggableElement: boolean;
draggableElementTime?: number;

View File

@ -78,7 +78,7 @@ function removeEventListeners(
}
export function useFullscreen<T extends HTMLElement = HTMLElement>(
elementRef: RefObject<T>,
elementRef: RefObject<T | null>,
) {
const [fullscreen, setFullscreen] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
const useImageLoaded = (): [
React.RefObject<HTMLImageElement>,
React.RefObject<HTMLImageElement | null>,
boolean,
() => void,
] => {

View File

@ -3,7 +3,7 @@ import { useCallback } from "react";
export type TimelineUtilsProps = {
segmentDuration: number;
timelineDuration?: number;
timelineRef?: React.RefObject<HTMLElement>;
timelineRef?: React.RefObject<HTMLElement | null>;
};
export function useTimelineUtils({

View File

@ -11,7 +11,7 @@ type UseTimelineZoomProps = {
zoomLevels: ZoomSettings[];
onZoomChange: (newZoomLevel: number) => void;
pinchThresholdPercent?: number;
timelineRef: React.RefObject<HTMLDivElement>;
timelineRef: React.RefObject<HTMLDivElement | null>;
timelineDuration: number;
};

View File

@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react";
type UseUserInteractionProps = {
elementRef: React.RefObject<HTMLElement>;
elementRef: React.RefObject<HTMLElement | null>;
};
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
const [userInteracting, setUserInteracting] = useState(false);
const interactionTimeout = useRef<NodeJS.Timeout>();
const interactionTimeout = useRef<NodeJS.Timeout>(undefined);
const isProgrammaticScroll = useRef(false);
const setProgrammaticScroll = useCallback(() => {

View File

@ -7,7 +7,7 @@ export type VideoResolutionType = {
};
export function useVideoDimensions(
containerRef: React.RefObject<HTMLDivElement>,
containerRef: React.RefObject<HTMLDivElement | null>,
) {
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);

View File

@ -14,10 +14,9 @@ import React, {
useState,
} from "react";
import {
ItemCallback,
Layout,
Responsive,
WidthProvider,
LayoutItem,
ResponsiveGridLayout as Responsive,
} from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
@ -56,7 +55,7 @@ type DraggableGridLayoutProps = {
cameras: CameraConfig[];
cameraGroup: string;
cameraRef: (node: HTMLElement | null) => void;
containerRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement | null>;
includeBirdseye: boolean;
onSelectCamera: (camera: string) => void;
windowVisible: boolean;
@ -116,11 +115,8 @@ export default function DraggableGridLayout({
// grid layout
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence<
Layout[]
>(`${cameraGroup}-draggable-layout`);
const [gridLayout, setGridLayout, isGridLayoutLoaded] =
useUserPersistence<Layout>(`${cameraGroup}-draggable-layout`);
const [group] = useUserPersistedOverlayState(
"cameraGroup",
@ -158,11 +154,11 @@ export default function DraggableGridLayout({
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
useState<boolean>();
const [currentGridLayout, setCurrentGridLayout] = useState<
Layout[] | undefined
Layout | undefined
>();
const handleLayoutChange = useCallback(
(currentLayout: Layout[]) => {
(currentLayout: Layout) => {
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
return;
}
@ -174,7 +170,7 @@ export default function DraggableGridLayout({
);
const generateLayout = useCallback(
(baseLayout: Layout[] | undefined) => {
(baseLayout: Layout | undefined) => {
if (!isGridLayoutLoaded) {
return;
}
@ -184,7 +180,7 @@ export default function DraggableGridLayout({
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
: cameras.map((camera) => camera?.name || "");
const optionsMap: Layout[] = baseLayout
const optionsMap: LayoutItem[] = baseLayout
? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
: [];
@ -363,12 +359,14 @@ export default function DraggableGridLayout({
);
}, [availableWidth, marginValue]);
const handleResize: ItemCallback = (
_: Layout[],
oldLayoutItem: Layout,
layoutItem: Layout,
placeholder: Layout,
const handleResize = (
_layout: Layout,
oldLayoutItem: LayoutItem | null,
layoutItem: LayoutItem | null,
placeholder: LayoutItem | null,
) => {
if (!oldLayoutItem || !layoutItem || !placeholder) return;
const heightDiff = layoutItem.h - oldLayoutItem.h;
const widthDiff = layoutItem.w - oldLayoutItem.w;
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
@ -537,8 +535,9 @@ export default function DraggableGridLayout({
currentGroups={groups}
activeGroup={group}
/>
<ResponsiveGridLayout
<Responsive
className="grid-layout"
width={availableWidth ?? window.innerWidth}
layouts={{
lg: currentGridLayout,
md: currentGridLayout,
@ -551,13 +550,17 @@ export default function DraggableGridLayout({
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
margin={[marginValue, marginValue]}
containerPadding={[0, isEditMode ? 6 : 3]}
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
resizeConfig={{
enabled: isEditMode,
handles: isEditMode ? ["sw", "nw", "se", "ne"] : [],
}}
dragConfig={{
enabled: isEditMode,
}}
onDragStop={handleLayoutChange}
onResize={handleResize}
onResizeStart={() => setShowCircles(false)}
onResizeStop={handleLayoutChange}
isDraggable={isEditMode}
isResizable={isEditMode}
>
{includeBirdseye && birdseyeConfig?.enabled && (
<BirdseyeLivePlayerGridItem
@ -685,7 +688,7 @@ export default function DraggableGridLayout({
</GridLiveContextMenu>
);
})}
</ResponsiveGridLayout>
</Responsive>
{isDesktop && (
<div
className={cn(

View File

@ -419,7 +419,7 @@ export default function SearchView({
>();
// keep track of previous ref to outline thumbnail when dialog closes
const prevSearchDetailRef = useRef<SearchResult | undefined>();
const prevSearchDetailRef = useRef<SearchResult | undefined>(undefined);
useEffect(() => {
if (searchDetail === undefined && prevSearchDetailRef.current) {
@ -619,7 +619,9 @@ export default function SearchView({
return (
<div
key={value.id}
ref={(item) => (itemRefs.current[index] = item)}
ref={(item) => {
itemRefs.current[index] = item;
}}
data-start={value.start_time}
className="relative flex flex-col rounded-lg"
>