From 3ef074b3617c51631dcf2d3bb18bd81ff78a6ce0 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 14 Jan 2021 01:01:08 +0300 Subject: [PATCH] refactor: CB-491 Database Data API --- .../src/DataExportMenuService.ts | 28 ++-- .../src/ContainerDataSource.ts | 27 +++- .../src/DataViewerTableService.ts | 143 +++++------------- .../DatabaseDataModel/DatabaseDataSource.ts | 10 +- .../DatabaseDataModel/IDatabaseDataSource.ts | 3 + .../TableViewer/TableFooter/TableFooter.tsx | 4 +- .../TableFooterMenu/TableFooterMenu.tsx | 9 +- .../TableFooterMenu/TableFooterMenuService.ts | 23 +-- .../src/TableViewer/TableViewer.tsx | 14 +- .../src/TableViewer/TableViewerModel.ts | 7 - .../SqlResultPanelController.ts | 36 +++-- 11 files changed, 143 insertions(+), 161 deletions(-) diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 112dc18bf5..0d54d5fa03 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -11,9 +11,9 @@ import { } from '@cloudbeaver/core-app'; import { injectable } from '@cloudbeaver/core-di'; import { - IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService + IMenuContext, CommonDialogService, ContextMenuService } from '@cloudbeaver/core-dialogs'; -import { TableFooterMenuService, DataModelWrapper } from '@cloudbeaver/plugin-data-viewer'; +import { TableFooterMenuService, ITableFooterMenuContext } from '@cloudbeaver/plugin-data-viewer'; import { DataExportDialog } from './Dialog/DataExportDialog'; @@ -25,8 +25,8 @@ export class DataExportMenuService { private contextMenuService: ContextMenuService ) { } - register() { - const exportData: IContextMenuItem = { + register(): void { + this.tableFooterMenuService.registerMenuItem({ id: 'export ', isPresent(context) { return context.contextType === TableFooterMenuService.nodeContextType; @@ -35,8 +35,7 @@ export class DataExportMenuService { title: 'data_transfer_dialog_export', icon: 'table-export', onClick: this.exportData.bind(this), - }; - this.tableFooterMenuService.registerMenuItem(exportData); + }); this.contextMenuService.addMenuItem( this.contextMenuService.getRootMenuToken(), @@ -60,13 +59,18 @@ export class DataExportMenuService { ); } - private exportData(context: IMenuContext) { + private exportData(context: IMenuContext) { + const result = context.data.model.getResult(context.data.resultIndex); + if (!result) { + throw new Error('Result must be provided'); + } + this.commonDialogService.open(DataExportDialog, { - connectionId: context.data.deprecatedModel.connectionId, - contextId: context.data.deprecatedModel.executionContext?.contextId, - containerNodePath: context.data.deprecatedModel.containerNodePath, - resultId: context.data.deprecatedModel.resultId, - sourceName: context.data.deprecatedModel.sourceName, + connectionId: context.data.model.deprecatedModel.connectionId, + contextId: context.data.model.source.executionContext?.contextId, + containerNodePath: context.data.model.deprecatedModel.containerNodePath, + resultId: result.id, + sourceName: context.data.model.deprecatedModel.sourceName, }); } } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 875acbd712..e31d9d0360 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -17,6 +17,8 @@ import { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult'; import { DataUpdate } from './DatabaseDataModel/IDatabaseDataSource'; import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess'; import { IExecutionContext } from './IExecutionContext'; +import { RowDiff } from './TableViewer/TableDataModel/EditedRow'; +import { IRequestDataResult } from './TableViewer/TableViewerModel'; export interface IDataContainerOptions { containerNodePath: string; @@ -31,7 +33,6 @@ export interface IDataContainerResult extends IDatabaseDataResult { export class ContainerDataSource extends DatabaseDataSource { @observable currentFetchTableProcess: FetchTableDataAsyncProcess | null; - private executionContext: IExecutionContext | null; get canCancel(): boolean { return this.currentFetchTableProcess ? this.currentFetchTableProcess.getState() === EDeferredState.PENDING : false; @@ -122,6 +123,30 @@ export class ContainerDataSource extends DatabaseDataSource { + const executionContext = await this.ensureContextCreated(); + + const response = await this.graphQLService.sdk.updateResultsDataBatch({ + connectionId: executionContext.connectionId, + contextId: executionContext.contextId, + resultsId: resultId, + updatedRows: rows.map(row => ({ data: row.source, updateValues: row.values })), + }); + + const dataSet = response.result!.results[0].resultSet!; // we expect only one dataset for a table + + return { + rows: dataSet.rows!, + columns: [], // not in use while saving data + duration: response.result!.duration, + isFullyLoaded: false, // not in use while saving data + statusMessage: 'Saved successfully', + }; + } + async dispose(): Promise { if (this.executionContext) { await this.graphQLService.sdk.sqlContextDestroy({ diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index 1cff8ab07d..e9f2667f14 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -13,8 +13,6 @@ import { GraphQLService } from '@cloudbeaver/core-sdk'; import { ContainerDataSource } from './ContainerDataSource'; import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataModel'; -import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess'; -import { IExecutionContext } from './IExecutionContext'; import { RowDiff } from './TableViewer/TableDataModel/EditedRow'; import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; @@ -35,15 +33,6 @@ export class DataViewerTableService { const model = this.tableViewerStorageService.get(tableId); if (model) { await model.dispose(); - - if (model.deprecatedModel.executionContext) { - try { - await this.graphQLService.sdk.sqlContextDestroy({ - connectionId: model.deprecatedModel.executionContext.connectionId, - contextId: model.deprecatedModel.executionContext.contextId, - }); - } catch { } - } } this.tableViewerStorageService.remove(tableId); } @@ -56,14 +45,44 @@ export class DataViewerTableService { const connectionInfo = await this.connectionInfoResource.load(connectionId); const source = new ContainerDataSource(this.graphQLService, this.notificationService); - return this.tableViewerStorageService.create( + const dataModel = this.tableViewerStorageService.create( { tableId: tabId, connectionId, containerNodePath, access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default, - requestDataAsync: this.requestDataAsync.bind(this, source), - saveChanges: this.saveChanges.bind(this), + requestDataAsync: async ( + model: TableViewerModel, + offset: number, + count: number, + ): Promise => { + await dataModel + .setSlice(0, dataModel.countGain + dataModel.source.count) + .requestData(); + + const result = dataModel.getResult(0); + + if (!result) { + throw new Error('Result not exists'); + } + + return { + rows: result.data.rows!, + columns: result.data.columns!, + duration: dataModel.source.requestInfo.requestDuration, + statusMessage: dataModel.source.requestInfo.requestMessage, + isFullyLoaded: result.loadedFully, + }; + }, + saveChanges: async (data: TableViewerModel, rows: RowDiff[]): Promise => { + const result = dataModel.getResult(0); + + if (!result) { + throw new Error('It is expected that result was set after first fetch'); + } + + return await source.saveDataDeprecated(result.id, rows); + }, }, source .setOptions({ @@ -74,100 +93,8 @@ export class DataViewerTableService { }) .setSupportedDataFormats(connectionInfo.supportedDataFormats) ) - .setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default) - .deprecatedModel; - } + .setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default); - private async createExecutionContext( - connectionId: string, - defaultCatalog?: string, - defaultSchema?: string - ): Promise { - const response = await this.graphQLService.sdk.sqlContextCreate({ - connectionId, - defaultCatalog, - defaultSchema, - }); - return { - contextId: response.context.id, - connectionId, - objectCatalogId: response.context.defaultCatalog, - objectSchemaId: response.context.defaultSchema, - }; - } - - private async saveChanges(data: TableViewerModel, rows: RowDiff[]): Promise { - if (!data.resultId) { - throw new Error('It is expected that resultId was set after first fetch'); - } - - if (!data.executionContext) { - throw new Error('It is expected that data.sqlContextParams was set after first fetch'); - } - - const response = await this.graphQLService.sdk.updateResultsDataBatch({ - connectionId: data.executionContext.connectionId, - contextId: data.executionContext.contextId, - resultsId: data.resultId, - updatedRows: rows.map(row => ({ data: row.source, updateValues: row.values })), - }); - - const dataSet = response.result!.results[0].resultSet!; // we expect only one dataset for a table - - const result: IRequestDataResult = { - rows: dataSet.rows!, - columns: [], // not in use while saving data - duration: response.result!.duration, - isFullyLoaded: false, // not in use while saving data - statusMessage: 'Saved successfully', - }; - return result; - } - - private async requestDataAsync( - source: ContainerDataSource, - model: TableViewerModel, - offset: number, - count: number, - ): Promise { - if (!model.containerNodePath) { - throw new Error('containerNodePath must be provided for table'); - } - if (!model.executionContext) { - // it is first data request - const executionContext: IExecutionContext = await this.createExecutionContext(model.connectionId); - model.executionContext = executionContext; - } - - const fetchTableProcess = new FetchTableDataAsyncProcess(this.graphQLService, this.notificationService); - - fetchTableProcess.start( - { - connectionId: model.executionContext.connectionId, - contextId: model.executionContext.contextId, - containerNodePath: model.containerNodePath, - }, - { - offset, - limit: count, - constraints: Array.from(model.getSortedColumns()), - where: model.getQueryWhereFilter() || undefined, - }, - ); - - source.currentFetchTableProcess = fetchTableProcess; - const response = await fetchTableProcess.promise; - - const dataSet = response!.results[0].resultSet!; // we expect only one dataset for a table - model.resultId = dataSet.id; // server generates new resultId on each fetch - - const result: IRequestDataResult = { - rows: dataSet.rows!, - columns: dataSet.columns!, - duration: response!.duration, - statusMessage: response!.statusMessage || '', - isFullyLoaded: (dataSet.rows?.length || 0) < count, - }; - return result; + return dataModel.deprecatedModel; } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 45fea92ed0..2692328313 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -10,16 +10,18 @@ import { observable } from 'mobx'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { IExecutionContext } from '../IExecutionContext'; import { IDatabaseDataResult } from './IDatabaseDataResult'; import { DataUpdate, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export abstract class DatabaseDataSource - implements IDatabaseDataSource { +implements IDatabaseDataSource { @observable offset: number; @observable count: number; @observable dataFormat: ResultDataFormat; @observable options: TOptions | null; @observable requestInfo: IRequestInfo; + @observable executionContext: IExecutionContext | null; @observable supportedDataFormats: ResultDataFormat[]; abstract get canCancel(): boolean; @@ -32,6 +34,7 @@ export abstract class DatabaseDataSource { if (this.activeSave) { try { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 08c84ffca0..a678769f36 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -8,6 +8,7 @@ import { ResultDataFormat, SqlResultSet } from '@cloudbeaver/core-sdk'; +import { IExecutionContext } from '../IExecutionContext'; import { IDatabaseDataResult } from './IDatabaseDataResult'; export enum DataUpdateType { @@ -33,6 +34,7 @@ export interface IDatabaseDataSource this; setDataFormat: (dataFormat: ResultDataFormat) => this; setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this; + setExecutionContext: (context: IExecutionContext | null) => this; requestData: ( prevResults: TResult[] ) => Promise | TResult[]; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index 023e9093b5..5ec9a1aad6 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -68,10 +68,12 @@ const tableFooterStyles = composes( ); interface TableFooterProps { + resultIndex: number; model: DataModelWrapper; } export const TableFooter = observer(function TableFooter({ + resultIndex, model, }: TableFooterProps) { const ref = useRef(null); @@ -100,7 +102,7 @@ export const TableFooter = observer(function TableFooter({ - + {(model.source.dataFormat === ResultDataFormat.Resultset && model.deprecatedModel.requestStatusMessage.length > 0) && (