Add attributes to UI filters list (#23250)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* preserve user-set min_score on attribute filters instead of bumping any 0.5 value

use model_fields_set to distinguish "user explicitly set min_score" from "Pydantic applied the generic FilterConfig default of 0.5"

* add config test for attributes

* fix attributes frontend type

* add expanded hidden field context

* extend schema modification

* special case for attributes

* i18n for attributes

* handle dedicated lpr mode

* strip unrendered FilterConfig fields from attribute filter form data to fix validation errors
This commit is contained in:
Josh Hawkins
2026-05-19 08:31:50 -06:00
committed by GitHub
parent 4fdc107987
commit b1de5e2290
16 changed files with 535 additions and 43 deletions
+74 -11
View File
@@ -19,9 +19,10 @@ import {
sanitizeOverridesForSection,
} from "@/components/config-form/sections/section-special-cases";
import type { RJSFSchema } from "@rjsf/utils";
import type { FrigateConfig } from "@/types/frigateConfig";
import type { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import type {
ConfigSectionData,
HiddenFieldContext,
JsonObject,
JsonValue,
} from "@/types/configForm";
@@ -568,6 +569,17 @@ export function prepareSectionSavePayload(opts: {
schemaSection,
level,
sectionSchema,
config
? {
fullConfig: config,
fullCameraConfig:
level === "camera" && cameraName
? config.cameras?.[cameraName]
: undefined,
level,
cameraName,
}
: undefined,
);
// Compute rawFormData (the current stored value for this section)
@@ -615,10 +627,16 @@ export function prepareSectionSavePayload(opts: {
// For profile sections, also hide restart-required fields to match
// effectiveHiddenFields in BaseSection (prevents spurious deletion markers
// for fields that are hidden from the form during profile editing).
const resolvedHidden = resolveHiddenFieldEntries(
sectionConfig.hiddenFields,
config,
);
const resolvedHidden = resolveHiddenFieldEntries(sectionConfig.hiddenFields, {
fullConfig: config,
fullCameraConfig:
level === "camera" && cameraName
? config.cameras?.[cameraName]
: undefined,
level,
cameraName,
formData: rawFormData as ConfigSectionData,
});
const hiddenFieldsForSanitize =
profileInfo.isProfile && sectionConfig.restartRequired?.length
? [...new Set([...resolvedHidden, ...sectionConfig.restartRequired])]
@@ -731,32 +749,77 @@ export function getSectionConfig(
return mergeSectionConfig(entry.base, overrides);
}
/**
* Resolve the effective attribute label set at a given scope. At camera
* (and replay) scope on a dedicated LPR camera (`camera.type === "lpr"`),
* `license_plate` is treated as a regular tracked object — not an
* attribute — to match the backend's per-camera carve-out in
* `frigate/video/detect.py`. Returns the full attribute list at global
* scope and for non-LPR cameras.
*/
export function getEffectiveAttributeLabels(
fullConfig: FrigateConfig | undefined,
fullCameraConfig: CameraConfig | undefined,
level: "global" | "camera" | "replay" | undefined,
): string[] {
const all = fullConfig?.model?.all_attributes ?? [];
if (level !== "global" && fullCameraConfig?.type === "lpr") {
return all.filter((attr) => attr !== "license_plate");
}
return all;
}
/**
* Build a `HiddenFieldContext` for the common case where a callsite has
* `config`, an optional `cameraName`, and a level, but no per-section
* saved form data to thread through. Resolvers that don't read `formData`
* (which is most of them) just fall through to `fullCameraConfig` /
* `fullConfig`.
*/
export function buildHiddenFieldContext(
config: FrigateConfig | undefined,
level: "global" | "camera" | "replay",
cameraName?: string,
): HiddenFieldContext | undefined {
if (!config) return undefined;
return {
fullConfig: config,
fullCameraConfig:
level !== "global" && cameraName
? config.cameras?.[cameraName]
: undefined,
level,
cameraName,
};
}
/**
* 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).
* patterns from the loaded config and scope (e.g. `filters.<attr>` for each
* `model.all_attributes` entry on the objects section, gated by the
* effective `objects.track` list at the current scope).
*/
export function getEffectiveHiddenFields(
sectionKey: string,
level: "global" | "camera" | "replay",
config: FrigateConfig | undefined,
ctx: HiddenFieldContext | undefined,
): string[] {
return resolveHiddenFieldEntries(
getSectionConfig(sectionKey, level).hiddenFields,
config,
ctx,
);
}
export function resolveHiddenFieldEntries(
entries: SectionConfig["hiddenFields"] | undefined,
config: FrigateConfig | undefined,
ctx: HiddenFieldContext | 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));
if (ctx) result.push(...entry(ctx));
} else {
result.push(entry);
}