mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 14:47:40 +03:00
fix save button race conditions, add reset spinner, and fix enrichments profile leak
- Disable both Save and SaveAll buttons while either operation is in progress so users cannot trigger concurrent saves - Show activity indicator on Reset to Default/Global button during the API call - Enrichments panes (semantic search, genai, face recognition) now always show base config fields regardless of profile selection in the header dropdown
This commit is contained in:
parent
996967dece
commit
5f61079f81
@ -152,6 +152,10 @@ export interface BaseSectionProps {
|
|||||||
profileBorderColor?: string;
|
profileBorderColor?: string;
|
||||||
/** Callback to delete the current profile's overrides for this section */
|
/** Callback to delete the current profile's overrides for this section */
|
||||||
onDeleteProfileSection?: () => void;
|
onDeleteProfileSection?: () => void;
|
||||||
|
/** Whether a SaveAll operation is in progress (disables individual Save) */
|
||||||
|
isSavingAll?: boolean;
|
||||||
|
/** Callback when this section's saving state changes */
|
||||||
|
onSavingChange?: (isSaving: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSectionOptions {
|
export interface CreateSectionOptions {
|
||||||
@ -186,6 +190,8 @@ export function ConfigSection({
|
|||||||
profileFriendlyName,
|
profileFriendlyName,
|
||||||
profileBorderColor,
|
profileBorderColor,
|
||||||
onDeleteProfileSection,
|
onDeleteProfileSection,
|
||||||
|
isSavingAll = false,
|
||||||
|
onSavingChange,
|
||||||
}: 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;
|
||||||
@ -246,6 +252,7 @@ export function ConfigSection({
|
|||||||
[onPendingDataChange, effectiveSectionPath, cameraName],
|
[onPendingDataChange, effectiveSectionPath, cameraName],
|
||||||
);
|
);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isResettingToDefault, setIsResettingToDefault] = useState(false);
|
||||||
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
||||||
const [extraHasChanges, setExtraHasChanges] = useState(false);
|
const [extraHasChanges, setExtraHasChanges] = useState(false);
|
||||||
const [formKey, setFormKey] = useState(0);
|
const [formKey, setFormKey] = useState(0);
|
||||||
@ -577,6 +584,7 @@ export function ConfigSection({
|
|||||||
if (!pendingData) return;
|
if (!pendingData) return;
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
onSavingChange?.(true);
|
||||||
try {
|
try {
|
||||||
const basePath =
|
const basePath =
|
||||||
effectiveLevel === "camera" && cameraName
|
effectiveLevel === "camera" && cameraName
|
||||||
@ -699,6 +707,7 @@ export function ConfigSection({
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
onSavingChange?.(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
sectionPath,
|
sectionPath,
|
||||||
@ -718,12 +727,14 @@ export function ConfigSection({
|
|||||||
setPendingData,
|
setPendingData,
|
||||||
requiresRestartForOverrides,
|
requiresRestartForOverrides,
|
||||||
skipSave,
|
skipSave,
|
||||||
|
onSavingChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle reset to global/defaults - removes camera-level override or resets global to defaults
|
// Handle reset to global/defaults - removes camera-level override or resets global to defaults
|
||||||
const handleResetToGlobal = useCallback(async () => {
|
const handleResetToGlobal = useCallback(async () => {
|
||||||
if (effectiveLevel === "camera" && !cameraName) return;
|
if (effectiveLevel === "camera" && !cameraName) return;
|
||||||
|
|
||||||
|
setIsResettingToDefault(true);
|
||||||
try {
|
try {
|
||||||
const basePath =
|
const basePath =
|
||||||
effectiveLevel === "camera" && cameraName
|
effectiveLevel === "camera" && cameraName
|
||||||
@ -758,6 +769,8 @@ export function ConfigSection({
|
|||||||
defaultValue: "Failed to reset settings",
|
defaultValue: "Failed to reset settings",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
setIsResettingToDefault(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
effectiveSectionPath,
|
effectiveSectionPath,
|
||||||
@ -945,9 +958,12 @@ export function ConfigSection({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setIsResetDialogOpen(true)}
|
onClick={() => setIsResetDialogOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isSaving || disabled}
|
disabled={isSaving || isResettingToDefault || disabled}
|
||||||
className="flex flex-1 gap-2"
|
className="flex flex-1 gap-2"
|
||||||
>
|
>
|
||||||
|
{isResettingToDefault && (
|
||||||
|
<ActivityIndicator className="h-4 w-4" />
|
||||||
|
)}
|
||||||
{effectiveLevel === "global"
|
{effectiveLevel === "global"
|
||||||
? t("button.resetToDefault", {
|
? t("button.resetToDefault", {
|
||||||
ns: "common",
|
ns: "common",
|
||||||
@ -990,7 +1006,11 @@ export function ConfigSection({
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
variant="select"
|
variant="select"
|
||||||
disabled={
|
disabled={
|
||||||
!hasChanges || hasValidationErrors || isSaving || disabled
|
!hasChanges ||
|
||||||
|
hasValidationErrors ||
|
||||||
|
isSaving ||
|
||||||
|
isSavingAll ||
|
||||||
|
disabled
|
||||||
}
|
}
|
||||||
className="flex min-w-36 flex-1 gap-2"
|
className="flex min-w-36 flex-1 gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -724,6 +724,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
// Save All state
|
// Save All state
|
||||||
const [isSavingAll, setIsSavingAll] = useState(false);
|
const [isSavingAll, setIsSavingAll] = useState(false);
|
||||||
|
const [isAnySectionSaving, setIsAnySectionSaving] = useState(false);
|
||||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||||
const { send: sendRestart } = useRestart();
|
const { send: sendRestart } = useRestart();
|
||||||
const { data: fullSchema } = useSWR<RJSFSchema>("config/schema.json");
|
const { data: fullSchema } = useSWR<RJSFSchema>("config/schema.json");
|
||||||
@ -1299,6 +1300,10 @@ export default function Settings() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSectionSavingChange = useCallback((saving: boolean) => {
|
||||||
|
setIsAnySectionSaving(saving);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// The active profile being edited for the selected camera
|
// The active profile being edited for the selected camera
|
||||||
const activeEditingProfile = selectedCamera
|
const activeEditingProfile = selectedCamera
|
||||||
? (editingProfile[selectedCamera] ?? null)
|
? (editingProfile[selectedCamera] ?? null)
|
||||||
@ -1520,7 +1525,7 @@ export default function Settings() {
|
|||||||
onClick={handleSaveAll}
|
onClick={handleSaveAll}
|
||||||
variant="select"
|
variant="select"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={isSavingAll || hasPendingValidationErrors}
|
disabled={isSavingAll || isAnySectionSaving || hasPendingValidationErrors}
|
||||||
className="flex w-full items-center justify-center gap-2"
|
className="flex w-full items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{isSavingAll ? (
|
{isSavingAll ? (
|
||||||
@ -1606,6 +1611,8 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
profilesUIEnabled={profilesUIEnabled}
|
profilesUIEnabled={profilesUIEnabled}
|
||||||
setProfilesUIEnabled={setProfilesUIEnabled}
|
setProfilesUIEnabled={setProfilesUIEnabled}
|
||||||
|
isSavingAll={isSavingAll}
|
||||||
|
onSectionSavingChange={handleSectionSavingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -1686,7 +1693,7 @@ export default function Settings() {
|
|||||||
variant="select"
|
variant="select"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSaveAll}
|
onClick={handleSaveAll}
|
||||||
disabled={isSavingAll || hasPendingValidationErrors}
|
disabled={isSavingAll || isAnySectionSaving || hasPendingValidationErrors}
|
||||||
className="flex items-center justify-center gap-2"
|
className="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{isSavingAll ? (
|
{isSavingAll ? (
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export type SettingsPageProps = {
|
|||||||
onDeleteProfileSection?: (profileName: string) => void;
|
onDeleteProfileSection?: (profileName: string) => void;
|
||||||
profilesUIEnabled?: boolean;
|
profilesUIEnabled?: boolean;
|
||||||
setProfilesUIEnabled?: React.Dispatch<React.SetStateAction<boolean>>;
|
setProfilesUIEnabled?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
/** Whether a SaveAll operation is in progress */
|
||||||
|
isSavingAll?: boolean;
|
||||||
|
/** Callback when a section's saving state changes */
|
||||||
|
onSectionSavingChange?: (isSaving: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SectionStatus = {
|
export type SectionStatus = {
|
||||||
@ -73,6 +77,8 @@ export function SingleSectionPage({
|
|||||||
onPendingDataChange,
|
onPendingDataChange,
|
||||||
profileState,
|
profileState,
|
||||||
onDeleteProfileSection,
|
onDeleteProfileSection,
|
||||||
|
isSavingAll,
|
||||||
|
onSectionSavingChange,
|
||||||
}: SingleSectionPageProps) {
|
}: SingleSectionPageProps) {
|
||||||
const sectionNamespace =
|
const sectionNamespace =
|
||||||
level === "camera" ? "config/cameras" : "config/global";
|
level === "camera" ? "config/cameras" : "config/global";
|
||||||
@ -95,7 +101,8 @@ export function SingleSectionPage({
|
|||||||
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
|
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const currentEditingProfile = selectedCamera
|
const currentEditingProfile =
|
||||||
|
level === "camera" && selectedCamera
|
||||||
? (profileState?.editingProfile[selectedCamera] ?? null)
|
? (profileState?.editingProfile[selectedCamera] ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@ -273,6 +280,8 @@ export function SingleSectionPage({
|
|||||||
onDeleteProfileSection={
|
onDeleteProfileSection={
|
||||||
currentEditingProfile ? handleDeleteProfileSection : undefined
|
currentEditingProfile ? handleDeleteProfileSection : undefined
|
||||||
}
|
}
|
||||||
|
isSavingAll={isSavingAll}
|
||||||
|
onSavingChange={onSectionSavingChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user