From e09928a7f0c505649598a51510e66e413e02af0f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:35:23 -0600 Subject: [PATCH] refactor sections and overrides --- web/public/locales/en/config/global.json | 6 +- web/public/locales/en/views/settings.json | 2 + .../components/config-form/sectionConfigs.ts | 837 ++++++++++++++++++ .../config-form/sections/AudioSection.tsx | 23 +- .../sections/AudioTranscriptionSection.tsx | 8 +- .../config-form/sections/AuthSection.tsx | 12 + .../config-form/sections/BirdseyeSection.tsx | 8 +- .../sections/CameraMqttSection.tsx | 21 +- .../config-form/sections/CameraUiSection.tsx | 8 +- .../sections/ClassificationSection.tsx | 12 + .../config-form/sections/DatabaseSection.tsx | 12 + .../config-form/sections/DetectSection.tsx | 25 +- .../config-form/sections/DetectorsSection.tsx | 12 + .../sections/EnvironmentVarsSection.tsx | 12 + .../sections/FaceRecognitionSection.tsx | 8 +- .../config-form/sections/FfmpegSection.tsx | 129 +-- .../config-form/sections/GenaiSection.tsx | 12 + .../config-form/sections/LiveSection.tsx | 8 +- .../config-form/sections/LoggerSection.tsx | 12 + .../config-form/sections/LprSection.tsx | 8 +- .../config-form/sections/ModelSection.tsx | 12 + .../config-form/sections/MotionSection.tsx | 28 +- .../config-form/sections/MqttSection.tsx | 12 + .../sections/NetworkingSection.tsx | 12 + .../sections/NotificationsSection.tsx | 8 +- .../config-form/sections/ObjectsSection.tsx | 53 +- .../config-form/sections/OnvifSection.tsx | 29 +- .../config-form/sections/ProxySection.tsx | 12 + .../config-form/sections/RecordSection.tsx | 20 +- .../config-form/sections/ReviewSection.tsx | 27 +- .../sections/SemanticSearchSection.tsx | 8 +- .../config-form/sections/SnapshotsSection.tsx | 25 +- .../config-form/sections/TelemetrySection.tsx | 12 + .../config-form/sections/TimestampSection.tsx | 7 +- .../config-form/sections/TlsSection.tsx | 12 + .../config-form/sections/UiSection.tsx | 12 + .../components/config-form/sections/index.ts | 14 + web/src/pages/Settings.tsx | 23 + web/src/views/settings/GlobalConfigView.tsx | 726 +++------------ web/src/views/settings/SingleSectionPage.tsx | 78 ++ 40 files changed, 1264 insertions(+), 1041 deletions(-) create mode 100644 web/src/components/config-form/sectionConfigs.ts create mode 100644 web/src/components/config-form/sections/AuthSection.tsx create mode 100644 web/src/components/config-form/sections/ClassificationSection.tsx create mode 100644 web/src/components/config-form/sections/DatabaseSection.tsx create mode 100644 web/src/components/config-form/sections/DetectorsSection.tsx create mode 100644 web/src/components/config-form/sections/EnvironmentVarsSection.tsx create mode 100644 web/src/components/config-form/sections/GenaiSection.tsx create mode 100644 web/src/components/config-form/sections/LoggerSection.tsx create mode 100644 web/src/components/config-form/sections/ModelSection.tsx create mode 100644 web/src/components/config-form/sections/MqttSection.tsx create mode 100644 web/src/components/config-form/sections/NetworkingSection.tsx create mode 100644 web/src/components/config-form/sections/ProxySection.tsx create mode 100644 web/src/components/config-form/sections/TelemetrySection.tsx create mode 100644 web/src/components/config-form/sections/TlsSection.tsx create mode 100644 web/src/components/config-form/sections/UiSection.tsx create mode 100644 web/src/views/settings/SingleSectionPage.tsx diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 395dcbc8a..0a2373b7b 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1069,15 +1069,15 @@ }, "language": { "label": "Transcription language", - "description": "Language code used for transcription/translation (for example 'en' for English)." + "description": "Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes." }, "device": { "label": "Transcription device", - "description": "Device key (CPU/GPU) to run the transcription model on." + "description": "Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription." }, "model_size": { "label": "Model size", - "description": "Model size to use for transcription; the small model runs on CPU, large model requires a GPU." + "description": "Model size to use for offline audio event transcription." }, "live_enabled": { "label": "Live transcription", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 6e1fe387e..639d1a322 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -18,7 +18,9 @@ "menu": { "ui": "UI", "globalConfig": "Global Config", + "mqtt": "MQTT", "cameraConfig": "Camera Config", + "cameraMqtt": "Camera MQTT", "enrichments": "Enrichments", "cameraManagement": "Management", "cameraReview": "Review", diff --git a/web/src/components/config-form/sectionConfigs.ts b/web/src/components/config-form/sectionConfigs.ts new file mode 100644 index 000000000..1c9e87f69 --- /dev/null +++ b/web/src/components/config-form/sectionConfigs.ts @@ -0,0 +1,837 @@ +/* + sectionConfigs.ts — section configuration overrides + + Purpose: + - Centralize UI configuration hints for each config section (field ordering, + grouping, hidden/advanced fields, uiSchema overrides, and overrideFields). + + Shape: + - Each section key maps to an object with optional `base`, `global`, and + `camera` entries where each is a `SectionConfig` (or partial): + { + base?: SectionConfig; // common defaults (typically camera-level) + 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"; + +export type SectionConfigOverrides = { + base?: SectionConfig; + global?: Partial; + camera?: Partial; +}; + +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: [], + }, + 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); +} diff --git a/web/src/components/config-form/sections/AudioSection.tsx b/web/src/components/config-form/sections/AudioSection.tsx index 72e805bcf..b50d45b93 100644 --- a/web/src/components/config-form/sections/AudioSection.tsx +++ b/web/src/components/config-form/sections/AudioSection.tsx @@ -2,30 +2,11 @@ // Reusable for both global and camera-level audio settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const AudioSection = createConfigSection({ sectionPath: "audio", - defaultConfig: { - 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", - }, - }, - }, + defaultConfig: getSectionConfig("audio", "camera"), }); export default AudioSection; diff --git a/web/src/components/config-form/sections/AudioTranscriptionSection.tsx b/web/src/components/config-form/sections/AudioTranscriptionSection.tsx index 8beb09e0b..f9d8c2653 100644 --- a/web/src/components/config-form/sections/AudioTranscriptionSection.tsx +++ b/web/src/components/config-form/sections/AudioTranscriptionSection.tsx @@ -2,15 +2,11 @@ // Global and camera-level audio transcription settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const AudioTranscriptionSection = createConfigSection({ sectionPath: "audio_transcription", - defaultConfig: { - fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"], - hiddenFields: ["enabled_in_config"], - advancedFields: ["language", "device", "model_size"], - overrideFields: ["enabled", "live_enabled"], - }, + defaultConfig: getSectionConfig("audio_transcription", "camera"), }); export default AudioTranscriptionSection; diff --git a/web/src/components/config-form/sections/AuthSection.tsx b/web/src/components/config-form/sections/AuthSection.tsx new file mode 100644 index 000000000..abaf26692 --- /dev/null +++ b/web/src/components/config-form/sections/AuthSection.tsx @@ -0,0 +1,12 @@ +// Auth Section Component +// Global authentication configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const AuthSection = createConfigSection({ + sectionPath: "auth", + defaultConfig: getSectionConfig("auth", "global"), +}); + +export default AuthSection; diff --git a/web/src/components/config-form/sections/BirdseyeSection.tsx b/web/src/components/config-form/sections/BirdseyeSection.tsx index a96bb2511..f82edacf4 100644 --- a/web/src/components/config-form/sections/BirdseyeSection.tsx +++ b/web/src/components/config-form/sections/BirdseyeSection.tsx @@ -2,15 +2,11 @@ // Camera-level birdseye settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const BirdseyeSection = createConfigSection({ sectionPath: "birdseye", - defaultConfig: { - fieldOrder: ["enabled", "mode", "order"], - hiddenFields: [], - advancedFields: [], - overrideFields: ["enabled", "mode"], - }, + defaultConfig: getSectionConfig("birdseye", "camera"), }); export default BirdseyeSection; diff --git a/web/src/components/config-form/sections/CameraMqttSection.tsx b/web/src/components/config-form/sections/CameraMqttSection.tsx index e00fe7203..3a782537c 100644 --- a/web/src/components/config-form/sections/CameraMqttSection.tsx +++ b/web/src/components/config-form/sections/CameraMqttSection.tsx @@ -2,28 +2,11 @@ // Camera-specific MQTT image publishing settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const CameraMqttSection = createConfigSection({ sectionPath: "mqtt", - defaultConfig: { - fieldOrder: [ - "enabled", - "timestamp", - "bounding_box", - "crop", - "height", - "required_zones", - "quality", - ], - hiddenFields: [], - advancedFields: ["height", "quality"], - overrideFields: [], - uiSchema: { - required_zones: { - "ui:widget": "zoneNames", - }, - }, - }, + defaultConfig: getSectionConfig("mqtt", "camera"), }); export default CameraMqttSection; diff --git a/web/src/components/config-form/sections/CameraUiSection.tsx b/web/src/components/config-form/sections/CameraUiSection.tsx index b8d812ae4..4627d69f1 100644 --- a/web/src/components/config-form/sections/CameraUiSection.tsx +++ b/web/src/components/config-form/sections/CameraUiSection.tsx @@ -2,15 +2,11 @@ // Camera UI display settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const CameraUiSection = createConfigSection({ sectionPath: "ui", - defaultConfig: { - fieldOrder: ["dashboard", "order"], - hiddenFields: [], - advancedFields: [], - overrideFields: [], - }, + defaultConfig: getSectionConfig("ui", "camera"), }); export default CameraUiSection; diff --git a/web/src/components/config-form/sections/ClassificationSection.tsx b/web/src/components/config-form/sections/ClassificationSection.tsx new file mode 100644 index 000000000..4c94ded55 --- /dev/null +++ b/web/src/components/config-form/sections/ClassificationSection.tsx @@ -0,0 +1,12 @@ +// Classification Section Component +// Global classification configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const ClassificationSection = createConfigSection({ + sectionPath: "classification", + defaultConfig: getSectionConfig("classification", "global"), +}); + +export default ClassificationSection; diff --git a/web/src/components/config-form/sections/DatabaseSection.tsx b/web/src/components/config-form/sections/DatabaseSection.tsx new file mode 100644 index 000000000..0cf51469d --- /dev/null +++ b/web/src/components/config-form/sections/DatabaseSection.tsx @@ -0,0 +1,12 @@ +// Database Section Component +// Global database configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const DatabaseSection = createConfigSection({ + sectionPath: "database", + defaultConfig: getSectionConfig("database", "global"), +}); + +export default DatabaseSection; diff --git a/web/src/components/config-form/sections/DetectSection.tsx b/web/src/components/config-form/sections/DetectSection.tsx index d686cb27b..206d16f2c 100644 --- a/web/src/components/config-form/sections/DetectSection.tsx +++ b/web/src/components/config-form/sections/DetectSection.tsx @@ -2,32 +2,11 @@ // Reusable for both global and camera-level detect settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const DetectSection = createConfigSection({ sectionPath: "detect", - defaultConfig: { - 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", - ], - }, + defaultConfig: getSectionConfig("detect", "camera"), }); export default DetectSection; diff --git a/web/src/components/config-form/sections/DetectorsSection.tsx b/web/src/components/config-form/sections/DetectorsSection.tsx new file mode 100644 index 000000000..4a724205b --- /dev/null +++ b/web/src/components/config-form/sections/DetectorsSection.tsx @@ -0,0 +1,12 @@ +// Detectors Section Component +// Global detectors configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const DetectorsSection = createConfigSection({ + sectionPath: "detectors", + defaultConfig: getSectionConfig("detectors", "global"), +}); + +export default DetectorsSection; diff --git a/web/src/components/config-form/sections/EnvironmentVarsSection.tsx b/web/src/components/config-form/sections/EnvironmentVarsSection.tsx new file mode 100644 index 000000000..86f1db6fa --- /dev/null +++ b/web/src/components/config-form/sections/EnvironmentVarsSection.tsx @@ -0,0 +1,12 @@ +// Environment Variables Section Component +// Global environment variables configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const EnvironmentVarsSection = createConfigSection({ + sectionPath: "environment_vars", + defaultConfig: getSectionConfig("environment_vars", "global"), +}); + +export default EnvironmentVarsSection; diff --git a/web/src/components/config-form/sections/FaceRecognitionSection.tsx b/web/src/components/config-form/sections/FaceRecognitionSection.tsx index 405a5373f..b86909bdb 100644 --- a/web/src/components/config-form/sections/FaceRecognitionSection.tsx +++ b/web/src/components/config-form/sections/FaceRecognitionSection.tsx @@ -2,15 +2,11 @@ // Camera-level face recognition settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const FaceRecognitionSection = createConfigSection({ sectionPath: "face_recognition", - defaultConfig: { - fieldOrder: ["enabled", "min_area"], - hiddenFields: [], - advancedFields: ["min_area"], - overrideFields: ["enabled", "min_area"], - }, + defaultConfig: getSectionConfig("face_recognition", "camera"), }); export default FaceRecognitionSection; diff --git a/web/src/components/config-form/sections/FfmpegSection.tsx b/web/src/components/config-form/sections/FfmpegSection.tsx index 54f1978a1..f3f267649 100644 --- a/web/src/components/config-form/sections/FfmpegSection.tsx +++ b/web/src/components/config-form/sections/FfmpegSection.tsx @@ -2,136 +2,11 @@ // Global and camera-level FFmpeg settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const FfmpegSection = createConfigSection({ sectionPath: "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", - ], - 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, - }, - }, - }, - }, - }, - }, - }, - }, + defaultConfig: getSectionConfig("ffmpeg", "camera"), }); export default FfmpegSection; diff --git a/web/src/components/config-form/sections/GenaiSection.tsx b/web/src/components/config-form/sections/GenaiSection.tsx new file mode 100644 index 000000000..c7eb2aaa1 --- /dev/null +++ b/web/src/components/config-form/sections/GenaiSection.tsx @@ -0,0 +1,12 @@ +// GenAI Section Component +// Global GenAI configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const GenaiSection = createConfigSection({ + sectionPath: "genai", + defaultConfig: getSectionConfig("genai", "global"), +}); + +export default GenaiSection; diff --git a/web/src/components/config-form/sections/LiveSection.tsx b/web/src/components/config-form/sections/LiveSection.tsx index ba578b68d..20171d20c 100644 --- a/web/src/components/config-form/sections/LiveSection.tsx +++ b/web/src/components/config-form/sections/LiveSection.tsx @@ -2,15 +2,11 @@ // Reusable for both global and camera-level live settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const LiveSection = createConfigSection({ sectionPath: "live", - defaultConfig: { - fieldOrder: ["stream_name", "height", "quality"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: ["quality"], - }, + defaultConfig: getSectionConfig("live", "camera"), }); export default LiveSection; diff --git a/web/src/components/config-form/sections/LoggerSection.tsx b/web/src/components/config-form/sections/LoggerSection.tsx new file mode 100644 index 000000000..4cb690992 --- /dev/null +++ b/web/src/components/config-form/sections/LoggerSection.tsx @@ -0,0 +1,12 @@ +// Logger Section Component +// Global logger configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const LoggerSection = createConfigSection({ + sectionPath: "logger", + defaultConfig: getSectionConfig("logger", "global"), +}); + +export default LoggerSection; diff --git a/web/src/components/config-form/sections/LprSection.tsx b/web/src/components/config-form/sections/LprSection.tsx index fe19aca6b..6eea7166e 100644 --- a/web/src/components/config-form/sections/LprSection.tsx +++ b/web/src/components/config-form/sections/LprSection.tsx @@ -2,15 +2,11 @@ // Camera-level LPR settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const LprSection = createConfigSection({ sectionPath: "lpr", - defaultConfig: { - fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], - hiddenFields: [], - advancedFields: ["expire_time", "min_area", "enhancement"], - overrideFields: ["enabled", "min_area", "enhancement"], - }, + defaultConfig: getSectionConfig("lpr", "camera"), }); export default LprSection; diff --git a/web/src/components/config-form/sections/ModelSection.tsx b/web/src/components/config-form/sections/ModelSection.tsx new file mode 100644 index 000000000..2c3587e1d --- /dev/null +++ b/web/src/components/config-form/sections/ModelSection.tsx @@ -0,0 +1,12 @@ +// Model Section Component +// Global model configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const ModelSection = createConfigSection({ + sectionPath: "model", + defaultConfig: getSectionConfig("model", "global"), +}); + +export default ModelSection; diff --git a/web/src/components/config-form/sections/MotionSection.tsx b/web/src/components/config-form/sections/MotionSection.tsx index 69765b67d..d688476cc 100644 --- a/web/src/components/config-form/sections/MotionSection.tsx +++ b/web/src/components/config-form/sections/MotionSection.tsx @@ -2,35 +2,11 @@ // Reusable for both global and camera-level motion settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const MotionSection = createConfigSection({ sectionPath: "motion", - defaultConfig: { - 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", - ], - }, + defaultConfig: getSectionConfig("motion", "camera"), }); export default MotionSection; diff --git a/web/src/components/config-form/sections/MqttSection.tsx b/web/src/components/config-form/sections/MqttSection.tsx new file mode 100644 index 000000000..6ee1e9ce7 --- /dev/null +++ b/web/src/components/config-form/sections/MqttSection.tsx @@ -0,0 +1,12 @@ +// MQTT Section Component +// Global MQTT configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const MqttSection = createConfigSection({ + sectionPath: "mqtt", + defaultConfig: getSectionConfig("mqtt", "global"), +}); + +export default MqttSection; diff --git a/web/src/components/config-form/sections/NetworkingSection.tsx b/web/src/components/config-form/sections/NetworkingSection.tsx new file mode 100644 index 000000000..1d9167116 --- /dev/null +++ b/web/src/components/config-form/sections/NetworkingSection.tsx @@ -0,0 +1,12 @@ +// Networking Section Component +// Global networking configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const NetworkingSection = createConfigSection({ + sectionPath: "networking", + defaultConfig: getSectionConfig("networking", "global"), +}); + +export default NetworkingSection; diff --git a/web/src/components/config-form/sections/NotificationsSection.tsx b/web/src/components/config-form/sections/NotificationsSection.tsx index b89d0a8c3..265a199af 100644 --- a/web/src/components/config-form/sections/NotificationsSection.tsx +++ b/web/src/components/config-form/sections/NotificationsSection.tsx @@ -2,15 +2,11 @@ // Reusable for both global and camera-level notification settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const NotificationsSection = createConfigSection({ sectionPath: "notifications", - defaultConfig: { - fieldOrder: ["enabled", "email"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: [], - }, + defaultConfig: getSectionConfig("notifications", "camera"), }); export default NotificationsSection; diff --git a/web/src/components/config-form/sections/ObjectsSection.tsx b/web/src/components/config-form/sections/ObjectsSection.tsx index 19404d989..97f68a2a5 100644 --- a/web/src/components/config-form/sections/ObjectsSection.tsx +++ b/web/src/components/config-form/sections/ObjectsSection.tsx @@ -2,60 +2,11 @@ // Reusable for both global and camera-level objects settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const ObjectsSection = createConfigSection({ sectionPath: "objects", - defaultConfig: { - 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", - }, - }, - }, - }, + defaultConfig: getSectionConfig("objects", "camera"), }); export default ObjectsSection; diff --git a/web/src/components/config-form/sections/OnvifSection.tsx b/web/src/components/config-form/sections/OnvifSection.tsx index 349152511..4fc028cc8 100644 --- a/web/src/components/config-form/sections/OnvifSection.tsx +++ b/web/src/components/config-form/sections/OnvifSection.tsx @@ -2,36 +2,11 @@ // Camera-level ONVIF and autotracking settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const OnvifSection = createConfigSection({ sectionPath: "onvif", - defaultConfig: { - 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", - }, - }, - }, - }, + defaultConfig: getSectionConfig("onvif", "camera"), }); export default OnvifSection; diff --git a/web/src/components/config-form/sections/ProxySection.tsx b/web/src/components/config-form/sections/ProxySection.tsx new file mode 100644 index 000000000..2e86d18f9 --- /dev/null +++ b/web/src/components/config-form/sections/ProxySection.tsx @@ -0,0 +1,12 @@ +// Proxy Section Component +// Global proxy configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const ProxySection = createConfigSection({ + sectionPath: "proxy", + defaultConfig: getSectionConfig("proxy", "global"), +}); + +export default ProxySection; diff --git a/web/src/components/config-form/sections/RecordSection.tsx b/web/src/components/config-form/sections/RecordSection.tsx index bfff95bbb..d2adf488e 100644 --- a/web/src/components/config-form/sections/RecordSection.tsx +++ b/web/src/components/config-form/sections/RecordSection.tsx @@ -2,27 +2,11 @@ // Reusable for both global and camera-level record settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const RecordSection = createConfigSection({ sectionPath: "record", - defaultConfig: { - 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"], - }, + defaultConfig: getSectionConfig("record", "camera"), }); export default RecordSection; diff --git a/web/src/components/config-form/sections/ReviewSection.tsx b/web/src/components/config-form/sections/ReviewSection.tsx index 7e42f5107..33618eece 100644 --- a/web/src/components/config-form/sections/ReviewSection.tsx +++ b/web/src/components/config-form/sections/ReviewSection.tsx @@ -2,34 +2,11 @@ // Reusable for both global and camera-level review settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const ReviewSection = createConfigSection({ sectionPath: "review", - defaultConfig: { - 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", - }, - }, - }, - }, + defaultConfig: getSectionConfig("review", "camera"), }); export default ReviewSection; diff --git a/web/src/components/config-form/sections/SemanticSearchSection.tsx b/web/src/components/config-form/sections/SemanticSearchSection.tsx index 62250ca74..12df9c31b 100644 --- a/web/src/components/config-form/sections/SemanticSearchSection.tsx +++ b/web/src/components/config-form/sections/SemanticSearchSection.tsx @@ -2,15 +2,11 @@ // Camera-level semantic search trigger settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const SemanticSearchSection = createConfigSection({ sectionPath: "semantic_search", - defaultConfig: { - fieldOrder: ["triggers"], - hiddenFields: [], - advancedFields: [], - overrideFields: [], - }, + defaultConfig: getSectionConfig("semantic_search", "camera"), }); export default SemanticSearchSection; diff --git a/web/src/components/config-form/sections/SnapshotsSection.tsx b/web/src/components/config-form/sections/SnapshotsSection.tsx index 499c39c39..d61a0a931 100644 --- a/web/src/components/config-form/sections/SnapshotsSection.tsx +++ b/web/src/components/config-form/sections/SnapshotsSection.tsx @@ -2,32 +2,11 @@ // Reusable for both global and camera-level snapshots settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const SnapshotsSection = createConfigSection({ sectionPath: "snapshots", - defaultConfig: { - 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, - }, - }, - }, - }, + defaultConfig: getSectionConfig("snapshots", "camera"), }); export default SnapshotsSection; diff --git a/web/src/components/config-form/sections/TelemetrySection.tsx b/web/src/components/config-form/sections/TelemetrySection.tsx new file mode 100644 index 000000000..8f17f481b --- /dev/null +++ b/web/src/components/config-form/sections/TelemetrySection.tsx @@ -0,0 +1,12 @@ +// Telemetry Section Component +// Global telemetry configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const TelemetrySection = createConfigSection({ + sectionPath: "telemetry", + defaultConfig: getSectionConfig("telemetry", "global"), +}); + +export default TelemetrySection; diff --git a/web/src/components/config-form/sections/TimestampSection.tsx b/web/src/components/config-form/sections/TimestampSection.tsx index 4f0f2f4d7..9835a22a3 100644 --- a/web/src/components/config-form/sections/TimestampSection.tsx +++ b/web/src/components/config-form/sections/TimestampSection.tsx @@ -2,14 +2,11 @@ // Reusable for both global and camera-level timestamp_style settings import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; export const TimestampSection = createConfigSection({ sectionPath: "timestamp_style", - defaultConfig: { - fieldOrder: ["position", "format", "color", "thickness"], - hiddenFields: ["effect", "enabled_in_config"], - advancedFields: [], - }, + defaultConfig: getSectionConfig("timestamp_style", "camera"), }); export default TimestampSection; diff --git a/web/src/components/config-form/sections/TlsSection.tsx b/web/src/components/config-form/sections/TlsSection.tsx new file mode 100644 index 000000000..261c476d2 --- /dev/null +++ b/web/src/components/config-form/sections/TlsSection.tsx @@ -0,0 +1,12 @@ +// TLS Section Component +// Global TLS configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const TlsSection = createConfigSection({ + sectionPath: "tls", + defaultConfig: getSectionConfig("tls", "global"), +}); + +export default TlsSection; diff --git a/web/src/components/config-form/sections/UiSection.tsx b/web/src/components/config-form/sections/UiSection.tsx new file mode 100644 index 000000000..72736788b --- /dev/null +++ b/web/src/components/config-form/sections/UiSection.tsx @@ -0,0 +1,12 @@ +// UI Section Component +// Global UI configuration settings + +import { createConfigSection } from "./BaseSection"; +import { getSectionConfig } from "../sectionConfigs"; + +export const UiSection = createConfigSection({ + sectionPath: "ui", + defaultConfig: getSectionConfig("ui", "global"), +}); + +export default UiSection; diff --git a/web/src/components/config-form/sections/index.ts b/web/src/components/config-form/sections/index.ts index e34a2d370..ca92c1409 100644 --- a/web/src/components/config-form/sections/index.ts +++ b/web/src/components/config-form/sections/index.ts @@ -17,13 +17,27 @@ 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"; diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index c2ffbd122..f74cfc817 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -39,6 +39,7 @@ import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; import MaintenanceSettingsView from "@/views/settings/MaintenanceSettingsView"; import GlobalConfigView from "@/views/settings/GlobalConfigView"; import CameraConfigView from "@/views/settings/CameraConfigView"; +import { createSingleSectionPage } from "@/views/settings/SingleSectionPage"; import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useInitialCameraState } from "@/api/ws"; @@ -64,6 +65,10 @@ 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, @@ -74,7 +79,9 @@ import { const allSettingsViews = [ "ui", "globalConfig", + "mqtt", "cameraConfig", + "cameraMqtt", "enrichments", "cameraManagement", "cameraReview", @@ -90,18 +97,33 @@ const allSettingsViews = [ ] as const; 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, +}); + const settingsGroups = [ { label: "general", items: [ { key: "ui", component: UiSettingsView }, { key: "globalConfig", component: GlobalConfigView }, + { key: "mqtt", component: MqttSettingsPage }, ], }, { label: "cameras", items: [ { key: "cameraConfig", component: CameraConfigView }, + { key: "cameraMqtt", component: CameraMqttSettingsPage }, { key: "cameraManagement", component: CameraManagementView }, { key: "cameraReview", component: CameraReviewSettingsView }, { key: "masksAndZones", component: MasksAndZonesView }, @@ -139,6 +161,7 @@ const settingsGroups = [ const CAMERA_SELECT_BUTTON_PAGES = [ "debug", "cameraConfig", + "cameraMqtt", "cameraReview", "masksAndZones", "motionTuner", diff --git a/web/src/views/settings/GlobalConfigView.tsx b/web/src/views/settings/GlobalConfigView.tsx index f6deed86e..bed579cf7 100644 --- a/web/src/views/settings/GlobalConfigView.tsx +++ b/web/src/views/settings/GlobalConfigView.tsx @@ -1,12 +1,9 @@ // Global Configuration View // Main view for configuring global Frigate settings -import { useMemo, useCallback, useState, useEffect, useRef } from "react"; +import { useMemo, useCallback, useState } from "react"; import useSWR from "swr"; -import axios from "axios"; -import { toast } from "sonner"; import { useTranslation } from "react-i18next"; -import { ConfigForm } from "@/components/config-form/ConfigForm"; import { DetectSection } from "@/components/config-form/sections/DetectSection"; import { RecordSection } from "@/components/config-form/sections/RecordSection"; import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection"; @@ -14,545 +11,90 @@ 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 type { RJSFSchema } from "@rjsf/utils"; +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 type { FrigateConfig } from "@/types/frigateConfig"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { extractSchemaSection } from "@/lib/config-schema"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import Heading from "@/components/ui/heading"; -import { LuSave } from "react-icons/lu"; -import isEqual from "lodash/isEqual"; 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: "snapshots", component: SnapshotsSection }, { key: "motion", component: MotionSection }, - { - key: "objects", - component: ObjectsSection, - }, + { key: "objects", component: ObjectsSection }, { key: "review", component: ReviewSection }, { key: "audio", component: AudioSection }, { key: "live", component: LiveSection }, - { - key: "timestamp_style", - component: TimestampSection, - }, + { key: "timestamp_style", component: TimestampSection }, ]; -// Section configurations for global-only settings (system and integrations) -const globalSectionConfigs: Record< - string, - { - fieldOrder?: string[]; - hiddenFields?: string[]; - advancedFields?: string[]; - liveValidate?: boolean; - uiSchema?: Record; - } -> = { - mqtt: { - 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, - }, - database: { - fieldOrder: ["path"], - advancedFields: [], - }, - auth: { - 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: { - fieldOrder: ["enabled", "cert", "key"], - advancedFields: [], - }, - networking: { - fieldOrder: ["ipv6"], - advancedFields: [], - }, - proxy: { - fieldOrder: [ - "header_map", - "logout_url", - "auth_secret", - "default_role", - "separator", - ], - advancedFields: ["header_map", "auth_secret", "separator"], - liveValidate: true, - }, - ui: { - fieldOrder: [ - "timezone", - "time_format", - "date_style", - "time_style", - "unit_system", - ], - advancedFields: [], - }, - logger: { - fieldOrder: ["default", "logs"], - advancedFields: ["logs"], - }, - environment_vars: { - fieldOrder: [], - advancedFields: [], - }, - telemetry: { - fieldOrder: ["network_interfaces", "stats", "version_check"], - advancedFields: [], - }, - birdseye: { - fieldOrder: [ - "enabled", - "restream", - "width", - "height", - "quality", - "mode", - "layout", - "inactivity_threshold", - "idle_heartbeat_fps", - ], - advancedFields: ["width", "height", "quality", "inactivity_threshold"], - }, - 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", - ], - 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, - }, - }, - }, - }, - }, - detectors: { - fieldOrder: [], - advancedFields: [], - }, - model: { - 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: { - fieldOrder: [ - "provider", - "api_key", - "base_url", - "model", - "provider_options", - "runtime_options", - ], - advancedFields: ["base_url", "provider_options", "runtime_options"], - hiddenFields: ["genai.enabled_in_config"], - }, - classification: { - hiddenFields: ["custom"], - advancedFields: [], - }, - semantic_search: { - fieldOrder: ["enabled", "reindex", "model", "model_size", "device"], - advancedFields: ["reindex", "device"], - }, - audio_transcription: { - fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"], - advancedFields: ["language", "device", "model_size"], - }, - face_recognition: { - 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: { - 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", - ], - }, -}; - // System sections (global only) const systemSections = [ - "database", - "tls", - "auth", - "networking", - "proxy", - "ui", - "logger", - "environment_vars", - "telemetry", - "birdseye", - "ffmpeg", - "detectors", - "model", + { 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 }, ]; // Integration sections (global only) const integrationSections = [ - "mqtt", - "semantic_search", - "genai", - "face_recognition", - "lpr", - "classification", - "audio_transcription", + { 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 }, ]; -interface GlobalConfigSectionProps { - sectionKey: string; - schema: RJSFSchema | null; - config: FrigateConfig | undefined; - onSave: () => void; - title: string; -} - -function GlobalConfigSection({ - sectionKey, - schema, - config, - onSave, - title, -}: GlobalConfigSectionProps) { - const sectionConfig = globalSectionConfigs[sectionKey]; - const { t, i18n } = useTranslation([ - "config/global", - "views/settings", - "common", - ]); - const [pendingData, setPendingData] = useState(null); - const [isSaving, setIsSaving] = useState(false); - const [formKey, setFormKey] = useState(0); - const isResettingRef = useRef(false); - - const formData = useMemo((): unknown => { - if (!config) return {}; - return (config as unknown as Record)[sectionKey]; - }, [config, sectionKey]); - - useEffect(() => { - setPendingData(null); - }, [formData]); - - useEffect(() => { - if (isResettingRef.current) { - isResettingRef.current = false; - } - }, [formKey]); - - const hasChanges = useMemo(() => { - if (!pendingData) return false; - return !isEqual(formData, pendingData); - }, [formData, pendingData]); - - const handleChange = useCallback( - (data: unknown) => { - if (isResettingRef.current) { - setPendingData(null); - return; - } - if (!data || typeof data !== "object") { - setPendingData(null); - return; - } - if (isEqual(formData, data)) { - setPendingData(null); - return; - } - setPendingData(data); - }, - [formData], - ); - - const handleReset = useCallback(() => { - isResettingRef.current = true; - setPendingData(null); - setFormKey((prev) => prev + 1); - }, []); - - const handleSave = useCallback(async () => { - if (!pendingData) return; - - setIsSaving(true); - try { - // await axios.put("config/set", { - // update_topic: `config/${sectionKey}`, - // config_data: { - // [sectionKey]: pendingData, - // }, - // }); - - // log axios for debugging - console.log("Saved config section", sectionKey, pendingData); - - toast.success( - t("toast.success", { - ns: "views/settings", - defaultValue: "Settings saved successfully", - }), - ); - - setPendingData(null); - onSave(); - } catch { - toast.error( - t("toast.error", { - ns: "views/settings", - defaultValue: "Failed to save settings", - }), - ); - } finally { - setIsSaving(false); - } - }, [sectionKey, pendingData, t, onSave]); - - if (!schema || !sectionConfig) { - return null; - } - - return ( -
-
- {title} - {hasChanges && ( - - {t("modified", { ns: "common", defaultValue: "Modified" })} - - )} -
- - -
-
- {hasChanges && ( - - {t("unsavedChanges", { - ns: "views/settings", - defaultValue: "You have unsaved changes", - })} - - )} -
-
- {hasChanges && ( - - )} - -
-
-
- ); -} - export default function GlobalConfigView() { const { t, i18n } = useTranslation([ "views/settings", "config/global", "common", ]); + const defaultSharedSection = sharedSections[0]?.key ?? ""; + const defaultSystemSection = systemSections[0]?.key ?? ""; + const defaultIntegrationSection = integrationSections[0]?.key ?? ""; const [activeTab, setActiveTab] = useState("shared"); - const [activeSection, setActiveSection] = useState("detect"); + const [activeSection, setActiveSection] = useState(defaultSharedSection); const { data: config, mutate: refreshConfig } = useSWR("config"); - const { data: schema } = useSWR("config/schema.json"); const handleSave = useCallback(() => { refreshConfig(); @@ -562,32 +104,29 @@ export default function GlobalConfigView() { const currentSections = useMemo(() => { if (activeTab === "shared") { return sharedSections; - } else if (activeTab === "system") { - return systemSections.map((key) => ({ - key, - component: null, // Uses GlobalConfigSection instead - })); - } else { - return integrationSections.map((key) => ({ - key, - component: null, - })); } + if (activeTab === "system") { + return systemSections; + } + return integrationSections; }, [activeTab]); // Reset active section when tab changes - const handleTabChange = useCallback((tab: string) => { - setActiveTab(tab); - if (tab === "shared") { - setActiveSection("detect"); - } else if (tab === "system") { - setActiveSection("database"); - } else { - setActiveSection("mqtt"); - } - }, []); + const handleTabChange = useCallback( + (tab: string) => { + setActiveTab(tab); + if (tab === "shared") { + setActiveSection(defaultSharedSection); + } else if (tab === "system") { + setActiveSection(defaultSystemSection); + } else { + setActiveSection(defaultIntegrationSection); + } + }, + [defaultSharedSection, defaultSystemSection, defaultIntegrationSection], + ); - if (!config || !schema) { + if (!config) { return (
@@ -666,105 +205,42 @@ export default function GlobalConfigView() { {/* Section Content */}
- {activeTab === "shared" && ( - <> - {sharedSections.map((section) => { - const SectionComponent = section.component; - return ( -
- - {t(`${section.key}.label`, { - ns: "config/global", - defaultValue: - section.key.charAt(0).toUpperCase() + - section.key.slice(1).replace(/_/g, " "), - })} - - {i18n.exists(`${section.key}.description`, { + {currentSections.map((section) => { + const SectionComponent = section.component; + return ( +
+ + {t(`${section.key}.label`, { + ns: "config/global", + defaultValue: + section.key.charAt(0).toUpperCase() + + section.key.slice(1).replace(/_/g, " "), + })} + + {i18n.exists(`${section.key}.description`, { + ns: "config/global", + }) && ( +

+ {t(`${section.key}.description`, { ns: "config/global", - }) && ( -

- {t(`${section.key}.description`, { - ns: "config/global", - })} -

- )} + })} +

+ )} - -
- ); - })} - - )} - - {activeTab === "system" && ( - <> - {systemSections.map((sectionKey) => { - const sectionTitle = t(`${sectionKey}.label`, { - ns: "config/global", - defaultValue: - sectionKey.charAt(0).toUpperCase() + - sectionKey.slice(1).replace(/_/g, " "), - }); - - return ( -
- -
- ); - })} - - )} - - {activeTab === "integrations" && ( - <> - {integrationSections.map((sectionKey) => { - const sectionTitle = t(`${sectionKey}.label`, { - ns: "config/global", - defaultValue: - sectionKey.charAt(0).toUpperCase() + - sectionKey.slice(1).replace(/_/g, " "), - }); - - return ( -
- -
- ); - })} - - )} + +
+ ); + })}
diff --git a/web/src/views/settings/SingleSectionPage.tsx b/web/src/views/settings/SingleSectionPage.tsx new file mode 100644 index 000000000..d99a146c7 --- /dev/null +++ b/web/src/views/settings/SingleSectionPage.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from "react-i18next"; +import Heading from "@/components/ui/heading"; +import type { + BaseSectionProps, + SectionConfig, +} from "@/components/config-form/sections"; +import type { PolygonType } from "@/types/canvas"; + +export type SettingsPageProps = { + selectedCamera?: string; + setUnsavedChanges?: React.Dispatch>; + selectedZoneMask?: PolygonType[]; +}; + +export type SingleSectionPageOptions = { + sectionKey: string; + level: "global" | "camera"; + SectionComponent: React.ComponentType; + sectionConfig?: SectionConfig; + requiresRestart?: boolean; + showOverrideIndicator?: boolean; +}; + +export function createSingleSectionPage({ + sectionKey, + level, + SectionComponent, + sectionConfig, + requiresRestart, + showOverrideIndicator = true, +}: SingleSectionPageOptions) { + return function SingleSectionPage({ + selectedCamera, + setUnsavedChanges, + }: SettingsPageProps) { + const sectionNamespace = + level === "camera" ? "config/cameras" : "config/global"; + const { t, i18n } = useTranslation([ + sectionNamespace, + "views/settings", + "common", + ]); + + if (level === "camera" && !selectedCamera) { + return ( +
+ {t("configForm.camera.noCameras", { ns: "views/settings" })} +
+ ); + } + + return ( +
+
+ + {t(`${sectionKey}.label`, { ns: sectionNamespace })} + + {i18n.exists(`${sectionKey}.description`, { + ns: sectionNamespace, + }) && ( +

+ {t(`${sectionKey}.description`, { ns: sectionNamespace })} +

+ )} +
+ setUnsavedChanges?.(false)} + showTitle={false} + sectionConfig={sectionConfig} + requiresRestart={requiresRestart} + /> +
+ ); + }; +}