mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-12 19:37:35 +03:00
immediately create profiles on backend instead of deferring to Save All
This commit is contained in:
parent
e92fa2b4ba
commit
611316906a
@ -1480,6 +1480,7 @@
|
||||
"deleteProfile": "Delete Profile",
|
||||
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
|
||||
"deleteSuccess": "Profile '{{profile}}' deleted",
|
||||
"createSuccess": "Profile '{{profile}}' created",
|
||||
"removeOverride": "Remove Profile Override",
|
||||
"deleteSection": "Delete Section Overrides",
|
||||
"deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?",
|
||||
|
||||
@ -660,32 +660,20 @@ export default function Settings() {
|
||||
const [editingProfile, setEditingProfile] = useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
const [newProfiles, setNewProfiles] = useState<string[]>([]);
|
||||
const [profilesUIEnabled, setProfilesUIEnabled] = useState(false);
|
||||
|
||||
const allProfileNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
if (config?.profiles) {
|
||||
Object.keys(config.profiles).forEach((p) => names.add(p));
|
||||
}
|
||||
newProfiles.forEach((p) => names.add(p));
|
||||
return [...names].sort();
|
||||
}, [config, newProfiles]);
|
||||
if (!config?.profiles) return [];
|
||||
return Object.keys(config.profiles).sort();
|
||||
}, [config]);
|
||||
|
||||
const profileFriendlyNames = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
if (profilesData?.profiles) {
|
||||
profilesData.profiles.forEach((p) => map.set(p.name, p.friendly_name));
|
||||
}
|
||||
// Include pending (unsaved) profile definitions
|
||||
for (const [key, data] of Object.entries(pendingDataBySection)) {
|
||||
if (key.startsWith("__profile_def__.") && data?.friendly_name) {
|
||||
const id = key.slice("__profile_def__.".length);
|
||||
map.set(id, String(data.friendly_name));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [profilesData, pendingDataBySection]);
|
||||
}, [profilesData]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -842,27 +830,6 @@ export default function Settings() {
|
||||
for (const key of pendingKeys) {
|
||||
const pendingData = pendingDataBySection[key];
|
||||
|
||||
// Handle top-level profile definition saves
|
||||
if (key.startsWith("__profile_def__.")) {
|
||||
const profileId = key.replace("__profile_def__.", "");
|
||||
try {
|
||||
const configData = { profiles: { [profileId]: pendingData } };
|
||||
await axios.put("config/set", {
|
||||
requires_restart: 0,
|
||||
config_data: configData,
|
||||
});
|
||||
setPendingDataBySection((prev) => {
|
||||
const { [key]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
savedKeys.push(key);
|
||||
successCount++;
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = prepareSectionSavePayload({
|
||||
pendingDataKey: key,
|
||||
@ -912,11 +879,6 @@ export default function Settings() {
|
||||
// Refresh config from server once
|
||||
await mutate("config");
|
||||
|
||||
// If any profile definitions were saved, refresh profiles data too
|
||||
if (savedKeys.some((key) => key.startsWith("__profile_def__."))) {
|
||||
await mutate("profiles");
|
||||
}
|
||||
|
||||
// Clear hasChanges in sidebar for all successfully saved sections
|
||||
if (savedKeys.length > 0) {
|
||||
setSectionStatusByKey((prev) => {
|
||||
@ -995,12 +957,6 @@ export default function Settings() {
|
||||
setUnsavedChanges(false);
|
||||
setEditingProfile({});
|
||||
|
||||
// Clear new profiles that now exist in top-level config
|
||||
if (config) {
|
||||
const savedNames = new Set<string>(Object.keys(config.profiles ?? {}));
|
||||
setNewProfiles((prev) => prev.filter((p) => !savedNames.has(p)));
|
||||
}
|
||||
|
||||
setSectionStatusByKey((prev) => {
|
||||
const updated = { ...prev };
|
||||
for (const key of pendingKeys) {
|
||||
@ -1015,7 +971,7 @@ export default function Settings() {
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, [pendingDataBySection, pendingKeyToMenuKey, config]);
|
||||
}, [pendingDataBySection, pendingKeyToMenuKey]);
|
||||
|
||||
const handleDialog = useCallback(
|
||||
(save: boolean) => {
|
||||
@ -1100,43 +1056,6 @@ export default function Settings() {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAddProfile = useCallback((id: string, friendlyName: string) => {
|
||||
setNewProfiles((prev) => (prev.includes(id) ? prev : [...prev, id]));
|
||||
|
||||
// Stage the top-level profile definition for saving
|
||||
setPendingDataBySection((prev) => ({
|
||||
...prev,
|
||||
[`__profile_def__.${id}`]: { friendly_name: friendlyName },
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleRemoveNewProfile = useCallback((name: string) => {
|
||||
setNewProfiles((prev) => prev.filter((p) => p !== name));
|
||||
// Clear any editing state for this profile
|
||||
setEditingProfile((prev) => {
|
||||
const updated = { ...prev };
|
||||
for (const key of Object.keys(updated)) {
|
||||
if (updated[key] === name) {
|
||||
delete updated[key];
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
// Clear any pending data for this profile
|
||||
setPendingDataBySection((prev) => {
|
||||
const profileSegment = `profiles.${name}.`;
|
||||
const updated = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(updated)) {
|
||||
if (key.includes(profileSegment)) {
|
||||
delete updated[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? updated : prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDeleteProfileSection = useCallback(
|
||||
async (camera: string, section: string, profile: string) => {
|
||||
try {
|
||||
@ -1177,22 +1096,16 @@ export default function Settings() {
|
||||
const profileState: ProfileState = useMemo(
|
||||
() => ({
|
||||
editingProfile,
|
||||
newProfiles,
|
||||
allProfileNames,
|
||||
profileFriendlyNames,
|
||||
onSelectProfile: handleSelectProfile,
|
||||
onAddProfile: handleAddProfile,
|
||||
onRemoveNewProfile: handleRemoveNewProfile,
|
||||
onDeleteProfileSection: handleDeleteProfileSection,
|
||||
}),
|
||||
[
|
||||
editingProfile,
|
||||
newProfiles,
|
||||
allProfileNames,
|
||||
profileFriendlyNames,
|
||||
handleSelectProfile,
|
||||
handleAddProfile,
|
||||
handleRemoveNewProfile,
|
||||
handleDeleteProfileSection,
|
||||
],
|
||||
);
|
||||
|
||||
@ -18,7 +18,6 @@ export type ProfilesApiResponse = {
|
||||
|
||||
export type ProfileState = {
|
||||
editingProfile: Record<string, string | null>;
|
||||
newProfiles: string[];
|
||||
allProfileNames: string[];
|
||||
profileFriendlyNames: Map<string, string>;
|
||||
onSelectProfile: (
|
||||
@ -26,8 +25,6 @@ export type ProfileState = {
|
||||
section: string,
|
||||
profile: string | null,
|
||||
) => void;
|
||||
onAddProfile: (id: string, friendlyName: string) => void;
|
||||
onRemoveNewProfile: (name: string) => void;
|
||||
onDeleteProfileSection: (
|
||||
camera: string,
|
||||
section: string,
|
||||
|
||||
@ -165,16 +165,42 @@ export default function ProfilesView({
|
||||
return data;
|
||||
}, [config, allProfileNames]);
|
||||
|
||||
const [addingProfile, setAddingProfile] = useState(false);
|
||||
|
||||
const handleAddSubmit = useCallback(
|
||||
(data: AddProfileForm) => {
|
||||
async (data: AddProfileForm) => {
|
||||
const id = data.name.trim();
|
||||
const friendlyName = data.friendly_name.trim();
|
||||
if (!id || !friendlyName) return;
|
||||
profileState?.onAddProfile(id, friendlyName);
|
||||
setAddDialogOpen(false);
|
||||
addForm.reset();
|
||||
|
||||
setAddingProfile(true);
|
||||
try {
|
||||
await axios.put("config/set", {
|
||||
requires_restart: 0,
|
||||
config_data: {
|
||||
profiles: { [id]: { friendly_name: friendlyName } },
|
||||
},
|
||||
});
|
||||
await updateConfig();
|
||||
await updateProfiles();
|
||||
toast.success(
|
||||
t("profiles.createSuccess", {
|
||||
ns: "views/settings",
|
||||
profile: friendlyName,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
setAddDialogOpen(false);
|
||||
addForm.reset();
|
||||
} catch {
|
||||
toast.error(t("toast.save.error.noMessage", { ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
} finally {
|
||||
setAddingProfile(false);
|
||||
}
|
||||
},
|
||||
[profileState, addForm],
|
||||
[updateConfig, updateProfiles, addForm, t],
|
||||
);
|
||||
|
||||
const handleActivateProfile = useCallback(
|
||||
@ -213,14 +239,6 @@ export default function ProfilesView({
|
||||
const handleDeleteProfile = useCallback(async () => {
|
||||
if (!deleteProfile || !config) return;
|
||||
|
||||
// If this is an unsaved (new) profile, just remove it from local state
|
||||
const isNewProfile = profileState?.newProfiles.includes(deleteProfile);
|
||||
if (isNewProfile) {
|
||||
profileState?.onRemoveNewProfile(deleteProfile);
|
||||
setDeleteProfile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleting(true);
|
||||
|
||||
try {
|
||||
@ -254,9 +272,6 @@ export default function ProfilesView({
|
||||
await updateConfig();
|
||||
await updateProfiles();
|
||||
|
||||
// Also clean up local newProfiles state if this profile was in it
|
||||
profileState?.onRemoveNewProfile(deleteProfile);
|
||||
|
||||
toast.success(
|
||||
t("profiles.deleteSuccess", {
|
||||
ns: "views/settings",
|
||||
@ -281,7 +296,6 @@ export default function ProfilesView({
|
||||
deleteProfile,
|
||||
activeProfile,
|
||||
config,
|
||||
profileState,
|
||||
profileFriendlyNames,
|
||||
updateConfig,
|
||||
updateProfiles,
|
||||
@ -609,6 +623,7 @@ export default function ProfilesView({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAddDialogOpen(false)}
|
||||
disabled={addingProfile}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
@ -616,10 +631,14 @@ export default function ProfilesView({
|
||||
type="submit"
|
||||
variant="select"
|
||||
disabled={
|
||||
addingProfile ||
|
||||
!addForm.watch("friendly_name").trim() ||
|
||||
!addForm.watch("name").trim()
|
||||
}
|
||||
>
|
||||
{addingProfile && (
|
||||
<ActivityIndicator className="mr-2 size-4" />
|
||||
)}
|
||||
{t("button.add", { ns: "common" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user