refactor configs to use individual files with a template

This commit is contained in:
Josh Hawkins 2026-02-01 11:35:04 -06:00
parent c9c29b7c33
commit a97333d881
40 changed files with 1111 additions and 1057 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const classification: SectionConfigOverrides = {
base: {
hiddenFields: ["custom"],
advancedFields: [],
},
};
export default classification;

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const database: SectionConfigOverrides = {
base: {
fieldOrder: ["path"],
advancedFields: [],
},
};
export default database;

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const detectors: SectionConfigOverrides = {
base: {
fieldOrder: [],
advancedFields: [],
},
};
export default detectors;

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const environmentVars: SectionConfigOverrides = {
base: {
fieldOrder: [],
advancedFields: [],
},
};
export default environmentVars;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const logger: SectionConfigOverrides = {
base: {
fieldOrder: ["default", "logs"],
advancedFields: ["logs"],
},
};
export default logger;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const networking: SectionConfigOverrides = {
base: {
fieldOrder: [],
advancedFields: [],
},
};
export default networking;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const telemetry: SectionConfigOverrides = {
base: {
fieldOrder: ["network_interfaces", "stats", "version_check"],
advancedFields: [],
},
};
export default telemetry;

View File

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

View File

@ -0,0 +1,10 @@
import type { SectionConfigOverrides } from "./types";
const tls: SectionConfigOverrides = {
base: {
fieldOrder: ["enabled", "cert", "key"],
advancedFields: [],
},
};
export default tls;

View File

@ -0,0 +1,7 @@
import type { SectionConfig } from "../sections/BaseSection";
export type SectionConfigOverrides = {
base?: SectionConfig;
global?: Partial<SectionConfig>;
camera?: Partial<SectionConfig>;
};

View File

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

View File

@ -13,830 +13,73 @@
global?: Partial<SectionConfig>; // overrides for global-level UI global?: Partial<SectionConfig>; // overrides for global-level UI
camera?: Partial<SectionConfig>; // overrides for camera-level UI camera?: Partial<SectionConfig>; // 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 { SectionConfigOverrides } from "./section-configs/types";
import type { SectionConfig } from "./sections/BaseSection"; 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 = { export const sectionConfigs: Record<string, SectionConfigOverrides> = {
base?: SectionConfig; detect,
global?: Partial<SectionConfig>; record,
camera?: Partial<SectionConfig>; 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<string, SectionConfigOverrides> = { export type { SectionConfigOverrides } from "./section-configs/types";
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<SectionConfig> | 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);
}

View File

@ -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<SectionConfig> | 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);
}

View File

@ -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 (
<SectionComponent
level={level}
sectionConfig={sectionConfig ?? defaultConfig}
{...rest}
/>
);
}
export default ConfigSectionTemplate;

View File

@ -7,37 +7,7 @@ export {
type SectionConfig, type SectionConfig,
type CreateSectionOptions, type CreateSectionOptions,
} from "./BaseSection"; } from "./BaseSection";
export {
export { DetectSection } from "./DetectSection"; ConfigSectionTemplate,
export { RecordSection } from "./RecordSection"; type ConfigSectionTemplateProps,
export { SnapshotsSection } from "./SnapshotsSection"; } from "./ConfigSectionTemplate";
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";

View File

@ -65,10 +65,6 @@ import { cn } from "@/lib/utils";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { LuChevronRight } from "react-icons/lu"; import { LuChevronRight } from "react-icons/lu";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import {
CameraMqttSection,
MqttSection,
} from "@/components/config-form/sections";
import { import {
MobilePage, MobilePage,
MobilePageContent, MobilePageContent,
@ -100,13 +96,11 @@ type SettingsType = (typeof allSettingsViews)[number];
const MqttSettingsPage = createSingleSectionPage({ const MqttSettingsPage = createSingleSectionPage({
sectionKey: "mqtt", sectionKey: "mqtt",
level: "global", level: "global",
SectionComponent: MqttSection,
}); });
const CameraMqttSettingsPage = createSingleSectionPage({ const CameraMqttSettingsPage = createSingleSectionPage({
sectionKey: "mqtt", sectionKey: "mqtt",
level: "camera", level: "camera",
SectionComponent: CameraMqttSection,
showOverrideIndicator: false, showOverrideIndicator: false,
}); });

View File

@ -4,24 +4,7 @@
import { useMemo, useCallback, useState, memo } from "react"; import { useMemo, useCallback, useState, memo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DetectSection } from "@/components/config-form/sections/DetectSection"; import { ConfigSectionTemplate } from "@/components/config-form/sections";
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 { useAllCameraOverrides } from "@/hooks/use-config-override"; import { useAllCameraOverrides } from "@/hooks/use-config-override";
import type { FrigateConfig } from "@/types/frigateConfig"; import type { FrigateConfig } from "@/types/frigateConfig";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -203,83 +186,26 @@ const CameraConfigContent = memo(function CameraConfigContent({
const sections: Array<{ const sections: Array<{
key: string; key: string;
component: typeof DetectSection;
showOverrideIndicator?: boolean; showOverrideIndicator?: boolean;
}> = [ }> = [
{ { key: "detect" },
key: "detect", { key: "ffmpeg", showOverrideIndicator: true },
component: DetectSection, { key: "record" },
}, { key: "snapshots" },
{ { key: "motion" },
key: "ffmpeg", { key: "objects" },
component: FfmpegSection, { key: "review" },
showOverrideIndicator: true, { key: "audio" },
}, { key: "audio_transcription", showOverrideIndicator: true },
{ { key: "notifications" },
key: "record", { key: "live" },
component: RecordSection, { key: "birdseye", showOverrideIndicator: true },
}, { key: "face_recognition", showOverrideIndicator: true },
{ { key: "lpr", showOverrideIndicator: true },
key: "snapshots", { key: "mqtt", showOverrideIndicator: false },
component: SnapshotsSection, { key: "onvif", showOverrideIndicator: false },
}, { key: "ui", showOverrideIndicator: false },
{ { key: "timestamp_style" },
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,
},
]; ];
return ( return (
@ -327,23 +253,21 @@ const CameraConfigContent = memo(function CameraConfigContent({
{/* Section Content */} {/* Section Content */}
<div className="scrollbar-container flex-1 overflow-y-auto pr-4"> <div className="scrollbar-container flex-1 overflow-y-auto pr-4">
{sections.map((section) => { {sections.map((section) => (
const SectionComponent = section.component; <div
return ( key={section.key}
<div className={cn(activeSection === section.key ? "block" : "hidden")}
key={section.key} >
className={cn(activeSection === section.key ? "block" : "hidden")} <ConfigSectionTemplate
> sectionKey={section.key}
<SectionComponent level="camera"
level="camera" cameraName={cameraName}
cameraName={cameraName} showOverrideIndicator={section.showOverrideIndicator !== false}
showOverrideIndicator={section.showOverrideIndicator !== false} onSave={onSave}
onSave={onSave} showTitle={true}
showTitle={true} />
/> </div>
</div> ))}
);
})}
</div> </div>
</div> </div>
); );

View File

@ -4,81 +4,51 @@
import { useMemo, useCallback, useState } from "react"; import { useMemo, useCallback, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DetectSection } from "@/components/config-form/sections/DetectSection"; import { ConfigSectionTemplate } from "@/components/config-form/sections";
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 type { FrigateConfig } from "@/types/frigateConfig"; import type { FrigateConfig } from "@/types/frigateConfig";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getSectionConfig } from "@/components/config-form/sectionConfigs";
// Shared sections that can be overridden at camera level // Shared sections that can be overridden at camera level
const sharedSections = [ const sharedSections = [
{ key: "detect", component: DetectSection }, { key: "detect" },
{ key: "record", component: RecordSection }, { key: "record" },
{ key: "snapshots", component: SnapshotsSection }, { key: "snapshots" },
{ key: "motion", component: MotionSection }, { key: "motion" },
{ key: "objects", component: ObjectsSection }, { key: "objects" },
{ key: "review", component: ReviewSection }, { key: "review" },
{ key: "audio", component: AudioSection }, { key: "audio" },
{ key: "live", component: LiveSection }, { key: "live" },
{ key: "timestamp_style", component: TimestampSection }, { key: "timestamp_style" },
]; ];
// System sections (global only) // System sections (global only)
const systemSections = [ const systemSections = [
{ key: "database", component: DatabaseSection }, { key: "database" },
{ key: "tls", component: TlsSection }, { key: "tls" },
{ key: "auth", component: AuthSection }, { key: "auth" },
{ key: "networking", component: NetworkingSection }, { key: "networking" },
{ key: "proxy", component: ProxySection }, { key: "proxy" },
{ key: "ui", component: UiSection }, { key: "ui" },
{ key: "logger", component: LoggerSection }, { key: "logger" },
{ key: "environment_vars", component: EnvironmentVarsSection }, { key: "environment_vars" },
{ key: "telemetry", component: TelemetrySection }, { key: "telemetry" },
{ key: "birdseye", component: BirdseyeSection }, { key: "birdseye" },
{ key: "ffmpeg", component: FfmpegSection }, { key: "ffmpeg" },
{ key: "detectors", component: DetectorsSection }, { key: "detectors" },
{ key: "model", component: ModelSection }, { key: "model" },
]; ];
// Integration sections (global only) // Integration sections (global only)
const integrationSections = [ const integrationSections = [
{ key: "mqtt", component: MqttSection }, { key: "mqtt" },
{ key: "semantic_search", component: SemanticSearchSection }, { key: "semantic_search" },
{ key: "genai", component: GenaiSection }, { key: "genai" },
{ key: "face_recognition", component: FaceRecognitionSection }, { key: "face_recognition" },
{ key: "lpr", component: LprSection }, { key: "lpr" },
{ key: "classification", component: ClassificationSection }, { key: "classification" },
{ key: "audio_transcription", component: AudioTranscriptionSection }, { key: "audio_transcription" },
]; ];
export default function GlobalConfigView() { export default function GlobalConfigView() {
@ -201,24 +171,21 @@ export default function GlobalConfigView() {
{/* Section Content */} {/* Section Content */}
<div className="scrollbar-container flex-1 overflow-y-auto pr-4"> <div className="scrollbar-container flex-1 overflow-y-auto pr-4">
{currentSections.map((section) => { {currentSections.map((section) => (
const SectionComponent = section.component; <div
return ( key={section.key}
<div className={cn(
key={section.key} activeSection === section.key ? "block" : "hidden",
className={cn( )}
activeSection === section.key ? "block" : "hidden", >
)} <ConfigSectionTemplate
> sectionKey={section.key}
<SectionComponent level="global"
level="global" onSave={handleSave}
onSave={handleSave} showTitle={true}
showTitle={true} />
sectionConfig={getSectionConfig(section.key, "global")} </div>
/> ))}
</div>
);
})}
</div> </div>
</div> </div>
</Tabs> </Tabs>

View File

@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import type { import type { SectionConfig } from "@/components/config-form/sections";
BaseSectionProps, import { ConfigSectionTemplate } from "@/components/config-form/sections";
SectionConfig,
} from "@/components/config-form/sections";
import type { PolygonType } from "@/types/canvas"; import type { PolygonType } from "@/types/canvas";
export type SettingsPageProps = { export type SettingsPageProps = {
@ -15,7 +13,6 @@ export type SettingsPageProps = {
export type SingleSectionPageOptions = { export type SingleSectionPageOptions = {
sectionKey: string; sectionKey: string;
level: "global" | "camera"; level: "global" | "camera";
SectionComponent: React.ComponentType<BaseSectionProps>;
sectionConfig?: SectionConfig; sectionConfig?: SectionConfig;
requiresRestart?: boolean; requiresRestart?: boolean;
showOverrideIndicator?: boolean; showOverrideIndicator?: boolean;
@ -24,7 +21,6 @@ export type SingleSectionPageOptions = {
export function createSingleSectionPage({ export function createSingleSectionPage({
sectionKey, sectionKey,
level, level,
SectionComponent,
sectionConfig, sectionConfig,
requiresRestart, requiresRestart,
showOverrideIndicator = true, showOverrideIndicator = true,
@ -63,7 +59,8 @@ export function createSingleSectionPage({
</p> </p>
)} )}
</div> </div>
<SectionComponent <ConfigSectionTemplate
sectionKey={sectionKey}
level={level} level={level}
cameraName={level === "camera" ? selectedCamera : undefined} cameraName={level === "camera" ? selectedCamera : undefined}
showOverrideIndicator={showOverrideIndicator} showOverrideIndicator={showOverrideIndicator}