From 3a8ffb9c24849a4e40ca3e183699eeb8dc5e67d8 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 30 Apr 2020 14:48:51 +0300 Subject: [PATCH] feat(data-viewer-plugin): grid footer menu refactor(data-viewer-plugin): TableViewerModel --- .../ConnectionsManagerService.ts | 4 +- .../Menu/ContextMenu/ContextMenuService.ts | 4 +- .../src/DataViewerPage/DataViewerPanel.tsx | 2 +- .../src/DataViewerTabService.ts | 19 +++- .../src/DataViewerTableService.ts | 82 +++++++--------- .../src/IExecutionContext.ts | 14 +++ .../TableViewer/TableFooter/TableFooter.tsx | 48 +++++----- .../TableFooterMenu/TableFooterMenu.tsx | 44 +++++++++ .../TableFooterMenu/TableFooterMenuItem.tsx | 95 +++++++++++++++++++ .../TableFooterMenu/TableFooterMenuService.ts | 37 ++++++++ .../TableViewer/{TableGrid => }/TableGrid.tsx | 2 +- .../src/TableViewer/TableViewer.tsx | 12 +-- .../src/TableViewer/TableViewerModel.ts | 64 +++++++++---- .../TableViewer/TableViewerStorageService.ts | 21 ++-- .../packages/data-viewer-plugin/src/index.ts | 3 + .../data-viewer-plugin/src/manifest.ts | 2 + .../sql-editor/src/ISqlEditorTabState.ts | 14 +-- .../src/SqlEditorNavigatorService.ts | 5 +- .../SqlResultPanelController.ts | 49 ++++++---- .../src/SqlResultTabs/SqlResultService.ts | 1 - .../src/SqlResultTabs/SqlResultTabsService.ts | 9 +- 21 files changed, 383 insertions(+), 148 deletions(-) create mode 100644 webapp/packages/data-viewer-plugin/src/IExecutionContext.ts create mode 100644 webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx create mode 100644 webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx create mode 100644 webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts rename webapp/packages/data-viewer-plugin/src/TableViewer/{TableGrid => }/TableGrid.tsx (95%) diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts index 1eaa86765a..2340cc4b53 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts @@ -42,8 +42,8 @@ export type ObjectContainer = Pick = new Map(); - dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this)); - connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this)); + readonly dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this)); + readonly connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this)); @computed get connections(): Connection[] { return Array.from(this.connectionsMap.values()); diff --git a/webapp/packages/core/src/dialogs/Menu/ContextMenu/ContextMenuService.ts b/webapp/packages/core/src/dialogs/Menu/ContextMenu/ContextMenuService.ts index 79ed9cf652..2237b34e28 100644 --- a/webapp/packages/core/src/dialogs/Menu/ContextMenu/ContextMenuService.ts +++ b/webapp/packages/core/src/dialogs/Menu/ContextMenu/ContextMenuService.ts @@ -32,7 +32,7 @@ export class ContextMenuService { this.contextMenu.addMenuItem(panelId, menuItem); } - createContextMenu(context: IMenuContext): IMenuPanel { - return this.contextMenu.constructMenuWithContext(ContextMenuService.rootPanelId, context); + createContextMenu(context: IMenuContext, panelId?: string): IMenuPanel { + return this.contextMenu.constructMenuWithContext(panelId || ContextMenuService.rootPanelId, context); } } diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx index a336f705c2..abda5aaf20 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx +++ b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx @@ -14,5 +14,5 @@ export const DataViewerPanel = function DataViewerPanel({ tab, }: ObjectPagePanelProps) { - return ; + return ; }; diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts index 76ccb360c9..943969d663 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts @@ -12,6 +12,7 @@ import { IContextProvider, ITab, NavigationType, + NodeManagerUtils, } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; @@ -87,7 +88,23 @@ export class DataViewerTabService { if (!this.nodesManagerService.isNodeHasData(objectInfo)) { return; } - this.dataViewerTableService.createTableModelIfNotExists(tab.handlerState.objectId); + + if (this.dataViewerTableService.has(tab.id)) { + return; + } + + const nodeInfo = this.nodesManagerService + .getNodeContainerInfo(tab.handlerState.objectId); + + if (!nodeInfo.connectionId) { + return; + } + + this.dataViewerTableService.create( + tab.id, + NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId), + tab.handlerState.objectId + ); } private async handleTabRestore(tab: ITab) { diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts index dde604c1d9..3f1715d875 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts @@ -6,73 +6,50 @@ * you may not use this file except in compliance with the License. */ -import { ConnectionsManagerService } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; -import { CommonDialogService } from '@dbeaver/core/dialogs'; import { GraphQLService } from '@dbeaver/core/sdk'; +import { IExecutionContext } from './IExecutionContext'; import { RowDiff } from './TableViewer/TableDataModel/EditedRow'; -import { IRequestDataResult, ITableViewerModelInit, TableViewerModel } from './TableViewer/TableViewerModel'; +import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; -interface ISqlContextParams { - contextId: string; - connectionId: string; - objectCatalogId?: string; - objectSchemaId?: string; - -} - -export interface IDataViewerTableModel { - tableId: string; - connectionId: string; - resultId: string | null; - sqlContextParams: ISqlContextParams | null; -} - @injectable() export class DataViewerTableService { constructor(private tableViewerStorageService: TableViewerStorageService, - private connectionsManagerService: ConnectionsManagerService, - private commonDialogService: CommonDialogService, private graphQLService: GraphQLService) { } - createTableModelIfNotExists(tableId: string): void { - if (!this.tableViewerStorageService.hasTableModel(tableId)) { - const tableModel = this.createTableModel(tableId); - this.tableViewerStorageService.addTableModel(tableId, tableModel); - } + has(tableId: string) { + return this.tableViewerStorageService.has(tableId); } removeTableModel(tableId: string): void { - this.tableViewerStorageService.removeTableModel(tableId); + this.tableViewerStorageService.remove(tableId); } - private createTableModel(tableId: string): TableViewerModel { - // todo delete this dirty trick. pass connection id with tableId - const connection = this.connectionsManagerService.connections.find(c => tableId.includes(c.id)); - - const tableModel: IDataViewerTableModel = { - tableId, - connectionId: connection!.id, - sqlContextParams: null, // will be filled before fist data fetch - resultId: null, // will be filled after fist data fetch - }; - - const callbacks: ITableViewerModelInit = { - requestDataAsync: (rowOffset, count: number) => this.requestDataAsync(tableModel, rowOffset, count), - saveChanges: diffs => this.saveChanges(tableModel, diffs), - }; - return new TableViewerModel(callbacks, this.commonDialogService); + create( + tabId: string, + connectionId: string, + containerNodePath?: string, + ): TableViewerModel { + return this.tableViewerStorageService.create( + { + tableId: tabId, + connectionId, + containerNodePath, + requestDataAsync: this.requestDataAsync.bind(this), + saveChanges: this.saveChanges.bind(this), + } + ); } - private async createSqlContext( + private async createExecutionContext( connectionId: string, defaultCatalog?: string, defaultSchema?: string - ): Promise { + ): Promise { const response = await this.graphQLService.gql.sqlContextCreate({ connectionId, @@ -87,7 +64,7 @@ export class DataViewerTableService { }; } - private async saveChanges(data: IDataViewerTableModel, rows: RowDiff[]): Promise { + private async saveChanges(data: TableViewerModel, rows: RowDiff[]): Promise { const firstRow = rows[0]; // we support updating only one value if (!data.resultId) { @@ -118,20 +95,25 @@ export class DataViewerTableService { return result; } - private async requestDataAsync(data: IDataViewerTableModel, - rowOffset: number, - count: number): Promise { + private async requestDataAsync( + data: TableViewerModel, + rowOffset: number, + count: number + ): Promise { + if (!data.containerNodePath) { + throw new Error('containerNodePath must be provided for table'); + } if (!data.sqlContextParams) { // it is first data request - const sqlContextParams: ISqlContextParams = await this.createSqlContext(data.connectionId); + const sqlContextParams: IExecutionContext = await this.createExecutionContext(data.connectionId); data.sqlContextParams = sqlContextParams; } const { readDataFromContainer } = await this.graphQLService.gql.readDataFromContainer({ connectionId: data.sqlContextParams.connectionId, contextId: data.sqlContextParams.contextId, - containerNodePath: data.tableId, + containerNodePath: data.containerNodePath, filter: { offset: rowOffset, limit: count, diff --git a/webapp/packages/data-viewer-plugin/src/IExecutionContext.ts b/webapp/packages/data-viewer-plugin/src/IExecutionContext.ts new file mode 100644 index 0000000000..6e5700c0bb --- /dev/null +++ b/webapp/packages/data-viewer-plugin/src/IExecutionContext.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 IExecutionContext { + connectionId: string; + contextId: string; + objectCatalogId?: string; + objectSchemaId?: string; +} diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooter.tsx index 5e2fc8a53f..2e66a0adca 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooter.tsx @@ -6,19 +6,23 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react'; import { useCallback } from 'react'; import styled, { css, use } from 'reshadow'; import { Icon } from '@dbeaver/core/blocks'; import { composes, useStyles } from '@dbeaver/core/theming'; +import { TableViewerModel } from '../TableViewerModel'; +import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu'; + const tableFooterStyles = composes( css` table-footer { composes: theme-background-secondary theme-text-on-secondary from global; } button { - composes: theme-text-primary from global; + composes: theme-text-primary theme-ripple from global; } `, css` @@ -51,53 +55,51 @@ const tableFooterStyles = composes( font-size: 13px; line-height: 24px; } - time placeholder { - height: 16px; - width: 150px; + reload, + reload button { + height: 100%; } reload, count, - time { + TableFooterMenu { margin-left: 16px; } + time { + composes: theme-typography--caption from global; + margin-left: auto; + margin-right: 16px; + } ` ); type TableFooterProps = { - chunkSize: number; - requestStatusMessage: string; - queryDuration: number; - onRefresh: () => void; - onDataChange: (value: number) => void; + model: TableViewerModel; } -export function TableFooter({ - chunkSize, - requestStatusMessage, - queryDuration, - onRefresh, - onDataChange, +export const TableFooter = observer(function TableFooter({ + model, }: TableFooterProps) { const handleChange = useCallback( - (e: React.ChangeEvent) => onDataChange(parseInt(e.target.value, 10)), - [onDataChange] + (e: React.ChangeEvent) => model.setChunkSize(parseInt(e.target.value, 10)), + [model] ); return styled(useStyles(tableFooterStyles))( - - + - {requestStatusMessage.length > 0 && ( + + {model.requestStatusMessage.length > 0 && ( )} ); -} +}); diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx new file mode 100644 index 0000000000..a85cdf7e12 --- /dev/null +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -0,0 +1,44 @@ +/* + * 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 { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { useService } from '@dbeaver/core/di'; + +import { TableViewerModel } from '../../TableViewerModel'; +import { TableFooterMenuItem } from './TableFooterMenuItem'; +import { TableFooterMenuService } from './TableFooterMenuService'; + +const styles = css` + menu-wrapper { + display: flex; + height: 100%; + } + TableFooterMenuItem { + text-transform: uppercase; + font-weight: 700; + } +`; + +type TableFooterMenuProps = { + model: TableViewerModel; + className?: string; +} + +export const TableFooterMenu = observer(function TableFooterMenu({ model, className }: TableFooterMenuProps) { + const mainMenuService = useService(TableFooterMenuService); + + return styled(styles)( + + {mainMenuService.constructMenuWithContext(model).map((topItem, i) => ( + + ))} + + ); +}); diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx new file mode 100644 index 0000000000..49d439a556 --- /dev/null +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx @@ -0,0 +1,95 @@ +/* + * 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 { observer } from 'mobx-react'; +import { ButtonHTMLAttributes } from 'react'; +import { Button } from 'reakit'; +import styled, { css } from 'reshadow'; + +import { IconOrImage } from '@dbeaver/core/blocks'; +import { IMenuItem, MenuTrigger } from '@dbeaver/core/dialogs'; +import { useTranslate } from '@dbeaver/core/localization'; +import { composes, useStyles } from '@dbeaver/core/theming'; + +type TableFooterMenuItemProps = ButtonHTMLAttributes & { + menuItem: IMenuItem; +}; + + +export const tableFooterMenuStyles = composes( + css` + Menu { + composes: theme-text-on-surface from global; + } + Button { + composes: theme-text-on-secondary from global; + } + MenuTrigger, Button { + composes: theme-ripple from global; + } + `, + css` + MenuTrigger, Button { + height: 100%; + padding: 0 8px !important; + display: flex; + align-items: center; + cursor: pointer; + } + Button { + outline: none; + } + menu-trigger-icon IconOrImage { + display: block; + width: 24px; + } + menu-trigger-title { + display: block; + padding-left: 8px; + } + ` +); + +export const TableFooterMenuItem = observer(function TableFooterMenuItem({ + menuItem, + ...props +}: TableFooterMenuItemProps) { + const translate = useTranslate(); + + if (!menuItem.panel) { + return styled(useStyles(tableFooterMenuStyles))( + + ); + } + + return styled(useStyles(tableFooterMenuStyles))( + + {menuItem.icon && ( + + + + )} + {menuItem.title && {translate(menuItem.title)}} + + ); +}); diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts new file mode 100644 index 0000000000..4c4581b53b --- /dev/null +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -0,0 +1,37 @@ +/* + * 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 { injectable } from '@dbeaver/core/di'; +import { + ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem +} from '@dbeaver/core/dialogs'; + +import { TableViewerModel } from '../../TableViewerModel'; + +@injectable() +export class TableFooterMenuService { + + static nodeContextType = 'NodeWithParent'; + private tableFooterMenuToken = 'tableFooterMenu'; + + constructor(private contextMenuService: ContextMenuService) {} + + constructMenuWithContext(model: TableViewerModel): IMenuItem[] { + const context: IMenuContext = { + menuId: this.tableFooterMenuToken, + contextId: model.tableId, + contextType: TableFooterMenuService.nodeContextType, + data: model, + }; + return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems; + } + + registerMenuItem(options: IContextMenuItem, panelId?: string): void { + this.contextMenuService.addMenuItem(panelId || this.tableFooterMenuToken, options); + } +} diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid/TableGrid.tsx b/webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid.tsx similarity index 95% rename from webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid/TableGrid.tsx rename to webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid.tsx index 458b11ddea..be2a5c34f9 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid/TableGrid.tsx +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableGrid.tsx @@ -13,7 +13,7 @@ import styled, { css } from 'reshadow'; import { AgGridTable } from '@dbeaver/ag-grid-plugin'; import { Button } from '@dbeaver/core/blocks'; -import { TableViewerModel } from '../TableViewerModel'; +import { TableViewerModel } from './TableViewerModel'; type TableGridProps = PropsWithChildren<{ model: TableViewerModel; diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewer.tsx b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewer.tsx index 832c76c921..f7465ca04e 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewer.tsx @@ -14,7 +14,7 @@ import { Loader } from '@dbeaver/core/blocks'; import { useService } from '@dbeaver/core/di'; import { TableFooter } from './TableFooter/TableFooter'; -import { TableGrid } from './TableGrid/TableGrid'; +import { TableGrid } from './TableGrid'; import { TableViewerStorageService } from './TableViewerStorageService'; const viewerStyles = css` @@ -37,7 +37,7 @@ export const TableViewer = observer(function TableViewer({ }: TableViewerProps) { const tableViewerStorageService = useService(TableViewerStorageService); - const tableViewerModel = tableViewerStorageService.getTableModel(tableId); + const tableViewerModel = tableViewerStorageService.get(tableId); if (!tableViewerModel) { return ; @@ -46,13 +46,7 @@ export const TableViewer = observer(function TableViewer({ return styled(viewerStyles)( - + ); diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts index f3a5177a69..6516d461bc 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerModel.ts @@ -12,7 +12,9 @@ import { IAgGridModel, IRequestedData } from '@dbeaver/ag-grid-plugin'; import { ErrorDetailsDialog } from '@dbeaver/core/app'; import { CommonDialogService } from '@dbeaver/core/dialogs'; import { GQLError } from '@dbeaver/core/sdk'; +import { uuid } from '@dbeaver/core/utils'; +import { IExecutionContext } from '../IExecutionContext'; import { ErrorDialog } from './ErrorDialog'; import { RowDiff } from './TableDataModel/EditedRow'; import { TableColumn } from './TableDataModel/TableColumn'; @@ -26,6 +28,18 @@ export const fetchingSettings = { fetchDefault: 200, }; +export interface ITableViewerModelOptions { + tableId?: string; + connectionId: string; + containerNodePath?: string; + resultId?: string | null; // will be filled after fist data fetch + sqlContextParams?: IExecutionContext | null; // will be filled before fist data fetch + initialState?: IRequestDataResult; + noLoaderWhileRequestingDataAsync?: boolean; + requestDataAsync(model: TableViewerModel, rowOffset: number, count: number): Promise; + saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise; +} + export interface IRequestDataResult { rows: TableRow[]; columns: TableColumn[]; @@ -34,14 +48,16 @@ export interface IRequestDataResult { statusMessage: string; } -export interface ITableViewerModelInit { +export class TableViewerModel implements ITableViewerModelOptions { + tableId: string; + connectionId: string; + containerNodePath?: string; + resultId: string | null; + sqlContextParams: IExecutionContext | null; initialState?: IRequestDataResult; - requestDataAsync(rowOffset: number, count: number): Promise; noLoaderWhileRequestingDataAsync?: boolean; - saveChanges(diffs: RowDiff[]): Promise; -} - -export class TableViewerModel { + requestDataAsync: (model: TableViewerModel, rowOffset: number, count: number) => Promise; + saveChanges: (model: TableViewerModel, diffs: RowDiff[]) => Promise; agGridModel: IAgGridModel = { initialRows: [], @@ -84,16 +100,20 @@ export class TableViewerModel { private tableEditor = new TableEditor(this.tableDataModel); constructor( - private init: ITableViewerModelInit, + options: ITableViewerModelOptions, private commonDialogService: CommonDialogService ) { - if (init.initialState) { - this.insertRows(0, init.initialState.rows, !init.initialState.isFullyLoaded); - this.tableDataModel.overWrite(init.initialState.columns); - this.updateInfo(init.initialState.statusMessage, init.initialState.duration); - this.agGridModel.initialRows = this.tableDataModel.getRows(); - this.agGridModel.initialColumns = this.tableDataModel.getColumns(); - } + this.tableId = options.tableId || uuid(); + this.connectionId = options.connectionId; + this.containerNodePath = options.containerNodePath; + this.resultId = options.resultId || null; + this.sqlContextParams = options.sqlContextParams || null; + this.initialState = options.initialState; + this.noLoaderWhileRequestingDataAsync = options.noLoaderWhileRequestingDataAsync; + this.requestDataAsync = options.requestDataAsync; + this.saveChanges = options.saveChanges; + + this.init(); } cancelFetch = () => { @@ -105,6 +125,16 @@ export class TableViewerModel { } } + private init() { + if (this.initialState) { + this.insertRows(0, this.initialState.rows, !this.initialState.isFullyLoaded); + this.tableDataModel.overWrite(this.initialState.columns); + this.updateInfo(this.initialState.statusMessage, this.initialState.duration); + this.agGridModel.initialRows = this.tableDataModel.getRows(); + this.agGridModel.initialColumns = this.tableDataModel.getColumns(); + } + } + private async onCellEditingStopped(rowNumber: number, colNumber: number, value: any): Promise { this.tableEditor.editCellValue(rowNumber, colNumber, value); } @@ -120,10 +150,10 @@ export class TableViewerModel { return data; } - this._isLoaderVisible = !this.init.noLoaderWhileRequestingDataAsync; + this._isLoaderVisible = !this.noLoaderWhileRequestingDataAsync; try { - const response = await this.init.requestDataAsync(rowOffset, count); + const response = await this.requestDataAsync(this, rowOffset, count); this.insertRows(rowOffset, response.rows, !response.isFullyLoaded); if (!this.tableDataModel.getColumns().length) { @@ -234,7 +264,7 @@ export class TableViewerModel { this._isLoaderVisible = true; try { - const data = await this.init.saveChanges(diffs); + const data = await this.saveChanges(this, diffs); const someRows = this.zipDiffAndResults(diffs, data.rows); this.tableEditor.cancelChanges(); diff --git a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerStorageService.ts b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerStorageService.ts index 289d4162d0..52aac251c6 100644 --- a/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerStorageService.ts +++ b/webapp/packages/data-viewer-plugin/src/TableViewer/TableViewerStorageService.ts @@ -9,26 +9,33 @@ import { observable } from 'mobx'; import { injectable } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; -import { TableViewerModel } from './TableViewerModel'; +import { TableViewerModel, ITableViewerModelOptions } from './TableViewerModel'; -// todo this service must be removed. Store TableViewerModel directly where it is required @injectable() export class TableViewerStorageService { @observable private tableModelMap: Map = new Map(); - hasTableModel(tableId: string): boolean { + constructor(private commonDialogService: CommonDialogService) {} + + has(tableId: string): boolean { return this.tableModelMap.has(tableId); } - getTableModel(tableId: string) { + get(tableId: string) { return this.tableModelMap.get(tableId); } - addTableModel(tableId: string, tableModel: TableViewerModel) { - this.tableModelMap.set(tableId, tableModel); + create( + options: ITableViewerModelOptions, + ): TableViewerModel { + + const tableModel = new TableViewerModel(options, this.commonDialogService); + this.tableModelMap.set(tableModel.tableId, tableModel); + return tableModel; } - removeTableModel(tableId: string) { + remove(tableId: string) { this.tableModelMap.delete(tableId); } } diff --git a/webapp/packages/data-viewer-plugin/src/index.ts b/webapp/packages/data-viewer-plugin/src/index.ts index b69a8b1f53..cfc5078a2f 100644 --- a/webapp/packages/data-viewer-plugin/src/index.ts +++ b/webapp/packages/data-viewer-plugin/src/index.ts @@ -6,8 +6,11 @@ export default manifest; export * from './TableViewer/TableViewerStorageService'; export * from './TableViewer/TableViewer'; +export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; export * from './TableViewer/TableViewerModel'; export * from './TableViewer/TableDataModel/TableColumn'; export * from './TableViewer/TableDataModel/TableRow'; export * from './TableViewer/TableDataModel/EditedRow'; + +export * from './IExecutionContext'; diff --git a/webapp/packages/data-viewer-plugin/src/manifest.ts b/webapp/packages/data-viewer-plugin/src/manifest.ts index 8b93429062..b9ecf9c2aa 100644 --- a/webapp/packages/data-viewer-plugin/src/manifest.ts +++ b/webapp/packages/data-viewer-plugin/src/manifest.ts @@ -11,6 +11,7 @@ import { IServiceInjector, PluginManifest } from '@dbeaver/core/di'; import { DataViewerBootstrap } from './DataViewerBootstrap'; import { DataViewerTableService } from './DataViewerTableService'; import { DataViewerTabService } from './DataViewerTabService'; +import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; export const manifest: PluginManifest = { @@ -22,6 +23,7 @@ export const manifest: PluginManifest = { DataViewerTabService, DataViewerTableService, TableViewerStorageService, + TableFooterMenuService, ], async initialize(services: IServiceInjector) { diff --git a/webapp/packages/sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/sql-editor/src/ISqlEditorTabState.ts index 8afe4a20a4..f7cc906878 100644 --- a/webapp/packages/sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/sql-editor/src/ISqlEditorTabState.ts @@ -7,23 +7,17 @@ */ +import { IExecutionContext } from '@dbeaver/data-viewer-plugin'; + import { SqlExecutionState } from './SqlExecutionState'; import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess'; -export interface ISqlContextParams { - contextId: string; - connectionId: string; - objectCatalogId?: string; - objectSchemaId?: string; -} - -export interface ISqlQueryParams extends ISqlContextParams { +export interface ISqlQueryParams extends IExecutionContext { query: string; } export interface ISqlResultPanelParams { resultTabId: string; // to store tableView in tableViewStore - resultId: string | null; // returns from the server, new id returns after each data fetch indexInResultSet: number; sqlQueryParams: ISqlQueryParams; sqlExecutionState: SqlExecutionState; @@ -40,7 +34,7 @@ export interface IResultsTabState { panelParams: ISqlResultPanelParams; } -export interface ISqlEditorTabState extends ISqlContextParams { +export interface ISqlEditorTabState extends IExecutionContext { order: number; query: string; currentResultTabId?: string; diff --git a/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts index e0e9818383..2ecdf6047c 100644 --- a/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts @@ -21,8 +21,9 @@ import { import { injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; import { GraphQLService } from '@dbeaver/core/sdk'; +import { IExecutionContext } from '@dbeaver/data-viewer-plugin'; -import { ISqlEditorTabState, ISqlContextParams } from './ISqlEditorTabState'; +import { ISqlEditorTabState } from './ISqlEditorTabState'; import { SqlDialectInfoService } from './SqlDialectInfoService'; import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; import { SqlExecutionState } from './SqlExecutionState'; @@ -201,7 +202,7 @@ export class SqlEditorNavigatorService { connectionId: string, defaultCatalog?: string, defaultSchema?: string - ): Promise { + ): Promise { const response = await this.gql.gql.sqlContextCreate({ connectionId, diff --git a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts index cf339e7d61..5695c75200 100644 --- a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts +++ b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultPanel/SqlResultPanelController.ts @@ -17,9 +17,9 @@ import { PromiseCancelledError } from '@dbeaver/core/utils'; import { fetchingSettings, IRequestDataResult, - ITableViewerModelInit, RowDiff, - TableViewerModel, + RowDiff, TableViewerStorageService, + TableViewerModel, } from '@dbeaver/data-viewer-plugin'; import { ISqlResultPanelParams } from '../../ISqlEditorTabState'; @@ -69,10 +69,14 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr } else { this.state = EPanelState.TABLE_RESULT; - this.panelInit.resultId = dataSet.resultSet.id; const initialState = this.sqlResultService .sqlExecuteInfoToData(response, this.panelInit.indexInResultSet, fetchingSettings.fetchDefault); - this.createTableModel(initialState, panelInit.sqlExecutionState); + this.createTableModel( + initialState, + panelInit.sqlQueryParams.connectionId, + dataSet.resultSet.id, + panelInit.sqlExecutionState + ); } } catch (exception) { @@ -109,7 +113,7 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr destruct() { if (this.state === EPanelState.TABLE_RESULT) { - this.tableViewerStorageService.removeTableModel(this.getTableId()); + this.tableViewerStorageService.remove(this.getTableId()); } } @@ -123,20 +127,29 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr } } - private createTableModel(initialState: IRequestDataResult, sqlExecutingState: SqlExecutionState): void { - const callbacks: ITableViewerModelInit = { + private createTableModel( + initialState: IRequestDataResult, + connectionId: string, + resultId: string, + sqlExecutingState: SqlExecutionState + ) { + return this.tableViewerStorageService.create({ + tableId: this.getTableId(), + connectionId, + resultId, initialState, requestDataAsync: this.requestDataAsync.bind(this, sqlExecutingState), noLoaderWhileRequestingDataAsync: true, saveChanges: this.saveChanges.bind(this), - }; - const table = new TableViewerModel(callbacks, this.commonDialogService); - this.tableViewerStorageService.addTableModel(this.getTableId(), table); + }); } - private async requestDataAsync(sqlExecutingState: SqlExecutionState, - rowOffset: number, - count: number): Promise { + private async requestDataAsync( + sqlExecutingState: SqlExecutionState, + model: TableViewerModel, + rowOffset: number, + count: number, + ): Promise { const queryExecutionProcess = this.sqlResultService.asyncSqlQuery(this.panelInit.sqlQueryParams, rowOffset, count); sqlExecutingState.setCurrentlyExecutingQuery(queryExecutionProcess); @@ -147,14 +160,16 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr * Note that each data fetching overwrites resultId */ const dataSet = response.results![this.panelInit.indexInResultSet]!.resultSet!; - this.panelInit.resultId = dataSet.id; + model.resultId = dataSet.id; return dataResults; } - async saveChanges(diffs: RowDiff[]): Promise { + async saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise { + if (!model.resultId) { + throw new Error('resultId must be provided before saving changes'); + } - const resultId = this.panelInit.resultId!; // must be set after first data request; - const response = await this.sqlResultService.saveChanges(this.panelInit.sqlQueryParams, resultId, diffs); + const response = await this.sqlResultService.saveChanges(this.panelInit.sqlQueryParams, model.resultId, diffs); return this.sqlResultService.sqlExecuteInfoToData(response, this.panelInit.indexInResultSet); } diff --git a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultService.ts b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultService.ts index 829a5b8ba6..fcfca55351 100644 --- a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultService.ts +++ b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultService.ts @@ -23,7 +23,6 @@ export class SqlResultService { constructor(private graphQLService: GraphQLService, private notificationService: NotificationService) { } - /** * @deprecated use asyncSqlQuery */ diff --git a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 8748ab411d..483fed9ec8 100644 --- a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -7,7 +7,7 @@ */ import { injectable } from '@dbeaver/core/di'; -import { isPromiseCancelledError, PromiseCancelledError, uuid } from '@dbeaver/core/utils'; +import { isPromiseCancelledError, uuid } from '@dbeaver/core/utils'; import { fetchingSettings } from '@dbeaver/data-viewer-plugin'; import { @@ -38,10 +38,9 @@ export class SqlResultTabsService { editorState.sqlExecutionState.setCurrentlyExecutingQuery(queryExecutionProcess); - function createTabPanelParams(resultId: string | null, indexInResultSet: number): ISqlResultPanelParams { + function createTabPanelParams(indexInResultSet: number): ISqlResultPanelParams { const params: ISqlResultPanelParams = { resultTabId: uuid(), - resultId, indexInResultSet, sqlQueryParams, firstDataPortion: queryExecutionProcess, @@ -50,7 +49,7 @@ export class SqlResultTabsService { return params; } - const firstTabPanelParams = createTabPanelParams(null, 0); + const firstTabPanelParams = createTabPanelParams(0); const currentTab = editorState.resultTabs.find(tab => tab.resultTabId === editorState.currentResultTabId); const groupToReplace = inNewTab ? undefined : currentTab?.groupId; @@ -75,7 +74,7 @@ export class SqlResultTabsService { if (data.results && data.results?.length > 1) { // ignore first dataset, it was already added for (let i = 1; i < data.results.length; i++) { - const tabPanelParams = createTabPanelParams(data.results[i].resultSet?.id || null, i); + const tabPanelParams = createTabPanelParams(i); this.addTabToGroup(editorState.resultTabs, tabPanelParams, newGroupId); } }