mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-27 23:01:54 +03:00
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:
parent
88755513c8
commit
feff214d73
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user