mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-29 11:30:17 +03:00
add profileName prop to BaseSection for profile-aware config editing
This commit is contained in:
parent
edf7fcb5b4
commit
d5dc77daa4
@ -34,6 +34,7 @@ import Heading from "@/components/ui/heading";
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
|
import merge from "lodash/merge";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@ -136,6 +137,8 @@ export interface BaseSectionProps {
|
|||||||
cameraName: string | undefined,
|
cameraName: string | undefined,
|
||||||
data: ConfigSectionData | null,
|
data: ConfigSectionData | null,
|
||||||
) => void;
|
) => void;
|
||||||
|
/** When set, editing this profile's overrides instead of the base config */
|
||||||
|
profileName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSectionOptions {
|
export interface CreateSectionOptions {
|
||||||
@ -166,6 +169,7 @@ export function ConfigSection({
|
|||||||
onStatusChange,
|
onStatusChange,
|
||||||
pendingDataBySection,
|
pendingDataBySection,
|
||||||
onPendingDataChange,
|
onPendingDataChange,
|
||||||
|
profileName,
|
||||||
}: ConfigSectionProps) {
|
}: ConfigSectionProps) {
|
||||||
// For replay level, treat as camera-level config access
|
// For replay level, treat as camera-level config access
|
||||||
const effectiveLevel = level === "replay" ? "camera" : level;
|
const effectiveLevel = level === "replay" ? "camera" : level;
|
||||||
@ -181,12 +185,17 @@ export function ConfigSection({
|
|||||||
const statusBar = useContext(StatusBarMessagesContext);
|
const statusBar = useContext(StatusBarMessagesContext);
|
||||||
|
|
||||||
// Create a key for this section's pending data
|
// Create a key for this section's pending data
|
||||||
|
// When editing a profile, use "cameraName::profiles.profileName.sectionPath"
|
||||||
|
const effectiveSectionPath = profileName
|
||||||
|
? `profiles.${profileName}.${sectionPath}`
|
||||||
|
: sectionPath;
|
||||||
|
|
||||||
const pendingDataKey = useMemo(
|
const pendingDataKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
effectiveLevel === "camera" && cameraName
|
effectiveLevel === "camera" && cameraName
|
||||||
? `${cameraName}::${sectionPath}`
|
? `${cameraName}::${effectiveSectionPath}`
|
||||||
: sectionPath,
|
: effectiveSectionPath,
|
||||||
[effectiveLevel, cameraName, sectionPath],
|
[effectiveLevel, cameraName, effectiveSectionPath],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use pending data from parent if available, otherwise use local state
|
// Use pending data from parent if available, otherwise use local state
|
||||||
@ -213,12 +222,12 @@ export function ConfigSection({
|
|||||||
const setPendingData = useCallback(
|
const setPendingData = useCallback(
|
||||||
(data: ConfigSectionData | null) => {
|
(data: ConfigSectionData | null) => {
|
||||||
if (onPendingDataChange) {
|
if (onPendingDataChange) {
|
||||||
onPendingDataChange(sectionPath, cameraName, data);
|
onPendingDataChange(effectiveSectionPath, cameraName, data);
|
||||||
} else {
|
} else {
|
||||||
setLocalPendingData(data);
|
setLocalPendingData(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onPendingDataChange, sectionPath, cameraName],
|
[onPendingDataChange, effectiveSectionPath, cameraName],
|
||||||
);
|
);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
||||||
@ -230,8 +239,10 @@ export function ConfigSection({
|
|||||||
const isInitializingRef = useRef(true);
|
const isInitializingRef = useRef(true);
|
||||||
const lastPendingDataKeyRef = useRef<string | null>(null);
|
const lastPendingDataKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const updateTopic =
|
// Profile definitions don't hot-reload — only PUT /api/profile/set applies them
|
||||||
effectiveLevel === "camera" && cameraName
|
const updateTopic = profileName
|
||||||
|
? undefined
|
||||||
|
: effectiveLevel === "camera" && cameraName
|
||||||
? cameraUpdateTopicMap[sectionPath]
|
? cameraUpdateTopicMap[sectionPath]
|
||||||
? `config/cameras/${cameraName}/${cameraUpdateTopicMap[sectionPath]}`
|
? `config/cameras/${cameraName}/${cameraUpdateTopicMap[sectionPath]}`
|
||||||
: undefined
|
: undefined
|
||||||
@ -265,15 +276,27 @@ export function ConfigSection({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get current form data
|
// Get current form data
|
||||||
|
// When editing a profile, show base camera config deep-merged with profile overrides
|
||||||
const rawSectionValue = useMemo(() => {
|
const rawSectionValue = useMemo(() => {
|
||||||
if (!config) return undefined;
|
if (!config) return undefined;
|
||||||
|
|
||||||
if (effectiveLevel === "camera" && cameraName) {
|
if (effectiveLevel === "camera" && cameraName) {
|
||||||
return get(config.cameras?.[cameraName], sectionPath);
|
const baseValue = get(config.cameras?.[cameraName], sectionPath);
|
||||||
|
if (profileName) {
|
||||||
|
const profileOverrides = get(
|
||||||
|
config.cameras?.[cameraName],
|
||||||
|
`profiles.${profileName}.${sectionPath}`,
|
||||||
|
);
|
||||||
|
if (profileOverrides && typeof profileOverrides === "object") {
|
||||||
|
return merge(cloneDeep(baseValue ?? {}), cloneDeep(profileOverrides));
|
||||||
|
}
|
||||||
|
return baseValue;
|
||||||
|
}
|
||||||
|
return baseValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return get(config, sectionPath);
|
return get(config, sectionPath);
|
||||||
}, [config, cameraName, sectionPath, effectiveLevel]);
|
}, [config, cameraName, sectionPath, effectiveLevel, profileName]);
|
||||||
|
|
||||||
const rawFormData = useMemo(() => {
|
const rawFormData = useMemo(() => {
|
||||||
if (!config) return {};
|
if (!config) return {};
|
||||||
@ -499,8 +522,8 @@ export function ConfigSection({
|
|||||||
try {
|
try {
|
||||||
const basePath =
|
const basePath =
|
||||||
effectiveLevel === "camera" && cameraName
|
effectiveLevel === "camera" && cameraName
|
||||||
? `cameras.${cameraName}.${sectionPath}`
|
? `cameras.${cameraName}.${effectiveSectionPath}`
|
||||||
: sectionPath;
|
: effectiveSectionPath;
|
||||||
const rawData = sanitizeSectionData(rawFormData);
|
const rawData = sanitizeSectionData(rawFormData);
|
||||||
const overrides = buildOverrides(
|
const overrides = buildOverrides(
|
||||||
pendingData,
|
pendingData,
|
||||||
@ -522,9 +545,11 @@ export function ConfigSection({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsRestart = skipSave
|
// Profile definition edits never require restart
|
||||||
? false
|
const needsRestart =
|
||||||
: requiresRestartForOverrides(sanitizedOverrides);
|
skipSave || profileName
|
||||||
|
? false
|
||||||
|
: requiresRestartForOverrides(sanitizedOverrides);
|
||||||
|
|
||||||
const configData = buildConfigDataForPath(basePath, sanitizedOverrides);
|
const configData = buildConfigDataForPath(basePath, sanitizedOverrides);
|
||||||
await axios.put("config/set", {
|
await axios.put("config/set", {
|
||||||
@ -619,6 +644,8 @@ export function ConfigSection({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
sectionPath,
|
sectionPath,
|
||||||
|
effectiveSectionPath,
|
||||||
|
profileName,
|
||||||
pendingData,
|
pendingData,
|
||||||
effectiveLevel,
|
effectiveLevel,
|
||||||
cameraName,
|
cameraName,
|
||||||
@ -642,8 +669,8 @@ export function ConfigSection({
|
|||||||
try {
|
try {
|
||||||
const basePath =
|
const basePath =
|
||||||
effectiveLevel === "camera" && cameraName
|
effectiveLevel === "camera" && cameraName
|
||||||
? `cameras.${cameraName}.${sectionPath}`
|
? `cameras.${cameraName}.${effectiveSectionPath}`
|
||||||
: sectionPath;
|
: effectiveSectionPath;
|
||||||
|
|
||||||
const configData = buildConfigDataForPath(basePath, "");
|
const configData = buildConfigDataForPath(basePath, "");
|
||||||
|
|
||||||
@ -675,7 +702,7 @@ export function ConfigSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
sectionPath,
|
effectiveSectionPath,
|
||||||
effectiveLevel,
|
effectiveLevel,
|
||||||
cameraName,
|
cameraName,
|
||||||
requiresRestart,
|
requiresRestart,
|
||||||
@ -855,7 +882,8 @@ export function ConfigSection({
|
|||||||
{((effectiveLevel === "camera" && isOverridden) ||
|
{((effectiveLevel === "camera" && isOverridden) ||
|
||||||
effectiveLevel === "global") &&
|
effectiveLevel === "global") &&
|
||||||
!hasChanges &&
|
!hasChanges &&
|
||||||
!skipSave && (
|
!skipSave &&
|
||||||
|
!profileName && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsResetDialogOpen(true)}
|
onClick={() => setIsResetDialogOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user