section fields

This commit is contained in:
Josh Hawkins 2026-01-23 10:04:50 -06:00
parent 737de2f53a
commit 0280c2ec43
13 changed files with 478 additions and 305 deletions

View File

@ -43,6 +43,8 @@ export interface ConfigFormProps {
liveValidate?: boolean; liveValidate?: boolean;
/** Form context passed to all widgets */ /** Form context passed to all widgets */
formContext?: Record<string, unknown>; formContext?: Record<string, unknown>;
/** i18n namespace for field labels */
i18nNamespace?: string;
} }
export function ConfigForm({ export function ConfigForm({
@ -61,8 +63,9 @@ export function ConfigForm({
className, className,
liveValidate = false, liveValidate = false,
formContext, formContext,
i18nNamespace,
}: ConfigFormProps) { }: ConfigFormProps) {
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation([i18nNamespace || "common", "views/settings"]);
const [showAdvanced, setShowAdvanced] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false);
// Determine which fields to hide based on advanced toggle // Determine which fields to hide based on advanced toggle
@ -81,8 +84,16 @@ export function ConfigForm({
fieldOrder, fieldOrder,
hiddenFields: effectiveHiddenFields, hiddenFields: effectiveHiddenFields,
advancedFields: showAdvanced ? advancedFields : [], advancedFields: showAdvanced ? advancedFields : [],
i18nNamespace,
}), }),
[schema, fieldOrder, effectiveHiddenFields, advancedFields, showAdvanced], [
schema,
fieldOrder,
effectiveHiddenFields,
advancedFields,
showAdvanced,
i18nNamespace,
],
); );
// Merge generated uiSchema with custom overrides // Merge generated uiSchema with custom overrides
@ -116,6 +127,16 @@ export function ConfigForm({
const hasAdvancedFields = advancedFields && advancedFields.length > 0; const hasAdvancedFields = advancedFields && advancedFields.length > 0;
// Extended form context with i18n info
const extendedFormContext = useMemo(
() => ({
...formContext,
i18nNamespace,
t,
}),
[formContext, i18nNamespace, t],
);
return ( return (
<div className={cn("config-form", className)}> <div className={cn("config-form", className)}>
{hasAdvancedFields && ( {hasAdvancedFields && (
@ -130,6 +151,7 @@ export function ConfigForm({
className="cursor-pointer text-sm text-muted-foreground" className="cursor-pointer text-sm text-muted-foreground"
> >
{t("configForm.showAdvanced", { {t("configForm.showAdvanced", {
ns: "views/settings",
defaultValue: "Show Advanced Settings", defaultValue: "Show Advanced Settings",
})} })}
</Label> </Label>
@ -146,7 +168,7 @@ export function ConfigForm({
disabled={disabled} disabled={disabled}
readonly={readonly} readonly={readonly}
liveValidate={liveValidate} liveValidate={liveValidate}
formContext={formContext} formContext={extendedFormContext}
transformErrors={errorTransformer} transformErrors={errorTransformer}
{...frigateTheme} {...frigateTheme}
/> />

View File

@ -1,30 +1,27 @@
// Audio Section Component // Audio Section Component
// Reusable for both global and camera-level audio settings // Reusable for both global and camera-level audio settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the audio section
export const audioSectionConfig: SectionConfig = {
fieldOrder: [
"enabled",
"listen",
"filters",
"min_volume",
"max_not_heard",
"num_threads",
],
fieldGroups: {
detection: ["listen", "filters"],
sensitivity: ["min_volume", "max_not_heard"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["min_volume", "max_not_heard", "num_threads"],
};
export const AudioSection = createConfigSection({ export const AudioSection = createConfigSection({
sectionPath: "audio", sectionPath: "audio",
translationKey: "configForm.audio", i18nNamespace: "config/audio",
defaultConfig: audioSectionConfig, defaultConfig: {
fieldOrder: [
"enabled",
"listen",
"filters",
"min_volume",
"max_not_heard",
"num_threads",
],
fieldGroups: {
detection: ["listen", "filters"],
sensitivity: ["min_volume", "max_not_heard"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["min_volume", "max_not_heard", "num_threads"],
},
}); });
export default AudioSection; export default AudioSection;

View File

@ -1,7 +1,7 @@
// Base Section Component for config form sections // Base Section Component for config form sections
// Used as a foundation for reusable section components // Used as a foundation for reusable section components
import { useMemo, useCallback } from "react"; import { useMemo, useCallback, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
@ -10,11 +10,22 @@ import { ConfigForm } from "../ConfigForm";
import { useConfigOverride } from "@/hooks/use-config-override"; import { useConfigOverride } from "@/hooks/use-config-override";
import { useSectionSchema } from "@/hooks/use-config-schema"; import { useSectionSchema } from "@/hooks/use-config-schema";
import type { FrigateConfig } from "@/types/frigateConfig"; import type { FrigateConfig } from "@/types/frigateConfig";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { LuRotateCcw } from "react-icons/lu"; import {
LuRotateCcw,
LuSave,
LuChevronDown,
LuChevronRight,
} from "react-icons/lu";
import Heading from "@/components/ui/heading";
import get from "lodash/get"; import get from "lodash/get";
import isEqual from "lodash/isEqual";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
export interface SectionConfig { export interface SectionConfig {
/** Field ordering within the section */ /** Field ordering within the section */
@ -44,13 +55,19 @@ export interface BaseSectionProps {
onSave?: () => void; onSave?: () => void;
/** Whether a restart is required after changes */ /** Whether a restart is required after changes */
requiresRestart?: boolean; requiresRestart?: boolean;
/** Whether section is collapsible */
collapsible?: boolean;
/** Default collapsed state */
defaultCollapsed?: boolean;
/** Whether to show the section title (default: false for global, true for camera) */
showTitle?: boolean;
} }
export interface CreateSectionOptions { export interface CreateSectionOptions {
/** The config path for this section (e.g., "detect", "record") */ /** The config path for this section (e.g., "detect", "record") */
sectionPath: string; sectionPath: string;
/** Translation key prefix for this section */ /** i18n namespace for this section (e.g., "config/detect") */
translationKey: string; i18nNamespace: string;
/** Default section configuration */ /** Default section configuration */
defaultConfig: SectionConfig; defaultConfig: SectionConfig;
} }
@ -60,10 +77,10 @@ export interface CreateSectionOptions {
*/ */
export function createConfigSection({ export function createConfigSection({
sectionPath, sectionPath,
translationKey, i18nNamespace,
defaultConfig, defaultConfig,
}: CreateSectionOptions) { }: CreateSectionOptions) {
return function ConfigSection({ const ConfigSection = function ConfigSection({
level, level,
cameraName, cameraName,
showOverrideIndicator = true, showOverrideIndicator = true,
@ -72,8 +89,20 @@ export function createConfigSection({
readonly = false, readonly = false,
onSave, onSave,
requiresRestart = true, requiresRestart = true,
collapsible = false,
defaultCollapsed = false,
showTitle,
}: BaseSectionProps) { }: BaseSectionProps) {
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation([i18nNamespace, "views/settings", "common"]);
const [isOpen, setIsOpen] = useState(!defaultCollapsed);
const [pendingData, setPendingData] = useState<Record<
string,
unknown
> | null>(null);
const [isSaving, setIsSaving] = useState(false);
// Default: show title for camera level (since it might be collapsible), hide for global
const shouldShowTitle = showTitle ?? level === "camera";
// Fetch config // Fetch config
const { data: config, mutate: refreshConfig } = const { data: config, mutate: refreshConfig } =
@ -83,12 +112,11 @@ export function createConfigSection({
const sectionSchema = useSectionSchema(sectionPath, level); const sectionSchema = useSectionSchema(sectionPath, level);
// Get override status // Get override status
const { isOverridden, globalValue, cameraValue, resetToGlobal } = const { isOverridden, globalValue, cameraValue } = useConfigOverride({
useConfigOverride({ config,
config, cameraName: level === "camera" ? cameraName : undefined,
cameraName: level === "camera" ? cameraName : undefined, sectionPath,
sectionPath, });
});
// Get current form data // Get current form data
const formData = useMemo(() => { const formData = useMemo(() => {
@ -101,119 +129,286 @@ export function createConfigSection({
return get(config, sectionPath) || {}; return get(config, sectionPath) || {};
}, [config, level, cameraName]); }, [config, level, cameraName]);
// Handle form submission // Track if there are unsaved changes
const handleSubmit = useCallback( const hasChanges = useMemo(() => {
async (data: Record<string, unknown>) => { if (!pendingData) return false;
try { return !isEqual(formData, pendingData);
const basePath = }, [formData, pendingData]);
level === "camera" && cameraName
? `cameras.${cameraName}.${sectionPath}`
: sectionPath;
await axios.put("config/set", { // Handle form data change
requires_restart: requiresRestart ? 1 : 0, const handleChange = useCallback((data: Record<string, unknown>) => {
config_data: { setPendingData(data);
[basePath]: data, }, []);
},
});
toast.success( // Handle save button click
t(`${translationKey}.toast.success`, { const handleSave = useCallback(async () => {
defaultValue: "Settings saved successfully", if (!pendingData) return;
}),
);
refreshConfig(); setIsSaving(true);
onSave?.(); try {
} catch (error) { const basePath =
// Parse Pydantic validation errors from API response level === "camera" && cameraName
if (axios.isAxiosError(error) && error.response?.data) { ? `cameras.${cameraName}.${sectionPath}`
const responseData = error.response.data; : sectionPath;
// Pydantic errors come as { detail: [...] } or { message: "..." }
if (responseData.detail && Array.isArray(responseData.detail)) { await axios.put("config/set", {
const validationMessages = responseData.detail requires_restart: requiresRestart ? 1 : 0,
.map((err: { loc?: string[]; msg?: string }) => { config_data: {
const field = err.loc?.slice(1).join(".") || "unknown"; [basePath]: pendingData,
return `${field}: ${err.msg || "Invalid value"}`; },
}) });
.join(", ");
toast.error( toast.success(
t(`${translationKey}.toast.validationError`, { t("toast.success", {
defaultValue: `Validation failed: ${validationMessages}`, ns: "views/settings",
}), defaultValue: "Settings saved successfully",
); }),
} else if (responseData.message) { );
toast.error(responseData.message);
} else { setPendingData(null);
toast.error( refreshConfig();
t(`${translationKey}.toast.error`, { onSave?.();
defaultValue: "Failed to save settings", } catch (error) {
}), // Parse Pydantic validation errors from API response
); if (axios.isAxiosError(error) && error.response?.data) {
} const responseData = error.response.data;
if (responseData.detail && Array.isArray(responseData.detail)) {
const validationMessages = responseData.detail
.map((err: { loc?: string[]; msg?: string }) => {
const field = err.loc?.slice(1).join(".") || "unknown";
return `${field}: ${err.msg || "Invalid value"}`;
})
.join(", ");
toast.error(
t("toast.validationError", {
ns: "views/settings",
defaultValue: `Validation failed: ${validationMessages}`,
}),
);
} else if (responseData.message) {
toast.error(responseData.message);
} else { } else {
toast.error( toast.error(
t(`${translationKey}.toast.error`, { t("toast.error", {
ns: "views/settings",
defaultValue: "Failed to save settings", defaultValue: "Failed to save settings",
}), }),
); );
} }
} else {
toast.error(
t("toast.error", {
ns: "views/settings",
defaultValue: "Failed to save settings",
}),
);
} }
}, } finally {
[level, cameraName, requiresRestart, t, refreshConfig, onSave], setIsSaving(false);
); }
}, [
pendingData,
level,
cameraName,
requiresRestart,
t,
refreshConfig,
onSave,
]);
// Handle reset to global // Handle reset to global - removes camera-level override by deleting the section
const handleResetToGlobal = useCallback(async () => { const handleResetToGlobal = useCallback(async () => {
if (level !== "camera" || !cameraName) return; if (level !== "camera" || !cameraName) return;
try { try {
const basePath = `cameras.${cameraName}.${sectionPath}`; const basePath = `cameras.${cameraName}.${sectionPath}`;
// Reset by setting to null/undefined or removing the override // Send empty string to delete the key from config (see update_yaml in backend)
await axios.put("config/set", { await axios.put("config/set", {
requires_restart: requiresRestart ? 1 : 0, requires_restart: requiresRestart ? 1 : 0,
config_data: { config_data: {
[basePath]: resetToGlobal(), [basePath]: "",
}, },
}); });
toast.success( toast.success(
t(`${translationKey}.toast.resetSuccess`, { t("toast.resetSuccess", {
ns: "views/settings",
defaultValue: "Reset to global defaults", defaultValue: "Reset to global defaults",
}), }),
); );
setPendingData(null);
refreshConfig(); refreshConfig();
} catch { } catch {
toast.error( toast.error(
t(`${translationKey}.toast.resetError`, { t("toast.resetError", {
ns: "views/settings",
defaultValue: "Failed to reset settings", defaultValue: "Failed to reset settings",
}), }),
); );
} }
}, [level, cameraName, requiresRestart, t, refreshConfig, resetToGlobal]); }, [level, cameraName, requiresRestart, t, refreshConfig]);
if (!sectionSchema) { if (!sectionSchema) {
return null; return null;
} }
const title = t(`${translationKey}.title`, { // Get section title from config namespace
defaultValue: sectionPath.charAt(0).toUpperCase() + sectionPath.slice(1), const title = t("label", {
ns: i18nNamespace,
defaultValue:
sectionPath.charAt(0).toUpperCase() +
sectionPath.slice(1).replace(/_/g, " "),
}); });
return ( const sectionContent = (
<Card> <div className="space-y-6">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <ConfigForm
schema={sectionSchema}
formData={pendingData || formData}
onChange={handleChange}
fieldOrder={sectionConfig.fieldOrder}
hiddenFields={sectionConfig.hiddenFields}
advancedFields={sectionConfig.advancedFields}
disabled={disabled || isSaving}
readonly={readonly}
showSubmit={false}
i18nNamespace={i18nNamespace}
formContext={{
level,
cameraName,
globalValue,
cameraValue,
}}
/>
{/* Save button */}
<div className="flex items-center justify-between pt-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-lg">{title}</CardTitle> {hasChanges && (
{showOverrideIndicator && level === "camera" && isOverridden && ( <span className="text-sm text-muted-foreground">
<Badge variant="secondary" className="text-xs"> {t("unsavedChanges", {
{t("common.overridden", { defaultValue: "Overridden" })} ns: "views/settings",
</Badge> defaultValue: "You have unsaved changes",
})}
</span>
)} )}
</div> </div>
{level === "camera" && isOverridden && ( <Button
onClick={handleSave}
disabled={!hasChanges || isSaving || disabled}
className="gap-2"
>
<LuSave className="h-4 w-4" />
{isSaving
? t("saving", { ns: "common", defaultValue: "Saving..." })
: t("save", { ns: "common", defaultValue: "Save" })}
</Button>
</div>
</div>
);
if (collapsible) {
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="space-y-3">
<CollapsibleTrigger asChild>
<div className="flex cursor-pointer items-center justify-between">
<div className="flex items-center gap-3">
{isOpen ? (
<LuChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<LuChevronRight className="h-4 w-4 text-muted-foreground" />
)}
<Heading as="h4">{title}</Heading>
{showOverrideIndicator &&
level === "camera" &&
isOverridden && (
<Badge variant="secondary" className="text-xs">
{t("overridden", {
ns: "common",
defaultValue: "Overridden",
})}
</Badge>
)}
{hasChanges && (
<Badge variant="outline" className="text-xs">
{t("modified", {
ns: "common",
defaultValue: "Modified",
})}
</Badge>
)}
</div>
{level === "camera" && isOverridden && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleResetToGlobal();
}}
className="gap-2"
>
<LuRotateCcw className="h-4 w-4" />
{t("button.resetToGlobal", {
ns: "common",
defaultValue: "Reset to Global",
})}
</Button>
)}
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="pl-7">{sectionContent}</div>
</CollapsibleContent>
</div>
</Collapsible>
);
}
return (
<div className="space-y-3">
{shouldShowTitle && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Heading as="h4">{title}</Heading>
{showOverrideIndicator && level === "camera" && isOverridden && (
<Badge variant="secondary" className="text-xs">
{t("overridden", {
ns: "common",
defaultValue: "Overridden",
})}
</Badge>
)}
{hasChanges && (
<Badge variant="outline" className="text-xs">
{t("modified", { ns: "common", defaultValue: "Modified" })}
</Badge>
)}
</div>
{level === "camera" && isOverridden && (
<Button
variant="ghost"
size="sm"
onClick={handleResetToGlobal}
className="gap-2"
>
<LuRotateCcw className="h-4 w-4" />
{t("button.resetToGlobal", {
ns: "common",
defaultValue: "Reset to Global",
})}
</Button>
)}
</div>
)}
{/* Reset button when title is hidden but we're at camera level with override */}
{!shouldShowTitle && level === "camera" && isOverridden && (
<div className="flex justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -221,29 +416,18 @@ export function createConfigSection({
className="gap-2" className="gap-2"
> >
<LuRotateCcw className="h-4 w-4" /> <LuRotateCcw className="h-4 w-4" />
{t("common.resetToGlobal", { defaultValue: "Reset to Global" })} {t("button.resetToGlobal", {
ns: "common",
defaultValue: "Reset to Global",
})}
</Button> </Button>
)} </div>
</CardHeader> )}
<CardContent>
<ConfigForm {sectionContent}
schema={sectionSchema} </div>
formData={formData}
onSubmit={handleSubmit}
fieldOrder={sectionConfig.fieldOrder}
hiddenFields={sectionConfig.hiddenFields}
advancedFields={sectionConfig.advancedFields}
disabled={disabled}
readonly={readonly}
formContext={{
level,
cameraName,
globalValue,
cameraValue,
}}
/>
</CardContent>
</Card>
); );
}; };
return ConfigSection;
} }

View File

@ -1,37 +1,34 @@
// Detect Section Component // Detect Section Component
// Reusable for both global and camera-level detect settings // Reusable for both global and camera-level detect settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the detect section
export const detectSectionConfig: SectionConfig = {
fieldOrder: [
"enabled",
"fps",
"width",
"height",
"min_initialized",
"max_disappeared",
"annotation_offset",
"stationary",
],
fieldGroups: {
resolution: ["width", "height"],
tracking: ["min_initialized", "max_disappeared"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: [
"min_initialized",
"max_disappeared",
"annotation_offset",
"stationary",
],
};
export const DetectSection = createConfigSection({ export const DetectSection = createConfigSection({
sectionPath: "detect", sectionPath: "detect",
translationKey: "configForm.detect", i18nNamespace: "config/detect",
defaultConfig: detectSectionConfig, defaultConfig: {
fieldOrder: [
"enabled",
"fps",
"width",
"height",
"min_initialized",
"max_disappeared",
"annotation_offset",
"stationary",
],
fieldGroups: {
resolution: ["width", "height"],
tracking: ["min_initialized", "max_disappeared"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: [
"min_initialized",
"max_disappeared",
"annotation_offset",
"stationary",
],
},
}); });
export default DetectSection; export default DetectSection;

View File

@ -1,20 +1,17 @@
// Live Section Component // Live Section Component
// Reusable for both global and camera-level live settings // Reusable for both global and camera-level live settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the live section
export const liveSectionConfig: SectionConfig = {
fieldOrder: ["stream_name", "height", "quality"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: ["quality"],
};
export const LiveSection = createConfigSection({ export const LiveSection = createConfigSection({
sectionPath: "live", sectionPath: "live",
translationKey: "configForm.live", i18nNamespace: "config/live",
defaultConfig: liveSectionConfig, defaultConfig: {
fieldOrder: ["stream_name", "height", "quality"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: ["quality"],
},
}); });
export default LiveSection; export default LiveSection;

View File

@ -1,42 +1,39 @@
// Motion Section Component // Motion Section Component
// Reusable for both global and camera-level motion settings // Reusable for both global and camera-level motion settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the motion section
export const motionSectionConfig: SectionConfig = {
fieldOrder: [
"enabled",
"threshold",
"lightning_threshold",
"improve_contrast",
"contour_area",
"delta_alpha",
"frame_alpha",
"frame_height",
"mask",
"mqtt_off_delay",
],
fieldGroups: {
sensitivity: ["threshold", "lightning_threshold", "contour_area"],
algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: [
"lightning_threshold",
"improve_contrast",
"contour_area",
"delta_alpha",
"frame_alpha",
"frame_height",
"mqtt_off_delay",
],
};
export const MotionSection = createConfigSection({ export const MotionSection = createConfigSection({
sectionPath: "motion", sectionPath: "motion",
translationKey: "configForm.motion", i18nNamespace: "config/motion",
defaultConfig: motionSectionConfig, defaultConfig: {
fieldOrder: [
"enabled",
"threshold",
"lightning_threshold",
"improve_contrast",
"contour_area",
"delta_alpha",
"frame_alpha",
"frame_height",
"mask",
"mqtt_off_delay",
],
fieldGroups: {
sensitivity: ["threshold", "lightning_threshold", "contour_area"],
algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: [
"lightning_threshold",
"improve_contrast",
"contour_area",
"delta_alpha",
"frame_alpha",
"frame_height",
"mqtt_off_delay",
],
},
}); });
export default MotionSection; export default MotionSection;

View File

@ -1,20 +1,17 @@
// Notifications Section Component // Notifications Section Component
// Reusable for both global and camera-level notification settings // Reusable for both global and camera-level notification settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the notifications section
export const notificationsSectionConfig: SectionConfig = {
fieldOrder: ["enabled", "email"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: [],
};
export const NotificationsSection = createConfigSection({ export const NotificationsSection = createConfigSection({
sectionPath: "notifications", sectionPath: "notifications",
translationKey: "configForm.notifications", i18nNamespace: "config/notifications",
defaultConfig: notificationsSectionConfig, defaultConfig: {
fieldOrder: ["enabled", "email"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: [],
},
}); });
export default NotificationsSection; export default NotificationsSection;

View File

@ -1,23 +1,20 @@
// Objects Section Component // Objects Section Component
// Reusable for both global and camera-level objects settings // Reusable for both global and camera-level objects settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the objects section
export const objectsSectionConfig: SectionConfig = {
fieldOrder: ["track", "alert", "detect", "filters", "mask"],
fieldGroups: {
tracking: ["track", "alert", "detect"],
filtering: ["filters", "mask"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["filters", "mask"],
};
export const ObjectsSection = createConfigSection({ export const ObjectsSection = createConfigSection({
sectionPath: "objects", sectionPath: "objects",
translationKey: "configForm.objects", i18nNamespace: "config/objects",
defaultConfig: objectsSectionConfig, defaultConfig: {
fieldOrder: ["track", "alert", "detect", "filters", "mask"],
fieldGroups: {
tracking: ["track", "alert", "detect"],
filtering: ["filters", "mask"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["filters", "mask"],
},
}); });
export default ObjectsSection; export default ObjectsSection;

View File

@ -1,32 +1,29 @@
// Record Section Component // Record Section Component
// Reusable for both global and camera-level record settings // Reusable for both global and camera-level record settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the record section
export const recordSectionConfig: SectionConfig = {
fieldOrder: [
"enabled",
"expire_interval",
"continuous",
"motion",
"alerts",
"detections",
"preview",
"export",
],
fieldGroups: {
retention: ["continuous", "motion"],
events: ["alerts", "detections"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["expire_interval", "preview", "export"],
};
export const RecordSection = createConfigSection({ export const RecordSection = createConfigSection({
sectionPath: "record", sectionPath: "record",
translationKey: "configForm.record", i18nNamespace: "config/record",
defaultConfig: recordSectionConfig, defaultConfig: {
fieldOrder: [
"enabled",
"expire_interval",
"continuous",
"motion",
"alerts",
"detections",
"preview",
"export",
],
fieldGroups: {
retention: ["continuous", "motion"],
events: ["alerts", "detections"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["expire_interval", "preview", "export"],
},
}); });
export default RecordSection; export default RecordSection;

View File

@ -1,20 +1,17 @@
// Review Section Component // Review Section Component
// Reusable for both global and camera-level review settings // Reusable for both global and camera-level review settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the review section
export const reviewSectionConfig: SectionConfig = {
fieldOrder: ["alerts", "detections"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: [],
};
export const ReviewSection = createConfigSection({ export const ReviewSection = createConfigSection({
sectionPath: "review", sectionPath: "review",
translationKey: "configForm.review", i18nNamespace: "config/review",
defaultConfig: reviewSectionConfig, defaultConfig: {
fieldOrder: ["alerts", "detections"],
fieldGroups: {},
hiddenFields: ["enabled_in_config"],
advancedFields: [],
},
}); });
export default ReviewSection; export default ReviewSection;

View File

@ -1,29 +1,26 @@
// Snapshots Section Component // Snapshots Section Component
// Reusable for both global and camera-level snapshots settings // Reusable for both global and camera-level snapshots settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the snapshots section
export const snapshotsSectionConfig: SectionConfig = {
fieldOrder: [
"enabled",
"bounding_box",
"crop",
"quality",
"timestamp",
"retain",
],
fieldGroups: {
display: ["bounding_box", "crop", "quality", "timestamp"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["quality", "retain"],
};
export const SnapshotsSection = createConfigSection({ export const SnapshotsSection = createConfigSection({
sectionPath: "snapshots", sectionPath: "snapshots",
translationKey: "configForm.snapshots", i18nNamespace: "config/snapshots",
defaultConfig: snapshotsSectionConfig, defaultConfig: {
fieldOrder: [
"enabled",
"bounding_box",
"crop",
"quality",
"timestamp",
"retain",
],
fieldGroups: {
display: ["bounding_box", "crop", "quality", "timestamp"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["quality", "retain"],
},
}); });
export default SnapshotsSection; export default SnapshotsSection;

View File

@ -1,22 +1,19 @@
// Timestamp Section Component // Timestamp Section Component
// Reusable for both global and camera-level timestamp_style settings // Reusable for both global and camera-level timestamp_style settings
import { createConfigSection, type SectionConfig } from "./BaseSection"; import { createConfigSection } from "./BaseSection";
// Configuration for the timestamp_style section
export const timestampSectionConfig: SectionConfig = {
fieldOrder: ["position", "format", "color", "thickness", "effect"],
fieldGroups: {
appearance: ["color", "thickness", "effect"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["thickness", "effect"],
};
export const TimestampSection = createConfigSection({ export const TimestampSection = createConfigSection({
sectionPath: "timestamp_style", sectionPath: "timestamp_style",
translationKey: "configForm.timestampStyle", i18nNamespace: "config/timestamp_style",
defaultConfig: timestampSectionConfig, defaultConfig: {
fieldOrder: ["position", "format", "color", "thickness", "effect"],
fieldGroups: {
appearance: ["color", "thickness", "effect"],
},
hiddenFields: ["enabled_in_config"],
advancedFields: ["thickness", "effect"],
},
}); });
export default TimestampSection; export default TimestampSection;

View File

@ -8,16 +8,13 @@ export {
type CreateSectionOptions, type CreateSectionOptions,
} from "./BaseSection"; } from "./BaseSection";
export { DetectSection, detectSectionConfig } from "./DetectSection"; export { DetectSection } from "./DetectSection";
export { RecordSection, recordSectionConfig } from "./RecordSection"; export { RecordSection } from "./RecordSection";
export { SnapshotsSection, snapshotsSectionConfig } from "./SnapshotsSection"; export { SnapshotsSection } from "./SnapshotsSection";
export { MotionSection, motionSectionConfig } from "./MotionSection"; export { MotionSection } from "./MotionSection";
export { ObjectsSection, objectsSectionConfig } from "./ObjectsSection"; export { ObjectsSection } from "./ObjectsSection";
export { ReviewSection, reviewSectionConfig } from "./ReviewSection"; export { ReviewSection } from "./ReviewSection";
export { AudioSection, audioSectionConfig } from "./AudioSection"; export { AudioSection } from "./AudioSection";
export { export { NotificationsSection } from "./NotificationsSection";
NotificationsSection, export { LiveSection } from "./LiveSection";
notificationsSectionConfig, export { TimestampSection } from "./TimestampSection";
} from "./NotificationsSection";
export { LiveSection, liveSectionConfig } from "./LiveSection";
export { TimestampSection, timestampSectionConfig } from "./TimestampSection";