mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-13 03:47:34 +03:00
add profile state management and save preview support
This commit is contained in:
parent
72b4a4ddad
commit
edf7fcb5b4
@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
||||
export type SaveAllPreviewItem = {
|
||||
scope: "global" | "camera";
|
||||
cameraName?: string;
|
||||
profileName?: string;
|
||||
fieldPath: string;
|
||||
value: unknown;
|
||||
};
|
||||
@ -114,6 +115,18 @@ export default function SaveAllPreviewPopover({
|
||||
})}
|
||||
</span>
|
||||
<span className="truncate">{scopeLabel}</span>
|
||||
{item.profileName && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
{t("saveAllPreview.profile.label", {
|
||||
ns: "views/settings",
|
||||
})}
|
||||
</span>
|
||||
<span className="truncate font-medium">
|
||||
{item.profileName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{t("saveAllPreview.field.label", {
|
||||
ns: "views/settings",
|
||||
|
||||
@ -87,8 +87,10 @@ import { mutate } from "swr";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import {
|
||||
buildConfigDataForPath,
|
||||
parseProfileFromSectionPath,
|
||||
prepareSectionSavePayload,
|
||||
} from "@/utils/configUtil";
|
||||
import type { ProfileState } from "@/types/profile";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
|
||||
import SaveAllPreviewPopover, {
|
||||
@ -621,6 +623,22 @@ export default function Settings() {
|
||||
Record<string, unknown>
|
||||
>({});
|
||||
|
||||
// Profile editing state
|
||||
const [editingProfile, setEditingProfile] = useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
const [newProfiles, setNewProfiles] = useState<string[]>([]);
|
||||
|
||||
const allProfileNames = useMemo(() => {
|
||||
if (!config) return [];
|
||||
const names = new Set<string>();
|
||||
Object.values(config.cameras).forEach((cam) => {
|
||||
Object.keys(cam.profiles ?? {}).forEach((p) => names.add(p));
|
||||
});
|
||||
newProfiles.forEach((p) => names.add(p));
|
||||
return [...names].sort();
|
||||
}, [config, newProfiles]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const cameras = useMemo(() => {
|
||||
@ -692,11 +710,20 @@ export default function Settings() {
|
||||
|
||||
const { scope, cameraName, sectionPath } =
|
||||
parsePendingDataKey(pendingDataKey);
|
||||
const { isProfile, profileName, actualSection } =
|
||||
parseProfileFromSectionPath(sectionPath);
|
||||
const flattened = flattenOverrides(payload.sanitizedOverrides);
|
||||
const displaySection = isProfile ? actualSection : sectionPath;
|
||||
|
||||
flattened.forEach(({ path, value }) => {
|
||||
const fieldPath = path ? `${sectionPath}.${path}` : sectionPath;
|
||||
items.push({ scope, cameraName, fieldPath, value });
|
||||
const fieldPath = path ? `${displaySection}.${path}` : displaySection;
|
||||
items.push({
|
||||
scope,
|
||||
cameraName,
|
||||
profileName: isProfile ? profileName : undefined,
|
||||
fieldPath,
|
||||
value,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@ -726,15 +753,20 @@ export default function Settings() {
|
||||
level = "global";
|
||||
}
|
||||
|
||||
// For profile keys like "profiles.armed.detect", extract the actual section
|
||||
const { actualSection } = parseProfileFromSectionPath(sectionPath);
|
||||
|
||||
if (level === "camera") {
|
||||
return CAMERA_SECTION_MAPPING[sectionPath] as SettingsType | undefined;
|
||||
return CAMERA_SECTION_MAPPING[actualSection] as
|
||||
| SettingsType
|
||||
| undefined;
|
||||
}
|
||||
return (
|
||||
(GLOBAL_SECTION_MAPPING[sectionPath] as SettingsType | undefined) ??
|
||||
(ENRICHMENTS_SECTION_MAPPING[sectionPath] as
|
||||
(GLOBAL_SECTION_MAPPING[actualSection] as SettingsType | undefined) ??
|
||||
(ENRICHMENTS_SECTION_MAPPING[actualSection] as
|
||||
| SettingsType
|
||||
| undefined) ??
|
||||
(SYSTEM_SECTION_MAPPING[sectionPath] as SettingsType | undefined)
|
||||
(SYSTEM_SECTION_MAPPING[actualSection] as SettingsType | undefined)
|
||||
);
|
||||
},
|
||||
[],
|
||||
@ -884,6 +916,16 @@ export default function Settings() {
|
||||
|
||||
setPendingDataBySection({});
|
||||
setUnsavedChanges(false);
|
||||
setEditingProfile({});
|
||||
|
||||
// Clear new profiles that don't exist in saved config
|
||||
if (config) {
|
||||
const savedNames = new Set<string>();
|
||||
Object.values(config.cameras).forEach((cam) => {
|
||||
Object.keys(cam.profiles ?? {}).forEach((p) => savedNames.add(p));
|
||||
});
|
||||
setNewProfiles((prev) => prev.filter((p) => savedNames.has(p)));
|
||||
}
|
||||
|
||||
setSectionStatusByKey((prev) => {
|
||||
const updated = { ...prev };
|
||||
@ -899,7 +941,7 @@ export default function Settings() {
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, [pendingDataBySection, pendingKeyToMenuKey]);
|
||||
}, [pendingDataBySection, pendingKeyToMenuKey, config]);
|
||||
|
||||
const handleDialog = useCallback(
|
||||
(save: boolean) => {
|
||||
@ -970,6 +1012,75 @@ export default function Settings() {
|
||||
}
|
||||
}, [t, contentMobileOpen]);
|
||||
|
||||
// Profile state handlers
|
||||
const handleSelectProfile = useCallback(
|
||||
(camera: string, section: string, profile: string | null) => {
|
||||
const key = `${camera}::${section}`;
|
||||
setEditingProfile((prev) => {
|
||||
if (profile === null) {
|
||||
const { [key]: _, ...rest } = prev;
|
||||
return rest;
|
||||
}
|
||||
return { ...prev, [key]: profile };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAddProfile = useCallback((name: string) => {
|
||||
setNewProfiles((prev) => (prev.includes(name) ? prev : [...prev, name]));
|
||||
}, []);
|
||||
|
||||
const handleDeleteProfileSection = useCallback(
|
||||
async (camera: string, section: string, profile: string) => {
|
||||
try {
|
||||
await axios.put("config/set", {
|
||||
config_data: {
|
||||
cameras: {
|
||||
[camera]: {
|
||||
profiles: {
|
||||
[profile]: {
|
||||
[section]: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await mutate("config");
|
||||
// Switch back to base config
|
||||
handleSelectProfile(camera, section, null);
|
||||
toast.success(
|
||||
t("toast.save.success", {
|
||||
ns: "common",
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("toast.save.error.title", { ns: "common" }));
|
||||
}
|
||||
},
|
||||
[handleSelectProfile, t],
|
||||
);
|
||||
|
||||
const profileState: ProfileState = useMemo(
|
||||
() => ({
|
||||
editingProfile,
|
||||
newProfiles,
|
||||
allProfileNames,
|
||||
onSelectProfile: handleSelectProfile,
|
||||
onAddProfile: handleAddProfile,
|
||||
onDeleteProfileSection: handleDeleteProfileSection,
|
||||
}),
|
||||
[
|
||||
editingProfile,
|
||||
newProfiles,
|
||||
allProfileNames,
|
||||
handleSelectProfile,
|
||||
handleAddProfile,
|
||||
handleDeleteProfileSection,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSectionStatusChange = useCallback(
|
||||
(sectionKey: string, level: "global" | "camera", status: SectionStatus) => {
|
||||
// Map section keys to menu keys based on level
|
||||
@ -1244,6 +1355,7 @@ export default function Settings() {
|
||||
onSectionStatusChange={handleSectionStatusChange}
|
||||
pendingDataBySection={pendingDataBySection}
|
||||
onPendingDataChange={handlePendingDataChange}
|
||||
profileState={profileState}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
@ -5,6 +5,7 @@ import { ConfigSectionTemplate } from "@/components/config-form/sections";
|
||||
import type { PolygonType } from "@/types/canvas";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { ConfigSectionData } from "@/types/configForm";
|
||||
import type { ProfileState } from "@/types/profile";
|
||||
import { getSectionConfig } from "@/utils/configUtil";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -26,6 +27,7 @@ export type SettingsPageProps = {
|
||||
cameraName: string | undefined,
|
||||
data: ConfigSectionData | null,
|
||||
) => void;
|
||||
profileState?: ProfileState;
|
||||
};
|
||||
|
||||
export type SectionStatus = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user