CB-6115 Add dispose to FormState instances (#3317)

* CB-6115 adds dispose API for forms

* CB-6115 adds git workspace helper extension for repo recommendations

* CB-6115 pr fixes

* CB-6115 adds dispose methods to tables

* CB-6115 eslint fix

* CB-6115 fixes dispose leaks for freshly created states

* CB-6115 fixes infinite loading of state

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
sergeyteleshev
2025-03-18 18:27:23 +01:00
committed by GitHub
co-authored by mr-anton-t Daria Marutkina
parent 06e2aa9f0e
commit d9a5ea053b
11 changed files with 78 additions and 24 deletions
@@ -174,4 +174,5 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
protected abstract loader(): Promise<void>;
protected abstract saveChanges(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): Promise<void>;
dispose(): void | Promise<void> {}
}
+24 -5
View File
@@ -29,7 +29,7 @@ export class FormState<TState> implements IFormState<TState> {
statusMessage: string | string[] | null;
statusType: ENotificationType | null;
promise: Promise<any> | null;
savingPromise: Promise<any> | null;
get isDisabled(): boolean {
return this.partsValues.some(part => part.isSaving || part?.isLoading?.());
@@ -48,6 +48,7 @@ export class FormState<TState> implements IFormState<TState> {
readonly submitTask: IExecutor<IFormState<TState>>;
readonly formatTask: IExecutor<IFormState<TState>>;
readonly validationTask: IExecutor<IFormState<TState>>;
readonly disposeTask: IExecutor<IFormState<TState>>;
constructor(serviceProvider: IServiceProvider, service: FormBaseService<TState, any>, state: TState) {
this.id = uuid();
@@ -61,7 +62,7 @@ export class FormState<TState> implements IFormState<TState> {
this.statusMessage = null;
this.statusType = null;
this.promise = null;
this.savingPromise = null;
this.formStateTask = new Executor<TState>(state, () => true);
this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this));
@@ -78,6 +79,8 @@ export class FormState<TState> implements IFormState<TState> {
this.submitTask = new Executor(this as IFormState<TState>, () => true);
this.submitTask.addCollection(service.onSubmit).before(this.validationTask);
this.disposeTask = new Executor(this as IFormState<TState>, () => true);
this.dataContext.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext(), this.id);
this.dataContext.set(DATA_CONTEXT_FORM_STATE, this, this.id);
dataContextAddDIProvider(this.dataContext, serviceProvider, this.id);
@@ -85,7 +88,7 @@ export class FormState<TState> implements IFormState<TState> {
makeObservable<this, 'updateFormState'>(this, {
mode: observable,
parts: observable.ref,
promise: observable.ref,
savingPromise: observable.ref,
state: observable,
isSaving: computed,
exception: computed,
@@ -171,14 +174,18 @@ export class FormState<TState> implements IFormState<TState> {
async save(): Promise<boolean> {
try {
const context = await this.submitTask.execute(this);
this.savingPromise = this.submitTask.execute(this);
const context = await this.savingPromise;
if (ExecutorInterrupter.isInterrupted(context)) {
return false;
}
return true;
} catch (exception: any) {}
} catch (exception: any) {
} finally {
this.savingPromise = null;
}
return false;
}
@@ -202,4 +209,16 @@ export class FormState<TState> implements IFormState<TState> {
this.statusMessage = context.statusMessage;
this.statusType = context.statusType;
}
async dispose(): Promise<void> {
if (this.savingPromise) {
await this.savingPromise;
}
for (const part of this.parts.values()) {
await part.dispose();
}
await this.disposeTask.execute(this);
}
}
@@ -17,4 +17,5 @@ export interface IFormPart<TState> extends ILoadableState {
load(): Promise<void>;
reset(): void;
dispose(): void | Promise<void>;
}
@@ -25,7 +25,7 @@ export interface IFormState<TState> {
readonly isDisabled: boolean;
readonly exception: Error | (Error | null)[] | null;
readonly promise: Promise<any> | null;
readonly savingPromise: Promise<any> | null;
readonly statusMessage: string | string[] | null;
readonly statusType: ENotificationType | null;
@@ -49,4 +49,5 @@ export interface IFormState<TState> {
save(): Promise<boolean>;
reset(): void;
cancel(): void;
dispose(): void | Promise<void>;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -95,6 +95,7 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap {
onLoad: () => {
this.serverConfigurationFormStateManager.create();
},
onDeActivate: this.serverConfigurationFormStateManager.destroy.bind(this.serverConfigurationFormStateManager),
canDeActivate: async configurationWizard => {
const state = this.serverConfigurationFormStateManager.formState;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ export class ServerConfigurationFormStateManager {
destroy() {
if (this.formState) {
this.formState?.dispose();
this.formState = null;
}
}
@@ -1,11 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import { IServiceProvider, useService } from '@cloudbeaver/core-di';
@@ -18,11 +18,19 @@ export function useTeamsAdministrationFormState(id: string | null, configure?: (
const ref = useRef<null | TeamsAdministrationFormState>(null);
if (ref.current?.state.teamId !== id) {
ref.current?.dispose();
ref.current = new TeamsAdministrationFormState(serviceProvider, service, {
teamId: id,
});
configure?.(ref.current);
}
useEffect(
() => () => {
ref.current?.dispose();
},
[],
);
return ref.current;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@ export class CreateTeamService {
}
fillData(): void {
this.dispose();
this.data = new TeamsAdministrationFormState(this.serviceProvider, this.service, {
teamId: null,
});
@@ -47,4 +48,9 @@ export class CreateTeamService {
create(): void {
this.teamsAdministrationNavService.navToCreate();
}
dispose() {
this.data?.dispose();
this.data = null;
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -59,16 +59,12 @@ export class UsersAdministrationService extends Bootstrap {
},
{
name: EUsersAdministrationSub.Users,
onDeActivate: this.cancelCreate.bind(this),
onDeActivate: this.cancelUserCreate.bind(this),
},
{
name: EUsersAdministrationSub.Teams,
onActivate: this.loadTeams.bind(this),
onDeActivate: (param, configurationWizard, outside) => {
if (outside) {
this.teamsResource.cleanNewFlags();
}
},
onDeActivate: this.cancelTeamCreate.bind(this),
},
],
defaultSub: EUsersAdministrationSub.Users,
@@ -78,7 +74,7 @@ export class UsersAdministrationService extends Bootstrap {
this.userDetailsInfoPlaceholder.add(UserCredentialsList, 0);
}
private async cancelCreate(param: string | null, configurationWizard: boolean, outside: boolean) {
private cancelUserCreate(param: string | null, configurationWizard: boolean, outside: boolean) {
if (param === 'create') {
this.createUserService.close();
}
@@ -88,7 +84,17 @@ export class UsersAdministrationService extends Bootstrap {
}
}
private async loadTeams(param: string | null) {
private cancelTeamCreate(param: string | null, configurationWizard: boolean, outside: boolean) {
if (param === 'create') {
this.createTeamService.dispose();
}
if (outside) {
this.teamsResource.cleanNewFlags();
}
}
private loadTeams(param: string | null) {
if (param === 'create') {
this.createTeamService.fillData();
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -55,6 +55,7 @@ export class CreateUserService {
}
clearUserTemplate(): void {
this.state?.dispose();
this.state = null;
}
@@ -1,11 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import { IServiceProvider, useService } from '@cloudbeaver/core-di';
@@ -17,12 +18,20 @@ export function useAdministrationUserFormState(id: string | null, configure?: (s
const serviceProvider = useService(IServiceProvider);
const ref = useRef<null | AdministrationUserFormState>(null);
if (ref.current?.id !== id) {
if (ref.current?.state.userId !== id) {
ref.current?.dispose();
ref.current = new AdministrationUserFormState(serviceProvider, service, {
userId: id,
});
configure?.(ref.current);
}
useEffect(
() => () => {
ref.current?.dispose();
},
[],
);
return ref.current;
}