mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(data-viewer-plugin): grid footer menu
refactor(data-viewer-plugin): TableViewerModel
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+44
@@ -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>
|
||||
);
|
||||
});
|
||||
+95
@@ -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>
|
||||
);
|
||||
});
|
||||
+37
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+32
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user