feat(data-viewer-plugin): grid footer menu

refactor(data-viewer-plugin): TableViewerModel
This commit is contained in:
Wroud
2020-04-30 14:48:51 +03:00
parent 75ea44588d
commit 3a8ffb9c24
21 changed files with 383 additions and 148 deletions
@@ -42,8 +42,8 @@ export type ObjectContainer = Pick<DatabaseObjectInfo, 'name' | 'description' |
export class ConnectionsManagerService {
@observable private connectionsMap: Map<string, Connection> = 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());
@@ -32,7 +32,7 @@ export class ContextMenuService {
this.contextMenu.addMenuItem(panelId, menuItem);
}
createContextMenu<T>(context: IMenuContext<T>): IMenuPanel {
return this.contextMenu.constructMenuWithContext(ContextMenuService.rootPanelId, context);
createContextMenu<T>(context: IMenuContext<T>, panelId?: string): IMenuPanel {
return this.contextMenu.constructMenuWithContext(panelId || ContextMenuService.rootPanelId, context);
}
}
@@ -14,5 +14,5 @@ export const DataViewerPanel = function DataViewerPanel({
tab,
}: ObjectPagePanelProps) {
return <TableViewer tableId={tab.handlerState.objectId}/>;
return <TableViewer tableId={tab.id}/>;
};
@@ -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<IObjectViewerTabState>) {
@@ -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<ISqlContextParams> {
): Promise<IExecutionContext> {
const response = await this.graphQLService.gql.sqlContextCreate({
connectionId,
@@ -87,7 +64,7 @@ export class DataViewerTableService {
};
}
private async saveChanges(data: IDataViewerTableModel, rows: RowDiff[]): Promise<IRequestDataResult> {
private async saveChanges(data: TableViewerModel, rows: RowDiff[]): Promise<IRequestDataResult> {
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<IRequestDataResult> {
private async requestDataAsync(
data: TableViewerModel,
rowOffset: number,
count: number
): Promise<IRequestDataResult> {
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,
@@ -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;
}
@@ -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<HTMLInputElement>) => onDataChange(parseInt(e.target.value, 10)),
[onDataChange]
(e: React.ChangeEvent<HTMLInputElement>) => model.setChunkSize(parseInt(e.target.value, 10)),
[model]
);
return styled(useStyles(tableFooterStyles))(
<table-footer as="div">
<reload as="div">
<button type="button" onClick={onRefresh}>
<button type="button" onClick={model.handleRefresh}>
<Icon name="reload" />
</button>
</reload>
<count as="div">
<input type="number" value={chunkSize} onBlur={handleChange} {...use({ mod: 'surface' })} />
<input type="number" value={model.getChunkSize()} onBlur={handleChange} {...use({ mod: 'surface' })} />
</count>
{requestStatusMessage.length > 0 && (
<TableFooterMenu model={model}/>
{model.requestStatusMessage.length > 0 && (
<time>
{requestStatusMessage} - {queryDuration}ms
{model.requestStatusMessage} - {model.queryDuration}ms
</time>
)}
</table-footer>
);
}
});
@@ -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)(
<menu-wrapper as="div" className={className}>
{mainMenuService.constructMenuWithContext(model).map((topItem, i) => (
<TableFooterMenuItem key={i} menuItem={topItem}/>
))}
</menu-wrapper>
);
});
@@ -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<any> & {
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))(
<Button
as="button" {...props}
disabled={menuItem.isDisabled}
onClick={() => menuItem.onClick && menuItem.onClick()}>
{menuItem.icon && (
<menu-trigger-icon as="div">
<IconOrImage icon={menuItem.icon} viewBox="0 0 32 32" />
</menu-trigger-icon>
)}
{menuItem.title && <menu-trigger-title as="div">{translate(menuItem.title)}</menu-trigger-title>}
</Button>
);
}
return styled(useStyles(tableFooterMenuStyles))(
<MenuTrigger
{...props}
panel={menuItem.panel}
disabled={menuItem.isDisabled}
style={[tableFooterMenuStyles]}
>
{menuItem.icon && (
<menu-trigger-icon as="div">
<IconOrImage icon={menuItem.icon} viewBox="0 0 32 32" />
</menu-trigger-icon>
)}
{menuItem.title && <menu-trigger-title as="div">{translate(menuItem.title)}</menu-trigger-title>}
</MenuTrigger>
);
});
@@ -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<TableViewerModel> = {
menuId: this.tableFooterMenuToken,
contextId: model.tableId,
contextType: TableFooterMenuService.nodeContextType,
data: model,
};
return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems;
}
registerMenuItem(options: IContextMenuItem<TableViewerModel>, panelId?: string): void {
this.contextMenuService.addMenuItem<TableViewerModel>(panelId || this.tableFooterMenuToken, options);
}
}
@@ -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;
@@ -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 <Loader />;
@@ -46,13 +46,7 @@ export const TableViewer = observer(function TableViewer({
return styled(viewerStyles)(
<table-viewer as="div" className={className}>
<TableGrid model={tableViewerModel} />
<TableFooter
chunkSize={tableViewerModel.getChunkSize()}
requestStatusMessage={tableViewerModel.requestStatusMessage}
queryDuration={tableViewerModel.queryDuration}
onRefresh={tableViewerModel.handleRefresh}
onDataChange={tableViewerModel.setChunkSize}
/>
<TableFooter model={tableViewerModel} />
<Loader loading={tableViewerModel.isLoaderVisible} overlay/>
</table-viewer>
);
@@ -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<IRequestDataResult>;
saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise<IRequestDataResult>;
}
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<IRequestDataResult>;
noLoaderWhileRequestingDataAsync?: boolean;
saveChanges(diffs: RowDiff[]): Promise<IRequestDataResult>;
}
export class TableViewerModel {
requestDataAsync: (model: TableViewerModel, rowOffset: number, count: number) => Promise<IRequestDataResult>;
saveChanges: (model: TableViewerModel, diffs: RowDiff[]) => Promise<IRequestDataResult>;
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<void> {
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();
@@ -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<string, TableViewerModel> = 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);
}
}
@@ -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';
@@ -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) {
@@ -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;
@@ -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<ISqlContextParams> {
): Promise<IExecutionContext> {
const response = await this.gql.gql.sqlContextCreate({
connectionId,
@@ -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<IRequestDataResult> {
private async requestDataAsync(
sqlExecutingState: SqlExecutionState,
model: TableViewerModel,
rowOffset: number,
count: number,
): Promise<IRequestDataResult> {
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<IRequestDataResult> {
async saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise<IRequestDataResult> {
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);
}
@@ -23,7 +23,6 @@ export class SqlResultService {
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) { }
/**
* @deprecated use asyncSqlQuery
*/
@@ -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);
}
}