mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Move review classification settings to camera settings view (#12410)
* Camera settings view for alerts/detections * flxes, beautifying, zone renaming, clean up * replace underscores with spaces in zone names * replace underscores with spaces in labels
This commit is contained in:
		
							parent
							
								
									a361372182
								
							
						
					
					
						commit
						aaafd63b94
					
				
							
								
								
									
										30
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -12,6 +12,7 @@
 | 
				
			|||||||
        "@hookform/resolvers": "^3.9.0",
 | 
					        "@hookform/resolvers": "^3.9.0",
 | 
				
			||||||
        "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
					        "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
				
			||||||
        "@radix-ui/react-aspect-ratio": "^1.1.0",
 | 
					        "@radix-ui/react-aspect-ratio": "^1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-checkbox": "^1.1.1",
 | 
				
			||||||
        "@radix-ui/react-context-menu": "^2.2.1",
 | 
					        "@radix-ui/react-context-menu": "^2.2.1",
 | 
				
			||||||
        "@radix-ui/react-dialog": "^1.1.1",
 | 
					        "@radix-ui/react-dialog": "^1.1.1",
 | 
				
			||||||
        "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
					        "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
				
			||||||
@ -1182,6 +1183,35 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@radix-ui/react-checkbox": {
 | 
				
			||||||
 | 
					      "version": "1.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@radix-ui/primitive": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-compose-refs": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-context": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-presence": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-primitive": "2.0.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-use-controllable-state": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-use-previous": "1.1.0",
 | 
				
			||||||
 | 
					        "@radix-ui/react-use-size": "1.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "@types/react": "*",
 | 
				
			||||||
 | 
					        "@types/react-dom": "*",
 | 
				
			||||||
 | 
					        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
 | 
				
			||||||
 | 
					        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
 | 
					        "@types/react": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "@types/react-dom": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@radix-ui/react-collection": {
 | 
					    "node_modules/@radix-ui/react-collection": {
 | 
				
			||||||
      "version": "1.1.0",
 | 
					      "version": "1.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@
 | 
				
			|||||||
    "@hookform/resolvers": "^3.9.0",
 | 
					    "@hookform/resolvers": "^3.9.0",
 | 
				
			||||||
    "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
					    "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
				
			||||||
    "@radix-ui/react-aspect-ratio": "^1.1.0",
 | 
					    "@radix-ui/react-aspect-ratio": "^1.1.0",
 | 
				
			||||||
 | 
					    "@radix-ui/react-checkbox": "^1.1.1",
 | 
				
			||||||
    "@radix-ui/react-context-menu": "^2.2.1",
 | 
					    "@radix-ui/react-context-menu": "^2.2.1",
 | 
				
			||||||
    "@radix-ui/react-dialog": "^1.1.1",
 | 
					    "@radix-ui/react-dialog": "^1.1.1",
 | 
				
			||||||
    "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
					    "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
				
			||||||
 | 
				
			|||||||
@ -143,20 +143,6 @@ export default function ZoneEditPane({
 | 
				
			|||||||
        config?.cameras[polygon.camera]?.zones[polygon.name]?.loitering_time,
 | 
					        config?.cameras[polygon.camera]?.zones[polygon.name]?.loitering_time,
 | 
				
			||||||
      isFinished: polygon?.isFinished ?? false,
 | 
					      isFinished: polygon?.isFinished ?? false,
 | 
				
			||||||
      objects: polygon?.objects ?? [],
 | 
					      objects: polygon?.objects ?? [],
 | 
				
			||||||
      review_alerts:
 | 
					 | 
				
			||||||
        (polygon?.camera &&
 | 
					 | 
				
			||||||
          polygon?.name &&
 | 
					 | 
				
			||||||
          config?.cameras[
 | 
					 | 
				
			||||||
            polygon.camera
 | 
					 | 
				
			||||||
          ]?.review.alerts.required_zones.includes(polygon.name)) ||
 | 
					 | 
				
			||||||
        false,
 | 
					 | 
				
			||||||
      review_detections:
 | 
					 | 
				
			||||||
        (polygon?.camera &&
 | 
					 | 
				
			||||||
          polygon?.name &&
 | 
					 | 
				
			||||||
          config?.cameras[
 | 
					 | 
				
			||||||
            polygon.camera
 | 
					 | 
				
			||||||
          ]?.review.detections.required_zones.includes(polygon.name)) ||
 | 
					 | 
				
			||||||
        false,
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -167,8 +153,6 @@ export default function ZoneEditPane({
 | 
				
			|||||||
        inertia,
 | 
					        inertia,
 | 
				
			||||||
        loitering_time,
 | 
					        loitering_time,
 | 
				
			||||||
        objects: form_objects,
 | 
					        objects: form_objects,
 | 
				
			||||||
        review_alerts,
 | 
					 | 
				
			||||||
        review_detections,
 | 
					 | 
				
			||||||
      }: ZoneFormValuesType, // values submitted via the form
 | 
					      }: ZoneFormValuesType, // values submitted via the form
 | 
				
			||||||
      objects: string[],
 | 
					      objects: string[],
 | 
				
			||||||
    ) => {
 | 
					    ) => {
 | 
				
			||||||
@ -176,11 +160,21 @@ export default function ZoneEditPane({
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      let mutatedConfig = config;
 | 
					      let mutatedConfig = config;
 | 
				
			||||||
 | 
					      let alertQueries = "";
 | 
				
			||||||
 | 
					      let detectionQueries = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const renamingZone = zoneName != polygon.name && polygon.name != "";
 | 
					      const renamingZone = zoneName != polygon.name && polygon.name != "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (renamingZone) {
 | 
					      if (renamingZone) {
 | 
				
			||||||
        // rename - delete old zone and replace with new
 | 
					        // rename - delete old zone and replace with new
 | 
				
			||||||
 | 
					        const zoneInAlerts =
 | 
				
			||||||
 | 
					          cameraConfig?.review.alerts.required_zones.includes(polygon.name) ??
 | 
				
			||||||
 | 
					          false;
 | 
				
			||||||
 | 
					        const zoneInDetections =
 | 
				
			||||||
 | 
					          cameraConfig?.review.detections.required_zones.includes(
 | 
				
			||||||
 | 
					            polygon.name,
 | 
				
			||||||
 | 
					          ) ?? false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const {
 | 
					        const {
 | 
				
			||||||
          alertQueries: renameAlertQueries,
 | 
					          alertQueries: renameAlertQueries,
 | 
				
			||||||
          detectionQueries: renameDetectionQueries,
 | 
					          detectionQueries: renameDetectionQueries,
 | 
				
			||||||
@ -209,6 +203,18 @@ export default function ZoneEditPane({
 | 
				
			|||||||
          });
 | 
					          });
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // make sure new zone name is readded to review
 | 
				
			||||||
 | 
					        ({ alertQueries, detectionQueries } = reviewQueries(
 | 
				
			||||||
 | 
					          zoneName,
 | 
				
			||||||
 | 
					          zoneInAlerts,
 | 
				
			||||||
 | 
					          zoneInDetections,
 | 
				
			||||||
 | 
					          polygon.camera,
 | 
				
			||||||
 | 
					          mutatedConfig?.cameras[polygon.camera]?.review.alerts
 | 
				
			||||||
 | 
					            .required_zones || [],
 | 
				
			||||||
 | 
					          mutatedConfig?.cameras[polygon.camera]?.review.detections
 | 
				
			||||||
 | 
					            .required_zones || [],
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const coordinates = flattenPoints(
 | 
					      const coordinates = flattenPoints(
 | 
				
			||||||
@ -233,17 +239,6 @@ export default function ZoneEditPane({
 | 
				
			|||||||
        objectQueries = `&cameras.${polygon?.camera}.zones.${zoneName}.objects`;
 | 
					        objectQueries = `&cameras.${polygon?.camera}.zones.${zoneName}.objects`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const { alertQueries, detectionQueries } = reviewQueries(
 | 
					 | 
				
			||||||
        zoneName,
 | 
					 | 
				
			||||||
        review_alerts,
 | 
					 | 
				
			||||||
        review_detections,
 | 
					 | 
				
			||||||
        polygon.camera,
 | 
					 | 
				
			||||||
        mutatedConfig?.cameras[polygon.camera]?.review.alerts.required_zones ||
 | 
					 | 
				
			||||||
          [],
 | 
					 | 
				
			||||||
        mutatedConfig?.cameras[polygon.camera]?.review.detections
 | 
					 | 
				
			||||||
          .required_zones || [],
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let inertiaQuery = "";
 | 
					      let inertiaQuery = "";
 | 
				
			||||||
      if (inertia) {
 | 
					      if (inertia) {
 | 
				
			||||||
        inertiaQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.inertia=${inertia}`;
 | 
					        inertiaQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.inertia=${inertia}`;
 | 
				
			||||||
@ -449,52 +444,6 @@ export default function ZoneEditPane({
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
          </FormItem>
 | 
					          </FormItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Separator className="my-2 flex bg-secondary" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <FormField
 | 
					 | 
				
			||||||
            control={form.control}
 | 
					 | 
				
			||||||
            name="review_alerts"
 | 
					 | 
				
			||||||
            render={({ field }) => (
 | 
					 | 
				
			||||||
              <FormItem className="flex flex-row items-center justify-between">
 | 
					 | 
				
			||||||
                <div className="space-y-0.5">
 | 
					 | 
				
			||||||
                  <FormLabel>Alerts</FormLabel>
 | 
					 | 
				
			||||||
                  <FormDescription>
 | 
					 | 
				
			||||||
                    When an object enters this zone, ensure it is marked as an
 | 
					 | 
				
			||||||
                    alert.
 | 
					 | 
				
			||||||
                  </FormDescription>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <FormControl>
 | 
					 | 
				
			||||||
                  <Switch
 | 
					 | 
				
			||||||
                    checked={field.value}
 | 
					 | 
				
			||||||
                    onCheckedChange={field.onChange}
 | 
					 | 
				
			||||||
                    className="ml-3"
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </FormControl>
 | 
					 | 
				
			||||||
              </FormItem>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <FormField
 | 
					 | 
				
			||||||
            control={form.control}
 | 
					 | 
				
			||||||
            name="review_detections"
 | 
					 | 
				
			||||||
            render={({ field }) => (
 | 
					 | 
				
			||||||
              <FormItem className="flex flex-row items-center justify-between">
 | 
					 | 
				
			||||||
                <div className="space-y-0.5">
 | 
					 | 
				
			||||||
                  <FormLabel className="text-base">Detections</FormLabel>
 | 
					 | 
				
			||||||
                  <FormDescription>
 | 
					 | 
				
			||||||
                    When an object enters this zone, ensure it is marked as a
 | 
					 | 
				
			||||||
                    detection.
 | 
					 | 
				
			||||||
                  </FormDescription>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <FormControl>
 | 
					 | 
				
			||||||
                  <Switch
 | 
					 | 
				
			||||||
                    checked={field.value}
 | 
					 | 
				
			||||||
                    onCheckedChange={field.onChange}
 | 
					 | 
				
			||||||
                    className="ml-3"
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </FormControl>
 | 
					 | 
				
			||||||
              </FormItem>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <FormField
 | 
					          <FormField
 | 
				
			||||||
            control={form.control}
 | 
					            control={form.control}
 | 
				
			||||||
            name="isFinished"
 | 
					            name="isFinished"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										28
									
								
								web/src/components/ui/checkbox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								web/src/components/ui/checkbox.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
 | 
				
			||||||
 | 
					import { Check } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Checkbox = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof CheckboxPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <CheckboxPrimitive.Root
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <CheckboxPrimitive.Indicator
 | 
				
			||||||
 | 
					      className={cn("flex items-center justify-center text-current")}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Check className="h-4 w-4" />
 | 
				
			||||||
 | 
					    </CheckboxPrimitive.Indicator>
 | 
				
			||||||
 | 
					  </CheckboxPrimitive.Root>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Checkbox.displayName = CheckboxPrimitive.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Checkbox }
 | 
				
			||||||
@ -30,6 +30,7 @@ import { PolygonType } from "@/types/canvas";
 | 
				
			|||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
 | 
					import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
 | 
				
			||||||
import scrollIntoView from "scroll-into-view-if-needed";
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
import GeneralSettingsView from "@/views/settings/GeneralSettingsView";
 | 
					import GeneralSettingsView from "@/views/settings/GeneralSettingsView";
 | 
				
			||||||
 | 
					import CameraSettingsView from "@/views/settings/CameraSettingsView";
 | 
				
			||||||
import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
 | 
					import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
 | 
				
			||||||
import MotionTunerView from "@/views/settings/MotionTunerView";
 | 
					import MotionTunerView from "@/views/settings/MotionTunerView";
 | 
				
			||||||
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
 | 
					import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
 | 
				
			||||||
@ -38,6 +39,7 @@ import AuthenticationView from "@/views/settings/AuthenticationView";
 | 
				
			|||||||
export default function Settings() {
 | 
					export default function Settings() {
 | 
				
			||||||
  const settingsViews = [
 | 
					  const settingsViews = [
 | 
				
			||||||
    "general",
 | 
					    "general",
 | 
				
			||||||
 | 
					    "camera settings",
 | 
				
			||||||
    "masks / zones",
 | 
					    "masks / zones",
 | 
				
			||||||
    "motion tuner",
 | 
					    "motion tuner",
 | 
				
			||||||
    "debug",
 | 
					    "debug",
 | 
				
			||||||
@ -136,6 +138,7 @@ export default function Settings() {
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </ScrollArea>
 | 
					        </ScrollArea>
 | 
				
			||||||
        {(page == "debug" ||
 | 
					        {(page == "debug" ||
 | 
				
			||||||
 | 
					          page == "camera settings" ||
 | 
				
			||||||
          page == "masks / zones" ||
 | 
					          page == "masks / zones" ||
 | 
				
			||||||
          page == "motion tuner") && (
 | 
					          page == "motion tuner") && (
 | 
				
			||||||
          <div className="ml-2 flex flex-shrink-0 items-center gap-2">
 | 
					          <div className="ml-2 flex flex-shrink-0 items-center gap-2">
 | 
				
			||||||
@ -158,6 +161,12 @@ export default function Settings() {
 | 
				
			|||||||
        {page == "debug" && (
 | 
					        {page == "debug" && (
 | 
				
			||||||
          <ObjectSettingsView selectedCamera={selectedCamera} />
 | 
					          <ObjectSettingsView selectedCamera={selectedCamera} />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
					        {page == "camera settings" && (
 | 
				
			||||||
 | 
					          <CameraSettingsView
 | 
				
			||||||
 | 
					            selectedCamera={selectedCamera}
 | 
				
			||||||
 | 
					            setUnsavedChanges={setUnsavedChanges}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        {page == "masks / zones" && (
 | 
					        {page == "masks / zones" && (
 | 
				
			||||||
          <MasksAndZonesView
 | 
					          <MasksAndZonesView
 | 
				
			||||||
            selectedCamera={selectedCamera}
 | 
					            selectedCamera={selectedCamera}
 | 
				
			||||||
 | 
				
			|||||||
@ -18,8 +18,6 @@ export type ZoneFormValuesType = {
 | 
				
			|||||||
  loitering_time: number;
 | 
					  loitering_time: number;
 | 
				
			||||||
  isFinished: boolean;
 | 
					  isFinished: boolean;
 | 
				
			||||||
  objects: string[];
 | 
					  objects: string[];
 | 
				
			||||||
  review_alerts: boolean;
 | 
					 | 
				
			||||||
  review_detections: boolean;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ObjectMaskFormValuesType = {
 | 
					export type ObjectMaskFormValuesType = {
 | 
				
			||||||
 | 
				
			|||||||
@ -172,9 +172,11 @@ export interface CameraConfig {
 | 
				
			|||||||
  review: {
 | 
					  review: {
 | 
				
			||||||
    alerts: {
 | 
					    alerts: {
 | 
				
			||||||
      required_zones: string[];
 | 
					      required_zones: string[];
 | 
				
			||||||
 | 
					      labels: string[];
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    detections: {
 | 
					    detections: {
 | 
				
			||||||
      required_zones: string[];
 | 
					      required_zones: string[];
 | 
				
			||||||
 | 
					      labels: string[];
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  rtmp: {
 | 
					  rtmp: {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										500
									
								
								web/src/views/settings/CameraSettingsView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										500
									
								
								web/src/views/settings/CameraSettingsView.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,500 @@
 | 
				
			|||||||
 | 
					import Heading from "@/components/ui/heading";
 | 
				
			||||||
 | 
					import { useCallback, useContext, useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { Toaster } from "sonner";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  FormControl,
 | 
				
			||||||
 | 
					  FormDescription,
 | 
				
			||||||
 | 
					  FormField,
 | 
				
			||||||
 | 
					  FormItem,
 | 
				
			||||||
 | 
					  FormLabel,
 | 
				
			||||||
 | 
					  FormMessage,
 | 
				
			||||||
 | 
					} from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { Separator } from "../../components/ui/separator";
 | 
				
			||||||
 | 
					import { Button } from "../../components/ui/button";
 | 
				
			||||||
 | 
					import useSWR from "swr";
 | 
				
			||||||
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
 | 
					import { Checkbox } from "@/components/ui/checkbox";
 | 
				
			||||||
 | 
					import ActivityIndicator from "@/components/indicators/activity-indicator";
 | 
				
			||||||
 | 
					import { StatusBarMessagesContext } from "@/context/statusbar-provider";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					import { LuExternalLink } from "react-icons/lu";
 | 
				
			||||||
 | 
					import { capitalizeFirstLetter } from "@/utils/stringUtil";
 | 
				
			||||||
 | 
					import { MdCircle } from "react-icons/md";
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CameraSettingsViewProps = {
 | 
				
			||||||
 | 
					  selectedCamera: string;
 | 
				
			||||||
 | 
					  setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CameraReviewSettingsValueType = {
 | 
				
			||||||
 | 
					  alerts_zones: string[];
 | 
				
			||||||
 | 
					  detections_zones: string[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function CameraSettingsView({
 | 
				
			||||||
 | 
					  selectedCamera,
 | 
				
			||||||
 | 
					  setUnsavedChanges,
 | 
				
			||||||
 | 
					}: CameraSettingsViewProps) {
 | 
				
			||||||
 | 
					  const { data: config, mutate: updateConfig } =
 | 
				
			||||||
 | 
					    useSWR<FrigateConfig>("config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cameraConfig = useMemo(() => {
 | 
				
			||||||
 | 
					    if (config && selectedCamera) {
 | 
				
			||||||
 | 
					      return config.cameras[selectedCamera];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [config, selectedCamera]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [changedValue, setChangedValue] = useState(false);
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [selectDetections, setSelectDetections] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // zones and labels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const zones = useMemo(() => {
 | 
				
			||||||
 | 
					    if (cameraConfig) {
 | 
				
			||||||
 | 
					      return Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
 | 
				
			||||||
 | 
					        camera: cameraConfig.name,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        objects: zoneData.objects,
 | 
				
			||||||
 | 
					        color: zoneData.color,
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [cameraConfig]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const alertsLabels = useMemo(() => {
 | 
				
			||||||
 | 
					    return cameraConfig?.review.alerts.labels
 | 
				
			||||||
 | 
					      ? cameraConfig.review.alerts.labels
 | 
				
			||||||
 | 
					          .map((label) => label.replaceAll("_", " "))
 | 
				
			||||||
 | 
					          .join(", ")
 | 
				
			||||||
 | 
					      : "";
 | 
				
			||||||
 | 
					  }, [cameraConfig]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const detectionsLabels = useMemo(() => {
 | 
				
			||||||
 | 
					    return cameraConfig?.review.detections.labels
 | 
				
			||||||
 | 
					      ? cameraConfig.review.detections.labels
 | 
				
			||||||
 | 
					          .map((label) => label.replaceAll("_", " "))
 | 
				
			||||||
 | 
					          .join(", ")
 | 
				
			||||||
 | 
					      : "";
 | 
				
			||||||
 | 
					  }, [cameraConfig]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const formSchema = z.object({
 | 
				
			||||||
 | 
					    alerts_zones: z.array(z.string()),
 | 
				
			||||||
 | 
					    detections_zones: z.array(z.string()),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<z.infer<typeof formSchema>>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    mode: "onChange",
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      alerts_zones: cameraConfig?.review.alerts.required_zones || [],
 | 
				
			||||||
 | 
					      detections_zones: cameraConfig?.review.detections.required_zones || [],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const watchedAlertsZones = form.watch("alerts_zones");
 | 
				
			||||||
 | 
					  const watchedDetectionsZones = form.watch("detections_zones");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCheckedChange = useCallback(
 | 
				
			||||||
 | 
					    (isChecked: boolean) => {
 | 
				
			||||||
 | 
					      if (!isChecked) {
 | 
				
			||||||
 | 
					        form.reset({
 | 
				
			||||||
 | 
					          alerts_zones: watchedAlertsZones,
 | 
				
			||||||
 | 
					          detections_zones:
 | 
				
			||||||
 | 
					            cameraConfig?.review.detections.required_zones || [],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setChangedValue(true);
 | 
				
			||||||
 | 
					      setSelectDetections(isChecked as boolean);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [watchedAlertsZones, cameraConfig, form],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const saveToConfig = useCallback(
 | 
				
			||||||
 | 
					    async (
 | 
				
			||||||
 | 
					      { alerts_zones, detections_zones }: CameraReviewSettingsValueType, // values submitted via the form
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					      const alertQueries = [...alerts_zones]
 | 
				
			||||||
 | 
					        .map(
 | 
				
			||||||
 | 
					          (zone) =>
 | 
				
			||||||
 | 
					            `&cameras.${selectedCamera}.review.alerts.required_zones=${zone}`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .join("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const detectionQueries = [...detections_zones]
 | 
				
			||||||
 | 
					        .map(
 | 
				
			||||||
 | 
					          (zone) =>
 | 
				
			||||||
 | 
					            `&cameras.${selectedCamera}.review.detections.required_zones=${zone}`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .join("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      axios
 | 
				
			||||||
 | 
					        .put(`config/set?${alertQueries}${detectionQueries}`, {
 | 
				
			||||||
 | 
					          requires_restart: 0,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then((res) => {
 | 
				
			||||||
 | 
					          if (res.status === 200) {
 | 
				
			||||||
 | 
					            toast.success(
 | 
				
			||||||
 | 
					              `Review classification configuration has been saved. Restart Frigate to apply changes.`,
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                position: "top-center",
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            updateConfig();
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            toast.error(`Failed to save config changes: ${res.statusText}`, {
 | 
				
			||||||
 | 
					              position: "top-center",
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          toast.error(
 | 
				
			||||||
 | 
					            `Failed to save config changes: ${error.response.data.message}`,
 | 
				
			||||||
 | 
					            { position: "top-center" },
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .finally(() => {
 | 
				
			||||||
 | 
					          setIsLoading(false);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [updateConfig, setIsLoading, selectedCamera],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onCancel = useCallback(() => {
 | 
				
			||||||
 | 
					    if (!cameraConfig) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setChangedValue(false);
 | 
				
			||||||
 | 
					    setUnsavedChanges(false);
 | 
				
			||||||
 | 
					    removeMessage(
 | 
				
			||||||
 | 
					      "camera_settings",
 | 
				
			||||||
 | 
					      `review_classification_settings_${selectedCamera}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    form.reset({
 | 
				
			||||||
 | 
					      alerts_zones: cameraConfig?.review.alerts.required_zones ?? [],
 | 
				
			||||||
 | 
					      detections_zones: cameraConfig?.review.detections.required_zones || [],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    setSelectDetections(
 | 
				
			||||||
 | 
					      !!cameraConfig?.review.detections.required_zones?.length,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, [removeMessage, selectedCamera, setUnsavedChanges, form, cameraConfig]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    onCancel();
 | 
				
			||||||
 | 
					    // we know that these deps are correct
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [selectedCamera]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (changedValue) {
 | 
				
			||||||
 | 
					      addMessage(
 | 
				
			||||||
 | 
					        "camera_settings",
 | 
				
			||||||
 | 
					        `Unsaved review classification settings for ${capitalizeFirstLetter(selectedCamera)}`,
 | 
				
			||||||
 | 
					        undefined,
 | 
				
			||||||
 | 
					        `review_classification_settings_${selectedCamera}`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      removeMessage(
 | 
				
			||||||
 | 
					        "camera_settings",
 | 
				
			||||||
 | 
					        `review_classification_settings_${selectedCamera}`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // we know that these deps are correct
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [changedValue, selectedCamera]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function onSubmit(values: z.infer<typeof formSchema>) {
 | 
				
			||||||
 | 
					    setIsLoading(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    saveToConfig(values as CameraReviewSettingsValueType);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    document.title = "Camera Settings - Frigate";
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!cameraConfig && !selectedCamera) {
 | 
				
			||||||
 | 
					    return <ActivityIndicator />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="flex size-full flex-col md:flex-row">
 | 
				
			||||||
 | 
					        <Toaster position="top-center" closeButton={true} />
 | 
				
			||||||
 | 
					        <div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
 | 
				
			||||||
 | 
					          <Heading as="h3" className="my-2">
 | 
				
			||||||
 | 
					            Camera Settings
 | 
				
			||||||
 | 
					          </Heading>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Separator className="my-2 flex bg-secondary" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Heading as="h4" className="my-2">
 | 
				
			||||||
 | 
					            Review Classification
 | 
				
			||||||
 | 
					          </Heading>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="max-w-6xl">
 | 
				
			||||||
 | 
					            <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
 | 
				
			||||||
 | 
					              <p>
 | 
				
			||||||
 | 
					                Frigate categorizes review items as Alerts and Detections. By
 | 
				
			||||||
 | 
					                default, all <em>person</em> and <em>car</em> objects are
 | 
				
			||||||
 | 
					                considered Alerts. You can refine categorization of your review
 | 
				
			||||||
 | 
					                items by configuring required zones for them.
 | 
				
			||||||
 | 
					              </p>
 | 
				
			||||||
 | 
					              <div className="flex items-center text-primary">
 | 
				
			||||||
 | 
					                <Link
 | 
				
			||||||
 | 
					                  to="https://docs.frigate.video/configuration/review"
 | 
				
			||||||
 | 
					                  target="_blank"
 | 
				
			||||||
 | 
					                  rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                  className="inline"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Read the Documentation{" "}
 | 
				
			||||||
 | 
					                  <LuExternalLink className="ml-2 inline-flex size-3" />
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Form {...form}>
 | 
				
			||||||
 | 
					            <form
 | 
				
			||||||
 | 
					              onSubmit={form.handleSubmit(onSubmit)}
 | 
				
			||||||
 | 
					              className="mt-2 space-y-6"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={cn(
 | 
				
			||||||
 | 
					                  "w-full max-w-5xl space-y-0",
 | 
				
			||||||
 | 
					                  zones &&
 | 
				
			||||||
 | 
					                    zones?.length > 0 &&
 | 
				
			||||||
 | 
					                    "grid items-start gap-5 md:grid-cols-2",
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <FormField
 | 
				
			||||||
 | 
					                  control={form.control}
 | 
				
			||||||
 | 
					                  name="alerts_zones"
 | 
				
			||||||
 | 
					                  render={() => (
 | 
				
			||||||
 | 
					                    <FormItem>
 | 
				
			||||||
 | 
					                      {zones && zones?.length > 0 ? (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                          <div className="mb-2">
 | 
				
			||||||
 | 
					                            <FormLabel className="flex flex-row items-center text-base">
 | 
				
			||||||
 | 
					                              Alerts{" "}
 | 
				
			||||||
 | 
					                              <MdCircle className="ml-3 size-2 text-severity_alert" />
 | 
				
			||||||
 | 
					                            </FormLabel>
 | 
				
			||||||
 | 
					                            <FormDescription>
 | 
				
			||||||
 | 
					                              Select zones for Alerts
 | 
				
			||||||
 | 
					                            </FormDescription>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                          <div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
 | 
				
			||||||
 | 
					                            {zones?.map((zone) => (
 | 
				
			||||||
 | 
					                              <FormField
 | 
				
			||||||
 | 
					                                key={zone.name}
 | 
				
			||||||
 | 
					                                control={form.control}
 | 
				
			||||||
 | 
					                                name="alerts_zones"
 | 
				
			||||||
 | 
					                                render={({ field }) => {
 | 
				
			||||||
 | 
					                                  return (
 | 
				
			||||||
 | 
					                                    <FormItem
 | 
				
			||||||
 | 
					                                      key={zone.name}
 | 
				
			||||||
 | 
					                                      className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                      <FormControl>
 | 
				
			||||||
 | 
					                                        <Checkbox
 | 
				
			||||||
 | 
					                                          className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
 | 
				
			||||||
 | 
					                                          checked={field.value?.includes(
 | 
				
			||||||
 | 
					                                            zone.name,
 | 
				
			||||||
 | 
					                                          )}
 | 
				
			||||||
 | 
					                                          onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
					                                            setChangedValue(true);
 | 
				
			||||||
 | 
					                                            return checked
 | 
				
			||||||
 | 
					                                              ? field.onChange([
 | 
				
			||||||
 | 
					                                                  ...field.value,
 | 
				
			||||||
 | 
					                                                  zone.name,
 | 
				
			||||||
 | 
					                                                ])
 | 
				
			||||||
 | 
					                                              : field.onChange(
 | 
				
			||||||
 | 
					                                                  field.value?.filter(
 | 
				
			||||||
 | 
					                                                    (value) =>
 | 
				
			||||||
 | 
					                                                      value !== zone.name,
 | 
				
			||||||
 | 
					                                                  ),
 | 
				
			||||||
 | 
					                                                );
 | 
				
			||||||
 | 
					                                          }}
 | 
				
			||||||
 | 
					                                        />
 | 
				
			||||||
 | 
					                                      </FormControl>
 | 
				
			||||||
 | 
					                                      <FormLabel className="font-normal capitalize">
 | 
				
			||||||
 | 
					                                        {zone.name.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					                                      </FormLabel>
 | 
				
			||||||
 | 
					                                    </FormItem>
 | 
				
			||||||
 | 
					                                  );
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                              />
 | 
				
			||||||
 | 
					                            ))}
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                      ) : (
 | 
				
			||||||
 | 
					                        <div className="font-normal text-destructive">
 | 
				
			||||||
 | 
					                          No zones are defined for this camera.
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                      <FormMessage />
 | 
				
			||||||
 | 
					                      <div className="text-sm">
 | 
				
			||||||
 | 
					                        All {alertsLabels} objects
 | 
				
			||||||
 | 
					                        {watchedAlertsZones && watchedAlertsZones.length > 0
 | 
				
			||||||
 | 
					                          ? ` detected in ${watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone).replaceAll("_", " ")).join(", ")}`
 | 
				
			||||||
 | 
					                          : ""}{" "}
 | 
				
			||||||
 | 
					                        on{" "}
 | 
				
			||||||
 | 
					                        {capitalizeFirstLetter(
 | 
				
			||||||
 | 
					                          cameraConfig?.name ?? "",
 | 
				
			||||||
 | 
					                        ).replaceAll("_", " ")}{" "}
 | 
				
			||||||
 | 
					                        will be shown as Alerts.
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </FormItem>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <FormField
 | 
				
			||||||
 | 
					                  control={form.control}
 | 
				
			||||||
 | 
					                  name="detections_zones"
 | 
				
			||||||
 | 
					                  render={() => (
 | 
				
			||||||
 | 
					                    <FormItem>
 | 
				
			||||||
 | 
					                      {zones && zones?.length > 0 && (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                          <div className="mb-2">
 | 
				
			||||||
 | 
					                            <FormLabel className="flex flex-row items-center text-base">
 | 
				
			||||||
 | 
					                              Detections{" "}
 | 
				
			||||||
 | 
					                              <MdCircle className="ml-3 size-2 text-severity_detection" />
 | 
				
			||||||
 | 
					                            </FormLabel>
 | 
				
			||||||
 | 
					                            {selectDetections && (
 | 
				
			||||||
 | 
					                              <FormDescription>
 | 
				
			||||||
 | 
					                                Select zones for Detections
 | 
				
			||||||
 | 
					                              </FormDescription>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          {selectDetections && (
 | 
				
			||||||
 | 
					                            <div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
 | 
				
			||||||
 | 
					                              {zones?.map((zone) => (
 | 
				
			||||||
 | 
					                                <FormField
 | 
				
			||||||
 | 
					                                  key={zone.name}
 | 
				
			||||||
 | 
					                                  control={form.control}
 | 
				
			||||||
 | 
					                                  name="detections_zones"
 | 
				
			||||||
 | 
					                                  render={({ field }) => {
 | 
				
			||||||
 | 
					                                    return (
 | 
				
			||||||
 | 
					                                      <FormItem
 | 
				
			||||||
 | 
					                                        key={zone.name}
 | 
				
			||||||
 | 
					                                        className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
 | 
				
			||||||
 | 
					                                      >
 | 
				
			||||||
 | 
					                                        <FormControl>
 | 
				
			||||||
 | 
					                                          <Checkbox
 | 
				
			||||||
 | 
					                                            className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
 | 
				
			||||||
 | 
					                                            checked={field.value?.includes(
 | 
				
			||||||
 | 
					                                              zone.name,
 | 
				
			||||||
 | 
					                                            )}
 | 
				
			||||||
 | 
					                                            onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
					                                              return checked
 | 
				
			||||||
 | 
					                                                ? field.onChange([
 | 
				
			||||||
 | 
					                                                    ...field.value,
 | 
				
			||||||
 | 
					                                                    zone.name,
 | 
				
			||||||
 | 
					                                                  ])
 | 
				
			||||||
 | 
					                                                : field.onChange(
 | 
				
			||||||
 | 
					                                                    field.value?.filter(
 | 
				
			||||||
 | 
					                                                      (value) =>
 | 
				
			||||||
 | 
					                                                        value !== zone.name,
 | 
				
			||||||
 | 
					                                                    ),
 | 
				
			||||||
 | 
					                                                  );
 | 
				
			||||||
 | 
					                                            }}
 | 
				
			||||||
 | 
					                                          />
 | 
				
			||||||
 | 
					                                        </FormControl>
 | 
				
			||||||
 | 
					                                        <FormLabel className="font-normal capitalize">
 | 
				
			||||||
 | 
					                                          {zone.name.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					                                        </FormLabel>
 | 
				
			||||||
 | 
					                                      </FormItem>
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                  }}
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                              ))}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                          )}
 | 
				
			||||||
 | 
					                          <FormMessage />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          <div className="mb-0 flex flex-row items-center gap-2">
 | 
				
			||||||
 | 
					                            <Checkbox
 | 
				
			||||||
 | 
					                              id="select-detections"
 | 
				
			||||||
 | 
					                              className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
 | 
				
			||||||
 | 
					                              checked={selectDetections}
 | 
				
			||||||
 | 
					                              onCheckedChange={handleCheckedChange}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <div className="grid gap-1.5 leading-none">
 | 
				
			||||||
 | 
					                              <label
 | 
				
			||||||
 | 
					                                htmlFor="select-detections"
 | 
				
			||||||
 | 
					                                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
 | 
				
			||||||
 | 
					                              >
 | 
				
			||||||
 | 
					                                Limit detections to specific zones
 | 
				
			||||||
 | 
					                              </label>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <div className="text-sm">
 | 
				
			||||||
 | 
					                        All {detectionsLabels} objects{" "}
 | 
				
			||||||
 | 
					                        <em>not classified as Alerts</em>{" "}
 | 
				
			||||||
 | 
					                        {watchedDetectionsZones &&
 | 
				
			||||||
 | 
					                        watchedDetectionsZones.length > 0
 | 
				
			||||||
 | 
					                          ? ` that are detected in ${watchedDetectionsZones.map((zone) => capitalizeFirstLetter(zone).replaceAll("_", " ")).join(", ")}`
 | 
				
			||||||
 | 
					                          : ""}{" "}
 | 
				
			||||||
 | 
					                        on{" "}
 | 
				
			||||||
 | 
					                        {capitalizeFirstLetter(
 | 
				
			||||||
 | 
					                          cameraConfig?.name ?? "",
 | 
				
			||||||
 | 
					                        ).replaceAll("_", " ")}{" "}
 | 
				
			||||||
 | 
					                        will be shown as Detections
 | 
				
			||||||
 | 
					                        {(!selectDetections ||
 | 
				
			||||||
 | 
					                          (watchedDetectionsZones &&
 | 
				
			||||||
 | 
					                            watchedDetectionsZones.length === 0)) &&
 | 
				
			||||||
 | 
					                          ", regardless of zone"}
 | 
				
			||||||
 | 
					                        .
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </FormItem>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <Separator className="my-2 flex bg-secondary" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  className="flex flex-1"
 | 
				
			||||||
 | 
					                  onClick={onCancel}
 | 
				
			||||||
 | 
					                  type="button"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Cancel
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  variant="select"
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                  className="flex flex-1"
 | 
				
			||||||
 | 
					                  type="submit"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {isLoading ? (
 | 
				
			||||||
 | 
					                    <div className="flex flex-row items-center gap-2">
 | 
				
			||||||
 | 
					                      <ActivityIndicator />
 | 
				
			||||||
 | 
					                      <span>Saving...</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  ) : (
 | 
				
			||||||
 | 
					                    "Save"
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					          </Form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user