From 1be6ddaf1c8c6570bccfd7b60485301ae1d95828 Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 4 Jul 2024 11:08:39 +0300 Subject: [PATCH] CB-5342 fix: database data source tasks result (#2756) * CB-5342 fix: database data source tasks result * CB-5342 fix: database data source tasks tracking * CB-5342 fix: add stop auto refresh button --- ...DataViewerDataChangeConfirmationService.ts | 2 +- .../src/DataViewerTabService.ts | 9 +---- .../ResultSet/ResultSetDataContentAction.ts | 14 ++++++-- .../DatabaseDataModel/DatabaseDataSource.ts | 35 ++++++++++++------- .../DatabaseDataModel/IDatabaseDataSource.ts | 26 ++++++++------ .../MENU_DATA_VIEWER_AUTO_REFRESH.ts | 8 ++++- .../TableRefreshActionBootstrap.ts | 27 +++++++++++--- .../plugin-data-viewer/src/locales/en.ts | 3 +- .../plugin-data-viewer/src/locales/fr.ts | 1 + .../plugin-data-viewer/src/locales/it.ts | 3 +- .../plugin-data-viewer/src/locales/ru.ts | 1 + .../plugin-data-viewer/src/locales/zh.ts | 3 +- .../SqlResultTabs/SqlQueryResultService.ts | 9 +---- .../src/ScriptPreview/ScriptPreviewService.ts | 6 +++- 14 files changed, 96 insertions(+), 51 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts index 3b4f3d076a..0ec64adf13 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts @@ -31,7 +31,7 @@ export class DataViewerDataChangeConfirmationService { const model = this.dataViewerTableService.get(modelId); if (model && !model.onRequest.hasHandler(this.checkUnsavedData)) { - model.onRequest.addHandler(executorHandlerFilter(({ operation }) => operation !== DatabaseDataSourceOperation.Save, this.checkUnsavedData)); + model.onRequest.addHandler(executorHandlerFilter(({ operation }) => operation === DatabaseDataSourceOperation.Request, this.checkUnsavedData)); } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 18a1a6e513..ed2b276f48 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -111,14 +111,7 @@ export class DataViewerTabService { const model = this.tableViewerStorageService.get(tab.handlerState.tableId || ''); if (model) { - let canClose = false; - try { - await model.source.runTask(async () => { - canClose = true; - }); - } catch {} - - return canClose; + return await model.source.canSafelyDispose(); } return true; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index 4d425571d8..78ed677a46 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -125,7 +125,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + const fullText = await this.source.runOperation(async () => { try { this.updateCache(element, { loading: true }); return await this.loadFileFullText(this.result, column.position, row); @@ -134,6 +134,10 @@ export class ResultSetDataContentAction extends DatabaseDataAction { const column = this.data.getColumn(element.column); const row = this.data.getRowValue(element.row); @@ -175,7 +179,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + const url = await this.source.runOperation(async () => { try { this.updateCache(element, { loading: true }); return await this.loadDataURL(this.result, column.position, row); @@ -184,6 +188,10 @@ export class ResultSetDataContentAction extends DatabaseDataAction | null { return this.activeOperationStack[this.activeOperationStack.length - 1] ?? null; } - private readonly activeOperationStack: Array>; + private readonly activeOperationStack: Array>; constructor(serviceInjector: IServiceInjector) { this.serviceInjector = serviceInjector; @@ -286,12 +286,12 @@ export abstract class DatabaseDataSource Promise): Promise { + async runOperation(task: () => Promise): Promise { return this.tryExecuteOperation(DatabaseDataSourceOperation.Task, task); } async requestData(mutation?: () => void): Promise { - await this.tryExecuteOperation(DatabaseDataSourceOperation.Load, () => { + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { this.lastAction = this.requestData.bind(this); mutation?.(); @@ -300,7 +300,7 @@ export abstract class DatabaseDataSource { - await this.tryExecuteOperation(DatabaseDataSourceOperation.Load, () => { + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { if (!this.isDataAvailable(offset, count)) { this.lastAction = this.requestDataPortion.bind(this, offset, count); @@ -312,7 +312,7 @@ export abstract class DatabaseDataSource { - await this.tryExecuteOperation(DatabaseDataSourceOperation.Refresh, () => { + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { this.lastAction = this.refreshData.bind(this); if (this.prevOptions) { @@ -333,6 +333,15 @@ export abstract class DatabaseDataSource { + try { + const result = await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, async () => true); + return result || false; + } catch { + return false; + } + } + clearError(): this { this.error = null; return this; @@ -358,7 +367,7 @@ export abstract class DatabaseDataSource>; abstract cancelLoadTotalCount(): Promise | null>; - private async requestDataAction(): Promise { + private async requestDataAction(): Promise { this.prevOptions = toJS(this.options); return this.request(this.results).then(data => { this.outdated = false; @@ -370,8 +379,8 @@ export abstract class DatabaseDataSource Promise): Promise { - if (this.activeOperation && type !== DatabaseDataSourceOperation.Task && type !== DatabaseDataSourceOperation.Save) { + private async tryExecuteOperation(type: DatabaseDataSourceOperation, operation: () => Promise): Promise { + if (this.activeOperation && type !== DatabaseDataSourceOperation.Request) { await this.activeOperation; } @@ -387,23 +396,25 @@ export abstract class DatabaseDataSource Promise | void): ITask { + private executeOperation(type: DatabaseDataSourceOperation, operation: () => Promise | T): ITask { return new Task(async () => await this.onOperation.execute({ stage: 'request', operation: type })).run().then(contexts => { + // TODO: maybe it's better to throw an exception instead, so we will not have unexpected undefined results if (ExecutorInterrupter.isInterrupted(contexts)) { - return; + return null; } return new Task(async () => await this.onOperation.execute({ stage: 'before', operation: type })) .run() .then(contexts => { if (ExecutorInterrupter.isInterrupted(contexts)) { - return; + return null; } return operation(); }) - .then(async () => { + .then(async result => { await this.onOperation.execute({ stage: 'after', operation: type }); + return result; }); }); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 810d06f433..29062697c7 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -15,10 +15,12 @@ import type { IDatabaseDataActions } from './IDatabaseDataActions'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; export enum DatabaseDataSourceOperation { - Load = 'load', - Save = 'save', - Refresh = 'refresh', + /** Abstract operation with data, should not lead to data lost */ Task = 'task', + /** Saving operation */ + Save = 'save', + /** May lead to data lost */ + Request = 'request', } export interface IDatabaseDataSourceOperationEvent { stage: 'request' | 'before' | 'after'; @@ -102,15 +104,19 @@ export interface IDatabaseDataSource Promise | null>; retry: () => Promise; - /** Allows to perform an asynchronous action on the data source, this action will wait previous action to finish and save or load requests. - * The data source will have a loading and disabled state while performing an action */ - runTask: (task: () => Promise) => Promise; + /** + * Perform operation with data source. This action should not lead to data lost. Can be cancelled when operation is Task. + * @param operation Task or Promise + * @returns + */ + runOperation: (operation: () => Promise) => Promise; requestDataPortion(offset: number, count: number): Promise; - requestData: (mutation?: () => void) => Promise | void; - refreshData: () => Promise | void; - saveData: () => Promise | void; - cancel: () => Promise | void; + requestData: (mutation?: () => void) => Promise; + refreshData: () => Promise; + saveData: () => Promise; + cancel: () => Promise; clearError: () => this; resetData: () => this; + canSafelyDispose: () => Promise; dispose: (keepExecutionContext?: boolean) => Promise; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts index 1fa0058ac8..3d19fbce78 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts @@ -7,4 +7,10 @@ */ import { ACTION_REFRESH, createMenu } from '@cloudbeaver/core-view'; -export const MENU_DATA_VIEWER_AUTO_REFRESH = createMenu('auto-refresh', 'Auto-Refresh', undefined, 'data_viewer_action_auto_refresh', ACTION_REFRESH); +export const MENU_DATA_VIEWER_AUTO_REFRESH = createMenu( + 'auto-refresh', + 'data_viewer_action_auto_refresh', + undefined, + 'data_viewer_action_auto_refresh', + ACTION_REFRESH, +); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts index 73800a5faa..e20d029632 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts @@ -13,7 +13,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { declensionOfNumber } from '@cloudbeaver/core-utils'; -import { ACTION_REFRESH, ActionService, MenuBaseItem, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_REFRESH, ActionService, MenuBaseItem, menuExtractItems, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; import { IDatabaseRefreshState } from '../../../../DatabaseDataModel/Actions/DatabaseRefreshAction'; import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; @@ -72,12 +72,13 @@ export class TableRefreshActionBootstrap extends Bootstrap { const state = getRefreshState(context); items = [...items]; for (const interval of AUTO_REFRESH_INTERVALS) { + const label = this.getLabel(interval); items.push( new MenuBaseItem( { id: `auto-refresh-${interval}`, - label: this.getLabel(interval), - tooltip: 'data_viewer_action_auto_refresh', + label, + tooltip: label, disabled: state?.interval === interval * 1000, }, { @@ -89,12 +90,14 @@ export class TableRefreshActionBootstrap extends Bootstrap { ); } + items.push(new MenuSeparatorItem()); + items.push( new MenuBaseItem( { id: 'auto-refresh-custom', label: 'ui_custom', - tooltip: 'data_viewer_action_auto_refresh', + tooltip: 'plugin_data_viewer_action_auto_refresh_custom', }, { onSelect: this.configureAutoRefresh.bind(this, context), @@ -102,6 +105,22 @@ export class TableRefreshActionBootstrap extends Bootstrap { ), ); + items.push( + new MenuBaseItem( + { + id: 'auto-refresh-stop', + label: 'ui_processing_stop', + tooltip: 'data_viewer_action_auto_refresh_stop', + disabled: !state?.isAutoRefresh, + }, + { + onSelect: () => { + state?.setInterval(0); + }, + }, + ), + ); + return items; }, }); diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 9b0c003aa5..9603be1841 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -21,6 +21,7 @@ export default [ ['data_viewer_action_refresh', 'Refresh'], ['data_viewer_action_auto_refresh', 'Auto-Refresh'], ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'], ['data_viewer_action_edit_delete', 'Delete selected'], ['data_viewer_action_edit_add', 'Add'], ['data_viewer_action_edit_add_copy', 'Duplicate'], @@ -28,7 +29,7 @@ export default [ ['data_viewer_result_edited_title', 'Save changes'], ['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'], ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], + ['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Value'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/fr.ts b/webapp/packages/plugin-data-viewer/src/locales/fr.ts index 6a6e6adce8..8f6878a2cf 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/fr.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/fr.ts @@ -21,6 +21,7 @@ export default [ ['data_viewer_action_refresh', 'Actualiser'], ['data_viewer_action_auto_refresh', 'Actualisation automatique'], ['data_viewer_action_auto_refresh_stop', "Arrêter l'actualisation automatique"], + ['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'], ['data_viewer_action_edit_delete', 'Supprimer la sélection'], ['data_viewer_action_edit_add', 'Ajouter'], ['data_viewer_action_edit_add_copy', 'Dupliquer'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index 30a2185b31..dfd5207dd6 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -21,10 +21,11 @@ export default [ ['data_viewer_action_refresh', 'Refresh'], ['data_viewer_action_auto_refresh', 'Auto-Refresh'], ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'], ['data_viewer_result_edited_title', 'Save changes'], ['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'], ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], + ['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Valore'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index 1738fcbf4d..caebc7f5f5 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -21,6 +21,7 @@ export default [ ['data_viewer_action_refresh', 'Обновить'], ['data_viewer_action_auto_refresh', 'Автообновление'], ['data_viewer_action_auto_refresh_stop', 'Остановить автообновление'], + ['plugin_data_viewer_action_auto_refresh_custom', 'Настроить автообновление'], ['data_viewer_action_edit_delete', 'Удалить'], ['data_viewer_action_edit_add', 'Добавить'], ['data_viewer_action_edit_add_copy', 'Дублировать'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index 460c73036a..201a3fa95f 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -21,6 +21,7 @@ export default [ ['data_viewer_action_refresh', 'Refresh'], ['data_viewer_action_auto_refresh', 'Auto-Refresh'], ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['plugin_data_viewer_action_auto_refresh_custom', 'Configure auto-refresh'], ['data_viewer_action_edit_delete', '删除选中'], ['data_viewer_action_edit_add', '添加'], ['data_viewer_action_edit_add_copy', 'Duplicate'], @@ -28,7 +29,7 @@ export default [ ['data_viewer_result_edited_title', '保存更改'], ['data_viewer_result_edited_message', '结果集已编辑。是否将更改保存到数据库?'], ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], + ['data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', '值'], diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts index 5cebcdc987..7ae8e5468f 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryResultService.ts @@ -136,14 +136,7 @@ export class SqlQueryResultService { const model = this.tableViewerStorageService.get(group.modelId); if (model) { - let canClose = false; - - try { - await model.source.runTask(async () => { - canClose = true; - }); - } catch {} - return canClose; + return await model.source.canSafelyDispose(); } } return true; diff --git a/webapp/packages/plugin-sql-generator/src/ScriptPreview/ScriptPreviewService.ts b/webapp/packages/plugin-sql-generator/src/ScriptPreview/ScriptPreviewService.ts index 3b71aea5e6..4d2e69e2fd 100644 --- a/webapp/packages/plugin-sql-generator/src/ScriptPreview/ScriptPreviewService.ts +++ b/webapp/packages/plugin-sql-generator/src/ScriptPreview/ScriptPreviewService.ts @@ -24,7 +24,11 @@ export class ScriptPreviewService { async open(model: IDatabaseDataModel, resultIndex: number): Promise { try { - const script = await model.source.runTask(() => this.tryGetScript(model, resultIndex)); + const script = await model.source.runOperation(() => this.tryGetScript(model, resultIndex)); + + if (script === null) { + throw new Error('Script is not provided'); + } this.commonDialogService.open(ScriptPreviewDialog, { script,