From a97333d881a89541d623ba98c91029c833841eaa Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:35:04 -0600 Subject: [PATCH] refactor configs to use individual files with a template --- .../config-form/section-configs/audio.ts | 27 + .../section-configs/audio_transcription.ts | 16 + .../config-form/section-configs/auth.ts | 37 + .../config-form/section-configs/birdseye.ts | 26 + .../section-configs/classification.ts | 10 + .../config-form/section-configs/database.ts | 10 + .../config-form/section-configs/detect.ts | 29 + .../config-form/section-configs/detectors.ts | 10 + .../section-configs/environment_vars.ts | 10 + .../section-configs/face_recognition.ts | 36 + .../config-form/section-configs/ffmpeg.ts | 192 ++++ .../config-form/section-configs/genai.ts | 18 + .../config-form/section-configs/live.ts | 12 + .../config-form/section-configs/logger.ts | 10 + .../config-form/section-configs/lpr.ts | 41 + .../config-form/section-configs/model.ts | 25 + .../config-form/section-configs/motion.ts | 32 + .../config-form/section-configs/mqtt.ts | 52 + .../config-form/section-configs/networking.ts | 10 + .../section-configs/notifications.ts | 12 + .../config-form/section-configs/objects.ts | 57 ++ .../config-form/section-configs/onvif.ts | 33 + .../config-form/section-configs/proxy.ts | 17 + .../config-form/section-configs/record.ts | 24 + .../config-form/section-configs/review.ts | 31 + .../section-configs/semantic_search.ts | 21 + .../config-form/section-configs/snapshots.ts | 29 + .../config-form/section-configs/telemetry.ts | 10 + .../section-configs/timestamp_style.ts | 11 + .../config-form/section-configs/tls.ts | 10 + .../config-form/section-configs/types.ts | 7 + .../config-form/section-configs/ui.ts | 22 + .../components/config-form/sectionConfigs.ts | 887 ++---------------- .../config-form/sectionConfigsUtils.ts | 32 + .../sections/ConfigSectionTemplate.tsx | 40 + .../components/config-form/sections/index.ts | 38 +- web/src/pages/Settings.tsx | 6 - web/src/views/settings/CameraConfigView.tsx | 144 +-- web/src/views/settings/GlobalConfigView.tsx | 123 +-- web/src/views/settings/SingleSectionPage.tsx | 11 +- 40 files changed, 1111 insertions(+), 1057 deletions(-) create mode 100644 web/src/components/config-form/section-configs/audio.ts create mode 100644 web/src/components/config-form/section-configs/audio_transcription.ts create mode 100644 web/src/components/config-form/section-configs/auth.ts create mode 100644 web/src/components/config-form/section-configs/birdseye.ts create mode 100644 web/src/components/config-form/section-configs/classification.ts create mode 100644 web/src/components/config-form/section-configs/database.ts create mode 100644 web/src/components/config-form/section-configs/detect.ts create mode 100644 web/src/components/config-form/section-configs/detectors.ts create mode 100644 web/src/components/config-form/section-configs/environment_vars.ts create mode 100644 web/src/components/config-form/section-configs/face_recognition.ts create mode 100644 web/src/components/config-form/section-configs/ffmpeg.ts create mode 100644 web/src/components/config-form/section-configs/genai.ts create mode 100644 web/src/components/config-form/section-configs/live.ts create mode 100644 web/src/components/config-form/section-configs/logger.ts create mode 100644 web/src/components/config-form/section-configs/lpr.ts create mode 100644 web/src/components/config-form/section-configs/model.ts create mode 100644 web/src/components/config-form/section-configs/motion.ts create mode 100644 web/src/components/config-form/section-configs/mqtt.ts create mode 100644 web/src/components/config-form/section-configs/networking.ts create mode 100644 web/src/components/config-form/section-configs/notifications.ts create mode 100644 web/src/components/config-form/section-configs/objects.ts create mode 100644 web/src/components/config-form/section-configs/onvif.ts create mode 100644 web/src/components/config-form/section-configs/proxy.ts create mode 100644 web/src/components/config-form/section-configs/record.ts create mode 100644 web/src/components/config-form/section-configs/review.ts create mode 100644 web/src/components/config-form/section-configs/semantic_search.ts create mode 100644 web/src/components/config-form/section-configs/snapshots.ts create mode 100644 web/src/components/config-form/section-configs/telemetry.ts create mode 100644 web/src/components/config-form/section-configs/timestamp_style.ts create mode 100644 web/src/components/config-form/section-configs/tls.ts create mode 100644 web/src/components/config-form/section-configs/types.ts create mode 100644 web/src/components/config-form/section-configs/ui.ts create mode 100644 web/src/components/config-form/sectionConfigsUtils.ts create mode 100644 web/src/components/config-form/sections/ConfigSectionTemplate.tsx diff --git a/web/src/components/config-form/section-configs/audio.ts b/web/src/components/config-form/section-configs/audio.ts new file mode 100644 index 000000000..99e6131a0 --- /dev/null +++ b/web/src/components/config-form/section-configs/audio.ts @@ -0,0 +1,27 @@ +import type { SectionConfigOverrides } from "./types"; + +const audio: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "listen", + "filters", + "min_volume", + "max_not_heard", + "num_threads", + ], + fieldGroups: { + detection: ["enabled", "listen", "filters"], + sensitivity: ["min_volume", "max_not_heard"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["min_volume", "max_not_heard", "num_threads"], + uiSchema: { + listen: { + "ui:widget": "audioLabels", + }, + }, + }, +}; + +export default audio; diff --git a/web/src/components/config-form/section-configs/audio_transcription.ts b/web/src/components/config-form/section-configs/audio_transcription.ts new file mode 100644 index 000000000..f9d822a0d --- /dev/null +++ b/web/src/components/config-form/section-configs/audio_transcription.ts @@ -0,0 +1,16 @@ +import type { SectionConfigOverrides } from "./types"; + +const audioTranscription: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"], + hiddenFields: ["enabled_in_config"], + advancedFields: ["language", "device", "model_size"], + overrideFields: ["enabled", "live_enabled"], + }, + global: { + fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"], + advancedFields: ["language", "device", "model_size"], + }, +}; + +export default audioTranscription; diff --git a/web/src/components/config-form/section-configs/auth.ts b/web/src/components/config-form/section-configs/auth.ts new file mode 100644 index 000000000..bc7374c6c --- /dev/null +++ b/web/src/components/config-form/section-configs/auth.ts @@ -0,0 +1,37 @@ +import type { SectionConfigOverrides } from "./types"; + +const auth: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "reset_admin_password", + "cookie_name", + "cookie_secure", + "session_length", + "refresh_time", + "native_oauth_url", + "failed_login_rate_limit", + "trusted_proxies", + "hash_iterations", + "roles", + ], + hiddenFields: ["admin_first_time_login"], + advancedFields: [ + "cookie_name", + "cookie_secure", + "session_length", + "refresh_time", + "failed_login_rate_limit", + "trusted_proxies", + "hash_iterations", + "roles", + ], + uiSchema: { + reset_admin_password: { + "ui:widget": "switch", + }, + }, + }, +}; + +export default auth; diff --git a/web/src/components/config-form/section-configs/birdseye.ts b/web/src/components/config-form/section-configs/birdseye.ts new file mode 100644 index 000000000..9b0fa5ce7 --- /dev/null +++ b/web/src/components/config-form/section-configs/birdseye.ts @@ -0,0 +1,26 @@ +import type { SectionConfigOverrides } from "./types"; + +const birdseye: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "mode", "order"], + hiddenFields: [], + advancedFields: [], + overrideFields: ["enabled", "mode"], + }, + global: { + fieldOrder: [ + "enabled", + "restream", + "width", + "height", + "quality", + "mode", + "layout", + "inactivity_threshold", + "idle_heartbeat_fps", + ], + advancedFields: ["width", "height", "quality", "inactivity_threshold"], + }, +}; + +export default birdseye; diff --git a/web/src/components/config-form/section-configs/classification.ts b/web/src/components/config-form/section-configs/classification.ts new file mode 100644 index 000000000..c76697a4f --- /dev/null +++ b/web/src/components/config-form/section-configs/classification.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const classification: SectionConfigOverrides = { + base: { + hiddenFields: ["custom"], + advancedFields: [], + }, +}; + +export default classification; diff --git a/web/src/components/config-form/section-configs/database.ts b/web/src/components/config-form/section-configs/database.ts new file mode 100644 index 000000000..6170a157f --- /dev/null +++ b/web/src/components/config-form/section-configs/database.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const database: SectionConfigOverrides = { + base: { + fieldOrder: ["path"], + advancedFields: [], + }, +}; + +export default database; diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts new file mode 100644 index 000000000..29353ab3f --- /dev/null +++ b/web/src/components/config-form/section-configs/detect.ts @@ -0,0 +1,29 @@ +import type { SectionConfigOverrides } from "./types"; + +const detect: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "fps", + "width", + "height", + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + ], + fieldGroups: { + resolution: ["enabled", "width", "height"], + tracking: ["min_initialized", "max_disappeared"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: [ + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + ], + }, +}; + +export default detect; diff --git a/web/src/components/config-form/section-configs/detectors.ts b/web/src/components/config-form/section-configs/detectors.ts new file mode 100644 index 000000000..a8259015b --- /dev/null +++ b/web/src/components/config-form/section-configs/detectors.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const detectors: SectionConfigOverrides = { + base: { + fieldOrder: [], + advancedFields: [], + }, +}; + +export default detectors; diff --git a/web/src/components/config-form/section-configs/environment_vars.ts b/web/src/components/config-form/section-configs/environment_vars.ts new file mode 100644 index 000000000..e8bdcad1e --- /dev/null +++ b/web/src/components/config-form/section-configs/environment_vars.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const environmentVars: SectionConfigOverrides = { + base: { + fieldOrder: [], + advancedFields: [], + }, +}; + +export default environmentVars; diff --git a/web/src/components/config-form/section-configs/face_recognition.ts b/web/src/components/config-form/section-configs/face_recognition.ts new file mode 100644 index 000000000..ab76379c7 --- /dev/null +++ b/web/src/components/config-form/section-configs/face_recognition.ts @@ -0,0 +1,36 @@ +import type { SectionConfigOverrides } from "./types"; + +const faceRecognition: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "min_area"], + hiddenFields: [], + advancedFields: ["min_area"], + overrideFields: ["enabled", "min_area"], + }, + global: { + fieldOrder: [ + "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", + ], + }, +}; + +export default faceRecognition; diff --git a/web/src/components/config-form/section-configs/ffmpeg.ts b/web/src/components/config-form/section-configs/ffmpeg.ts new file mode 100644 index 000000000..e725a88c0 --- /dev/null +++ b/web/src/components/config-form/section-configs/ffmpeg.ts @@ -0,0 +1,192 @@ +import type { SectionConfigOverrides } from "./types"; + +const ffmpeg: SectionConfigOverrides = { + base: { + 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", + ], + uiSchema: { + global_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + hwaccel_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + input_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + output_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + detect: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + record: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + items: { + detect: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + record: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + }, + }, + inputs: { + items: { + global_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + hwaccel_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + input_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + output_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + items: { + detect: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + record: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + }, + }, + }, + }, + }, + }, + global: { + 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", + ], + uiSchema: { + global_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + hwaccel_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + input_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + output_args: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + detect: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + record: { + "ui:widget": "ArrayAsTextWidget", + "ui:options": { + suppressMultiSchema: true, + }, + }, + }, + }, + }, +}; + +export default ffmpeg; diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts new file mode 100644 index 000000000..d6627fbac --- /dev/null +++ b/web/src/components/config-form/section-configs/genai.ts @@ -0,0 +1,18 @@ +import type { SectionConfigOverrides } from "./types"; + +const genai: SectionConfigOverrides = { + base: { + fieldOrder: [ + "provider", + "api_key", + "base_url", + "model", + "provider_options", + "runtime_options", + ], + advancedFields: ["base_url", "provider_options", "runtime_options"], + hiddenFields: ["genai.enabled_in_config"], + }, +}; + +export default genai; diff --git a/web/src/components/config-form/section-configs/live.ts b/web/src/components/config-form/section-configs/live.ts new file mode 100644 index 000000000..bec5c0d7e --- /dev/null +++ b/web/src/components/config-form/section-configs/live.ts @@ -0,0 +1,12 @@ +import type { SectionConfigOverrides } from "./types"; + +const live: SectionConfigOverrides = { + base: { + fieldOrder: ["stream_name", "height", "quality"], + fieldGroups: {}, + hiddenFields: ["enabled_in_config"], + advancedFields: ["quality"], + }, +}; + +export default live; diff --git a/web/src/components/config-form/section-configs/logger.ts b/web/src/components/config-form/section-configs/logger.ts new file mode 100644 index 000000000..75322e7b0 --- /dev/null +++ b/web/src/components/config-form/section-configs/logger.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const logger: SectionConfigOverrides = { + base: { + fieldOrder: ["default", "logs"], + advancedFields: ["logs"], + }, +}; + +export default logger; diff --git a/web/src/components/config-form/section-configs/lpr.ts b/web/src/components/config-form/section-configs/lpr.ts new file mode 100644 index 000000000..15c5ec7a8 --- /dev/null +++ b/web/src/components/config-form/section-configs/lpr.ts @@ -0,0 +1,41 @@ +import type { SectionConfigOverrides } from "./types"; + +const lpr: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], + hiddenFields: [], + advancedFields: ["expire_time", "min_area", "enhancement"], + overrideFields: ["enabled", "min_area", "enhancement"], + }, + global: { + fieldOrder: [ + "enabled", + "model_size", + "detection_threshold", + "min_area", + "recognition_threshold", + "min_plate_length", + "format", + "match_distance", + "known_plates", + "enhancement", + "debug_save_plates", + "device", + "replace_rules", + ], + advancedFields: [ + "detection_threshold", + "recognition_threshold", + "min_plate_length", + "format", + "match_distance", + "known_plates", + "enhancement", + "debug_save_plates", + "device", + "replace_rules", + ], + }, +}; + +export default lpr; diff --git a/web/src/components/config-form/section-configs/model.ts b/web/src/components/config-form/section-configs/model.ts new file mode 100644 index 000000000..81867dc0a --- /dev/null +++ b/web/src/components/config-form/section-configs/model.ts @@ -0,0 +1,25 @@ +import type { SectionConfigOverrides } from "./types"; + +const model: SectionConfigOverrides = { + base: { + fieldOrder: [ + "path", + "labelmap_path", + "width", + "height", + "input_pixel_format", + "input_tensor", + "input_dtype", + "model_type", + ], + advancedFields: [ + "input_pixel_format", + "input_tensor", + "input_dtype", + "model_type", + ], + hiddenFields: ["labelmap", "attributes_map"], + }, +}; + +export default model; diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts new file mode 100644 index 000000000..b22bebb0a --- /dev/null +++ b/web/src/components/config-form/section-configs/motion.ts @@ -0,0 +1,32 @@ +import type { SectionConfigOverrides } from "./types"; + +const motion: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "threshold", + "lightning_threshold", + "improve_contrast", + "contour_area", + "delta_alpha", + "frame_alpha", + "frame_height", + "mask", + "mqtt_off_delay", + ], + fieldGroups: { + sensitivity: ["enabled", "threshold", "contour_area"], + algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], + }, + hiddenFields: ["enabled_in_config", "mask", "raw_mask"], + advancedFields: [ + "lightning_threshold", + "delta_alpha", + "frame_alpha", + "frame_height", + "mqtt_off_delay", + ], + }, +}; + +export default motion; diff --git a/web/src/components/config-form/section-configs/mqtt.ts b/web/src/components/config-form/section-configs/mqtt.ts new file mode 100644 index 000000000..0c9e3c34e --- /dev/null +++ b/web/src/components/config-form/section-configs/mqtt.ts @@ -0,0 +1,52 @@ +import type { SectionConfigOverrides } from "./types"; + +const mqtt: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "timestamp", + "bounding_box", + "crop", + "height", + "required_zones", + "quality", + ], + hiddenFields: [], + advancedFields: ["height", "quality"], + overrideFields: [], + uiSchema: { + required_zones: { + "ui:widget": "zoneNames", + }, + }, + }, + global: { + fieldOrder: [ + "enabled", + "host", + "port", + "user", + "password", + "topic_prefix", + "client_id", + "stats_interval", + "qos", + "tls_ca_certs", + "tls_client_cert", + "tls_client_key", + "tls_insecure", + ], + advancedFields: [ + "stats_interval", + "qos", + "tls_ca_certs", + "tls_client_cert", + "tls_client_key", + "tls_insecure", + ], + liveValidate: true, + uiSchema: {}, + }, +}; + +export default mqtt; diff --git a/web/src/components/config-form/section-configs/networking.ts b/web/src/components/config-form/section-configs/networking.ts new file mode 100644 index 000000000..4abe60451 --- /dev/null +++ b/web/src/components/config-form/section-configs/networking.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const networking: SectionConfigOverrides = { + base: { + fieldOrder: [], + advancedFields: [], + }, +}; + +export default networking; diff --git a/web/src/components/config-form/section-configs/notifications.ts b/web/src/components/config-form/section-configs/notifications.ts new file mode 100644 index 000000000..b0e436cba --- /dev/null +++ b/web/src/components/config-form/section-configs/notifications.ts @@ -0,0 +1,12 @@ +import type { SectionConfigOverrides } from "./types"; + +const notifications: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "email"], + fieldGroups: {}, + hiddenFields: ["enabled_in_config"], + advancedFields: [], + }, +}; + +export default notifications; diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts new file mode 100644 index 000000000..7652aff22 --- /dev/null +++ b/web/src/components/config-form/section-configs/objects.ts @@ -0,0 +1,57 @@ +import type { SectionConfigOverrides } from "./types"; + +const objects: SectionConfigOverrides = { + base: { + fieldOrder: ["track", "alert", "detect", "filters"], + fieldGroups: { + tracking: ["track", "alert", "detect"], + filtering: ["filters"], + }, + hiddenFields: [ + "enabled_in_config", + "mask", + "raw_mask", + "genai.enabled_in_config", + "filters.*.mask", + "filters.*.raw_mask", + ], + advancedFields: ["filters"], + uiSchema: { + "filters.*.min_area": { + "ui:options": { + suppressMultiSchema: true, + }, + }, + "filters.*.max_area": { + "ui:options": { + suppressMultiSchema: true, + }, + }, + track: { + "ui:widget": "objectLabels", + "ui:options": { + suppressMultiSchema: true, + }, + }, + genai: { + objects: { + "ui:widget": "objectLabels", + "ui:options": { + suppressMultiSchema: true, + }, + }, + required_zones: { + "ui:widget": "zoneNames", + "ui:options": { + suppressMultiSchema: true, + }, + }, + enabled_in_config: { + "ui:widget": "hidden", + }, + }, + }, + }, +}; + +export default objects; diff --git a/web/src/components/config-form/section-configs/onvif.ts b/web/src/components/config-form/section-configs/onvif.ts new file mode 100644 index 000000000..973945275 --- /dev/null +++ b/web/src/components/config-form/section-configs/onvif.ts @@ -0,0 +1,33 @@ +import type { SectionConfigOverrides } from "./types"; + +const onvif: SectionConfigOverrides = { + base: { + fieldOrder: [ + "host", + "port", + "user", + "password", + "tls_insecure", + "ignore_time_mismatch", + "autotracking", + ], + hiddenFields: [ + "autotracking.enabled_in_config", + "autotracking.movement_weights", + ], + advancedFields: ["tls_insecure", "ignore_time_mismatch"], + overrideFields: [], + uiSchema: { + autotracking: { + required_zones: { + "ui:widget": "zoneNames", + }, + track: { + "ui:widget": "objectLabels", + }, + }, + }, + }, +}; + +export default onvif; diff --git a/web/src/components/config-form/section-configs/proxy.ts b/web/src/components/config-form/section-configs/proxy.ts new file mode 100644 index 000000000..2530f0a98 --- /dev/null +++ b/web/src/components/config-form/section-configs/proxy.ts @@ -0,0 +1,17 @@ +import type { SectionConfigOverrides } from "./types"; + +const proxy: SectionConfigOverrides = { + base: { + fieldOrder: [ + "header_map", + "logout_url", + "auth_secret", + "default_role", + "separator", + ], + advancedFields: ["header_map", "auth_secret", "separator"], + liveValidate: true, + }, +}; + +export default proxy; diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts new file mode 100644 index 000000000..1a15631d6 --- /dev/null +++ b/web/src/components/config-form/section-configs/record.ts @@ -0,0 +1,24 @@ +import type { SectionConfigOverrides } from "./types"; + +const record: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "expire_interval", + "continuous", + "motion", + "alerts", + "detections", + "preview", + "export", + ], + fieldGroups: { + retention: ["enabled", "continuous", "motion"], + events: ["alerts", "detections"], + }, + hiddenFields: ["enabled_in_config", "sync_recordings"], + advancedFields: ["expire_interval", "preview", "export"], + }, +}; + +export default record; diff --git a/web/src/components/config-form/section-configs/review.ts b/web/src/components/config-form/section-configs/review.ts new file mode 100644 index 000000000..98301512f --- /dev/null +++ b/web/src/components/config-form/section-configs/review.ts @@ -0,0 +1,31 @@ +import type { SectionConfigOverrides } from "./types"; + +const review: SectionConfigOverrides = { + base: { + fieldOrder: ["alerts", "detections", "genai"], + fieldGroups: {}, + hiddenFields: [ + "enabled_in_config", + "alerts.labels", + "alerts.enabled_in_config", + "alerts.required_zones", + "detections.labels", + "detections.enabled_in_config", + "detections.required_zones", + "genai.enabled_in_config", + ], + advancedFields: [], + uiSchema: { + genai: { + additional_concerns: { + "ui:widget": "textarea", + }, + activity_context_prompt: { + "ui:widget": "textarea", + }, + }, + }, + }, +}; + +export default review; diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts new file mode 100644 index 000000000..a85a5bc5b --- /dev/null +++ b/web/src/components/config-form/section-configs/semantic_search.ts @@ -0,0 +1,21 @@ +import type { SectionConfigOverrides } from "./types"; + +const semanticSearch: SectionConfigOverrides = { + base: { + fieldOrder: ["triggers"], + hiddenFields: [], + advancedFields: [], + overrideFields: [], + uiSchema: { + enabled: { + "ui:after": { render: "SemanticSearchReindex" }, + }, + }, + }, + global: { + fieldOrder: ["enabled", "reindex", "model", "model_size", "device"], + advancedFields: ["reindex", "device"], + }, +}; + +export default semanticSearch; diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts new file mode 100644 index 000000000..6b496083b --- /dev/null +++ b/web/src/components/config-form/section-configs/snapshots.ts @@ -0,0 +1,29 @@ +import type { SectionConfigOverrides } from "./types"; + +const snapshots: SectionConfigOverrides = { + base: { + fieldOrder: [ + "enabled", + "bounding_box", + "crop", + "quality", + "timestamp", + "retain", + ], + fieldGroups: { + display: ["enabled", "bounding_box", "crop", "quality", "timestamp"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["quality", "retain"], + uiSchema: { + required_zones: { + "ui:widget": "zoneNames", + "ui:options": { + suppressMultiSchema: true, + }, + }, + }, + }, +}; + +export default snapshots; diff --git a/web/src/components/config-form/section-configs/telemetry.ts b/web/src/components/config-form/section-configs/telemetry.ts new file mode 100644 index 000000000..838258d59 --- /dev/null +++ b/web/src/components/config-form/section-configs/telemetry.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const telemetry: SectionConfigOverrides = { + base: { + fieldOrder: ["network_interfaces", "stats", "version_check"], + advancedFields: [], + }, +}; + +export default telemetry; diff --git a/web/src/components/config-form/section-configs/timestamp_style.ts b/web/src/components/config-form/section-configs/timestamp_style.ts new file mode 100644 index 000000000..85d4fd9f5 --- /dev/null +++ b/web/src/components/config-form/section-configs/timestamp_style.ts @@ -0,0 +1,11 @@ +import type { SectionConfigOverrides } from "./types"; + +const timestampStyle: SectionConfigOverrides = { + base: { + fieldOrder: ["position", "format", "color", "thickness"], + hiddenFields: ["effect", "enabled_in_config"], + advancedFields: [], + }, +}; + +export default timestampStyle; diff --git a/web/src/components/config-form/section-configs/tls.ts b/web/src/components/config-form/section-configs/tls.ts new file mode 100644 index 000000000..6f8b64c0b --- /dev/null +++ b/web/src/components/config-form/section-configs/tls.ts @@ -0,0 +1,10 @@ +import type { SectionConfigOverrides } from "./types"; + +const tls: SectionConfigOverrides = { + base: { + fieldOrder: ["enabled", "cert", "key"], + advancedFields: [], + }, +}; + +export default tls; diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts new file mode 100644 index 000000000..600a3ca50 --- /dev/null +++ b/web/src/components/config-form/section-configs/types.ts @@ -0,0 +1,7 @@ +import type { SectionConfig } from "../sections/BaseSection"; + +export type SectionConfigOverrides = { + base?: SectionConfig; + global?: Partial; + camera?: Partial; +}; diff --git a/web/src/components/config-form/section-configs/ui.ts b/web/src/components/config-form/section-configs/ui.ts new file mode 100644 index 000000000..4f2e47a98 --- /dev/null +++ b/web/src/components/config-form/section-configs/ui.ts @@ -0,0 +1,22 @@ +import type { SectionConfigOverrides } from "./types"; + +const ui: SectionConfigOverrides = { + base: { + fieldOrder: ["dashboard", "order"], + hiddenFields: [], + advancedFields: [], + overrideFields: [], + }, + global: { + fieldOrder: [ + "timezone", + "time_format", + "date_style", + "time_style", + "unit_system", + ], + advancedFields: [], + }, +}; + +export default ui; diff --git a/web/src/components/config-form/sectionConfigs.ts b/web/src/components/config-form/sectionConfigs.ts index e482089b0..c3a9158a9 100644 --- a/web/src/components/config-form/sectionConfigs.ts +++ b/web/src/components/config-form/sectionConfigs.ts @@ -13,830 +13,73 @@ global?: Partial; // overrides for global-level UI camera?: Partial; // overrides for camera-level UI } - - Merge rules (used by getSectionConfig): - - `base` is the canonical default and is merged with level-specific overrides. - - Arrays (e.g., `fieldOrder`, `advancedFields`, etc.) in overrides **replace** - the `base` arrays (they are not concatenated). - - `uiSchema` in an override **replaces** the base `uiSchema` rather than deep-merging - (this keeps widget overrides explicit per level). - - Other object properties are deep-merged using lodash.mergeWith with custom - behavior for arrays and `uiSchema` as described. - - Example — `ffmpeg`: - - `base` (camera defaults) may include `inputs` and a `fieldOrder` that shows - `"inputs"` first. - - `global` override can replace `fieldOrder` with a different ordering - (e.g., omit `inputs` and show `path` first). Calling - `getSectionConfig("ffmpeg", "global")` will return the merged config - where `fieldOrder` comes from `global` (not concatenated with `base`). */ -import mergeWith from "lodash/mergeWith"; -import type { SectionConfig } from "./sections/BaseSection"; +import type { SectionConfigOverrides } from "./section-configs/types"; +import audio from "./section-configs/audio"; +import audioTranscription from "./section-configs/audio_transcription"; +import auth from "./section-configs/auth"; +import birdseye from "./section-configs/birdseye"; +import classification from "./section-configs/classification"; +import database from "./section-configs/database"; +import detect from "./section-configs/detect"; +import detectors from "./section-configs/detectors"; +import environmentVars from "./section-configs/environment_vars"; +import faceRecognition from "./section-configs/face_recognition"; +import ffmpeg from "./section-configs/ffmpeg"; +import genai from "./section-configs/genai"; +import live from "./section-configs/live"; +import logger from "./section-configs/logger"; +import lpr from "./section-configs/lpr"; +import model from "./section-configs/model"; +import motion from "./section-configs/motion"; +import mqtt from "./section-configs/mqtt"; +import networking from "./section-configs/networking"; +import notifications from "./section-configs/notifications"; +import objects from "./section-configs/objects"; +import onvif from "./section-configs/onvif"; +import proxy from "./section-configs/proxy"; +import record from "./section-configs/record"; +import review from "./section-configs/review"; +import semanticSearch from "./section-configs/semantic_search"; +import snapshots from "./section-configs/snapshots"; +import telemetry from "./section-configs/telemetry"; +import timestampStyle from "./section-configs/timestamp_style"; +import tls from "./section-configs/tls"; +import ui from "./section-configs/ui"; -export type SectionConfigOverrides = { - base?: SectionConfig; - global?: Partial; - camera?: Partial; +export const sectionConfigs: Record = { + detect, + record, + snapshots, + motion, + objects, + review, + audio, + live, + timestamp_style: timestampStyle, + notifications, + onvif, + ffmpeg, + audio_transcription: audioTranscription, + birdseye, + face_recognition: faceRecognition, + lpr, + semantic_search: semanticSearch, + mqtt, + ui, + database, + auth, + tls, + networking, + proxy, + logger, + environment_vars: environmentVars, + telemetry, + detectors, + model, + genai, + classification, }; -const sectionConfigs: Record = { - detect: { - base: { - fieldOrder: [ - "enabled", - "fps", - "width", - "height", - "min_initialized", - "max_disappeared", - "annotation_offset", - "stationary", - ], - fieldGroups: { - resolution: ["enabled", "width", "height"], - tracking: ["min_initialized", "max_disappeared"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: [ - "min_initialized", - "max_disappeared", - "annotation_offset", - "stationary", - ], - }, - }, - record: { - base: { - fieldOrder: [ - "enabled", - "expire_interval", - "continuous", - "motion", - "alerts", - "detections", - "preview", - "export", - ], - fieldGroups: { - retention: ["enabled", "continuous", "motion"], - events: ["alerts", "detections"], - }, - hiddenFields: ["enabled_in_config", "sync_recordings"], - advancedFields: ["expire_interval", "preview", "export"], - }, - }, - snapshots: { - base: { - fieldOrder: [ - "enabled", - "bounding_box", - "crop", - "quality", - "timestamp", - "retain", - ], - fieldGroups: { - display: ["enabled", "bounding_box", "crop", "quality", "timestamp"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["quality", "retain"], - uiSchema: { - required_zones: { - "ui:widget": "zoneNames", - "ui:options": { - suppressMultiSchema: true, - }, - }, - }, - }, - }, - motion: { - base: { - fieldOrder: [ - "enabled", - "threshold", - "lightning_threshold", - "improve_contrast", - "contour_area", - "delta_alpha", - "frame_alpha", - "frame_height", - "mask", - "mqtt_off_delay", - ], - fieldGroups: { - sensitivity: ["enabled", "threshold", "contour_area"], - algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], - }, - hiddenFields: ["enabled_in_config", "mask", "raw_mask"], - advancedFields: [ - "lightning_threshold", - "delta_alpha", - "frame_alpha", - "frame_height", - "mqtt_off_delay", - ], - }, - }, - objects: { - base: { - fieldOrder: ["track", "alert", "detect", "filters"], - fieldGroups: { - tracking: ["track", "alert", "detect"], - filtering: ["filters"], - }, - hiddenFields: [ - "enabled_in_config", - "mask", - "raw_mask", - "genai.enabled_in_config", - "filters.*.mask", - "filters.*.raw_mask", - ], - advancedFields: ["filters"], - uiSchema: { - "filters.*.min_area": { - "ui:options": { - suppressMultiSchema: true, - }, - }, - "filters.*.max_area": { - "ui:options": { - suppressMultiSchema: true, - }, - }, - track: { - "ui:widget": "objectLabels", - "ui:options": { - suppressMultiSchema: true, - }, - }, - genai: { - objects: { - "ui:widget": "objectLabels", - "ui:options": { - suppressMultiSchema: true, - }, - }, - required_zones: { - "ui:widget": "zoneNames", - "ui:options": { - suppressMultiSchema: true, - }, - }, - enabled_in_config: { - "ui:widget": "hidden", - }, - }, - }, - }, - }, - review: { - base: { - fieldOrder: ["alerts", "detections", "genai"], - fieldGroups: {}, - hiddenFields: [ - "enabled_in_config", - "alerts.labels", - "alerts.enabled_in_config", - "alerts.required_zones", - "detections.labels", - "detections.enabled_in_config", - "detections.required_zones", - "genai.enabled_in_config", - ], - advancedFields: [], - uiSchema: { - genai: { - additional_concerns: { - "ui:widget": "textarea", - }, - activity_context_prompt: { - "ui:widget": "textarea", - }, - }, - }, - }, - }, - audio: { - base: { - fieldOrder: [ - "enabled", - "listen", - "filters", - "min_volume", - "max_not_heard", - "num_threads", - ], - fieldGroups: { - detection: ["enabled", "listen", "filters"], - sensitivity: ["min_volume", "max_not_heard"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["min_volume", "max_not_heard", "num_threads"], - uiSchema: { - listen: { - "ui:widget": "audioLabels", - }, - }, - }, - }, - live: { - base: { - fieldOrder: ["stream_name", "height", "quality"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: ["quality"], - }, - }, - timestamp_style: { - base: { - fieldOrder: ["position", "format", "color", "thickness"], - hiddenFields: ["effect", "enabled_in_config"], - advancedFields: [], - }, - }, - notifications: { - base: { - fieldOrder: ["enabled", "email"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: [], - }, - }, - onvif: { - base: { - fieldOrder: [ - "host", - "port", - "user", - "password", - "tls_insecure", - "ignore_time_mismatch", - "autotracking", - ], - hiddenFields: [ - "autotracking.enabled_in_config", - "autotracking.movement_weights", - ], - advancedFields: ["tls_insecure", "ignore_time_mismatch"], - overrideFields: [], - uiSchema: { - autotracking: { - required_zones: { - "ui:widget": "zoneNames", - }, - track: { - "ui:widget": "objectLabels", - }, - }, - }, - }, - }, - ffmpeg: { - base: { - 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", - ], - uiSchema: { - global_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - hwaccel_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - input_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - output_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - detect: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - record: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - items: { - detect: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - record: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - }, - }, - inputs: { - items: { - global_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - hwaccel_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - input_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - output_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - items: { - detect: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - record: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - }, - }, - }, - }, - }, - }, - global: { - 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", - ], - uiSchema: { - global_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - hwaccel_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - input_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - output_args: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - detect: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - record: { - "ui:widget": "ArrayAsTextWidget", - "ui:options": { - suppressMultiSchema: true, - }, - }, - }, - }, - }, - }, - audio_transcription: { - base: { - fieldOrder: [ - "enabled", - "language", - "device", - "model_size", - "live_enabled", - ], - hiddenFields: ["enabled_in_config"], - advancedFields: ["language", "device", "model_size"], - overrideFields: ["enabled", "live_enabled"], - }, - global: { - fieldOrder: [ - "enabled", - "language", - "device", - "model_size", - "live_enabled", - ], - advancedFields: ["language", "device", "model_size"], - }, - }, - birdseye: { - base: { - fieldOrder: ["enabled", "mode", "order"], - hiddenFields: [], - advancedFields: [], - overrideFields: ["enabled", "mode"], - }, - global: { - fieldOrder: [ - "enabled", - "restream", - "width", - "height", - "quality", - "mode", - "layout", - "inactivity_threshold", - "idle_heartbeat_fps", - ], - advancedFields: ["width", "height", "quality", "inactivity_threshold"], - }, - }, - face_recognition: { - base: { - fieldOrder: ["enabled", "min_area"], - hiddenFields: [], - advancedFields: ["min_area"], - overrideFields: ["enabled", "min_area"], - }, - global: { - fieldOrder: [ - "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: { - base: { - fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], - hiddenFields: [], - advancedFields: ["expire_time", "min_area", "enhancement"], - overrideFields: ["enabled", "min_area", "enhancement"], - }, - global: { - fieldOrder: [ - "enabled", - "model_size", - "detection_threshold", - "min_area", - "recognition_threshold", - "min_plate_length", - "format", - "match_distance", - "known_plates", - "enhancement", - "debug_save_plates", - "device", - "replace_rules", - ], - advancedFields: [ - "detection_threshold", - "recognition_threshold", - "min_plate_length", - "format", - "match_distance", - "known_plates", - "enhancement", - "debug_save_plates", - "device", - "replace_rules", - ], - }, - }, - semantic_search: { - base: { - fieldOrder: ["triggers"], - hiddenFields: [], - advancedFields: [], - overrideFields: [], - uiSchema: { - enabled: { - "ui:after": { render: "SemanticSearchReindex" }, - }, - }, - }, - global: { - fieldOrder: ["enabled", "reindex", "model", "model_size", "device"], - advancedFields: ["reindex", "device"], - }, - }, - mqtt: { - base: { - fieldOrder: [ - "enabled", - "timestamp", - "bounding_box", - "crop", - "height", - "required_zones", - "quality", - ], - hiddenFields: [], - advancedFields: ["height", "quality"], - overrideFields: [], - uiSchema: { - required_zones: { - "ui:widget": "zoneNames", - }, - }, - }, - global: { - fieldOrder: [ - "enabled", - "host", - "port", - "user", - "password", - "topic_prefix", - "client_id", - "stats_interval", - "qos", - "tls_ca_certs", - "tls_client_cert", - "tls_client_key", - "tls_insecure", - ], - advancedFields: [ - "stats_interval", - "qos", - "tls_ca_certs", - "tls_client_cert", - "tls_client_key", - "tls_insecure", - ], - liveValidate: true, - uiSchema: {}, - }, - }, - ui: { - base: { - fieldOrder: ["dashboard", "order"], - hiddenFields: [], - advancedFields: [], - overrideFields: [], - }, - global: { - fieldOrder: [ - "timezone", - "time_format", - "date_style", - "time_style", - "unit_system", - ], - advancedFields: [], - }, - }, - database: { - base: { - fieldOrder: ["path"], - advancedFields: [], - }, - }, - auth: { - base: { - fieldOrder: [ - "enabled", - "reset_admin_password", - "cookie_name", - "cookie_secure", - "session_length", - "refresh_time", - "native_oauth_url", - "failed_login_rate_limit", - "trusted_proxies", - "hash_iterations", - "roles", - ], - hiddenFields: ["admin_first_time_login"], - advancedFields: [ - "cookie_name", - "cookie_secure", - "session_length", - "refresh_time", - "failed_login_rate_limit", - "trusted_proxies", - "hash_iterations", - "roles", - ], - uiSchema: { - reset_admin_password: { - "ui:widget": "switch", - }, - }, - }, - }, - tls: { - base: { - fieldOrder: ["enabled", "cert", "key"], - advancedFields: [], - }, - }, - networking: { - base: { - fieldOrder: ["ipv6"], - advancedFields: [], - }, - }, - proxy: { - base: { - fieldOrder: [ - "header_map", - "logout_url", - "auth_secret", - "default_role", - "separator", - ], - advancedFields: ["header_map", "auth_secret", "separator"], - liveValidate: true, - }, - }, - logger: { - base: { - fieldOrder: ["default", "logs"], - advancedFields: ["logs"], - }, - }, - environment_vars: { - base: { - fieldOrder: [], - advancedFields: [], - }, - }, - telemetry: { - base: { - fieldOrder: ["network_interfaces", "stats", "version_check"], - advancedFields: [], - }, - }, - detectors: { - base: { - fieldOrder: [], - advancedFields: [], - }, - }, - model: { - base: { - fieldOrder: [ - "path", - "labelmap_path", - "width", - "height", - "input_pixel_format", - "input_tensor", - "input_dtype", - "model_type", - ], - advancedFields: [ - "input_pixel_format", - "input_tensor", - "input_dtype", - "model_type", - ], - hiddenFields: ["labelmap", "attributes_map"], - }, - }, - genai: { - base: { - fieldOrder: [ - "provider", - "api_key", - "base_url", - "model", - "provider_options", - "runtime_options", - ], - advancedFields: ["base_url", "provider_options", "runtime_options"], - hiddenFields: ["genai.enabled_in_config"], - }, - }, - classification: { - base: { - hiddenFields: ["custom"], - advancedFields: [], - }, - }, -}; - -const mergeSectionConfig = ( - base: SectionConfig | undefined, - overrides: Partial | undefined, -): SectionConfig => - mergeWith({}, base ?? {}, overrides ?? {}, (objValue, srcValue, key) => { - if (Array.isArray(objValue) || Array.isArray(srcValue)) { - return srcValue ?? objValue; - } - - if (key === "uiSchema" && srcValue !== undefined) { - return srcValue; - } - - return undefined; - }); - -export function getSectionConfig( - sectionKey: string, - level: "global" | "camera", -): SectionConfig { - const entry = sectionConfigs[sectionKey]; - if (!entry) { - return {}; - } - - const overrides = level === "global" ? entry.global : entry.camera; - return mergeSectionConfig(entry.base, overrides); -} +export type { SectionConfigOverrides } from "./section-configs/types"; diff --git a/web/src/components/config-form/sectionConfigsUtils.ts b/web/src/components/config-form/sectionConfigsUtils.ts new file mode 100644 index 000000000..c8467bf75 --- /dev/null +++ b/web/src/components/config-form/sectionConfigsUtils.ts @@ -0,0 +1,32 @@ +import mergeWith from "lodash/mergeWith"; +import type { SectionConfig } from "./sections/BaseSection"; +import { sectionConfigs } from "./sectionConfigs"; + +const mergeSectionConfig = ( + base: SectionConfig | undefined, + overrides: Partial | undefined, +): SectionConfig => + mergeWith({}, base ?? {}, overrides ?? {}, (objValue, srcValue, key) => { + if (Array.isArray(objValue) || Array.isArray(srcValue)) { + return srcValue ?? objValue; + } + + if (key === "uiSchema" && srcValue !== undefined) { + return srcValue; + } + + return undefined; + }); + +export function getSectionConfig( + sectionKey: string, + level: "global" | "camera", +): SectionConfig { + const entry = sectionConfigs[sectionKey]; + if (!entry) { + return {}; + } + + const overrides = level === "global" ? entry.global : entry.camera; + return mergeSectionConfig(entry.base, overrides); +} diff --git a/web/src/components/config-form/sections/ConfigSectionTemplate.tsx b/web/src/components/config-form/sections/ConfigSectionTemplate.tsx new file mode 100644 index 000000000..18cd58ede --- /dev/null +++ b/web/src/components/config-form/sections/ConfigSectionTemplate.tsx @@ -0,0 +1,40 @@ +import { useMemo } from "react"; +import { createConfigSection } from "./BaseSection"; +import type { BaseSectionProps, SectionConfig } from "./BaseSection"; +import { getSectionConfig } from "@/components/config-form/sectionConfigsUtils"; + +export type ConfigSectionTemplateProps = BaseSectionProps & { + sectionKey: string; + sectionConfig?: SectionConfig; +}; + +export function ConfigSectionTemplate({ + sectionKey, + level, + sectionConfig, + ...rest +}: ConfigSectionTemplateProps) { + const defaultConfig = useMemo( + () => getSectionConfig(sectionKey, level), + [sectionKey, level], + ); + + const SectionComponent = useMemo( + () => + createConfigSection({ + sectionPath: sectionKey, + defaultConfig, + }), + [sectionKey, defaultConfig], + ); + + return ( + + ); +} + +export default ConfigSectionTemplate; diff --git a/web/src/components/config-form/sections/index.ts b/web/src/components/config-form/sections/index.ts index ca92c1409..3dce059f0 100644 --- a/web/src/components/config-form/sections/index.ts +++ b/web/src/components/config-form/sections/index.ts @@ -7,37 +7,7 @@ export { type SectionConfig, type CreateSectionOptions, } from "./BaseSection"; - -export { DetectSection } from "./DetectSection"; -export { RecordSection } from "./RecordSection"; -export { SnapshotsSection } from "./SnapshotsSection"; -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 { AuthSection } from "./AuthSection"; -export { ClassificationSection } from "./ClassificationSection"; -export { CameraMqttSection } from "./CameraMqttSection"; -export { CameraUiSection } from "./CameraUiSection"; -export { DatabaseSection } from "./DatabaseSection"; -export { DetectorsSection } from "./DetectorsSection"; -export { EnvironmentVarsSection } from "./EnvironmentVarsSection"; -export { FaceRecognitionSection } from "./FaceRecognitionSection"; -export { FfmpegSection } from "./FfmpegSection"; -export { GenaiSection } from "./GenaiSection"; -export { LprSection } from "./LprSection"; -export { LoggerSection } from "./LoggerSection"; -export { NotificationsSection } from "./NotificationsSection"; -export { OnvifSection } from "./OnvifSection"; -export { LiveSection } from "./LiveSection"; -export { ModelSection } from "./ModelSection"; -export { MqttSection } from "./MqttSection"; -export { NetworkingSection } from "./NetworkingSection"; -export { ProxySection } from "./ProxySection"; -export { SemanticSearchSection } from "./SemanticSearchSection"; -export { TelemetrySection } from "./TelemetrySection"; -export { TimestampSection } from "./TimestampSection"; -export { TlsSection } from "./TlsSection"; -export { UiSection } from "./UiSection"; +export { + ConfigSectionTemplate, + type ConfigSectionTemplateProps, +} from "./ConfigSectionTemplate"; diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index f74cfc817..e76899e9b 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -65,10 +65,6 @@ import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; import { LuChevronRight } from "react-icons/lu"; import Logo from "@/components/Logo"; -import { - CameraMqttSection, - MqttSection, -} from "@/components/config-form/sections"; import { MobilePage, MobilePageContent, @@ -100,13 +96,11 @@ type SettingsType = (typeof allSettingsViews)[number]; const MqttSettingsPage = createSingleSectionPage({ sectionKey: "mqtt", level: "global", - SectionComponent: MqttSection, }); const CameraMqttSettingsPage = createSingleSectionPage({ sectionKey: "mqtt", level: "camera", - SectionComponent: CameraMqttSection, showOverrideIndicator: false, }); diff --git a/web/src/views/settings/CameraConfigView.tsx b/web/src/views/settings/CameraConfigView.tsx index 51ec0d8bb..d073dacbb 100644 --- a/web/src/views/settings/CameraConfigView.tsx +++ b/web/src/views/settings/CameraConfigView.tsx @@ -4,24 +4,7 @@ import { useMemo, useCallback, useState, memo } from "react"; import useSWR from "swr"; import { useTranslation } from "react-i18next"; -import { DetectSection } from "@/components/config-form/sections/DetectSection"; -import { RecordSection } from "@/components/config-form/sections/RecordSection"; -import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection"; -import { MotionSection } from "@/components/config-form/sections/MotionSection"; -import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection"; -import { ReviewSection } from "@/components/config-form/sections/ReviewSection"; -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 { OnvifSection } from "@/components/config-form/sections/OnvifSection"; -import { LiveSection } from "@/components/config-form/sections/LiveSection"; -import { TimestampSection } from "@/components/config-form/sections/TimestampSection"; +import { ConfigSectionTemplate } from "@/components/config-form/sections"; import { useAllCameraOverrides } from "@/hooks/use-config-override"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -203,83 +186,26 @@ const CameraConfigContent = memo(function CameraConfigContent({ const sections: Array<{ key: string; - component: typeof DetectSection; showOverrideIndicator?: boolean; }> = [ - { - key: "detect", - component: DetectSection, - }, - { - key: "ffmpeg", - component: FfmpegSection, - showOverrideIndicator: true, - }, - { - key: "record", - component: RecordSection, - }, - { - key: "snapshots", - component: SnapshotsSection, - }, - { - key: "motion", - component: MotionSection, - }, - { - key: "objects", - component: ObjectsSection, - }, - { - key: "review", - component: ReviewSection, - }, - { key: "audio", component: AudioSection }, - { - key: "audio_transcription", - component: AudioTranscriptionSection, - showOverrideIndicator: true, - }, - { - key: "notifications", - component: NotificationsSection, - }, - { key: "live", component: LiveSection }, - { - key: "birdseye", - component: BirdseyeSection, - showOverrideIndicator: true, - }, - { - key: "face_recognition", - component: FaceRecognitionSection, - showOverrideIndicator: true, - }, - { - key: "lpr", - component: LprSection, - showOverrideIndicator: true, - }, - { - key: "mqtt", - component: CameraMqttSection, - showOverrideIndicator: false, - }, - { - key: "onvif", - component: OnvifSection, - showOverrideIndicator: false, - }, - { - key: "ui", - component: CameraUiSection, - showOverrideIndicator: false, - }, - { - key: "timestamp_style", - component: TimestampSection, - }, + { key: "detect" }, + { key: "ffmpeg", showOverrideIndicator: true }, + { key: "record" }, + { key: "snapshots" }, + { key: "motion" }, + { key: "objects" }, + { key: "review" }, + { key: "audio" }, + { key: "audio_transcription", showOverrideIndicator: true }, + { key: "notifications" }, + { key: "live" }, + { key: "birdseye", showOverrideIndicator: true }, + { key: "face_recognition", showOverrideIndicator: true }, + { key: "lpr", showOverrideIndicator: true }, + { key: "mqtt", showOverrideIndicator: false }, + { key: "onvif", showOverrideIndicator: false }, + { key: "ui", showOverrideIndicator: false }, + { key: "timestamp_style" }, ]; return ( @@ -327,23 +253,21 @@ const CameraConfigContent = memo(function CameraConfigContent({ {/* Section Content */}
- {sections.map((section) => { - const SectionComponent = section.component; - return ( -
- -
- ); - })} + {sections.map((section) => ( +
+ +
+ ))}
); diff --git a/web/src/views/settings/GlobalConfigView.tsx b/web/src/views/settings/GlobalConfigView.tsx index 72b0df7be..d06f928fa 100644 --- a/web/src/views/settings/GlobalConfigView.tsx +++ b/web/src/views/settings/GlobalConfigView.tsx @@ -4,81 +4,51 @@ import { useMemo, useCallback, useState } from "react"; import useSWR from "swr"; import { useTranslation } from "react-i18next"; -import { DetectSection } from "@/components/config-form/sections/DetectSection"; -import { RecordSection } from "@/components/config-form/sections/RecordSection"; -import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection"; -import { MotionSection } from "@/components/config-form/sections/MotionSection"; -import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection"; -import { ReviewSection } from "@/components/config-form/sections/ReviewSection"; -import { AudioSection } from "@/components/config-form/sections/AudioSection"; -import { AudioTranscriptionSection } from "@/components/config-form/sections/AudioTranscriptionSection"; -import { AuthSection } from "@/components/config-form/sections/AuthSection"; -import { BirdseyeSection } from "@/components/config-form/sections/BirdseyeSection"; -import { ClassificationSection } from "@/components/config-form/sections/ClassificationSection"; -import { DatabaseSection } from "@/components/config-form/sections/DatabaseSection"; -import { DetectorsSection } from "@/components/config-form/sections/DetectorsSection"; -import { EnvironmentVarsSection } from "@/components/config-form/sections/EnvironmentVarsSection"; -import { FaceRecognitionSection } from "@/components/config-form/sections/FaceRecognitionSection"; -import { FfmpegSection } from "@/components/config-form/sections/FfmpegSection"; -import { GenaiSection } from "@/components/config-form/sections/GenaiSection"; -import { LiveSection } from "@/components/config-form/sections/LiveSection"; -import { LoggerSection } from "@/components/config-form/sections/LoggerSection"; -import { LprSection } from "@/components/config-form/sections/LprSection"; -import { ModelSection } from "@/components/config-form/sections/ModelSection"; -import { MqttSection } from "@/components/config-form/sections/MqttSection"; -import { NetworkingSection } from "@/components/config-form/sections/NetworkingSection"; -import { ProxySection } from "@/components/config-form/sections/ProxySection"; -import { SemanticSearchSection } from "@/components/config-form/sections/SemanticSearchSection"; -import { TimestampSection } from "@/components/config-form/sections/TimestampSection"; -import { TelemetrySection } from "@/components/config-form/sections/TelemetrySection"; -import { TlsSection } from "@/components/config-form/sections/TlsSection"; -import { UiSection } from "@/components/config-form/sections/UiSection"; +import { ConfigSectionTemplate } from "@/components/config-form/sections"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import Heading from "@/components/ui/heading"; import { cn } from "@/lib/utils"; -import { getSectionConfig } from "@/components/config-form/sectionConfigs"; - // Shared sections that can be overridden at camera level const sharedSections = [ - { key: "detect", component: DetectSection }, - { key: "record", component: RecordSection }, - { key: "snapshots", component: SnapshotsSection }, - { key: "motion", component: MotionSection }, - { key: "objects", component: ObjectsSection }, - { key: "review", component: ReviewSection }, - { key: "audio", component: AudioSection }, - { key: "live", component: LiveSection }, - { key: "timestamp_style", component: TimestampSection }, + { key: "detect" }, + { key: "record" }, + { key: "snapshots" }, + { key: "motion" }, + { key: "objects" }, + { key: "review" }, + { key: "audio" }, + { key: "live" }, + { key: "timestamp_style" }, ]; // System sections (global only) const systemSections = [ - { key: "database", component: DatabaseSection }, - { key: "tls", component: TlsSection }, - { key: "auth", component: AuthSection }, - { key: "networking", component: NetworkingSection }, - { key: "proxy", component: ProxySection }, - { key: "ui", component: UiSection }, - { key: "logger", component: LoggerSection }, - { key: "environment_vars", component: EnvironmentVarsSection }, - { key: "telemetry", component: TelemetrySection }, - { key: "birdseye", component: BirdseyeSection }, - { key: "ffmpeg", component: FfmpegSection }, - { key: "detectors", component: DetectorsSection }, - { key: "model", component: ModelSection }, + { key: "database" }, + { key: "tls" }, + { key: "auth" }, + { key: "networking" }, + { key: "proxy" }, + { key: "ui" }, + { key: "logger" }, + { key: "environment_vars" }, + { key: "telemetry" }, + { key: "birdseye" }, + { key: "ffmpeg" }, + { key: "detectors" }, + { key: "model" }, ]; // Integration sections (global only) const integrationSections = [ - { key: "mqtt", component: MqttSection }, - { key: "semantic_search", component: SemanticSearchSection }, - { key: "genai", component: GenaiSection }, - { key: "face_recognition", component: FaceRecognitionSection }, - { key: "lpr", component: LprSection }, - { key: "classification", component: ClassificationSection }, - { key: "audio_transcription", component: AudioTranscriptionSection }, + { key: "mqtt" }, + { key: "semantic_search" }, + { key: "genai" }, + { key: "face_recognition" }, + { key: "lpr" }, + { key: "classification" }, + { key: "audio_transcription" }, ]; export default function GlobalConfigView() { @@ -201,24 +171,21 @@ export default function GlobalConfigView() { {/* Section Content */}
- {currentSections.map((section) => { - const SectionComponent = section.component; - return ( -
- -
- ); - })} + {currentSections.map((section) => ( +
+ +
+ ))}
diff --git a/web/src/views/settings/SingleSectionPage.tsx b/web/src/views/settings/SingleSectionPage.tsx index e73b08947..24f9cfbc9 100644 --- a/web/src/views/settings/SingleSectionPage.tsx +++ b/web/src/views/settings/SingleSectionPage.tsx @@ -1,9 +1,7 @@ import { useTranslation } from "react-i18next"; import Heading from "@/components/ui/heading"; -import type { - BaseSectionProps, - SectionConfig, -} from "@/components/config-form/sections"; +import type { SectionConfig } from "@/components/config-form/sections"; +import { ConfigSectionTemplate } from "@/components/config-form/sections"; import type { PolygonType } from "@/types/canvas"; export type SettingsPageProps = { @@ -15,7 +13,6 @@ export type SettingsPageProps = { export type SingleSectionPageOptions = { sectionKey: string; level: "global" | "camera"; - SectionComponent: React.ComponentType; sectionConfig?: SectionConfig; requiresRestart?: boolean; showOverrideIndicator?: boolean; @@ -24,7 +21,6 @@ export type SingleSectionPageOptions = { export function createSingleSectionPage({ sectionKey, level, - SectionComponent, sectionConfig, requiresRestart, showOverrideIndicator = true, @@ -63,7 +59,8 @@ export function createSingleSectionPage({

)} -