flxes, beautifying, zone renaming, clean up

This commit is contained in:
Josh Hawkins 2024-07-12 08:13:59 -05:00
parent 998a734fe3
commit d79e54265f
3 changed files with 167 additions and 199 deletions

View File

@ -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"

View File

@ -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 = {

View File

@ -21,18 +21,23 @@ import { FrigateConfig } from "@/types/frigateConfig";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { reviewQueries } from "@/utils/zoneEdutUtil";
import axios from "axios"; import axios from "axios";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu"; import { LuExternalLink } from "react-icons/lu";
import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { cn } from "@/lib/utils";
type CameraSettingsViewProps = { type CameraSettingsViewProps = {
selectedCamera: string; selectedCamera: string;
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>; setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
}; };
type CameraReviewSettingsValueType = {
alerts_zones: string[];
detections_zones: string[];
};
export default function CameraSettingsView({ export default function CameraSettingsView({
selectedCamera, selectedCamera,
setUnsavedChanges, setUnsavedChanges,
@ -52,6 +57,8 @@ export default function CameraSettingsView({
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
// zones and labels
const zones = useMemo(() => { const zones = useMemo(() => {
if (cameraConfig) { if (cameraConfig) {
return Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ return Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
@ -63,6 +70,20 @@ export default function CameraSettingsView({
} }
}, [cameraConfig]); }, [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({ const formSchema = z.object({
alerts_zones: z.array(z.string()), alerts_zones: z.array(z.string()),
detections_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 watchedAlertsZones = form.watch("alerts_zones");
const watchedDetectionsZones = form.watch("detections_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( const handleCheckedChange = useCallback(
(isChecked: boolean) => { (isChecked: boolean) => {
if (!isChecked) { if (!isChecked) {
@ -99,56 +110,38 @@ export default function CameraSettingsView({
cameraConfig?.review.detections.required_zones || [], cameraConfig?.review.detections.required_zones || [],
}); });
} }
setChangedValue(true);
setSelectDetections(isChecked as boolean); setSelectDetections(isChecked as boolean);
}, },
[watchedAlertsZones, cameraConfig, form], [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( const saveToConfig = useCallback(
async ( async (
{ { alerts_zones, detections_zones }: CameraReviewSettingsValueType, // values submitted via the form
name: zoneName,
review_alerts,
review_detections,
}: CameraSettingsValuesType, // values submitted via the form
) => { ) => {
if (!zoneName) { const alertQueries = [...alerts_zones]
return; .map(
} (zone) =>
let mutatedConfig = config; `&cameras.${selectedCamera}.review.alerts.required_zones=${zone}`,
)
.join("");
const { alertQueries, detectionQueries } = reviewQueries( const detectionQueries = [...detections_zones]
zoneName, .map(
review_alerts, (zone) =>
review_detections, `&cameras.${selectedCamera}.review.detections.required_zones=${zone}`,
selectedCamera, )
mutatedConfig?.cameras[selectedCamera]?.review.alerts.required_zones || .join("");
[],
mutatedConfig?.cameras[selectedCamera]?.review.detections
.required_zones || [],
);
axios axios
.put( .put(`config/set?${alertQueries}${detectionQueries}`, {
`config/set?cameras.${selectedCamera}.zones.${zoneName}?????${alertQueries}${detectionQueries}`, requires_restart: 0,
{ requires_restart: 0 }, })
)
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success( 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", position: "top-center",
}, },
@ -170,29 +163,47 @@ export default function CameraSettingsView({
setIsLoading(false); setIsLoading(false);
}); });
}, },
[config, updateConfig, setIsLoading, selectedCamera], [updateConfig, setIsLoading, selectedCamera],
); );
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
if (!cameraConfig) {
return;
}
setChangedValue(false); setChangedValue(false);
setUnsavedChanges(false);
removeMessage( removeMessage(
"camera_settings", "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(() => { useEffect(() => {
if (changedValue) { if (changedValue) {
addMessage( addMessage(
"motion_tuner", "camera_settings",
`Unsaved changes to alert/detection settings for (${selectedCamera})`, `Unsaved review classification settings for ${capitalizeFirstLetter(selectedCamera)}`,
undefined, undefined,
`alert_detection_settings_${selectedCamera}`, `review_classification_settings_${selectedCamera}`,
); );
} else { } else {
removeMessage( removeMessage(
"camera_settings", "camera_settings",
`alert_detection_settings_${selectedCamera}`, `review_classification_settings_${selectedCamera}`,
); );
} }
// we know that these deps are correct // we know that these deps are correct
@ -202,7 +213,7 @@ export default function CameraSettingsView({
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true); setIsLoading(true);
saveToConfig(values as CameraSettingsValuesType); saveToConfig(values as CameraReviewSettingsValueType);
} }
useEffect(() => { useEffect(() => {
@ -228,25 +239,25 @@ export default function CameraSettingsView({
Review Classification Review Classification
</Heading> </Heading>
<div className="mb-5 mt-2 flex flex-col gap-2 text-sm text-primary-variant"> <div className="max-w-6xl">
<p> <div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
Not every segment of video captured by Frigate may be of the same <p>
level of interest to you. Frigate categorizes review items as Frigate categorizes review items as Alerts and Detections. By
alerts and detections. By default, all <em>person</em> and{" "} default, all <em>person</em> and <em>car</em> objects are
<em>car</em> objects are considered alerts. You can refine considered Alerts. You can refine categorization of your review
categorization of your review items by configuring required zones items by configuring required zones for them.
for them. </p>
</p> <div className="flex items-center text-primary">
<div className="flex items-center text-primary"> <Link
<Link to="https://docs.frigate.video/configuration/review"
to="https://docs.frigate.video/configuration/review" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="inline"
className="inline" >
> Read the Documentation{" "}
Read the Documentation{" "} <LuExternalLink className="ml-2 inline-flex size-3" />
<LuExternalLink className="ml-2 inline-flex size-3" /> </Link>
</Link> </div>
</div> </div>
</div> </div>
@ -255,7 +266,14 @@ export default function CameraSettingsView({
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="mt-2 space-y-6" className="mt-2 space-y-6"
> >
<div className="grid items-start gap-5 md:grid-cols-2"> <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 <FormField
control={form.control} control={form.control}
name="alerts_zones" name="alerts_zones"
@ -272,7 +290,7 @@ export default function CameraSettingsView({
Select zones for Alerts Select zones for Alerts
</FormDescription> </FormDescription>
</div> </div>
<div className="rounded-lg bg-secondary p-4"> <div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
{zones?.map((zone) => ( {zones?.map((zone) => (
<FormField <FormField
key={zone.name} key={zone.name}
@ -282,15 +300,16 @@ export default function CameraSettingsView({
return ( return (
<FormItem <FormItem
key={zone.name} key={zone.name}
className="mb-3 flex flex-row items-start space-x-3 space-y-0 last:mb-0" className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
> >
<FormControl> <FormControl>
<Checkbox <Checkbox
className="data-[state=checked]:bg-selected data-[state=checked]:text-primary" className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
checked={field.value?.includes( checked={field.value?.includes(
zone.name, zone.name,
)} )}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setChangedValue(true);
return checked return checked
? field.onChange([ ? field.onChange([
...field.value, ...field.value,
@ -316,12 +335,12 @@ export default function CameraSettingsView({
</div> </div>
</> </>
) : ( ) : (
<div className="font-normal"> <div className="font-normal text-destructive">
No zones are defined for this camera. No zones are defined for this camera.
</div> </div>
)} )}
<FormMessage /> <FormMessage />
<div className="flex flex-row text-sm"> <div className="text-sm">
All {alertsLabels} objects All {alertsLabels} objects
{watchedAlertsZones && watchedAlertsZones.length > 0 {watchedAlertsZones && watchedAlertsZones.length > 0
? ` detected in ${watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}` ? ` detected in ${watchedAlertsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}`
@ -341,43 +360,22 @@ export default function CameraSettingsView({
name="detections_zones" name="detections_zones"
render={() => ( render={() => (
<FormItem> <FormItem>
{zones && zones?.length > 0 ? ( {zones && zones?.length > 0 && (
<> <>
<div className="items-top flex flex-col space-x-0"> <div className="mb-2">
<div className="mb-4"> <FormLabel className="flex flex-row items-center text-base">
<FormLabel className="flex flex-row items-center text-base"> Detections{" "}
Detections{" "} <MdCircle className="ml-3 size-2 text-severity_detection" />
<MdCircle className="ml-3 size-2 text-severity_detection" /> </FormLabel>
</FormLabel> {selectDetections && (
</div> <FormDescription>
<div className="mb-1 flex flex-row gap-2"> Select zones for Detections
<Checkbox </FormDescription>
id="select-detections" )}
className="data-[state=checked]:bg-selected data-[state=checked]:text-primary"
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> </div>
{selectDetections && ( {selectDetections && (
<div className="mb-4"> <div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
<FormDescription className="mb-2">
Select zones for Detections
</FormDescription>
</div>
)}
{selectDetections && (
<div className="rounded-lg bg-secondary p-4">
{zones?.map((zone) => ( {zones?.map((zone) => (
<FormField <FormField
key={zone.name} key={zone.name}
@ -387,11 +385,11 @@ export default function CameraSettingsView({
return ( return (
<FormItem <FormItem
key={zone.name} key={zone.name}
className="mb-3 flex flex-row items-start space-x-3 space-y-0 last:mb-0" className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
> >
<FormControl> <FormControl>
<Checkbox <Checkbox
className="data-[state=checked]:bg-selected data-[state=checked]:text-primary" className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
checked={field.value?.includes( checked={field.value?.includes(
zone.name, zone.name,
)} )}
@ -421,13 +419,29 @@ export default function CameraSettingsView({
</div> </div>
)} )}
<FormMessage /> <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="flex flex-row text-sm"> <div className="text-sm">
All {detectionsLabels} objects not classified as Alerts{" "} All {detectionsLabels} objects{" "}
<em>not classified as Alerts</em>{" "}
{watchedDetectionsZones && {watchedDetectionsZones &&
watchedDetectionsZones.length > 0 watchedDetectionsZones.length > 0
? ` that are detected in ${watchedDetectionsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}` ? ` that are detected in ${watchedDetectionsZones.map((zone) => capitalizeFirstLetter(zone)).join(", ")}`
@ -437,15 +451,24 @@ export default function CameraSettingsView({
cameraConfig?.name ?? "", cameraConfig?.name ?? "",
).replaceAll("_", " ")}{" "} ).replaceAll("_", " ")}{" "}
will be shown as Detections will be shown as Detections
{!selectDetections && ", regardless of zone"}. {(!selectDetections ||
(watchedDetectionsZones &&
watchedDetectionsZones.length === 0)) &&
", regardless of zone"}
.
</div> </div>
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
<Separator className="my-2 flex bg-secondary" />
<div className="flex flex-row gap-2 pt-5"> <div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button className="flex flex-1" onClick={onCancel}> <Button
className="flex flex-1"
onClick={onCancel}
type="button"
>
Cancel Cancel
</Button> </Button>
<Button <Button
@ -466,8 +489,6 @@ export default function CameraSettingsView({
</div> </div>
</form> </form>
</Form> </Form>
<Separator className="my-2 flex bg-secondary" />
</div> </div>
</div> </div>
</> </>