refactor: CB-491 Database Data API

This commit is contained in:
Wroud
2021-01-14 01:01:08 +03:00
parent d067b38e0d
commit 3ef074b361
11 changed files with 143 additions and 161 deletions
@@ -11,9 +11,9 @@ import {
} from '@cloudbeaver/core-app';
import { injectable } from '@cloudbeaver/core-di';
import {
IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService
IMenuContext, CommonDialogService, ContextMenuService
} from '@cloudbeaver/core-dialogs';
import { TableFooterMenuService, DataModelWrapper } from '@cloudbeaver/plugin-data-viewer';
import { TableFooterMenuService, ITableFooterMenuContext } from '@cloudbeaver/plugin-data-viewer';
import { DataExportDialog } from './Dialog/DataExportDialog';
@@ -25,8 +25,8 @@ export class DataExportMenuService {
private contextMenuService: ContextMenuService
) { }
register() {
const exportData: IContextMenuItem<DataModelWrapper> = {
register(): void {
this.tableFooterMenuService.registerMenuItem({
id: 'export ',
isPresent(context) {
return context.contextType === TableFooterMenuService.nodeContextType;
@@ -35,8 +35,7 @@ export class DataExportMenuService {
title: 'data_transfer_dialog_export',
icon: 'table-export',
onClick: this.exportData.bind(this),
};
this.tableFooterMenuService.registerMenuItem(exportData);
});
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.getRootMenuToken(),
@@ -60,13 +59,18 @@ export class DataExportMenuService {
);
}
private exportData(context: IMenuContext<DataModelWrapper>) {
private exportData(context: IMenuContext<ITableFooterMenuContext>) {
const result = context.data.model.getResult(context.data.resultIndex);
if (!result) {
throw new Error('Result must be provided');
}
this.commonDialogService.open(DataExportDialog, {
connectionId: context.data.deprecatedModel.connectionId,
contextId: context.data.deprecatedModel.executionContext?.contextId,
containerNodePath: context.data.deprecatedModel.containerNodePath,
resultId: context.data.deprecatedModel.resultId,
sourceName: context.data.deprecatedModel.sourceName,
connectionId: context.data.model.deprecatedModel.connectionId,
contextId: context.data.model.source.executionContext?.contextId,
containerNodePath: context.data.model.deprecatedModel.containerNodePath,
resultId: result.id,
sourceName: context.data.model.deprecatedModel.sourceName,
});
}
}
@@ -17,6 +17,8 @@ import { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult';
import { DataUpdate } from './DatabaseDataModel/IDatabaseDataSource';
import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess';
import { IExecutionContext } from './IExecutionContext';
import { RowDiff } from './TableViewer/TableDataModel/EditedRow';
import { IRequestDataResult } from './TableViewer/TableViewerModel';
export interface IDataContainerOptions {
containerNodePath: string;
@@ -31,7 +33,6 @@ export interface IDataContainerResult extends IDatabaseDataResult {
export class ContainerDataSource extends DatabaseDataSource<IDataContainerOptions, IDataContainerResult> {
@observable currentFetchTableProcess: FetchTableDataAsyncProcess | null;
private executionContext: IExecutionContext | null;
get canCancel(): boolean {
return this.currentFetchTableProcess ? this.currentFetchTableProcess.getState() === EDeferredState.PENDING : false;
@@ -122,6 +123,30 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
throw new Error('Not implemented');
}
/**
* @deprecated will be refactored
*/
async saveDataDeprecated(resultId: string, rows: RowDiff[]): Promise<IRequestDataResult> {
const executionContext = await this.ensureContextCreated();
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
resultsId: resultId,
updatedRows: rows.map(row => ({ data: row.source, updateValues: row.values })),
});
const dataSet = response.result!.results[0].resultSet!; // we expect only one dataset for a table
return {
rows: dataSet.rows!,
columns: [], // not in use while saving data
duration: response.result!.duration,
isFullyLoaded: false, // not in use while saving data
statusMessage: 'Saved successfully',
};
}
async dispose(): Promise<void> {
if (this.executionContext) {
await this.graphQLService.sdk.sqlContextDestroy({
@@ -13,8 +13,6 @@ import { GraphQLService } from '@cloudbeaver/core-sdk';
import { ContainerDataSource } from './ContainerDataSource';
import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataModel';
import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess';
import { IExecutionContext } from './IExecutionContext';
import { RowDiff } from './TableViewer/TableDataModel/EditedRow';
import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
@@ -35,15 +33,6 @@ export class DataViewerTableService {
const model = this.tableViewerStorageService.get(tableId);
if (model) {
await model.dispose();
if (model.deprecatedModel.executionContext) {
try {
await this.graphQLService.sdk.sqlContextDestroy({
connectionId: model.deprecatedModel.executionContext.connectionId,
contextId: model.deprecatedModel.executionContext.contextId,
});
} catch { }
}
}
this.tableViewerStorageService.remove(tableId);
}
@@ -56,14 +45,44 @@ export class DataViewerTableService {
const connectionInfo = await this.connectionInfoResource.load(connectionId);
const source = new ContainerDataSource(this.graphQLService, this.notificationService);
return this.tableViewerStorageService.create(
const dataModel = this.tableViewerStorageService.create(
{
tableId: tabId,
connectionId,
containerNodePath,
access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default,
requestDataAsync: this.requestDataAsync.bind(this, source),
saveChanges: this.saveChanges.bind(this),
requestDataAsync: async (
model: TableViewerModel,
offset: number,
count: number,
): Promise<IRequestDataResult> => {
await dataModel
.setSlice(0, dataModel.countGain + dataModel.source.count)
.requestData();
const result = dataModel.getResult(0);
if (!result) {
throw new Error('Result not exists');
}
return {
rows: result.data.rows!,
columns: result.data.columns!,
duration: dataModel.source.requestInfo.requestDuration,
statusMessage: dataModel.source.requestInfo.requestMessage,
isFullyLoaded: result.loadedFully,
};
},
saveChanges: async (data: TableViewerModel, rows: RowDiff[]): Promise<IRequestDataResult> => {
const result = dataModel.getResult(0);
if (!result) {
throw new Error('It is expected that result was set after first fetch');
}
return await source.saveDataDeprecated(result.id, rows);
},
},
source
.setOptions({
@@ -74,100 +93,8 @@ export class DataViewerTableService {
})
.setSupportedDataFormats(connectionInfo.supportedDataFormats)
)
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.deprecatedModel;
}
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default);
private async createExecutionContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<IExecutionContext> {
const response = await this.graphQLService.sdk.sqlContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
return {
contextId: response.context.id,
connectionId,
objectCatalogId: response.context.defaultCatalog,
objectSchemaId: response.context.defaultSchema,
};
}
private async saveChanges(data: TableViewerModel, rows: RowDiff[]): Promise<IRequestDataResult> {
if (!data.resultId) {
throw new Error('It is expected that resultId was set after first fetch');
}
if (!data.executionContext) {
throw new Error('It is expected that data.sqlContextParams was set after first fetch');
}
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: data.executionContext.connectionId,
contextId: data.executionContext.contextId,
resultsId: data.resultId,
updatedRows: rows.map(row => ({ data: row.source, updateValues: row.values })),
});
const dataSet = response.result!.results[0].resultSet!; // we expect only one dataset for a table
const result: IRequestDataResult = {
rows: dataSet.rows!,
columns: [], // not in use while saving data
duration: response.result!.duration,
isFullyLoaded: false, // not in use while saving data
statusMessage: 'Saved successfully',
};
return result;
}
private async requestDataAsync(
source: ContainerDataSource,
model: TableViewerModel,
offset: number,
count: number,
): Promise<IRequestDataResult> {
if (!model.containerNodePath) {
throw new Error('containerNodePath must be provided for table');
}
if (!model.executionContext) {
// it is first data request
const executionContext: IExecutionContext = await this.createExecutionContext(model.connectionId);
model.executionContext = executionContext;
}
const fetchTableProcess = new FetchTableDataAsyncProcess(this.graphQLService, this.notificationService);
fetchTableProcess.start(
{
connectionId: model.executionContext.connectionId,
contextId: model.executionContext.contextId,
containerNodePath: model.containerNodePath,
},
{
offset,
limit: count,
constraints: Array.from(model.getSortedColumns()),
where: model.getQueryWhereFilter() || undefined,
},
);
source.currentFetchTableProcess = fetchTableProcess;
const response = await fetchTableProcess.promise;
const dataSet = response!.results[0].resultSet!; // we expect only one dataset for a table
model.resultId = dataSet.id; // server generates new resultId on each fetch
const result: IRequestDataResult = {
rows: dataSet.rows!,
columns: dataSet.columns!,
duration: response!.duration,
statusMessage: response!.statusMessage || '',
isFullyLoaded: (dataSet.rows?.length || 0) < count,
};
return result;
return dataModel.deprecatedModel;
}
}
@@ -10,16 +10,18 @@ import { observable } from 'mobx';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { IExecutionContext } from '../IExecutionContext';
import { IDatabaseDataResult } from './IDatabaseDataResult';
import { DataUpdate, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataSource<TOptions, TResult> {
implements IDatabaseDataSource<TOptions, TResult> {
@observable offset: number;
@observable count: number;
@observable dataFormat: ResultDataFormat;
@observable options: TOptions | null;
@observable requestInfo: IRequestInfo;
@observable executionContext: IExecutionContext | null;
@observable supportedDataFormats: ResultDataFormat[];
abstract get canCancel(): boolean;
@@ -32,6 +34,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
this.options = null;
this.activeRequest = null;
this.activeSave = null;
this.executionContext = null;
this.dataFormat = ResultDataFormat.Resultset;
this.supportedDataFormats = [];
this.requestInfo = {
@@ -67,6 +70,11 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
return this;
}
setExecutionContext(context: IExecutionContext | null): this {
this.executionContext = context;
return this;
}
async requestData(prevResults: TResult[]): Promise<TResult[]> {
if (this.activeSave) {
try {
@@ -8,6 +8,7 @@
import { ResultDataFormat, SqlResultSet } from '@cloudbeaver/core-sdk';
import { IExecutionContext } from '../IExecutionContext';
import { IDatabaseDataResult } from './IDatabaseDataResult';
export enum DataUpdateType {
@@ -33,6 +34,7 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
readonly options: TOptions | null;
readonly requestInfo: IRequestInfo;
readonly dataFormat: ResultDataFormat;
readonly executionContext: IExecutionContext | null;
readonly supportedDataFormats: ResultDataFormat[];
readonly canCancel: boolean;
@@ -41,6 +43,7 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
setOptions: (options: TOptions) => this;
setDataFormat: (dataFormat: ResultDataFormat) => this;
setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this;
setExecutionContext: (context: IExecutionContext | null) => this;
requestData: (
prevResults: TResult[]
) => Promise<TResult[]> | TResult[];
@@ -68,10 +68,12 @@ const tableFooterStyles = composes(
);
interface TableFooterProps {
resultIndex: number;
model: DataModelWrapper;
}
export const TableFooter = observer(function TableFooter({
resultIndex,
model,
}: TableFooterProps) {
const ref = useRef<HTMLInputElement>(null);
@@ -100,7 +102,7 @@ export const TableFooter = observer(function TableFooter({
<input ref={ref} type="number" value={model.countGain} onBlur={handleChange} {...use({ mod: 'surface' })} />
</SubmittingForm>
</count>
<TableFooterMenu model={model} />
<TableFooterMenu model={model} resultIndex={resultIndex} />
{(model.source.dataFormat === ResultDataFormat.Resultset
&& model.deprecatedModel.requestStatusMessage.length > 0) && (
<time>
@@ -27,16 +27,21 @@ const styles = css`
`;
interface TableFooterMenuProps {
resultIndex: number;
model: DataModelWrapper;
className?: string;
}
export const TableFooterMenu = observer(function TableFooterMenu({ model, className }: TableFooterMenuProps) {
export const TableFooterMenu = observer(function TableFooterMenu({
resultIndex,
model,
className,
}: TableFooterMenuProps) {
const mainMenuService = useService(TableFooterMenuService);
return styled(styles)(
<menu-wrapper as="div" className={className}>
{mainMenuService.constructMenuWithContext(model).map((topItem, i) => (
{mainMenuService.constructMenuWithContext(model, resultIndex).map((topItem, i) => (
<TableFooterMenuItem key={i} menuItem={topItem} />
))}
</menu-wrapper>
@@ -13,6 +13,11 @@ import {
import { DataModelWrapper } from '../../DataModelWrapper';
export interface ITableFooterMenuContext {
model: DataModelWrapper;
resultIndex: number;
}
@injectable()
export class TableFooterMenuService {
static nodeContextType = 'NodeWithParent';
@@ -27,12 +32,12 @@ export class TableFooterMenuService {
return context.contextType === TableFooterMenuService.nodeContextType;
},
isDisabled(context) {
return !context.data.deprecatedModel.isEdited();
return !context.data.model.deprecatedModel.isEdited();
},
order: 1,
title: 'ui_processing_save',
icon: 'table-save',
onClick: context => context.data.deprecatedModel.saveChanges(),
onClick: context => context.data.model.deprecatedModel.saveChanges(),
});
this.registerMenuItem({
id: 'cancel ',
@@ -40,26 +45,26 @@ export class TableFooterMenuService {
return context.contextType === TableFooterMenuService.nodeContextType;
},
isDisabled(context) {
return !context.data.deprecatedModel.isEdited();
return !context.data.model.deprecatedModel.isEdited();
},
order: 2,
title: 'ui_processing_cancel',
icon: 'table-cancel',
onClick: context => context.data.deprecatedModel.cancelChanges(),
onClick: context => context.data.model.deprecatedModel.cancelChanges(),
});
}
constructMenuWithContext(model: DataModelWrapper): IMenuItem[] {
const context: IMenuContext<DataModelWrapper> = {
constructMenuWithContext(model: DataModelWrapper, resultIndex: number): IMenuItem[] {
const context: IMenuContext<ITableFooterMenuContext> = {
menuId: this.tableFooterMenuToken,
contextId: model.id,
contextType: TableFooterMenuService.nodeContextType,
data: model,
data: { model, resultIndex },
};
return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems;
}
registerMenuItem(options: IContextMenuItem<DataModelWrapper>): void {
this.contextMenuService.addMenuItem<DataModelWrapper>(this.tableFooterMenuToken, options);
registerMenuItem(options: IContextMenuItem<ITableFooterMenuContext>): void {
this.contextMenuService.addMenuItem<ITableFooterMenuContext>(this.tableFooterMenuToken, options);
}
}
@@ -52,7 +52,7 @@ type TableViewerProps = PropsWithChildren<{
export const TableViewer = observer(function TableViewer({
tableId,
resultIndex,
resultIndex = 0,
presentationId,
className,
onPresentationChange,
@@ -60,13 +60,15 @@ export const TableViewer = observer(function TableViewer({
const dataPresentationService = useService(DataPresentationService);
const tableViewerStorageService = useService(TableViewerStorageService);
const dataModel = tableViewerStorageService.get(tableId);
const result = dataModel?.getResult(resultIndex || 0);
const result = dataModel?.getResult(resultIndex);
const handlePresentationChange = useCallback((id: string) => {
const presentation = dataPresentationService.get(id);
if (presentation && presentation.dataFormat !== dataModel?.source.dataFormat) {
dataModel?.setDataFormat(presentation.dataFormat)
.reload();
if (presentation) {
if (presentation.dataFormat !== dataModel?.source.dataFormat) {
dataModel?.setDataFormat(presentation.dataFormat)
.reload();
}
onPresentationChange(id);
}
}, [onPresentationChange, dataModel]);
@@ -97,7 +99,7 @@ export const TableViewer = observer(function TableViewer({
/>
<table-data as='div'>
<TableGrid model={dataModel} presentation={presentation} />
<TableFooter model={dataModel} />
<TableFooter model={dataModel} resultIndex={resultIndex} />
</table-data>
</table-content>
<Loader
@@ -15,7 +15,6 @@ import { GQLError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import { uuid, MetadataMap } from '@cloudbeaver/core-utils';
import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
import { IExecutionContext } from '../IExecutionContext';
import { ErrorDialog } from './ErrorDialog';
import { RowDiff } from './TableDataModel/EditedRow';
import { TableColumn } from './TableDataModel/TableColumn';
@@ -65,8 +64,6 @@ export interface ITableViewerModelOptions {
tableId?: string;
connectionId: string;
containerNodePath?: string;
resultId?: string | null; // will be filled after fist data fetch
executionContext?: IExecutionContext | null; // will be filled before fist data fetch
sourceName?: string; // TODO: refactor it, used for showing sql query for export
noLoaderWhileRequestingDataAsync?: boolean;
access?: DatabaseDataAccessMode;
@@ -90,8 +87,6 @@ export class TableViewerModel {
tableId: string;
connectionId: string;
containerNodePath?: string;
resultId: string | null;
executionContext: IExecutionContext | null;
sourceName?: string;
noLoaderWhileRequestingDataAsync?: boolean;
@@ -153,8 +148,6 @@ export class TableViewerModel {
this.tableId = options.tableId || uuid();
this.connectionId = options.connectionId;
this.containerNodePath = options.containerNodePath;
this.resultId = options.resultId || null;
this.executionContext = options.executionContext || null;
this.sourceName = options.sourceName;
this.noLoaderWhileRequestingDataAsync = options.noLoaderWhileRequestingDataAsync;
this.access = options.access || DatabaseDataAccessMode.Default;
@@ -23,7 +23,8 @@ import {
RowDiff,
TableViewerStorageService,
TableViewerModel,
DatabaseDataAccessMode
DatabaseDataAccessMode,
DataModelWrapper
} from '@cloudbeaver/plugin-data-viewer';
import { IResultDataTab, IQueryTabGroup } from '../../ISqlEditorTabState';
@@ -53,6 +54,7 @@ implements IInitializableController, IDestructibleController {
private group!: IQueryTabGroup;
private tabId!: string;
private source: QueryDataSource;
private model: DataModelWrapper | null;
constructor(
private sqlResultService: SqlResultService,
@@ -64,6 +66,7 @@ implements IInitializableController, IDestructibleController {
private sqlResultTabsService: SqlResultTabsService
) {
this.source = new QueryDataSource(this.sqlEditorGroupMetadataService, this.sqlResultTabsService);
this.model = null;
}
async init(tabId: string, panelInit: IResultDataTab, group: IQueryTabGroup) {
@@ -112,15 +115,19 @@ implements IInitializableController, IDestructibleController {
constraints: [],
whereFilter: '',
})
.setExecutionContext({
connectionId: this.group.sqlQueryParams.connectionId,
contextId: this.group.sqlQueryParams.contextId,
objectCatalogId: this.group.sqlQueryParams.objectCatalogId,
objectSchemaId: this.group.sqlQueryParams.objectSchemaId,
})
.setDataFormat(dataSet.dataFormat || ResultDataFormat.Resultset)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
const tableModel = this.tableViewerStorageService.create(
this.model = this.tableViewerStorageService.create(
{
tableId: this.getTableId(),
connectionId: this.group.sqlQueryParams.connectionId,
executionContext: this.group.sqlQueryParams,
resultId: dataSet.resultSet.id,
sourceName: this.group.sqlQueryParams.query,
access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default,
requestDataAsync: this.requestDataAsync.bind(this, sqlExecutionContext),
@@ -130,8 +137,9 @@ implements IInitializableController, IDestructibleController {
this.source
)
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.setResults(this.source.getResults(response, fetchingSettings.fetchDefault) || [])
.deprecatedModel;
.setResults(this.source.getResults(response, fetchingSettings.fetchDefault) || []);
const tableModel = this.model.deprecatedModel;
tableModel.insertRows(0, initialState.rows, !initialState.isFullyLoaded);
tableModel.setColumns(initialState.columns);
@@ -214,27 +222,27 @@ implements IInitializableController, IDestructibleController {
// /**
// * Note that each data fetching overwrites resultId
// */
const dataSet = response.results![this.panelInit.indexInResultSet]!.resultSet!;
await this.updateTableInfo(model, dataSet.id);
this.model?.setResults(this.source.getResults(response, fetchingSettings.fetchDefault) || []);
await this.updateTableInfo(model);
return dataResults;
}
private async updateTableInfo(model: TableViewerModel, resultId: string) {
private async updateTableInfo(model: TableViewerModel) {
const connectionInfo = await this.connectionInfoResource.load(this.group.sqlQueryParams.connectionId);
model.resultId = resultId;
model.access = connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default;
model.sourceName = this.group.sqlQueryParams.query;
model.executionContext = this.group.sqlQueryParams;
model.connectionId = this.group.sqlQueryParams.connectionId;
}
async saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise<IRequestDataResult> {
if (!model.resultId) {
throw new Error('resultId must be provided before saving changes');
const result = this.model?.getResult(this.panelInit.indexInResultSet);
if (!result) {
throw new Error('result must be provided before saving changes');
}
const response = await this.sqlResultService.saveChanges(this.group.sqlQueryParams, model.resultId, diffs);
const response = await this.sqlResultService.saveChanges(this.group.sqlQueryParams, result.id, diffs);
return this.sqlResultService.sqlExecuteInfoToData(response, this.panelInit.indexInResultSet);
}