mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 16:42:18 +03:00
add global sections, camera config overrides, and reset button
This commit is contained in:
@@ -16,11 +16,11 @@ export interface ConfigFormProps {
|
||||
/** JSON Schema for the form */
|
||||
schema: RJSFSchema;
|
||||
/** Current form data */
|
||||
formData?: Record<string, unknown>;
|
||||
formData?: unknown;
|
||||
/** Called when form data changes */
|
||||
onChange?: (data: Record<string, unknown>) => void;
|
||||
onChange?: (data: unknown) => void;
|
||||
/** Called when form is submitted */
|
||||
onSubmit?: (data: Record<string, unknown>) => void;
|
||||
onSubmit?: (data: unknown) => void;
|
||||
/** Called when form has errors on submit */
|
||||
onError?: (errors: unknown[]) => void;
|
||||
/** Additional uiSchema overrides */
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Audio Transcription Section Component
|
||||
// Global and camera-level audio transcription settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const AudioTranscriptionSection = createConfigSection({
|
||||
sectionPath: "audio_transcription",
|
||||
i18nNamespace: "config/audio_transcription",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
advancedFields: ["language", "device", "model_size"],
|
||||
overrideFields: ["enabled", "live_enabled"],
|
||||
},
|
||||
});
|
||||
|
||||
export default AudioTranscriptionSection;
|
||||
@@ -1,7 +1,7 @@
|
||||
// Base Section Component for config form sections
|
||||
// Used as a foundation for reusable section components
|
||||
|
||||
import { useMemo, useCallback, useState } from "react";
|
||||
import { useMemo, useCallback, useState, useEffect, useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
@@ -43,6 +43,8 @@ export interface SectionConfig {
|
||||
hiddenFields?: string[];
|
||||
/** Fields to show in advanced section */
|
||||
advancedFields?: string[];
|
||||
/** Fields to compare for override detection */
|
||||
overrideFields?: string[];
|
||||
/** Additional uiSchema overrides */
|
||||
uiSchema?: UiSchema;
|
||||
}
|
||||
@@ -98,6 +100,17 @@ export function createConfigSection({
|
||||
review: "review",
|
||||
audio: "audio",
|
||||
notifications: "notifications",
|
||||
live: "live",
|
||||
timestamp_style: "timestamp_style",
|
||||
audio_transcription: "audio_transcription",
|
||||
birdseye: "birdseye",
|
||||
face_recognition: "face_recognition",
|
||||
ffmpeg: "ffmpeg",
|
||||
lpr: "lpr",
|
||||
semantic_search: "semantic_search",
|
||||
mqtt: "mqtt",
|
||||
onvif: "onvif",
|
||||
ui: "ui",
|
||||
};
|
||||
|
||||
const ConfigSection = function ConfigSection({
|
||||
@@ -120,6 +133,8 @@ export function createConfigSection({
|
||||
unknown
|
||||
> | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
const isResettingRef = useRef(false);
|
||||
|
||||
const updateTopic =
|
||||
level === "camera" && cameraName
|
||||
@@ -143,6 +158,7 @@ export function createConfigSection({
|
||||
config,
|
||||
cameraName: level === "camera" ? cameraName : undefined,
|
||||
sectionPath,
|
||||
compareFields: sectionConfig.overrideFields,
|
||||
});
|
||||
|
||||
// Get current form data
|
||||
@@ -193,6 +209,18 @@ export function createConfigSection({
|
||||
return applySchemaDefaults(sectionSchema, {});
|
||||
}, [sectionSchema]);
|
||||
|
||||
// Clear pendingData whenever formData changes (e.g., from server refresh)
|
||||
// This prevents RJSF's initial onChange call from being treated as a user edit
|
||||
useEffect(() => {
|
||||
setPendingData(null);
|
||||
}, [formData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResettingRef.current) {
|
||||
isResettingRef.current = false;
|
||||
}
|
||||
}, [formKey]);
|
||||
|
||||
const buildOverrides = useCallback(
|
||||
(
|
||||
current: unknown,
|
||||
@@ -266,8 +294,18 @@ export function createConfigSection({
|
||||
|
||||
// Handle form data change
|
||||
const handleChange = useCallback(
|
||||
(data: Record<string, unknown>) => {
|
||||
const sanitizedData = sanitizeSectionData(data);
|
||||
(data: unknown) => {
|
||||
if (isResettingRef.current) {
|
||||
setPendingData(null);
|
||||
return;
|
||||
}
|
||||
if (!data || typeof data !== "object") {
|
||||
setPendingData(null);
|
||||
return;
|
||||
}
|
||||
const sanitizedData = sanitizeSectionData(
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
if (isEqual(formData, sanitizedData)) {
|
||||
setPendingData(null);
|
||||
return;
|
||||
@@ -277,6 +315,12 @@ export function createConfigSection({
|
||||
[formData, sanitizeSectionData],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
isResettingRef.current = true;
|
||||
setPendingData(null);
|
||||
setFormKey((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
// Handle save button click
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!pendingData) return;
|
||||
@@ -417,6 +461,7 @@ export function createConfigSection({
|
||||
const sectionContent = (
|
||||
<div className="space-y-6">
|
||||
<ConfigForm
|
||||
key={formKey}
|
||||
schema={sectionSchema}
|
||||
formData={pendingData || formData}
|
||||
onChange={handleChange}
|
||||
@@ -454,16 +499,28 @@ export function createConfigSection({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuSave className="h-4 w-4" />
|
||||
{isSaving
|
||||
? t("saving", { ns: "common", defaultValue: "Saving..." })
|
||||
: t("save", { ns: "common", defaultValue: "Save" })}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
variant="outline"
|
||||
disabled={isSaving || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
{t("reset", { ns: "common", defaultValue: "Reset" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuSave className="h-4 w-4" />
|
||||
{isSaving
|
||||
? t("saving", { ns: "common", defaultValue: "Saving..." })
|
||||
: t("save", { ns: "common", defaultValue: "Save" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Birdseye Section Component
|
||||
// Camera-level birdseye settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const BirdseyeSection = createConfigSection({
|
||||
sectionPath: "birdseye",
|
||||
i18nNamespace: "config/birdseye",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["enabled", "mode", "order"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
overrideFields: ["enabled", "mode"],
|
||||
},
|
||||
});
|
||||
|
||||
export default BirdseyeSection;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Camera MQTT Section Component
|
||||
// Camera-specific MQTT image publishing settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const CameraMqttSection = createConfigSection({
|
||||
sectionPath: "mqtt",
|
||||
i18nNamespace: "config/camera_mqtt",
|
||||
defaultConfig: {
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
"timestamp",
|
||||
"bounding_box",
|
||||
"crop",
|
||||
"height",
|
||||
"required_zones",
|
||||
"quality",
|
||||
],
|
||||
hiddenFields: [],
|
||||
advancedFields: ["height", "quality"],
|
||||
overrideFields: [],
|
||||
},
|
||||
});
|
||||
|
||||
export default CameraMqttSection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Camera UI Section Component
|
||||
// Camera UI display settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const CameraUiSection = createConfigSection({
|
||||
sectionPath: "ui",
|
||||
i18nNamespace: "config/camera_ui",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["dashboard", "order"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
overrideFields: [],
|
||||
},
|
||||
});
|
||||
|
||||
export default CameraUiSection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Face Recognition Section Component
|
||||
// Camera-level face recognition settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const FaceRecognitionSection = createConfigSection({
|
||||
sectionPath: "face_recognition",
|
||||
i18nNamespace: "config/face_recognition",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["enabled", "min_area"],
|
||||
hiddenFields: [],
|
||||
advancedFields: ["min_area"],
|
||||
overrideFields: ["enabled", "min_area"],
|
||||
},
|
||||
});
|
||||
|
||||
export default FaceRecognitionSection;
|
||||
@@ -0,0 +1,44 @@
|
||||
// FFmpeg Section Component
|
||||
// Global and camera-level FFmpeg settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const FfmpegSection = createConfigSection({
|
||||
sectionPath: "ffmpeg",
|
||||
i18nNamespace: "config/ffmpeg",
|
||||
defaultConfig: {
|
||||
fieldOrder: [
|
||||
"inputs",
|
||||
"path",
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
hiddenFields: [],
|
||||
advancedFields: [
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
overrideFields: [
|
||||
"path",
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export default FfmpegSection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// License Plate Recognition Section Component
|
||||
// Camera-level LPR settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const LprSection = createConfigSection({
|
||||
sectionPath: "lpr",
|
||||
i18nNamespace: "config/lpr",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
|
||||
hiddenFields: [],
|
||||
advancedFields: ["expire_time", "min_area", "enhancement"],
|
||||
overrideFields: ["enabled", "min_area", "enhancement"],
|
||||
},
|
||||
});
|
||||
|
||||
export default LprSection;
|
||||
@@ -0,0 +1,25 @@
|
||||
// ONVIF Section Component
|
||||
// Camera-level ONVIF and autotracking settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const OnvifSection = createConfigSection({
|
||||
sectionPath: "onvif",
|
||||
i18nNamespace: "config/onvif",
|
||||
defaultConfig: {
|
||||
fieldOrder: [
|
||||
"host",
|
||||
"port",
|
||||
"user",
|
||||
"password",
|
||||
"tls_insecure",
|
||||
"autotracking",
|
||||
"ignore_time_mismatch",
|
||||
],
|
||||
hiddenFields: ["autotracking.enabled_in_config"],
|
||||
advancedFields: ["tls_insecure", "autotracking", "ignore_time_mismatch"],
|
||||
overrideFields: [],
|
||||
},
|
||||
});
|
||||
|
||||
export default OnvifSection;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Semantic Search Section Component
|
||||
// Camera-level semantic search trigger settings
|
||||
|
||||
import { createConfigSection } from "./BaseSection";
|
||||
|
||||
export const SemanticSearchSection = createConfigSection({
|
||||
sectionPath: "semantic_search",
|
||||
i18nNamespace: "config/semantic_search",
|
||||
defaultConfig: {
|
||||
fieldOrder: ["triggers"],
|
||||
hiddenFields: [],
|
||||
advancedFields: [],
|
||||
overrideFields: [],
|
||||
},
|
||||
});
|
||||
|
||||
export default SemanticSearchSection;
|
||||
@@ -15,6 +15,15 @@ export { MotionSection } from "./MotionSection";
|
||||
export { ObjectsSection } from "./ObjectsSection";
|
||||
export { ReviewSection } from "./ReviewSection";
|
||||
export { AudioSection } from "./AudioSection";
|
||||
export { AudioTranscriptionSection } from "./AudioTranscriptionSection";
|
||||
export { BirdseyeSection } from "./BirdseyeSection";
|
||||
export { CameraMqttSection } from "./CameraMqttSection";
|
||||
export { CameraUiSection } from "./CameraUiSection";
|
||||
export { FaceRecognitionSection } from "./FaceRecognitionSection";
|
||||
export { FfmpegSection } from "./FfmpegSection";
|
||||
export { LprSection } from "./LprSection";
|
||||
export { NotificationsSection } from "./NotificationsSection";
|
||||
export { OnvifSection } from "./OnvifSection";
|
||||
export { LiveSection } from "./LiveSection";
|
||||
export { SemanticSearchSection } from "./SemanticSearchSection";
|
||||
export { TimestampSection } from "./TimestampSection";
|
||||
|
||||
Reference in New Issue
Block a user