diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 3d6a40c39..9193d9c2d 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -143,20 +143,6 @@ export default function ZoneEditPane({ config?.cameras[polygon.camera]?.zones[polygon.name]?.loitering_time, isFinished: polygon?.isFinished ?? false, 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, loitering_time, objects: form_objects, - review_alerts, - review_detections, }: ZoneFormValuesType, // values submitted via the form objects: string[], ) => { @@ -176,11 +160,21 @@ export default function ZoneEditPane({ return; } let mutatedConfig = config; + let alertQueries = ""; + let detectionQueries = ""; const renamingZone = zoneName != polygon.name && polygon.name != ""; if (renamingZone) { // 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 { alertQueries: renameAlertQueries, detectionQueries: renameDetectionQueries, @@ -209,6 +203,18 @@ export default function ZoneEditPane({ }); 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( @@ -233,17 +239,6 @@ export default function ZoneEditPane({ 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 = ""; if (inertia) { inertiaQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.inertia=${inertia}`; @@ -449,52 +444,6 @@ export default function ZoneEditPane({ /> - - - ( - -
- Alerts - - When an object enters this zone, ensure it is marked as an - alert. - -
- - - -
- )} - /> - ( - -
- Detections - - When an object enters this zone, ensure it is marked as a - detection. - -
- - - -
- )} - /> >; }; +type CameraReviewSettingsValueType = { + alerts_zones: string[]; + detections_zones: string[]; +}; + export default function CameraSettingsView({ selectedCamera, setUnsavedChanges, @@ -52,6 +57,8 @@ export default function CameraSettingsView({ const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; + // zones and labels + const zones = useMemo(() => { if (cameraConfig) { return Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ @@ -63,6 +70,20 @@ export default function CameraSettingsView({ } }, [cameraConfig]); + const alertsLabels = useMemo(() => { + return cameraConfig?.review.alerts.labels + ? cameraConfig.review.alerts.labels.join(", ") + : ""; + }, [cameraConfig]); + + const detectionsLabels = useMemo(() => { + return cameraConfig?.review.detections.labels + ? cameraConfig.review.detections.labels.join(", ") + : ""; + }, [cameraConfig]); + + // form + const formSchema = z.object({ alerts_zones: z.array(z.string()), detections_zones: z.array(z.string()), @@ -80,16 +101,6 @@ export default function CameraSettingsView({ const watchedAlertsZones = form.watch("alerts_zones"); const watchedDetectionsZones = form.watch("detections_zones"); - useEffect(() => { - form.reset({ - alerts_zones: cameraConfig?.review.alerts.required_zones ?? [], - detections_zones: cameraConfig?.review.detections.required_zones || [], - }); - setSelectDetections( - !!cameraConfig?.review.detections.required_zones?.length, - ); - }, [cameraConfig, form]); - const handleCheckedChange = useCallback( (isChecked: boolean) => { if (!isChecked) { @@ -99,56 +110,38 @@ export default function CameraSettingsView({ cameraConfig?.review.detections.required_zones || [], }); } + setChangedValue(true); setSelectDetections(isChecked as boolean); }, [watchedAlertsZones, cameraConfig, form], ); - const alertsLabels = useMemo(() => { - return cameraConfig?.review.alerts.labels - ? cameraConfig.review.alerts.labels.join(", ") - : ""; - }, [cameraConfig]); - - const detectionsLabels = useMemo(() => { - return cameraConfig?.review.detections.labels - ? cameraConfig.review.detections.labels.join(", ") - : ""; - }, [cameraConfig]); - const saveToConfig = useCallback( async ( - { - name: zoneName, - review_alerts, - review_detections, - }: CameraSettingsValuesType, // values submitted via the form + { alerts_zones, detections_zones }: CameraReviewSettingsValueType, // values submitted via the form ) => { - if (!zoneName) { - return; - } - let mutatedConfig = config; + const alertQueries = [...alerts_zones] + .map( + (zone) => + `&cameras.${selectedCamera}.review.alerts.required_zones=${zone}`, + ) + .join(""); - const { alertQueries, detectionQueries } = reviewQueries( - zoneName, - review_alerts, - review_detections, - selectedCamera, - mutatedConfig?.cameras[selectedCamera]?.review.alerts.required_zones || - [], - mutatedConfig?.cameras[selectedCamera]?.review.detections - .required_zones || [], - ); + const detectionQueries = [...detections_zones] + .map( + (zone) => + `&cameras.${selectedCamera}.review.detections.required_zones=${zone}`, + ) + .join(""); axios - .put( - `config/set?cameras.${selectedCamera}.zones.${zoneName}?????${alertQueries}${detectionQueries}`, - { requires_restart: 0 }, - ) + .put(`config/set?${alertQueries}${detectionQueries}`, { + requires_restart: 0, + }) .then((res) => { if (res.status === 200) { toast.success( - `Zone (${zoneName}) has been saved. Restart Frigate to apply changes.`, + `Review classification configuration has been saved. Restart Frigate to apply changes.`, { position: "top-center", }, @@ -170,29 +163,47 @@ export default function CameraSettingsView({ setIsLoading(false); }); }, - [config, updateConfig, setIsLoading, selectedCamera], + [updateConfig, setIsLoading, selectedCamera], ); const onCancel = useCallback(() => { + if (!cameraConfig) { + return; + } + setChangedValue(false); + setUnsavedChanges(false); removeMessage( "camera_settings", - `alert_detection_settings_${selectedCamera}`, + `review_classification_settings_${selectedCamera}`, ); - }, [removeMessage, 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( - "motion_tuner", - `Unsaved changes to alert/detection settings for (${selectedCamera})`, + "camera_settings", + `Unsaved review classification settings for ${capitalizeFirstLetter(selectedCamera)}`, undefined, - `alert_detection_settings_${selectedCamera}`, + `review_classification_settings_${selectedCamera}`, ); } else { removeMessage( "camera_settings", - `alert_detection_settings_${selectedCamera}`, + `review_classification_settings_${selectedCamera}`, ); } // we know that these deps are correct @@ -202,7 +213,7 @@ export default function CameraSettingsView({ function onSubmit(values: z.infer) { setIsLoading(true); - saveToConfig(values as CameraSettingsValuesType); + saveToConfig(values as CameraReviewSettingsValueType); } useEffect(() => { @@ -228,25 +239,25 @@ export default function CameraSettingsView({ Review Classification -
-

- Not every segment of video captured by Frigate may be of the same - level of interest to you. Frigate categorizes review items as - alerts and detections. By default, all person and{" "} - car objects are considered alerts. You can refine - categorization of your review items by configuring required zones - for them. -

-
- - Read the Documentation{" "} - - +
+
+

+ Frigate categorizes review items as Alerts and Detections. By + default, all person and car objects are + considered Alerts. You can refine categorization of your review + items by configuring required zones for them. +

+
+ + Read the Documentation{" "} + + +
@@ -255,7 +266,14 @@ export default function CameraSettingsView({ onSubmit={form.handleSubmit(onSubmit)} className="mt-2 space-y-6" > -
+
0 && + "grid items-start gap-5 md:grid-cols-2", + )} + >
-
+
{zones?.map((zone) => ( { + setChangedValue(true); return checked ? field.onChange([ ...field.value, @@ -316,12 +335,12 @@ export default function CameraSettingsView({
) : ( -
+
No zones are defined for this camera.
)} -
+
All {alertsLabels} objects {watchedAlertsZones && watchedAlertsZones.length > 0 ? ` detected in ${watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}` @@ -341,43 +360,22 @@ export default function CameraSettingsView({ name="detections_zones" render={() => ( - {zones && zones?.length > 0 ? ( + {zones && zones?.length > 0 && ( <> -
-
- - Detections{" "} - - -
-
- -
- -
-
+
+ + Detections{" "} + + + {selectDetections && ( + + Select zones for Detections + + )}
{selectDetections && ( -
- - Select zones for Detections - -
- )} - - {selectDetections && ( -
+
{zones?.map((zone) => ( )} + +
+ +
+ +
+
- ) : ( - "" )} -
- All {detectionsLabels} objects not classified as Alerts{" "} +
+ All {detectionsLabels} objects{" "} + not classified as Alerts{" "} {watchedDetectionsZones && watchedDetectionsZones.length > 0 ? ` that are detected in ${watchedDetectionsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}` @@ -437,15 +451,24 @@ export default function CameraSettingsView({ cameraConfig?.name ?? "", ).replaceAll("_", " ")}{" "} will be shown as Detections - {!selectDetections && ", regardless of zone"}. + {(!selectDetections || + (watchedDetectionsZones && + watchedDetectionsZones.length === 0)) && + ", regardless of zone"} + .
)} />
+ -
-