From d9a5ea053b2c2c8d451e41ecbe5e156998fe85d4 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Tue, 18 Mar 2025 18:27:23 +0100 Subject: [PATCH] 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> --- webapp/packages/core-ui/src/Form/FormPart.ts | 1 + webapp/packages/core-ui/src/Form/FormState.ts | 29 +++++++++++++++---- webapp/packages/core-ui/src/Form/IFormPart.ts | 1 + .../packages/core-ui/src/Form/IFormState.ts | 3 +- ...onfigurationWizardPagesBootstrapService.ts | 3 +- .../ServerConfigurationFormStateManager.ts | 3 +- .../useTeamsAdministrationFormState.ts | 12 ++++++-- .../Teams/TeamsTable/CreateTeamService.ts | 8 ++++- .../Users/UsersAdministrationService.ts | 24 +++++++++------ .../Users/UsersTable/CreateUserService.ts | 3 +- .../useAdministrationUserFormState.ts | 15 ++++++++-- 11 files changed, 78 insertions(+), 24 deletions(-) diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 9b8eb4ba49..ce74bc76ab 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -174,4 +174,5 @@ export abstract class FormPart implements IFormPar protected abstract loader(): Promise; protected abstract saveChanges(data: IFormState, contexts: IExecutionContextProvider>): Promise; + dispose(): void | Promise {} } diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 25bcc384d3..2c8e17a196 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -29,7 +29,7 @@ export class FormState implements IFormState { statusMessage: string | string[] | null; statusType: ENotificationType | null; - promise: Promise | null; + savingPromise: Promise | null; get isDisabled(): boolean { return this.partsValues.some(part => part.isSaving || part?.isLoading?.()); @@ -48,6 +48,7 @@ export class FormState implements IFormState { readonly submitTask: IExecutor>; readonly formatTask: IExecutor>; readonly validationTask: IExecutor>; + readonly disposeTask: IExecutor>; constructor(serviceProvider: IServiceProvider, service: FormBaseService, state: TState) { this.id = uuid(); @@ -61,7 +62,7 @@ export class FormState implements IFormState { this.statusMessage = null; this.statusType = null; - this.promise = null; + this.savingPromise = null; this.formStateTask = new Executor(state, () => true); this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this)); @@ -78,6 +79,8 @@ export class FormState implements IFormState { this.submitTask = new Executor(this as IFormState, () => true); this.submitTask.addCollection(service.onSubmit).before(this.validationTask); + this.disposeTask = new Executor(this as IFormState, () => 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 implements IFormState { makeObservable(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 implements IFormState { async save(): Promise { 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 implements IFormState { this.statusMessage = context.statusMessage; this.statusType = context.statusType; } + + async dispose(): Promise { + if (this.savingPromise) { + await this.savingPromise; + } + + for (const part of this.parts.values()) { + await part.dispose(); + } + + await this.disposeTask.execute(this); + } } diff --git a/webapp/packages/core-ui/src/Form/IFormPart.ts b/webapp/packages/core-ui/src/Form/IFormPart.ts index 446c33fd97..01df3222bd 100644 --- a/webapp/packages/core-ui/src/Form/IFormPart.ts +++ b/webapp/packages/core-ui/src/Form/IFormPart.ts @@ -17,4 +17,5 @@ export interface IFormPart extends ILoadableState { load(): Promise; reset(): void; + dispose(): void | Promise; } diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 4b30e0589b..adb62afa84 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -25,7 +25,7 @@ export interface IFormState { readonly isDisabled: boolean; readonly exception: Error | (Error | null)[] | null; - readonly promise: Promise | null; + readonly savingPromise: Promise | null; readonly statusMessage: string | string[] | null; readonly statusType: ENotificationType | null; @@ -49,4 +49,5 @@ export interface IFormState { save(): Promise; reset(): void; cancel(): void; + dispose(): void | Promise; } diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts index 4f81c024f7..cb026d60f6 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts @@ -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; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts index e59a107637..bbc4d9828f 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts @@ -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; } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts index d776bc9d8d..f1567ef57d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts @@ -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); 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; } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts index 7bbaebcefe..de725728e2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts @@ -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; + } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts index e7fbd694f1..2904b95afa 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts @@ -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(); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts index 62aae4f736..b630b56915 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts @@ -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; } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts index 9b12fdc112..cfba509bf1 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts @@ -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); - 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; }