add support for config-aware patterns in section hiddenFields

Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
This commit is contained in:
Josh Hawkins 2026-05-06 21:34:55 -05:00
parent 88755513c8
commit feff214d73
5 changed files with 76 additions and 16 deletions

View File

@ -1,5 +1,13 @@
import type { FrigateConfig } from "@/types/frigateConfig";
import type { SectionConfigOverrides } from "./types"; import type { SectionConfigOverrides } from "./types";
// Attribute labels (face, license_plate, Frigate+ couriers like DHL/Amazon,
// etc.) are populated into objects.filters by the backend even when the
// model can't actually detect them. They aren't user-settable, so hide any
// `filters.<attr>` patterns from forms and override comparisons.
const hideAttributeFilters = (config: FrigateConfig): string[] =>
(config.model?.all_attributes ?? []).map((attr) => `filters.${attr}`);
const objects: SectionConfigOverrides = { const objects: SectionConfigOverrides = {
base: { base: {
sectionDocs: "/configuration/object_filters", sectionDocs: "/configuration/object_filters",
@ -26,6 +34,7 @@ const objects: SectionConfigOverrides = {
"filters.*.raw_mask", "filters.*.raw_mask",
"filters.mask", "filters.mask",
"filters.raw_mask", "filters.raw_mask",
hideAttributeFilters,
], ],
advancedFields: ["genai"], advancedFields: ["genai"],
uiSchema: { uiSchema: {
@ -99,6 +108,7 @@ const objects: SectionConfigOverrides = {
"filters.mask", "filters.mask",
"filters.raw_mask", "filters.raw_mask",
"genai.required_zones", "genai.required_zones",
hideAttributeFilters,
], ],
}, },
camera: { camera: {
@ -123,6 +133,7 @@ const objects: SectionConfigOverrides = {
"filters.*.raw_mask", "filters.*.raw_mask",
"filters.mask", "filters.mask",
"filters.raw_mask", "filters.raw_mask",
hideAttributeFilters,
], ],
advancedFields: [], advancedFields: [],
}, },

View File

@ -59,7 +59,11 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { applySchemaDefaults } from "@/lib/config-schema"; import { applySchemaDefaults } from "@/lib/config-schema";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ConfigSectionData, JsonValue } from "@/types/configForm"; import {
ConfigSectionData,
HiddenFieldEntry,
JsonValue,
} from "@/types/configForm";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { import {
@ -69,6 +73,7 @@ import {
buildConfigDataForPath, buildConfigDataForPath,
flattenOverrides, flattenOverrides,
getBaseCameraSectionValue, getBaseCameraSectionValue,
resolveHiddenFieldEntries,
sanitizeSectionData as sharedSanitizeSectionData, sanitizeSectionData as sharedSanitizeSectionData,
requiresRestartForOverrides as sharedRequiresRestartForOverrides, requiresRestartForOverrides as sharedRequiresRestartForOverrides,
} from "@/utils/configUtil"; } from "@/utils/configUtil";
@ -91,7 +96,7 @@ export interface SectionConfig {
/** Fields to group together */ /** Fields to group together */
fieldGroups?: Record<string, string[]>; fieldGroups?: Record<string, string[]>;
/** Fields to hide from UI */ /** Fields to hide from UI */
hiddenFields?: string[]; hiddenFields?: HiddenFieldEntry[];
/** Fields to show in advanced section */ /** Fields to show in advanced section */
advancedFields?: string[]; advancedFields?: string[];
/** Fields to compare for override detection */ /** Fields to compare for override detection */
@ -375,12 +380,17 @@ export function ConfigSection({
// When editing a profile, hide fields that require a restart since they // When editing a profile, hide fields that require a restart since they
// cannot take effect via profile switching alone. // cannot take effect via profile switching alone.
const effectiveHiddenFields = useMemo(() => { const effectiveHiddenFields = useMemo(() => {
const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, config);
if (!profileName || !sectionConfig.restartRequired?.length) { if (!profileName || !sectionConfig.restartRequired?.length) {
return sectionConfig.hiddenFields; return base;
} }
const base = sectionConfig.hiddenFields ?? [];
return [...new Set([...base, ...sectionConfig.restartRequired])]; return [...new Set([...base, ...sectionConfig.restartRequired])];
}, [profileName, sectionConfig.hiddenFields, sectionConfig.restartRequired]); }, [
profileName,
sectionConfig.hiddenFields,
sectionConfig.restartRequired,
config,
]);
const sanitizeSectionData = useCallback( const sanitizeSectionData = useCallback(
(data: ConfigSectionData) => (data: ConfigSectionData) =>

View File

@ -20,7 +20,7 @@ import type { ProfilesApiResponse } from "@/types/profile";
import { humanizeKey } from "@/components/config-form/theme/utils/i18n"; import { humanizeKey } from "@/components/config-form/theme/utils/i18n";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { formatList } from "@/utils/stringUtil"; import { formatList } from "@/utils/stringUtil";
import { getSectionConfig } from "@/utils/configUtil"; import { getEffectiveHiddenFields } from "@/utils/configUtil";
const CAMERA_PAGE_BY_SECTION: Record<string, string> = { const CAMERA_PAGE_BY_SECTION: Record<string, string> = {
detect: "cameraDetect", detect: "cameraDetect",
@ -247,8 +247,11 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) {
const rawEntries = useCamerasOverridingSection(config, sectionPath); const rawEntries = useCamerasOverridingSection(config, sectionPath);
const entries = useMemo(() => { const entries = useMemo(() => {
const hiddenFields = const hiddenFields = getEffectiveHiddenFields(
getSectionConfig(sectionPath, "global").hiddenFields ?? []; sectionPath,
"global",
config,
);
if (hiddenFields.length === 0) return rawEntries; if (hiddenFields.length === 0) return rawEntries;
return rawEntries return rawEntries
.map((entry) => ({ .map((entry) => ({
@ -261,7 +264,7 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) {
), ),
})) }))
.filter((entry) => entry.fieldDeltas.length > 0); .filter((entry) => entry.fieldDeltas.length > 0);
}, [rawEntries, sectionPath]); }, [rawEntries, sectionPath, config]);
if (SECTIONS_WITHOUT_OVERRIDE_BADGE.has(sectionPath)) { if (SECTIONS_WITHOUT_OVERRIDE_BADGE.has(sectionPath)) {
return null; return null;

View File

@ -13,6 +13,8 @@ export type JsonArray = JsonValue[];
export type ConfigSectionData = JsonObject; export type ConfigSectionData = JsonObject;
export type HiddenFieldEntry = string | ((config: FrigateConfig) => string[]);
export type ConfigFormContext = { export type ConfigFormContext = {
level?: "global" | "camera"; level?: "global" | "camera";
cameraName?: string; cameraName?: string;

View File

@ -587,13 +587,14 @@ export function prepareSectionSavePayload(opts: {
// For profile sections, also hide restart-required fields to match // For profile sections, also hide restart-required fields to match
// effectiveHiddenFields in BaseSection (prevents spurious deletion markers // effectiveHiddenFields in BaseSection (prevents spurious deletion markers
// for fields that are hidden from the form during profile editing). // for fields that are hidden from the form during profile editing).
let hiddenFieldsForSanitize = sectionConfig.hiddenFields; const resolvedHidden = resolveHiddenFieldEntries(
if (profileInfo.isProfile && sectionConfig.restartRequired?.length) { sectionConfig.hiddenFields,
const base = sectionConfig.hiddenFields ?? []; config,
hiddenFieldsForSanitize = [ );
...new Set([...base, ...sectionConfig.restartRequired]), const hiddenFieldsForSanitize =
]; profileInfo.isProfile && sectionConfig.restartRequired?.length
} ? [...new Set([...resolvedHidden, ...sectionConfig.restartRequired])]
: resolvedHidden;
// Sanitize raw form data // Sanitize raw form data
const rawData = sanitizeSectionData( const rawData = sanitizeSectionData(
@ -702,3 +703,36 @@ export function getSectionConfig(
: entry.camera; : entry.camera;
return mergeSectionConfig(entry.base, overrides); return mergeSectionConfig(entry.base, overrides);
} }
/**
* Resolve the effective hidden-field patterns for a section. Each entry in
* `hiddenFields` is either a literal pattern or a function that produces
* patterns from the loaded config (e.g. `filters.<attr>` for each
* `model.all_attributes` entry on the objects section).
*/
export function getEffectiveHiddenFields(
sectionKey: string,
level: "global" | "camera" | "replay",
config: FrigateConfig | undefined,
): string[] {
return resolveHiddenFieldEntries(
getSectionConfig(sectionKey, level).hiddenFields,
config,
);
}
export function resolveHiddenFieldEntries(
entries: SectionConfig["hiddenFields"] | undefined,
config: FrigateConfig | undefined,
): string[] {
if (!entries || entries.length === 0) return [];
const result: string[] = [];
for (const entry of entries) {
if (typeof entry === "function") {
if (config) result.push(...entry(config));
} else {
result.push(entry);
}
}
return result;
}