diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 40829363f8..1aa5c5e4cd 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -13,7 +13,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService } from '@cloudbeaver/core-dialogs'; -import { TableFooterMenuService, TableViewerModel } from '@cloudbeaver/plugin-data-viewer'; +import { TableFooterMenuService, DataModelWrapper } from '@cloudbeaver/plugin-data-viewer'; import { DataExportDialog } from './Dialog/DataExportDialog'; @@ -26,7 +26,7 @@ export class DataExportMenuService { ) { } register() { - const exportData: IContextMenuItem = { + const exportData: IContextMenuItem = { id: 'export ', isPresent(context) { return context.contextType === TableFooterMenuService.nodeContextType; @@ -60,13 +60,13 @@ export class DataExportMenuService { ); } - private exportData(context: IMenuContext) { + private exportData(context: IMenuContext) { this.commonDialogService.open(DataExportDialog, { - connectionId: context.data.connectionId, - contextId: context.data.executionContext?.contextId, - containerNodePath: context.data.containerNodePath, - resultId: context.data.resultId, - sourceName: context.data.sourceName, + 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, }); } } diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index 11b83ed75c..e7b407f994 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -24,7 +24,7 @@ import { } from '@ag-grid-community/core'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { - TableViewerModel, SortModel, AgGridRow, IAgGridCol, AccessMode + TableViewerModel, SortModel, AgGridRow, IAgGridCol, DatabaseDataAccessMode } from '@cloudbeaver/plugin-data-viewer'; import { AgGridContext } from './AgGridContext'; @@ -45,7 +45,7 @@ export class AgGridTableController implements IInitializableController, IDestruc */ private readonly context: AgGridContext = { selection: this.selection, - isReadonly: () => this.gridModel.access === AccessMode.Readonly, + isReadonly: () => this.gridModel.access === DatabaseDataAccessMode.Readonly, isCellEdited: this.isCellEdited.bind(this), editCellValue: this.editCellValue.bind(this), revertCellValue: this.revertCellValue.bind(this), diff --git a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx index a2bd6485a9..6df6bbd4be 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx @@ -6,12 +6,12 @@ * you may not use this file except in compliance with the License. */ -import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer'; +import { DataModelWrapper } from '@cloudbeaver/plugin-data-viewer'; import { AgGridTable } from './AgGridTable/AgGridTable'; type Props = { - tableModel: TableViewerModel; + tableModel: DataModelWrapper; className?: string; } @@ -20,5 +20,5 @@ export function Spreadsheet({ className, }: Props) { - return ; + return ; } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts new file mode 100644 index 0000000000..4f84cd7828 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -0,0 +1,117 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; +import { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult'; +import { DataUpdate } from './DatabaseDataModel/IDatabaseDataSource'; +import { IExecutionContext } from './IExecutionContext'; + +export interface IDataContainerOptions { + containerNodePath: string; + connectionId: string; + whereFilter: string; + constraints: SqlDataFilterConstraint[]; +} + +export interface IDataContainerResult extends IDatabaseDataResult { + +} + +export class ContainerDataSource extends DatabaseDataSource { + private executionContext: IExecutionContext | null; + + constructor(private graphQLService: GraphQLService) { + super(); + this.executionContext = null; + } + + async request( + prevResults: IDataContainerResult[] + ): Promise { + if (!this.options?.containerNodePath) { + throw new Error('containerNodePath must be provided for table'); + } + const executionContext = await this.ensureContextCreated(); + + const { readDataFromContainer } = await this.graphQLService.sdk.readDataFromContainer({ + connectionId: executionContext.connectionId, + contextId: executionContext.contextId, + containerNodePath: this.options.containerNodePath, + filter: { + offset: this.offset, + limit: this.count, + constraints: this.options.constraints, + where: this.options.whereFilter || undefined, + }, + }); + + this.requestInfo = { + requestDuration: readDataFromContainer?.duration || 0, + requestMessage: readDataFromContainer?.statusMessage || '', + }; + + return readDataFromContainer?.results.map(result => ({ + id: result.resultSet!.id, + dataFormat: result.dataFormat!, + loadedFully: (result.resultSet?.rows?.length || 0) < this.count || !result.resultSet?.hasMoreData, + data: result.resultSet, + })) || prevResults; + } + + async save( + prevResults: IDataContainerResult[], + data: DataUpdate + ): Promise { + const executionContext = await this.ensureContextCreated(); + + const response = await this.graphQLService.sdk.updateResultsDataBatch({ + connectionId: executionContext.connectionId, + contextId: executionContext.contextId, + resultsId: data.data.id, + // updatedRows: this.getRowsDiff(data), + }); + + this.requestInfo = { + requestDuration: response.result?.duration || 0, + requestMessage: 'Saved successfully', + }; + + throw new Error('Not implemented'); + } + + private async ensureContextCreated(): Promise { + if (!this.executionContext) { + if (!this.options) { + throw new Error('Options must be provided'); + } + this.executionContext = await this.createExecutionContext(this.options.connectionId); + } + return this.executionContext; + } + + 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, + }; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts b/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts index f8e798b49d..4a3616d8e2 100644 --- a/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts @@ -8,10 +8,10 @@ import { injectable } from '@cloudbeaver/core-di'; -import { TableViewerModel } from './TableViewer/TableViewerModel'; +import { DataModelWrapper } from './TableViewer/DataModelWrapper'; export interface IDataPresentationProps { - tableModel: TableViewerModel; + tableModel: DataModelWrapper; className?: string; } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index dd0d4687eb..88f4a47e41 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -10,9 +10,11 @@ import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { ContainerDataSource } from './ContainerDataSource'; +import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataModel'; import { IExecutionContext } from './IExecutionContext'; import { RowDiff } from './TableViewer/TableDataModel/EditedRow'; -import { IRequestDataResult, TableViewerModel, AccessMode } from './TableViewer/TableViewerModel'; +import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; @injectable() @@ -22,7 +24,7 @@ export class DataViewerTableService { private graphQLService: GraphQLService) { } - has(tableId: string) { + has(tableId: string): boolean { return this.tableViewerStorageService.has(tableId); } @@ -33,7 +35,7 @@ export class DataViewerTableService { async create( tabId: string, connectionId: string, - containerNodePath?: string, + containerNodePath = '', ): Promise { const connectionInfo = await this.connectionInfoResource.load(connectionId); @@ -42,11 +44,19 @@ export class DataViewerTableService { tableId: tabId, connectionId, containerNodePath, - access: connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default, + access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default, requestDataAsync: this.requestDataAsync.bind(this), saveChanges: this.saveChanges.bind(this), - } - ); + }, + new ContainerDataSource(this.graphQLService) + ) + .setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default) + .setOptions({ + connectionId, + containerNodePath, + constraints: [], + whereFilter: '', + }).deprecatedModel; } private async createExecutionContext( diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts new file mode 100644 index 0000000000..99a6265598 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts @@ -0,0 +1,13 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { DatabaseDataModel } from './DatabaseDataModel'; + +export interface DatabaseDataFormat { + processResult(model: DatabaseDataModel, data: T): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts new file mode 100644 index 0000000000..d22ca01f4b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts @@ -0,0 +1,55 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { uuid } from '@cloudbeaver/core-utils'; + +import { DatabaseDataAccessMode, IDatabaseDataModel } from './IDatabaseDataModel'; +import { IDatabaseDataResult } from './IDatabaseDataResult'; +import { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; + +export class DatabaseDataModel +implements IDatabaseDataModel { + id: string; + results: TResult[]; + source: IDatabaseDataSource; + access: DatabaseDataAccessMode; + + get requestInfo(): IRequestInfo { + return this.source.requestInfo; + } + + constructor(source: IDatabaseDataSource) { + this.id = uuid(); + this.source = source; + this.results = []; + this.access = DatabaseDataAccessMode.Default; + } + + isLoading(): boolean { + return this.source.isLoading(); + } + + setAccess(access: DatabaseDataAccessMode): this { + this.access = access; + return this; + } + + setSlice(offset: number, count: number): this { + this.source.setSlice(offset, count); + return this; + } + + setOptions(options: TOptions): this { + this.source.setOptions(options); + return this; + } + + async requestData(): Promise { + this.results = await this.source.requestData(this.results); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts new file mode 100644 index 0000000000..2301e6f878 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -0,0 +1,98 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { IDatabaseDataResult } from './IDatabaseDataResult'; +import { DataUpdate, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; + +export abstract class DatabaseDataSource +implements IDatabaseDataSource { + offset: number; + count: number; + options: TOptions | null; + requestInfo: IRequestInfo; + + private activeRequest: Promise | null; + private activeSave: Promise | null; + + constructor() { + this.offset = 0; + this.count = 0; + this.options = null; + this.activeRequest = null; + this.activeSave = null; + this.requestInfo = { + requestDuration: 0, + requestMessage: '', + }; + } + + isLoading(): boolean { + return !!this.activeRequest; + } + + setSlice(offset: number, count: number): void { + this.offset = offset; + this.count = count; + } + + setOptions(options: TOptions): void { + this.options = options; + } + + async requestData(prevResults: TResult[]): Promise { + if (this.activeSave) { + try { + await this.activeSave; + } finally {} + } + + if (this.activeRequest) { + return this.activeRequest; + } + + try { + const promise = this.request(prevResults); + + if (promise instanceof Promise) { + this.activeRequest = promise; + } + return promise; + } finally { + this.activeRequest = null; + } + } + + async saveData( + prevResults: TResult[], + data: DataUpdate + ): Promise { + if (this.activeRequest) { + try { + await this.activeRequest; + } finally {} + } + + if (this.activeSave) { + return this.activeSave; + } + + try { + const promise = this.save(prevResults, data); + + if (promise instanceof Promise) { + this.activeSave = promise; + } + return promise; + } finally { + this.activeSave = null; + } + } + + abstract request(prevResults: TResult[]): TResult[] | Promise; + abstract save(prevResults: TResult[], data: DataUpdate): Promise | TResult[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts new file mode 100644 index 0000000000..ade2ce77c1 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts @@ -0,0 +1,14 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IDatabaseDataEditor { + isEdited(): boolean; + getChanges(): void; + applyChanges(): void; + cancelChanges(skipUpdate?: boolean): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts new file mode 100644 index 0000000000..ca242420cb --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -0,0 +1,29 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { IDatabaseDataResult } from './IDatabaseDataResult'; +import { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; + +export enum DatabaseDataAccessMode { + Default, + Readonly +} + +export interface IDatabaseDataModel { + readonly id: string; + readonly results: TResult[]; + readonly source: IDatabaseDataSource; + readonly access: DatabaseDataAccessMode; + readonly requestInfo: IRequestInfo; + + isLoading(): boolean; + setAccess(access: DatabaseDataAccessMode): this; + setSlice(offset: number, count: number): this; + setOptions(options: TOptions): this; + requestData(): Promise; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts new file mode 100644 index 0000000000..9973ef8787 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts @@ -0,0 +1,16 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +export interface IDatabaseDataResult { + id: string; + dataFormat: ResultDataFormat; + loadedFully: boolean; + data: any; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts new file mode 100644 index 0000000000..330537c330 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -0,0 +1,46 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { SqlResultSet } from '@cloudbeaver/core-sdk'; + +import { IDatabaseDataResult } from './IDatabaseDataResult'; + +export enum DataUpdateType { + delete, + update, + add +} + +export interface IRequestInfo { + readonly requestDuration: number; + readonly requestMessage: string; +} + +export interface DataUpdate { + data: SqlResultSet; + dataUpdate: SqlResultSet; + type: DataUpdateType; +} + +export interface IDatabaseDataSource { + readonly offset: number; + readonly count: number; + readonly options: TOptions | null; + readonly requestInfo: IRequestInfo; + + isLoading(): boolean; + setSlice(offset: number, count: number): void; + setOptions(options: TOptions): void; + requestData( + prevResults: TResult[] + ): Promise | TResult[]; + saveData( + prevResults: TResult[], + data: DataUpdate + ): Promise | TResult[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts new file mode 100644 index 0000000000..474d8f2113 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts @@ -0,0 +1,57 @@ +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel'; +import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel'; +import { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource'; +import { ITableViewerModelOptions, TableViewerModel } from './TableViewerModel'; + +export class DataModelWrapper extends DatabaseDataModel { + /** + * @deprecated Use getModel instead + */ + readonly deprecatedModel: TableViewerModel; + + constructor( + commonDialogService: CommonDialogService, + options: ITableViewerModelOptions, + source: IDatabaseDataSource + ) { + super(source); + if (options.tableId) { + this.id = options.tableId; + } + this.deprecatedModel = new TableViewerModel(options, commonDialogService); + } + + isLoading(): boolean { + return this.deprecatedModel.isLoaderVisible || this.source.isLoading(); + } + + setAccess(access: DatabaseDataAccessMode): this { + this.access = access; + this.deprecatedModel.access = access; + return this; + } + + setSlice(offset: number, count: number): this { + this.source.setSlice(offset, count); + this.deprecatedModel.setChunkSize(count); + return this; + } + + setOptions(options: any): this { + this.source.setOptions(options); + return this; + } + + async requestData(): Promise { + this.results = await this.source.requestData(this.results); + } +} 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 712738587c..31e4c7a3c3 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -13,7 +13,7 @@ import styled, { css, use } from 'reshadow'; import { IconButton, SubmittingForm } from '@cloudbeaver/core-blocks'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import { TableViewerModel } from '../TableViewerModel'; +import { DataModelWrapper } from '../DataModelWrapper'; import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu'; const tableFooterStyles = composes( @@ -67,7 +67,7 @@ const tableFooterStyles = composes( ); type TableFooterProps = { - model: TableViewerModel; + model: DataModelWrapper; } export const TableFooter = observer(function TableFooter({ @@ -81,8 +81,8 @@ export const TableFooter = observer(function TableFooter({ } const value = parseInt(ref.current.value, 10); - if (model.getChunkSize() !== value) { - model.setChunkSize(value); + if (model.deprecatedModel.getChunkSize() !== value) { + model.deprecatedModel.setChunkSize(value); } }, [model] @@ -91,17 +91,17 @@ export const TableFooter = observer(function TableFooter({ return styled(useStyles(tableFooterStyles))( - + - + - {model.requestStatusMessage.length > 0 && ( + {model.deprecatedModel.requestStatusMessage.length > 0 && ( )} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx index f2ff4c4f6d..cecdecc7db 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -11,7 +11,7 @@ import styled, { css } from 'reshadow'; import { useService } from '@cloudbeaver/core-di'; -import { TableViewerModel } from '../../TableViewerModel'; +import { DataModelWrapper } from '../../DataModelWrapper'; import { TableFooterMenuItem } from './TableFooterMenuItem'; import { TableFooterMenuService } from './TableFooterMenuService'; @@ -27,7 +27,7 @@ const styles = css` `; type TableFooterMenuProps = { - model: TableViewerModel; + model: DataModelWrapper; className?: string; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index fa7ff873a1..7e025379d0 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -11,7 +11,7 @@ import { ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem } from '@cloudbeaver/core-dialogs'; -import { TableViewerModel } from '../../TableViewerModel'; +import { DataModelWrapper } from '../../DataModelWrapper'; @injectable() export class TableFooterMenuService { @@ -28,12 +28,12 @@ export class TableFooterMenuService { return context.contextType === TableFooterMenuService.nodeContextType; }, isDisabled(context) { - return !context.data.isEdited(); + return !context.data.deprecatedModel.isEdited(); }, order: 1, title: 'Save', icon: 'table-save', - onClick: context => context.data.saveChanges(), + onClick: context => context.data.deprecatedModel.saveChanges(), }); this.registerMenuItem({ id: 'cancel ', @@ -41,26 +41,26 @@ export class TableFooterMenuService { return context.contextType === TableFooterMenuService.nodeContextType; }, isDisabled(context) { - return !context.data.isEdited(); + return !context.data.deprecatedModel.isEdited(); }, order: 2, title: 'Cancel', icon: 'table-cancel', - onClick: context => context.data.cancelChanges(), + onClick: context => context.data.deprecatedModel.cancelChanges(), }); } - constructMenuWithContext(model: TableViewerModel): IMenuItem[] { - const context: IMenuContext = { + constructMenuWithContext(model: DataModelWrapper): IMenuItem[] { + const context: IMenuContext = { menuId: this.tableFooterMenuToken, - contextId: model.tableId, + contextId: model.id, contextType: TableFooterMenuService.nodeContextType, data: model, }; return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems; } - registerMenuItem(options: IContextMenuItem): void { - this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); + registerMenuItem(options: IContextMenuItem): void { + this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx index 134f4ed0a6..2bb0bb8164 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx @@ -15,10 +15,10 @@ import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { DataPresentationService } from '../DataPresentationService'; -import { TableViewerModel } from './TableViewerModel'; +import { DataModelWrapper } from './DataModelWrapper'; type TableGridProps = PropsWithChildren<{ - model: TableViewerModel; + model: DataModelWrapper; }> const styles = css` @@ -37,13 +37,14 @@ export const TableGrid = observer(function TableGrid({ const translate = useTranslate(); const dataPresentationService = useService(DataPresentationService); - if (model.errorMessage.length > 0) { + // TODO: probably must be implemented in presentation component + if (model.deprecatedModel.errorMessage.length > 0) { return styled(styles)( - {model.errorMessage} + {model.deprecatedModel.errorMessage}

- {model.hasDetails && ( - )} @@ -53,7 +54,7 @@ export const TableGrid = observer(function TableGrid({ const Spreadsheet = dataPresentationService.default?.component; - if ((model.isFullyLoaded && model.isEmpty) || !Spreadsheet) { + if ((model.deprecatedModel.isFullyLoaded && model.deprecatedModel.isEmpty) || !Spreadsheet) { return styled(styles)({translate('data_viewer_nodata_message')}); } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx index fed2bfbdff..c71a325e28 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx @@ -13,7 +13,7 @@ import { Placeholder } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import { TableViewerModel } from '../TableViewerModel'; +import { DataModelWrapper } from '../DataModelWrapper'; import { TableHeaderService } from './TableHeaderService'; const styles = composes( @@ -33,7 +33,7 @@ const styles = composes( ); type Props = { - model: TableViewerModel; + model: DataModelWrapper; } export const TableHeader = observer(function TableHeader({ diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index 300e8d31f2..72d556f9aa 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -9,12 +9,12 @@ import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; -import { TableViewerModel } from '../TableViewerModel'; +import { DataModelWrapper } from '../DataModelWrapper'; import { TableWhereFilter } from './TableWhereFilter'; @injectable() export class TableHeaderService extends Bootstrap { - readonly tableHeaderPlaceholder = new PlaceholderContainer() + readonly tableHeaderPlaceholder = new PlaceholderContainer() register() { this.tableHeaderPlaceholder.add(TableWhereFilter, 1); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index 8e0df24665..639c910a79 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -15,7 +15,7 @@ import { SubmittingForm } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import { TableViewerModel } from '../TableViewerModel'; +import { DataModelWrapper } from '../DataModelWrapper'; const styles = composes( css` @@ -39,29 +39,29 @@ const styles = composes( ); type Props = { - context: TableViewerModel; + context: DataModelWrapper; } export const TableWhereFilter = observer(function TableWhereFilter({ context, }: Props) { const translate = useTranslate(); - const [filterValue, setValue] = useState(() => context.getQueryWhereFilter() || ''); + const [filterValue, setValue] = useState(() => context.deprecatedModel.getQueryWhereFilter() || ''); const handleApply = useCallback(() => { - context.setQueryWhereFilter(filterValue); - context.refresh(); + context.deprecatedModel.setQueryWhereFilter(filterValue); + context.deprecatedModel.refresh(); }, [context, filterValue]); const resetFilter = useCallback( () => { - const applyNeeded = context.getQueryWhereFilter() === filterValue; + const applyNeeded = context.deprecatedModel.getQueryWhereFilter() === filterValue; setValue(''); if (applyNeeded) { - context.setQueryWhereFilter(''); - context.refresh(); + context.deprecatedModel.setQueryWhereFilter(''); + context.deprecatedModel.refresh(); } }, [context, filterValue] diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index b5d33d24c0..dd464d9ff8 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -49,7 +49,7 @@ export const TableViewer = observer(function TableViewer({ - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts index 0f56f4c05f..00c45ab4d9 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts @@ -14,6 +14,7 @@ import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { GQLError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; import { uuid, MetadataMap } from '@cloudbeaver/core-utils'; +import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel'; import { IExecutionContext } from '../IExecutionContext'; import { ErrorDialog } from './ErrorDialog'; import { RowDiff } from './TableDataModel/EditedRow'; @@ -28,11 +29,6 @@ export const fetchingSettings = { fetchDefault: 200, }; -export enum AccessMode { - Default, - Readonly -} - export type AgGridRow = any[]; export type SortMode = 'asc' | 'desc' | null; @@ -73,7 +69,7 @@ export interface ITableViewerModelOptions { executionContext?: IExecutionContext | null; // will be filled before fist data fetch sourceName?: string; // TODO: refactor it, used for showing sql query for export noLoaderWhileRequestingDataAsync?: boolean; - access?: AccessMode; + access?: DatabaseDataAccessMode; requestDataAsync( model: TableViewerModel, rowOffset: number, @@ -99,7 +95,7 @@ export class TableViewerModel { sourceName?: string; noLoaderWhileRequestingDataAsync?: boolean; - @observable access: AccessMode; + @observable access: DatabaseDataAccessMode; requestDataAsync: ( model: TableViewerModel, @@ -156,7 +152,7 @@ export class TableViewerModel { this.executionContext = options.executionContext || null; this.sourceName = options.sourceName; this.noLoaderWhileRequestingDataAsync = options.noLoaderWhileRequestingDataAsync; - this.access = options.access || AccessMode.Default; + this.access = options.access || DatabaseDataAccessMode.Default; this.requestDataAsync = options.requestDataAsync; this._saveChanges = options.saveChanges; this.resetSubject = new Subject(); @@ -223,7 +219,7 @@ export class TableViewerModel { } isEdited(): boolean { - if (this.access === AccessMode.Readonly) { + if (this.access === DatabaseDataAccessMode.Readonly) { return false; } @@ -243,7 +239,7 @@ export class TableViewerModel { } async saveChanges(): Promise { - if (this.access === AccessMode.Readonly) { + if (this.access === DatabaseDataAccessMode.Readonly) { return; } @@ -321,7 +317,7 @@ export class TableViewerModel { } onCellEditingStopped(rowNumber: number, column: string, value: any, editing: boolean): void { - if (this.access === AccessMode.Readonly) { + if (this.access === DatabaseDataAccessMode.Readonly) { return; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts index 230f230ebf..ee466890c4 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts @@ -11,31 +11,49 @@ import { observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { TableViewerModel, ITableViewerModelOptions } from './TableViewerModel'; +import { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; +import { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult'; +import { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource'; +import { DataModelWrapper } from './DataModelWrapper'; +import { ITableViewerModelOptions } from './TableViewerModel'; @injectable() export class TableViewerStorageService { - @observable private tableModelMap: Map = new Map(); + @observable private tableModelMap: Map> = new Map(); constructor(private commonDialogService: CommonDialogService) {} has(tableId: string): boolean { return this.tableModelMap.has(tableId); } - get(tableId: string) { - return this.tableModelMap.get(tableId); + + get(tableId: string): DataModelWrapper | null | undefined { + return this.tableModelMap.get(tableId) as DataModelWrapper; } + /** + * @deprecated Use add method instead + */ create( options: ITableViewerModelOptions, - ): TableViewerModel { + source: IDatabaseDataSource + ): DataModelWrapper { - const tableModel = new TableViewerModel(options, this.commonDialogService); - this.tableModelMap.set(tableModel.tableId, tableModel); - return tableModel; + return this.add(new DataModelWrapper( + this.commonDialogService, + options, + source + )) as DataModelWrapper; } - remove(tableId: string) { - this.tableModelMap.delete(tableId); + add( + model: IDatabaseDataModel, + ): IDatabaseDataModel { + this.tableModelMap.set(model.id, model); + return this.tableModelMap.get(model.id)!; + } + + remove(id: string): void { + this.tableModelMap.delete(id); } } diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 7c19833853..6064812da2 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -2,12 +2,21 @@ import { manifest } from './manifest'; export default manifest; +export * from './DatabaseDataModel/DatabaseDataFormat'; +export * from './DatabaseDataModel/DatabaseDataModel'; +export * from './DatabaseDataModel/DatabaseDataSource'; +export * from './DatabaseDataModel/IDatabaseDataEditor'; +export * from './DatabaseDataModel/IDatabaseDataModel'; +export * from './DatabaseDataModel/IDatabaseDataResult'; +export * from './DatabaseDataModel/IDatabaseDataSource'; + // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService'; export * from './TableViewer/TableViewer'; export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; export * from './TableViewer/TableViewerModel'; +export * from './TableViewer/DataModelWrapper'; export * from './TableViewer/TableDataModel/TableColumn'; export * from './TableViewer/TableDataModel/TableRow'; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts new file mode 100644 index 0000000000..bd7ef8265f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -0,0 +1,117 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; +import { + DatabaseDataSource, DataUpdate, IDatabaseDataResult, IExecutionContext +} from '@cloudbeaver/plugin-data-viewer'; + +export interface IDataContainerOptions { + sourceName: string; + connectionId: string; + whereFilter: string; + constraints: SqlDataFilterConstraint[]; +} + +export interface IDataContainerResult extends IDatabaseDataResult { + +} + +export class QueryDataSource extends DatabaseDataSource { + private executionContext: IExecutionContext | null; + + constructor(private graphQLService: GraphQLService) { + super(); + this.executionContext = null; + } + + async request( + prevResults: IDataContainerResult[] + ): Promise { + // if (!this.options?.containerNodePath) { + // throw new Error('containerNodePath must be provided for table'); + // } + // const executionContext = await this.ensureContextCreated(); + + // const { readDataFromContainer } = await this.graphQLService.sdk.readDataFromContainer({ + // connectionId: executionContext.connectionId, + // contextId: executionContext.contextId, + // containerNodePath: this.options.containerNodePath, + // filter: { + // offset: this.offset, + // limit: this.count, + // constraints: this.options.constraints, + // where: this.options.whereFilter || undefined, + // }, + // }); + + // this.requestInfo = { + // requestDuration: readDataFromContainer?.duration || 0, + // requestMessage: readDataFromContainer?.statusMessage || '', + // }; + + // return readDataFromContainer?.results.map(result => ({ + // id: result.resultSet!.id, + // dataFormat: result.dataFormat!, + // loadedFully: (result.resultSet?.rows?.length || 0) < this.count || !result.resultSet?.hasMoreData, + // data: result.resultSet, + // })) || prevResults; + + return prevResults; + } + + async save( + prevResults: IDataContainerResult[], + data: DataUpdate + ): Promise { + const executionContext = await this.ensureContextCreated(); + + const response = await this.graphQLService.sdk.updateResultsDataBatch({ + connectionId: executionContext.connectionId, + contextId: executionContext.contextId, + resultsId: data.data.id, + // updatedRows: this.getRowsDiff(data), + }); + + this.requestInfo = { + requestDuration: response.result?.duration || 0, + requestMessage: 'Saved successfully', + }; + + throw new Error('Not implemented'); + } + + private async ensureContextCreated(): Promise { + if (!this.executionContext) { + if (!this.options) { + throw new Error('Options must be provided'); + } + this.executionContext = await this.createExecutionContext(this.options.connectionId); + } + return this.executionContext; + } + + 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, + }; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts index 90fd8786e2..d1b4200b72 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts @@ -13,7 +13,7 @@ import { IDestructibleController, IInitializableController, injectable } from '@ import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLError, ServerInternalError } from '@cloudbeaver/core-sdk'; +import { GQLError, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk'; import { PromiseCancelledError } from '@cloudbeaver/core-utils'; import { fetchingSettings, @@ -21,10 +21,11 @@ import { RowDiff, TableViewerStorageService, TableViewerModel, - AccessMode, + DatabaseDataAccessMode, } from '@cloudbeaver/plugin-data-viewer'; import { IResultDataTab, IQueryTabGroup } from '../../ISqlEditorTabState'; +import { QueryDataSource } from '../../QueryDataSource'; import { SqlEditorGroupMetadataService } from '../../SqlEditorGroupMetadataService'; import { SqlExecutionState } from '../../SqlExecutionState'; import { SQLQueryExecutionProcess } from '../SQLQueryExecutionProcess'; @@ -54,6 +55,7 @@ implements IInitializableController, IDestructibleController { private sqlProcess: SQLQueryExecutionProcess | null = null; constructor( + private sdk: GraphQLService, private sqlResultService: SqlResultService, private tableViewerStorageService: TableViewerStorageService, private connectionInfoResource: ConnectionInfoResource, @@ -101,17 +103,27 @@ implements IInitializableController, IDestructibleController { this.tableViewerStorageService.remove(this.getTableId()); } - const tableModel = this.tableViewerStorageService.create({ - tableId: this.getTableId(), - connectionId: this.group.sqlQueryParams.connectionId, - executionContext: this.group.sqlQueryParams, - resultId: dataSet.resultSet.id, - sourceName: this.group.sqlQueryParams.query, - access: connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default, - requestDataAsync: this.requestDataAsync.bind(this, sqlExecutionContext), - noLoaderWhileRequestingDataAsync: true, - saveChanges: this.saveChanges.bind(this), - }); + const tableModel = this.tableViewerStorageService.create( + { + tableId: this.getTableId(), + connectionId: this.group.sqlQueryParams.connectionId, + executionContext: this.group.sqlQueryParams, + resultId: dataSet.resultSet.id, + sourceName: this.group.sqlQueryParams.query, + access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default, + requestDataAsync: this.requestDataAsync.bind(this, sqlExecutionContext), + noLoaderWhileRequestingDataAsync: true, + saveChanges: this.saveChanges.bind(this), + }, + new QueryDataSource(this.sdk) + ) + .setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default) + .setOptions({ + connectionId: this.group.sqlQueryParams.connectionId, + sourceName: this.group.sqlQueryParams.query, + constraints: [], + whereFilter: '', + }).deprecatedModel; tableModel.insertRows(0, initialState.rows, !initialState.isFullyLoaded); tableModel.setColumns(initialState.columns); @@ -204,7 +216,7 @@ implements IInitializableController, IDestructibleController { const connectionInfo = await this.connectionInfoResource.load(this.group.sqlQueryParams.connectionId); model.resultId = resultId; - model.access = connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default; + model.access = connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default; model.sourceName = this.group.sqlQueryParams.query; model.executionContext = this.group.sqlQueryParams; model.connectionId = this.group.sqlQueryParams.connectionId;