mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-28 02:58:22 +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",
|
"deleteProfile": "Delete Profile",
|
||||||
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
|
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
|
||||||
"deleteSuccess": "Profile '{{profile}}' deleted",
|
"deleteSuccess": "Profile '{{profile}}' deleted",
|
||||||
|
"createSuccess": "Profile '{{profile}}' created",
|
||||||
"removeOverride": "Remove Profile Override",
|
"removeOverride": "Remove Profile Override",
|
||||||
"deleteSection": "Delete Section Overrides",
|
"deleteSection": "Delete Section Overrides",
|
||||||
"deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?",
|
"deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?",
|
||||||
|
|||||||
@ -660,32 +660,20 @@ export default function Settings() {
|
|||||||
const [editingProfile, setEditingProfile] = useState<
|
const [editingProfile, setEditingProfile] = useState<
|
||||||
Record<string, string | null>
|
Record<string, string | null>
|
||||||
>({});
|
>({});
|
||||||
const [newProfiles, setNewProfiles] = useState<string[]>([]);
|
|
||||||
const [profilesUIEnabled, setProfilesUIEnabled] = useState(false);
|
const [profilesUIEnabled, setProfilesUIEnabled] = useState(false);
|
||||||
|
|
||||||
const allProfileNames = useMemo(() => {
|
const allProfileNames = useMemo(() => {
|
||||||
const names = new Set<string>();
|
if (!config?.profiles) return [];
|
||||||
if (config?.profiles) {
|
return Object.keys(config.profiles).sort();
|
||||||
Object.keys(config.profiles).forEach((p) => names.add(p));
|
}, [config]);
|
||||||
}
|
|
||||||
newProfiles.forEach((p) => names.add(p));
|
|
||||||
return [...names].sort();
|
|
||||||
}, [config, newProfiles]);
|
|
||||||
|
|
||||||
const profileFriendlyNames = useMemo(() => {
|
const profileFriendlyNames = useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
if (profilesData?.profiles) {
|
if (profilesData?.profiles) {
|
||||||
profilesData.profiles.forEach((p) => map.set(p.name, p.friendly_name));
|
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;
|
return map;
|
||||||
}, [profilesData, pendingDataBySection]);
|
}, [profilesData]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -842,27 +830,6 @@ export default function Settings() {
|
|||||||
for (const key of pendingKeys) {
|
for (const key of pendingKeys) {
|
||||||
const pendingData = pendingDataBySection[key];
|
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 {
|
try {
|
||||||
const payload = prepareSectionSavePayload({
|
const payload = prepareSectionSavePayload({
|
||||||
pendingDataKey: key,
|
pendingDataKey: key,
|
||||||
@ -912,11 +879,6 @@ export default function Settings() {
|
|||||||
// Refresh config from server once
|
// Refresh config from server once
|
||||||
await mutate("config");
|
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
|
// Clear hasChanges in sidebar for all successfully saved sections
|
||||||
if (savedKeys.length > 0) {
|
if (savedKeys.length > 0) {
|
||||||
setSectionStatusByKey((prev) => {
|
setSectionStatusByKey((prev) => {
|
||||||
@ -995,12 +957,6 @@ export default function Settings() {
|
|||||||
setUnsavedChanges(false);
|
setUnsavedChanges(false);
|
||||||
setEditingProfile({});
|
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) => {
|
setSectionStatusByKey((prev) => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
for (const key of pendingKeys) {
|
for (const key of pendingKeys) {
|
||||||
@ -1015,7 +971,7 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, [pendingDataBySection, pendingKeyToMenuKey, config]);
|
}, [pendingDataBySection, pendingKeyToMenuKey]);
|
||||||
|
|
||||||
const handleDialog = useCallback(
|
const handleDialog = useCallback(
|
||||||
(save: boolean) => {
|
(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(
|
const handleDeleteProfileSection = useCallback(
|
||||||
async (camera: string, section: string, profile: string) => {
|
async (camera: string, section: string, profile: string) => {
|
||||||
try {
|
try {
|
||||||
@ -1177,22 +1096,16 @@ export default function Settings() {
|
|||||||
const profileState: ProfileState = useMemo(
|
const profileState: ProfileState = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
editingProfile,
|
editingProfile,
|
||||||
newProfiles,
|
|
||||||
allProfileNames,
|
allProfileNames,
|
||||||
profileFriendlyNames,
|
profileFriendlyNames,
|
||||||
onSelectProfile: handleSelectProfile,
|
onSelectProfile: handleSelectProfile,
|
||||||
onAddProfile: handleAddProfile,
|
|
||||||
onRemoveNewProfile: handleRemoveNewProfile,
|
|
||||||
onDeleteProfileSection: handleDeleteProfileSection,
|
onDeleteProfileSection: handleDeleteProfileSection,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
editingProfile,
|
editingProfile,
|
||||||
newProfiles,
|
|
||||||
allProfileNames,
|
allProfileNames,
|
||||||
profileFriendlyNames,
|
profileFriendlyNames,
|
||||||
handleSelectProfile,
|
handleSelectProfile,
|
||||||
handleAddProfile,
|
|
||||||
handleRemoveNewProfile,
|
|
||||||
handleDeleteProfileSection,
|
handleDeleteProfileSection,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export type ProfilesApiResponse = {
|
|||||||
|
|
||||||
export type ProfileState = {
|
export type ProfileState = {
|
||||||
editingProfile: Record<string, string | null>;
|
editingProfile: Record<string, string | null>;
|
||||||
newProfiles: string[];
|
|
||||||
allProfileNames: string[];
|
allProfileNames: string[];
|
||||||
profileFriendlyNames: Map<string, string>;
|
profileFriendlyNames: Map<string, string>;
|
||||||
onSelectProfile: (
|
onSelectProfile: (
|
||||||
@ -26,8 +25,6 @@ export type ProfileState = {
|
|||||||
section: string,
|
section: string,
|
||||||
profile: string | null,
|
profile: string | null,
|
||||||
) => void;
|
) => void;
|
||||||
onAddProfile: (id: string, friendlyName: string) => void;
|
|
||||||
onRemoveNewProfile: (name: string) => void;
|
|
||||||
onDeleteProfileSection: (
|
onDeleteProfileSection: (
|
||||||
camera: string,
|
camera: string,
|
||||||
section: string,
|
section: string,
|
||||||
|
|||||||
@ -165,16 +165,42 @@ export default function ProfilesView({
|
|||||||
return data;
|
return data;
|
||||||
}, [config, allProfileNames]);
|
}, [config, allProfileNames]);
|
||||||
|
|
||||||
|
const [addingProfile, setAddingProfile] = useState(false);
|
||||||
|
|
||||||
const handleAddSubmit = useCallback(
|
const handleAddSubmit = useCallback(
|
||||||
(data: AddProfileForm) => {
|
async (data: AddProfileForm) => {
|
||||||
const id = data.name.trim();
|
const id = data.name.trim();
|
||||||
const friendlyName = data.friendly_name.trim();
|
const friendlyName = data.friendly_name.trim();
|
||||||
if (!id || !friendlyName) return;
|
if (!id || !friendlyName) return;
|
||||||
profileState?.onAddProfile(id, friendlyName);
|
|
||||||
setAddDialogOpen(false);
|
setAddingProfile(true);
|
||||||
addForm.reset();
|
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(
|
const handleActivateProfile = useCallback(
|
||||||
@ -213,14 +239,6 @@ export default function ProfilesView({
|
|||||||
const handleDeleteProfile = useCallback(async () => {
|
const handleDeleteProfile = useCallback(async () => {
|
||||||
if (!deleteProfile || !config) return;
|
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);
|
setDeleting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -254,9 +272,6 @@ export default function ProfilesView({
|
|||||||
await updateConfig();
|
await updateConfig();
|
||||||
await updateProfiles();
|
await updateProfiles();
|
||||||
|
|
||||||
// Also clean up local newProfiles state if this profile was in it
|
|
||||||
profileState?.onRemoveNewProfile(deleteProfile);
|
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("profiles.deleteSuccess", {
|
t("profiles.deleteSuccess", {
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
@ -281,7 +296,6 @@ export default function ProfilesView({
|
|||||||
deleteProfile,
|
deleteProfile,
|
||||||
activeProfile,
|
activeProfile,
|
||||||
config,
|
config,
|
||||||
profileState,
|
|
||||||
profileFriendlyNames,
|
profileFriendlyNames,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateProfiles,
|
updateProfiles,
|
||||||
@ -609,6 +623,7 @@ export default function ProfilesView({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setAddDialogOpen(false)}
|
onClick={() => setAddDialogOpen(false)}
|
||||||
|
disabled={addingProfile}
|
||||||
>
|
>
|
||||||
{t("button.cancel", { ns: "common" })}
|
{t("button.cancel", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
@ -616,10 +631,14 @@ export default function ProfilesView({
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="select"
|
variant="select"
|
||||||
disabled={
|
disabled={
|
||||||
|
addingProfile ||
|
||||||
!addForm.watch("friendly_name").trim() ||
|
!addForm.watch("friendly_name").trim() ||
|
||||||
!addForm.watch("name").trim()
|
!addForm.watch("name").trim()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{addingProfile && (
|
||||||
|
<ActivityIndicator className="mr-2 size-4" />
|
||||||
|
)}
|
||||||
{t("button.add", { ns: "common" })}
|
{t("button.add", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user