mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 06:44:53 +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;
|
||||
/** Callback to delete the current profile's overrides for this section */
|
||||
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 {
|
||||
@ -186,6 +190,8 @@ export function ConfigSection({
|
||||
profileFriendlyName,
|
||||
profileBorderColor,
|
||||
onDeleteProfileSection,
|
||||
isSavingAll = false,
|
||||
onSavingChange,
|
||||
}: ConfigSectionProps) {
|
||||
// For replay level, treat as camera-level config access
|
||||
const effectiveLevel = level === "replay" ? "camera" : level;
|
||||
@ -246,6 +252,7 @@ export function ConfigSection({
|
||||
[onPendingDataChange, effectiveSectionPath, cameraName],
|
||||
);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isResettingToDefault, setIsResettingToDefault] = useState(false);
|
||||
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
||||
const [extraHasChanges, setExtraHasChanges] = useState(false);
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
@ -577,6 +584,7 @@ export function ConfigSection({
|
||||
if (!pendingData) return;
|
||||
|
||||
setIsSaving(true);
|
||||
onSavingChange?.(true);
|
||||
try {
|
||||
const basePath =
|
||||
effectiveLevel === "camera" && cameraName
|
||||
@ -699,6 +707,7 @@ export function ConfigSection({
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
onSavingChange?.(false);
|
||||
}
|
||||
}, [
|
||||
sectionPath,
|
||||
@ -718,12 +727,14 @@ export function ConfigSection({
|
||||
setPendingData,
|
||||
requiresRestartForOverrides,
|
||||
skipSave,
|
||||
onSavingChange,
|
||||
]);
|
||||
|
||||
// Handle reset to global/defaults - removes camera-level override or resets global to defaults
|
||||
const handleResetToGlobal = useCallback(async () => {
|
||||
if (effectiveLevel === "camera" && !cameraName) return;
|
||||
|
||||
setIsResettingToDefault(true);
|
||||
try {
|
||||
const basePath =
|
||||
effectiveLevel === "camera" && cameraName
|
||||
@ -758,6 +769,8 @@ export function ConfigSection({
|
||||
defaultValue: "Failed to reset settings",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsResettingToDefault(false);
|
||||
}
|
||||
}, [
|
||||
effectiveSectionPath,
|
||||
@ -945,9 +958,12 @@ export function ConfigSection({
|
||||
<Button
|
||||
onClick={() => setIsResetDialogOpen(true)}
|
||||
variant="outline"
|
||||
disabled={isSaving || disabled}
|
||||
disabled={isSaving || isResettingToDefault || disabled}
|
||||
className="flex flex-1 gap-2"
|
||||
>
|
||||
{isResettingToDefault && (
|
||||
<ActivityIndicator className="h-4 w-4" />
|
||||
)}
|
||||
{effectiveLevel === "global"
|
||||
? t("button.resetToDefault", {
|
||||
ns: "common",
|
||||
@ -990,7 +1006,11 @@ export function ConfigSection({
|
||||
onClick={handleSave}
|
||||
variant="select"
|
||||
disabled={
|
||||
!hasChanges || hasValidationErrors || isSaving || disabled
|
||||
!hasChanges ||
|
||||
hasValidationErrors ||
|
||||
isSaving ||
|
||||
isSavingAll ||
|
||||
disabled
|
||||
}
|
||||
className="flex min-w-36 flex-1 gap-2"
|
||||
>
|
||||
|
||||
@ -724,6 +724,7 @@ export default function Settings() {
|
||||
|
||||
// Save All state
|
||||
const [isSavingAll, setIsSavingAll] = useState(false);
|
||||
const [isAnySectionSaving, setIsAnySectionSaving] = useState(false);
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||
const { send: sendRestart } = useRestart();
|
||||
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
|
||||
const activeEditingProfile = selectedCamera
|
||||
? (editingProfile[selectedCamera] ?? null)
|
||||
@ -1520,7 +1525,7 @@ export default function Settings() {
|
||||
onClick={handleSaveAll}
|
||||
variant="select"
|
||||
size="sm"
|
||||
disabled={isSavingAll || hasPendingValidationErrors}
|
||||
disabled={isSavingAll || isAnySectionSaving || hasPendingValidationErrors}
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
>
|
||||
{isSavingAll ? (
|
||||
@ -1606,6 +1611,8 @@ export default function Settings() {
|
||||
}
|
||||
profilesUIEnabled={profilesUIEnabled}
|
||||
setProfilesUIEnabled={setProfilesUIEnabled}
|
||||
isSavingAll={isSavingAll}
|
||||
onSectionSavingChange={handleSectionSavingChange}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
@ -1686,7 +1693,7 @@ export default function Settings() {
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={handleSaveAll}
|
||||
disabled={isSavingAll || hasPendingValidationErrors}
|
||||
disabled={isSavingAll || isAnySectionSaving || hasPendingValidationErrors}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
{isSavingAll ? (
|
||||
|
||||
@ -39,6 +39,10 @@ export type SettingsPageProps = {
|
||||
onDeleteProfileSection?: (profileName: string) => void;
|
||||
profilesUIEnabled?: 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 = {
|
||||
@ -73,6 +77,8 @@ export function SingleSectionPage({
|
||||
onPendingDataChange,
|
||||
profileState,
|
||||
onDeleteProfileSection,
|
||||
isSavingAll,
|
||||
onSectionSavingChange,
|
||||
}: SingleSectionPageProps) {
|
||||
const sectionNamespace =
|
||||
level === "camera" ? "config/cameras" : "config/global";
|
||||
@ -95,9 +101,10 @@ export function SingleSectionPage({
|
||||
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
|
||||
: undefined;
|
||||
|
||||
const currentEditingProfile = selectedCamera
|
||||
? (profileState?.editingProfile[selectedCamera] ?? null)
|
||||
: null;
|
||||
const currentEditingProfile =
|
||||
level === "camera" && selectedCamera
|
||||
? (profileState?.editingProfile[selectedCamera] ?? null)
|
||||
: null;
|
||||
|
||||
const profileColor = useMemo(
|
||||
() =>
|
||||
@ -273,6 +280,8 @@ export function SingleSectionPage({
|
||||
onDeleteProfileSection={
|
||||
currentEditingProfile ? handleDeleteProfileSection : undefined
|
||||
}
|
||||
isSavingAll={isSavingAll}
|
||||
onSavingChange={onSectionSavingChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user