mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-02 13:24:53 +03:00
add global sections, camera config overrides, and reset button
This commit is contained in:
parent
260237bf1a
commit
7692c89234
@ -16,11 +16,11 @@ export interface ConfigFormProps {
|
|||||||
/** JSON Schema for the form */
|
/** JSON Schema for the form */
|
||||||
schema: RJSFSchema;
|
schema: RJSFSchema;
|
||||||
/** Current form data */
|
/** Current form data */
|
||||||
formData?: Record<string, unknown>;
|
formData?: unknown;
|
||||||
/** Called when form data changes */
|
/** Called when form data changes */
|
||||||
onChange?: (data: Record<string, unknown>) => void;
|
onChange?: (data: unknown) => void;
|
||||||
/** Called when form is submitted */
|
/** Called when form is submitted */
|
||||||
onSubmit?: (data: Record<string, unknown>) => void;
|
onSubmit?: (data: unknown) => void;
|
||||||
/** Called when form has errors on submit */
|
/** Called when form has errors on submit */
|
||||||
onError?: (errors: unknown[]) => void;
|
onError?: (errors: unknown[]) => void;
|
||||||
/** Additional uiSchema overrides */
|
/** 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
|
// Base Section Component for config form sections
|
||||||
// Used as a foundation for reusable section components
|
// 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 useSWR from "swr";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -43,6 +43,8 @@ export interface SectionConfig {
|
|||||||
hiddenFields?: string[];
|
hiddenFields?: string[];
|
||||||
/** Fields to show in advanced section */
|
/** Fields to show in advanced section */
|
||||||
advancedFields?: string[];
|
advancedFields?: string[];
|
||||||
|
/** Fields to compare for override detection */
|
||||||
|
overrideFields?: string[];
|
||||||
/** Additional uiSchema overrides */
|
/** Additional uiSchema overrides */
|
||||||
uiSchema?: UiSchema;
|
uiSchema?: UiSchema;
|
||||||
}
|
}
|
||||||
@ -98,6 +100,17 @@ export function createConfigSection({
|
|||||||
review: "review",
|
review: "review",
|
||||||
audio: "audio",
|
audio: "audio",
|
||||||
notifications: "notifications",
|
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({
|
const ConfigSection = function ConfigSection({
|
||||||
@ -120,6 +133,8 @@ export function createConfigSection({
|
|||||||
unknown
|
unknown
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [formKey, setFormKey] = useState(0);
|
||||||
|
const isResettingRef = useRef(false);
|
||||||
|
|
||||||
const updateTopic =
|
const updateTopic =
|
||||||
level === "camera" && cameraName
|
level === "camera" && cameraName
|
||||||
@ -143,6 +158,7 @@ export function createConfigSection({
|
|||||||
config,
|
config,
|
||||||
cameraName: level === "camera" ? cameraName : undefined,
|
cameraName: level === "camera" ? cameraName : undefined,
|
||||||
sectionPath,
|
sectionPath,
|
||||||
|
compareFields: sectionConfig.overrideFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get current form data
|
// Get current form data
|
||||||
@ -193,6 +209,18 @@ export function createConfigSection({
|
|||||||
return applySchemaDefaults(sectionSchema, {});
|
return applySchemaDefaults(sectionSchema, {});
|
||||||
}, [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(
|
const buildOverrides = useCallback(
|
||||||
(
|
(
|
||||||
current: unknown,
|
current: unknown,
|
||||||
@ -266,8 +294,18 @@ export function createConfigSection({
|
|||||||
|
|
||||||
// Handle form data change
|
// Handle form data change
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(data: Record<string, unknown>) => {
|
(data: unknown) => {
|
||||||
const sanitizedData = sanitizeSectionData(data);
|
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)) {
|
if (isEqual(formData, sanitizedData)) {
|
||||||
setPendingData(null);
|
setPendingData(null);
|
||||||
return;
|
return;
|
||||||
@ -277,6 +315,12 @@ export function createConfigSection({
|
|||||||
[formData, sanitizeSectionData],
|
[formData, sanitizeSectionData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
isResettingRef.current = true;
|
||||||
|
setPendingData(null);
|
||||||
|
setFormKey((prev) => prev + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle save button click
|
// Handle save button click
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (!pendingData) return;
|
if (!pendingData) return;
|
||||||
@ -417,6 +461,7 @@ export function createConfigSection({
|
|||||||
const sectionContent = (
|
const sectionContent = (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ConfigForm
|
<ConfigForm
|
||||||
|
key={formKey}
|
||||||
schema={sectionSchema}
|
schema={sectionSchema}
|
||||||
formData={pendingData || formData}
|
formData={pendingData || formData}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -454,16 +499,28 @@ export function createConfigSection({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
onClick={handleSave}
|
{hasChanges && (
|
||||||
disabled={!hasChanges || isSaving || disabled}
|
<Button
|
||||||
className="gap-2"
|
onClick={handleReset}
|
||||||
>
|
variant="outline"
|
||||||
<LuSave className="h-4 w-4" />
|
disabled={isSaving || disabled}
|
||||||
{isSaving
|
className="gap-2"
|
||||||
? t("saving", { ns: "common", defaultValue: "Saving..." })
|
>
|
||||||
: t("save", { ns: "common", defaultValue: "Save" })}
|
{t("reset", { ns: "common", defaultValue: "Reset" })}
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
17
web/src/components/config-form/sections/BirdseyeSection.tsx
Normal file
17
web/src/components/config-form/sections/BirdseyeSection.tsx
Normal file
@ -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;
|
||||||
17
web/src/components/config-form/sections/CameraUiSection.tsx
Normal file
17
web/src/components/config-form/sections/CameraUiSection.tsx
Normal file
@ -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;
|
||||||
44
web/src/components/config-form/sections/FfmpegSection.tsx
Normal file
44
web/src/components/config-form/sections/FfmpegSection.tsx
Normal file
@ -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;
|
||||||
17
web/src/components/config-form/sections/LprSection.tsx
Normal file
17
web/src/components/config-form/sections/LprSection.tsx
Normal file
@ -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;
|
||||||
25
web/src/components/config-form/sections/OnvifSection.tsx
Normal file
25
web/src/components/config-form/sections/OnvifSection.tsx
Normal file
@ -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 { ObjectsSection } from "./ObjectsSection";
|
||||||
export { ReviewSection } from "./ReviewSection";
|
export { ReviewSection } from "./ReviewSection";
|
||||||
export { AudioSection } from "./AudioSection";
|
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 { NotificationsSection } from "./NotificationsSection";
|
||||||
|
export { OnvifSection } from "./OnvifSection";
|
||||||
export { LiveSection } from "./LiveSection";
|
export { LiveSection } from "./LiveSection";
|
||||||
|
export { SemanticSearchSection } from "./SemanticSearchSection";
|
||||||
export { TimestampSection } from "./TimestampSection";
|
export { TimestampSection } from "./TimestampSection";
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
|
import set from "lodash/set";
|
||||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
|
||||||
const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"];
|
const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"];
|
||||||
@ -46,6 +47,24 @@ export interface UseConfigOverrideOptions {
|
|||||||
cameraName?: string;
|
cameraName?: string;
|
||||||
/** Config section path (e.g., "detect", "record.events") */
|
/** Config section path (e.g., "detect", "record.events") */
|
||||||
sectionPath: string;
|
sectionPath: string;
|
||||||
|
/** Optional list of field paths to compare for overrides */
|
||||||
|
compareFields?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFields(value: unknown, fields: string[]): Record<string, unknown> {
|
||||||
|
if (!fields || fields.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
fields.forEach((path) => {
|
||||||
|
if (!path) return;
|
||||||
|
const fieldValue = get(value as Record<string, unknown>, path);
|
||||||
|
if (fieldValue !== undefined) {
|
||||||
|
set(result, path, fieldValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,6 +91,7 @@ export function useConfigOverride({
|
|||||||
config,
|
config,
|
||||||
cameraName,
|
cameraName,
|
||||||
sectionPath,
|
sectionPath,
|
||||||
|
compareFields,
|
||||||
}: UseConfigOverrideOptions) {
|
}: UseConfigOverrideOptions) {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -127,8 +147,17 @@ export function useConfigOverride({
|
|||||||
const normalizedGlobalValue = normalizeConfigValue(globalValue);
|
const normalizedGlobalValue = normalizeConfigValue(globalValue);
|
||||||
const normalizedCameraValue = normalizeConfigValue(cameraValue);
|
const normalizedCameraValue = normalizeConfigValue(cameraValue);
|
||||||
|
|
||||||
|
const comparisonGlobal = compareFields
|
||||||
|
? pickFields(normalizedGlobalValue, compareFields)
|
||||||
|
: normalizedGlobalValue;
|
||||||
|
const comparisonCamera = compareFields
|
||||||
|
? pickFields(normalizedCameraValue, compareFields)
|
||||||
|
: normalizedCameraValue;
|
||||||
|
|
||||||
// Check if the entire section is overridden
|
// Check if the entire section is overridden
|
||||||
const isOverridden = !isEqual(normalizedGlobalValue, normalizedCameraValue);
|
const isOverridden = compareFields
|
||||||
|
? compareFields.length > 0 && !isEqual(comparisonGlobal, comparisonCamera)
|
||||||
|
: !isEqual(comparisonGlobal, comparisonCamera);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get override status for a specific field within the section
|
* Get override status for a specific field within the section
|
||||||
@ -161,7 +190,7 @@ export function useConfigOverride({
|
|||||||
getFieldOverride,
|
getFieldOverride,
|
||||||
resetToGlobal,
|
resetToGlobal,
|
||||||
};
|
};
|
||||||
}, [config, cameraName, sectionPath]);
|
}, [config, cameraName, sectionPath, compareFields]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,25 +213,62 @@ export function useAllCameraOverrides(
|
|||||||
const overriddenSections: string[] = [];
|
const overriddenSections: string[] = [];
|
||||||
|
|
||||||
// Check each section that can be overridden
|
// Check each section that can be overridden
|
||||||
const sectionsToCheck = [
|
const sectionsToCheck: Array<{
|
||||||
"detect",
|
key: string;
|
||||||
"record",
|
compareFields?: string[];
|
||||||
"snapshots",
|
}> = [
|
||||||
"motion",
|
{ key: "detect" },
|
||||||
"objects",
|
{ key: "record" },
|
||||||
"review",
|
{ key: "snapshots" },
|
||||||
"audio",
|
{ key: "motion" },
|
||||||
"notifications",
|
{ key: "objects" },
|
||||||
"live",
|
{ key: "review" },
|
||||||
"timestamp_style",
|
{ key: "audio" },
|
||||||
|
{ key: "notifications" },
|
||||||
|
{ key: "live" },
|
||||||
|
{ key: "timestamp_style" },
|
||||||
|
{
|
||||||
|
key: "audio_transcription",
|
||||||
|
compareFields: ["enabled", "live_enabled"],
|
||||||
|
},
|
||||||
|
{ key: "birdseye", compareFields: ["enabled", "mode"] },
|
||||||
|
{ key: "face_recognition", compareFields: ["enabled", "min_area"] },
|
||||||
|
{
|
||||||
|
key: "ffmpeg",
|
||||||
|
compareFields: [
|
||||||
|
"path",
|
||||||
|
"global_args",
|
||||||
|
"hwaccel_args",
|
||||||
|
"input_args",
|
||||||
|
"output_args",
|
||||||
|
"retry_interval",
|
||||||
|
"apple_compatibility",
|
||||||
|
"gpu",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lpr",
|
||||||
|
compareFields: ["enabled", "min_area", "enhancement"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const section of sectionsToCheck) {
|
for (const { key, compareFields } of sectionsToCheck) {
|
||||||
const globalValue = normalizeConfigValue(get(config, section));
|
const globalValue = normalizeConfigValue(get(config, key));
|
||||||
const cameraValue = normalizeConfigValue(get(cameraConfig, section));
|
const cameraValue = normalizeConfigValue(get(cameraConfig, key));
|
||||||
|
|
||||||
if (!isEqual(globalValue, cameraValue)) {
|
const comparisonGlobal = compareFields
|
||||||
overriddenSections.push(section);
|
? pickFields(globalValue, compareFields)
|
||||||
|
: globalValue;
|
||||||
|
const comparisonCamera = compareFields
|
||||||
|
? pickFields(cameraValue, compareFields)
|
||||||
|
: cameraValue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
compareFields && compareFields.length === 0
|
||||||
|
? false
|
||||||
|
: !isEqual(comparisonGlobal, comparisonCamera)
|
||||||
|
) {
|
||||||
|
overriddenSections.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,17 @@ import { MotionSection } from "@/components/config-form/sections/MotionSection";
|
|||||||
import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection";
|
import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection";
|
||||||
import { ReviewSection } from "@/components/config-form/sections/ReviewSection";
|
import { ReviewSection } from "@/components/config-form/sections/ReviewSection";
|
||||||
import { AudioSection } from "@/components/config-form/sections/AudioSection";
|
import { AudioSection } from "@/components/config-form/sections/AudioSection";
|
||||||
|
import { AudioTranscriptionSection } from "@/components/config-form/sections/AudioTranscriptionSection";
|
||||||
|
import { BirdseyeSection } from "@/components/config-form/sections/BirdseyeSection";
|
||||||
|
import { CameraMqttSection } from "@/components/config-form/sections/CameraMqttSection";
|
||||||
|
import { CameraUiSection } from "@/components/config-form/sections/CameraUiSection";
|
||||||
|
import { FaceRecognitionSection } from "@/components/config-form/sections/FaceRecognitionSection";
|
||||||
|
import { FfmpegSection } from "@/components/config-form/sections/FfmpegSection";
|
||||||
|
import { LprSection } from "@/components/config-form/sections/LprSection";
|
||||||
import { NotificationsSection } from "@/components/config-form/sections/NotificationsSection";
|
import { NotificationsSection } from "@/components/config-form/sections/NotificationsSection";
|
||||||
|
import { OnvifSection } from "@/components/config-form/sections/OnvifSection";
|
||||||
import { LiveSection } from "@/components/config-form/sections/LiveSection";
|
import { LiveSection } from "@/components/config-form/sections/LiveSection";
|
||||||
|
import { SemanticSearchSection } from "@/components/config-form/sections/SemanticSearchSection";
|
||||||
import { TimestampSection } from "@/components/config-form/sections/TimestampSection";
|
import { TimestampSection } from "@/components/config-form/sections/TimestampSection";
|
||||||
import { useAllCameraOverrides } from "@/hooks/use-config-override";
|
import { useAllCameraOverrides } from "@/hooks/use-config-override";
|
||||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -179,8 +188,17 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
"config/objects",
|
"config/objects",
|
||||||
"config/review",
|
"config/review",
|
||||||
"config/audio",
|
"config/audio",
|
||||||
|
"config/audio_transcription",
|
||||||
|
"config/birdseye",
|
||||||
|
"config/camera_mqtt",
|
||||||
|
"config/camera_ui",
|
||||||
|
"config/face_recognition",
|
||||||
|
"config/ffmpeg",
|
||||||
|
"config/lpr",
|
||||||
"config/notifications",
|
"config/notifications",
|
||||||
|
"config/onvif",
|
||||||
"config/live",
|
"config/live",
|
||||||
|
"config/semantic_search",
|
||||||
"config/timestamp_style",
|
"config/timestamp_style",
|
||||||
"views/settings",
|
"views/settings",
|
||||||
"common",
|
"common",
|
||||||
@ -200,12 +218,23 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = [
|
const sections: Array<{
|
||||||
|
key: string;
|
||||||
|
i18nNamespace: string;
|
||||||
|
component: typeof DetectSection;
|
||||||
|
showOverrideIndicator?: boolean;
|
||||||
|
}> = [
|
||||||
{
|
{
|
||||||
key: "detect",
|
key: "detect",
|
||||||
i18nNamespace: "config/detect",
|
i18nNamespace: "config/detect",
|
||||||
component: DetectSection,
|
component: DetectSection,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "ffmpeg",
|
||||||
|
i18nNamespace: "config/ffmpeg",
|
||||||
|
component: FfmpegSection,
|
||||||
|
showOverrideIndicator: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "record",
|
key: "record",
|
||||||
i18nNamespace: "config/record",
|
i18nNamespace: "config/record",
|
||||||
@ -232,12 +261,60 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
component: ReviewSection,
|
component: ReviewSection,
|
||||||
},
|
},
|
||||||
{ key: "audio", i18nNamespace: "config/audio", component: AudioSection },
|
{ key: "audio", i18nNamespace: "config/audio", component: AudioSection },
|
||||||
|
{
|
||||||
|
key: "audio_transcription",
|
||||||
|
i18nNamespace: "config/audio_transcription",
|
||||||
|
component: AudioTranscriptionSection,
|
||||||
|
showOverrideIndicator: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "notifications",
|
key: "notifications",
|
||||||
i18nNamespace: "config/notifications",
|
i18nNamespace: "config/notifications",
|
||||||
component: NotificationsSection,
|
component: NotificationsSection,
|
||||||
},
|
},
|
||||||
{ key: "live", i18nNamespace: "config/live", component: LiveSection },
|
{ key: "live", i18nNamespace: "config/live", component: LiveSection },
|
||||||
|
{
|
||||||
|
key: "birdseye",
|
||||||
|
i18nNamespace: "config/birdseye",
|
||||||
|
component: BirdseyeSection,
|
||||||
|
showOverrideIndicator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "face_recognition",
|
||||||
|
i18nNamespace: "config/face_recognition",
|
||||||
|
component: FaceRecognitionSection,
|
||||||
|
showOverrideIndicator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lpr",
|
||||||
|
i18nNamespace: "config/lpr",
|
||||||
|
component: LprSection,
|
||||||
|
showOverrideIndicator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "semantic_search",
|
||||||
|
i18nNamespace: "config/semantic_search",
|
||||||
|
component: SemanticSearchSection,
|
||||||
|
showOverrideIndicator: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mqtt",
|
||||||
|
i18nNamespace: "config/camera_mqtt",
|
||||||
|
component: CameraMqttSection,
|
||||||
|
showOverrideIndicator: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "onvif",
|
||||||
|
i18nNamespace: "config/onvif",
|
||||||
|
component: OnvifSection,
|
||||||
|
showOverrideIndicator: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ui",
|
||||||
|
i18nNamespace: "config/camera_ui",
|
||||||
|
component: CameraUiSection,
|
||||||
|
showOverrideIndicator: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "timestamp_style",
|
key: "timestamp_style",
|
||||||
i18nNamespace: "config/timestamp_style",
|
i18nNamespace: "config/timestamp_style",
|
||||||
@ -273,9 +350,9 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
<span>{sectionLabel}</span>
|
<span>{sectionLabel}</span>
|
||||||
{isOverridden && (
|
{isOverridden && (
|
||||||
<Badge variant="secondary" className="h-5 px-1.5 text-xs">
|
<Badge variant="secondary" className="h-5 px-1.5 text-xs">
|
||||||
{t("button.modified", {
|
{t("button.overridden", {
|
||||||
ns: "common",
|
ns: "common",
|
||||||
defaultValue: "Modified",
|
defaultValue: "Overridden",
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@ -298,7 +375,7 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
<SectionComponent
|
<SectionComponent
|
||||||
level="camera"
|
level="camera"
|
||||||
cameraName={cameraName}
|
cameraName={cameraName}
|
||||||
showOverrideIndicator
|
showOverrideIndicator={section.showOverrideIndicator !== false}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
showTitle={true}
|
showTitle={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -79,6 +79,7 @@ const globalSectionConfigs: Record<
|
|||||||
"topic_prefix",
|
"topic_prefix",
|
||||||
"client_id",
|
"client_id",
|
||||||
"stats_interval",
|
"stats_interval",
|
||||||
|
"qos",
|
||||||
"tls_ca_certs",
|
"tls_ca_certs",
|
||||||
"tls_client_cert",
|
"tls_client_cert",
|
||||||
"tls_client_key",
|
"tls_client_key",
|
||||||
@ -86,6 +87,7 @@ const globalSectionConfigs: Record<
|
|||||||
],
|
],
|
||||||
advancedFields: [
|
advancedFields: [
|
||||||
"stats_interval",
|
"stats_interval",
|
||||||
|
"qos",
|
||||||
"tls_ca_certs",
|
"tls_ca_certs",
|
||||||
"tls_client_cert",
|
"tls_client_cert",
|
||||||
"tls_client_key",
|
"tls_client_key",
|
||||||
@ -102,17 +104,71 @@ const globalSectionConfigs: Record<
|
|||||||
fieldOrder: [
|
fieldOrder: [
|
||||||
"enabled",
|
"enabled",
|
||||||
"reset_admin_password",
|
"reset_admin_password",
|
||||||
|
"cookie_name",
|
||||||
|
"cookie_secure",
|
||||||
|
"session_length",
|
||||||
|
"refresh_time",
|
||||||
"native_oauth_url",
|
"native_oauth_url",
|
||||||
"failed_login_rate_limit",
|
"failed_login_rate_limit",
|
||||||
"trusted_proxies",
|
"trusted_proxies",
|
||||||
|
"hash_iterations",
|
||||||
|
"roles",
|
||||||
|
"admin_first_time_login",
|
||||||
|
],
|
||||||
|
advancedFields: [
|
||||||
|
"cookie_name",
|
||||||
|
"cookie_secure",
|
||||||
|
"session_length",
|
||||||
|
"refresh_time",
|
||||||
|
"failed_login_rate_limit",
|
||||||
|
"trusted_proxies",
|
||||||
|
"hash_iterations",
|
||||||
|
"roles",
|
||||||
|
"admin_first_time_login",
|
||||||
],
|
],
|
||||||
advancedFields: ["failed_login_rate_limit", "trusted_proxies"],
|
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
i18nNamespace: "config/tls",
|
i18nNamespace: "config/tls",
|
||||||
fieldOrder: ["enabled", "cert", "key"],
|
fieldOrder: ["enabled", "cert", "key"],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
},
|
},
|
||||||
|
networking: {
|
||||||
|
i18nNamespace: "config/networking",
|
||||||
|
fieldOrder: ["ipv6"],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
i18nNamespace: "config/proxy",
|
||||||
|
fieldOrder: [
|
||||||
|
"header_map",
|
||||||
|
"logout_url",
|
||||||
|
"auth_secret",
|
||||||
|
"default_role",
|
||||||
|
"separator",
|
||||||
|
],
|
||||||
|
advancedFields: ["header_map", "auth_secret", "separator"],
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
i18nNamespace: "config/ui",
|
||||||
|
fieldOrder: [
|
||||||
|
"timezone",
|
||||||
|
"time_format",
|
||||||
|
"date_style",
|
||||||
|
"time_style",
|
||||||
|
"unit_system",
|
||||||
|
],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
i18nNamespace: "config/logger",
|
||||||
|
fieldOrder: ["default", "logs"],
|
||||||
|
advancedFields: ["logs"],
|
||||||
|
},
|
||||||
|
environment_vars: {
|
||||||
|
i18nNamespace: "config/environment_vars",
|
||||||
|
fieldOrder: [],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
telemetry: {
|
telemetry: {
|
||||||
i18nNamespace: "config/telemetry",
|
i18nNamespace: "config/telemetry",
|
||||||
fieldOrder: ["network_interfaces", "stats", "version_check"],
|
fieldOrder: ["network_interfaces", "stats", "version_check"],
|
||||||
@ -129,39 +185,191 @@ const globalSectionConfigs: Record<
|
|||||||
"mode",
|
"mode",
|
||||||
"layout",
|
"layout",
|
||||||
"inactivity_threshold",
|
"inactivity_threshold",
|
||||||
|
"idle_heartbeat_fps",
|
||||||
],
|
],
|
||||||
advancedFields: ["width", "height", "quality", "inactivity_threshold"],
|
advancedFields: ["width", "height", "quality", "inactivity_threshold"],
|
||||||
},
|
},
|
||||||
|
ffmpeg: {
|
||||||
|
i18nNamespace: "config/ffmpeg",
|
||||||
|
fieldOrder: [
|
||||||
|
"path",
|
||||||
|
"global_args",
|
||||||
|
"hwaccel_args",
|
||||||
|
"input_args",
|
||||||
|
"output_args",
|
||||||
|
"retry_interval",
|
||||||
|
"apple_compatibility",
|
||||||
|
"gpu",
|
||||||
|
],
|
||||||
|
advancedFields: [
|
||||||
|
"global_args",
|
||||||
|
"hwaccel_args",
|
||||||
|
"input_args",
|
||||||
|
"output_args",
|
||||||
|
"retry_interval",
|
||||||
|
"apple_compatibility",
|
||||||
|
"gpu",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
detectors: {
|
||||||
|
i18nNamespace: "config/detectors",
|
||||||
|
fieldOrder: [],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
i18nNamespace: "config/model",
|
||||||
|
fieldOrder: [
|
||||||
|
"path",
|
||||||
|
"labelmap_path",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"input_pixel_format",
|
||||||
|
"input_tensor",
|
||||||
|
"input_dtype",
|
||||||
|
"model_type",
|
||||||
|
"labelmap",
|
||||||
|
"attributes_map",
|
||||||
|
],
|
||||||
|
advancedFields: [
|
||||||
|
"labelmap",
|
||||||
|
"attributes_map",
|
||||||
|
"input_pixel_format",
|
||||||
|
"input_tensor",
|
||||||
|
"input_dtype",
|
||||||
|
"model_type",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
genai: {
|
||||||
|
i18nNamespace: "config/genai",
|
||||||
|
fieldOrder: [
|
||||||
|
"provider",
|
||||||
|
"api_key",
|
||||||
|
"base_url",
|
||||||
|
"model",
|
||||||
|
"provider_options",
|
||||||
|
"runtime_options",
|
||||||
|
],
|
||||||
|
advancedFields: ["base_url", "provider_options", "runtime_options"],
|
||||||
|
},
|
||||||
|
classification: {
|
||||||
|
i18nNamespace: "config/classification",
|
||||||
|
fieldOrder: ["bird", "custom"],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
semantic_search: {
|
semantic_search: {
|
||||||
i18nNamespace: "config/semantic_search",
|
i18nNamespace: "config/semantic_search",
|
||||||
fieldOrder: ["enabled", "reindex", "model_size"],
|
fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
|
||||||
advancedFields: ["reindex"],
|
advancedFields: ["reindex", "device"],
|
||||||
|
},
|
||||||
|
audio_transcription: {
|
||||||
|
i18nNamespace: "config/audio_transcription",
|
||||||
|
fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
|
||||||
|
advancedFields: ["language", "device", "model_size"],
|
||||||
},
|
},
|
||||||
face_recognition: {
|
face_recognition: {
|
||||||
i18nNamespace: "config/face_recognition",
|
i18nNamespace: "config/face_recognition",
|
||||||
fieldOrder: ["enabled", "threshold", "min_area", "model_size"],
|
fieldOrder: [
|
||||||
advancedFields: ["threshold", "min_area"],
|
"enabled",
|
||||||
|
"model_size",
|
||||||
|
"unknown_score",
|
||||||
|
"detection_threshold",
|
||||||
|
"recognition_threshold",
|
||||||
|
"min_area",
|
||||||
|
"min_faces",
|
||||||
|
"save_attempts",
|
||||||
|
"blur_confidence_filter",
|
||||||
|
"device",
|
||||||
|
],
|
||||||
|
advancedFields: [
|
||||||
|
"unknown_score",
|
||||||
|
"detection_threshold",
|
||||||
|
"recognition_threshold",
|
||||||
|
"min_area",
|
||||||
|
"min_faces",
|
||||||
|
"save_attempts",
|
||||||
|
"blur_confidence_filter",
|
||||||
|
"device",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
lpr: {
|
lpr: {
|
||||||
i18nNamespace: "config/lpr",
|
i18nNamespace: "config/lpr",
|
||||||
fieldOrder: [
|
fieldOrder: [
|
||||||
"enabled",
|
"enabled",
|
||||||
"threshold",
|
|
||||||
"min_area",
|
|
||||||
"min_ratio",
|
|
||||||
"max_ratio",
|
|
||||||
"model_size",
|
"model_size",
|
||||||
|
"detection_threshold",
|
||||||
|
"min_area",
|
||||||
|
"recognition_threshold",
|
||||||
|
"min_plate_length",
|
||||||
|
"format",
|
||||||
|
"match_distance",
|
||||||
|
"known_plates",
|
||||||
|
"enhancement",
|
||||||
|
"debug_save_plates",
|
||||||
|
"device",
|
||||||
|
"replace_rules",
|
||||||
],
|
],
|
||||||
advancedFields: ["threshold", "min_area", "min_ratio", "max_ratio"],
|
advancedFields: [
|
||||||
|
"detection_threshold",
|
||||||
|
"recognition_threshold",
|
||||||
|
"min_plate_length",
|
||||||
|
"format",
|
||||||
|
"match_distance",
|
||||||
|
"known_plates",
|
||||||
|
"enhancement",
|
||||||
|
"debug_save_plates",
|
||||||
|
"device",
|
||||||
|
"replace_rules",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
go2rtc: {
|
||||||
|
i18nNamespace: "config/go2rtc",
|
||||||
|
fieldOrder: [],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
camera_groups: {
|
||||||
|
i18nNamespace: "config/camera_groups",
|
||||||
|
fieldOrder: ["cameras", "icon", "order"],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
safe_mode: {
|
||||||
|
i18nNamespace: "config/safe_mode",
|
||||||
|
fieldOrder: [],
|
||||||
|
advancedFields: [],
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
i18nNamespace: "config/version",
|
||||||
|
fieldOrder: [],
|
||||||
|
advancedFields: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// System sections (global only)
|
// System sections (global only)
|
||||||
const systemSections = ["database", "tls", "auth", "telemetry", "birdseye"];
|
const systemSections = [
|
||||||
|
"database",
|
||||||
|
"tls",
|
||||||
|
"auth",
|
||||||
|
"networking",
|
||||||
|
"proxy",
|
||||||
|
"ui",
|
||||||
|
"logger",
|
||||||
|
"environment_vars",
|
||||||
|
"telemetry",
|
||||||
|
"birdseye",
|
||||||
|
"ffmpeg",
|
||||||
|
"detectors",
|
||||||
|
"model",
|
||||||
|
"classification",
|
||||||
|
"go2rtc",
|
||||||
|
"camera_groups",
|
||||||
|
"safe_mode",
|
||||||
|
"version",
|
||||||
|
];
|
||||||
|
|
||||||
// Integration sections (global only)
|
// Integration sections (global only)
|
||||||
const integrationSections = [
|
const integrationSections = [
|
||||||
"mqtt",
|
"mqtt",
|
||||||
|
"audio_transcription",
|
||||||
|
"genai",
|
||||||
"semantic_search",
|
"semantic_search",
|
||||||
"face_recognition",
|
"face_recognition",
|
||||||
"lpr",
|
"lpr",
|
||||||
@ -186,18 +394,12 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
|
|||||||
"views/settings",
|
"views/settings",
|
||||||
"common",
|
"common",
|
||||||
]);
|
]);
|
||||||
const [pendingData, setPendingData] = useState<Record<
|
const [pendingData, setPendingData] = useState<unknown | null>(null);
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
> | null>(null);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const formData = useMemo((): Record<string, unknown> => {
|
const formData = useMemo((): unknown => {
|
||||||
if (!config) return {} as Record<string, unknown>;
|
if (!config) return {};
|
||||||
const value = (config as unknown as Record<string, unknown>)[sectionKey];
|
return (config as unknown as Record<string, unknown>)[sectionKey];
|
||||||
return (
|
|
||||||
(value as Record<string, unknown>) || ({} as Record<string, unknown>)
|
|
||||||
);
|
|
||||||
}, [config, sectionKey]);
|
}, [config, sectionKey]);
|
||||||
|
|
||||||
const hasChanges = useMemo(() => {
|
const hasChanges = useMemo(() => {
|
||||||
@ -205,7 +407,7 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
|
|||||||
return !isEqual(formData, pendingData);
|
return !isEqual(formData, pendingData);
|
||||||
}, [formData, pendingData]);
|
}, [formData, pendingData]);
|
||||||
|
|
||||||
const handleChange = useCallback((data: Record<string, unknown>) => {
|
const handleChange = useCallback((data: unknown) => {
|
||||||
setPendingData(data);
|
setPendingData(data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -300,14 +502,29 @@ export default function GlobalConfigView() {
|
|||||||
"config/live",
|
"config/live",
|
||||||
"config/timestamp_style",
|
"config/timestamp_style",
|
||||||
"config/mqtt",
|
"config/mqtt",
|
||||||
|
"config/audio_transcription",
|
||||||
"config/database",
|
"config/database",
|
||||||
"config/auth",
|
"config/auth",
|
||||||
"config/tls",
|
"config/tls",
|
||||||
|
"config/networking",
|
||||||
|
"config/proxy",
|
||||||
|
"config/ui",
|
||||||
|
"config/logger",
|
||||||
|
"config/environment_vars",
|
||||||
"config/telemetry",
|
"config/telemetry",
|
||||||
"config/birdseye",
|
"config/birdseye",
|
||||||
|
"config/ffmpeg",
|
||||||
|
"config/detectors",
|
||||||
|
"config/model",
|
||||||
|
"config/genai",
|
||||||
|
"config/classification",
|
||||||
"config/semantic_search",
|
"config/semantic_search",
|
||||||
"config/face_recognition",
|
"config/face_recognition",
|
||||||
"config/lpr",
|
"config/lpr",
|
||||||
|
"config/go2rtc",
|
||||||
|
"config/camera_groups",
|
||||||
|
"config/safe_mode",
|
||||||
|
"config/version",
|
||||||
"common",
|
"common",
|
||||||
]);
|
]);
|
||||||
const [activeTab, setActiveTab] = useState("shared");
|
const [activeTab, setActiveTab] = useState("shared");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user