refactor(plugin-data-viewer): DatabaseDataModel

CB-287
This commit is contained in:
Wroud
2020-09-28 21:33:26 +03:00
parent ade8a4ec20
commit cdcda9ee39
27 changed files with 704 additions and 96 deletions
@@ -13,7 +13,7 @@ import { injectable } from '@cloudbeaver/core-di';
import {
IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService
} from '@cloudbeaver/core-dialogs';
import { TableFooterMenuService, TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import { TableFooterMenuService, DataModelWrapper } from '@cloudbeaver/plugin-data-viewer';
import { DataExportDialog } from './Dialog/DataExportDialog';
@@ -26,7 +26,7 @@ export class DataExportMenuService {
) { }
register() {
const exportData: IContextMenuItem<TableViewerModel> = {
const exportData: IContextMenuItem<DataModelWrapper> = {
id: 'export ',
isPresent(context) {
return context.contextType === TableFooterMenuService.nodeContextType;
@@ -60,13 +60,13 @@ export class DataExportMenuService {
);
}
private exportData(context: IMenuContext<TableViewerModel>) {
private exportData(context: IMenuContext<DataModelWrapper>) {
this.commonDialogService.open(DataExportDialog, {
connectionId: context.data.connectionId,
contextId: context.data.executionContext?.contextId,
containerNodePath: context.data.containerNodePath,
resultId: context.data.resultId,
sourceName: context.data.sourceName,
connectionId: context.data.deprecatedModel.connectionId,
contextId: context.data.deprecatedModel.executionContext?.contextId,
containerNodePath: context.data.deprecatedModel.containerNodePath,
resultId: context.data.deprecatedModel.resultId,
sourceName: context.data.deprecatedModel.sourceName,
});
}
}
@@ -24,7 +24,7 @@ import {
} from '@ag-grid-community/core';
import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di';
import {
TableViewerModel, SortModel, AgGridRow, IAgGridCol, AccessMode
TableViewerModel, SortModel, AgGridRow, IAgGridCol, DatabaseDataAccessMode
} from '@cloudbeaver/plugin-data-viewer';
import { AgGridContext } from './AgGridContext';
@@ -45,7 +45,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
*/
private readonly context: AgGridContext = {
selection: this.selection,
isReadonly: () => this.gridModel.access === AccessMode.Readonly,
isReadonly: () => this.gridModel.access === DatabaseDataAccessMode.Readonly,
isCellEdited: this.isCellEdited.bind(this),
editCellValue: this.editCellValue.bind(this),
revertCellValue: this.revertCellValue.bind(this),
@@ -6,12 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import { DataModelWrapper } from '@cloudbeaver/plugin-data-viewer';
import { AgGridTable } from './AgGridTable/AgGridTable';
type Props = {
tableModel: TableViewerModel;
tableModel: DataModelWrapper;
className?: string;
}
@@ -20,5 +20,5 @@ export function Spreadsheet({
className,
}: Props) {
return <AgGridTable tableModel={tableModel} className={className} />;
return <AgGridTable tableModel={tableModel.deprecatedModel} className={className} />;
}
@@ -0,0 +1,117 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource';
import { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult';
import { DataUpdate } from './DatabaseDataModel/IDatabaseDataSource';
import { IExecutionContext } from './IExecutionContext';
export interface IDataContainerOptions {
containerNodePath: string;
connectionId: string;
whereFilter: string;
constraints: SqlDataFilterConstraint[];
}
export interface IDataContainerResult extends IDatabaseDataResult {
}
export class ContainerDataSource extends DatabaseDataSource<IDataContainerOptions, IDataContainerResult> {
private executionContext: IExecutionContext | null;
constructor(private graphQLService: GraphQLService) {
super();
this.executionContext = null;
}
async request(
prevResults: IDataContainerResult[]
): Promise<IDataContainerResult[]> {
if (!this.options?.containerNodePath) {
throw new Error('containerNodePath must be provided for table');
}
const executionContext = await this.ensureContextCreated();
const { readDataFromContainer } = await this.graphQLService.sdk.readDataFromContainer({
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
containerNodePath: this.options.containerNodePath,
filter: {
offset: this.offset,
limit: this.count,
constraints: this.options.constraints,
where: this.options.whereFilter || undefined,
},
});
this.requestInfo = {
requestDuration: readDataFromContainer?.duration || 0,
requestMessage: readDataFromContainer?.statusMessage || '',
};
return readDataFromContainer?.results.map<IDataContainerResult>(result => ({
id: result.resultSet!.id,
dataFormat: result.dataFormat!,
loadedFully: (result.resultSet?.rows?.length || 0) < this.count || !result.resultSet?.hasMoreData,
data: result.resultSet,
})) || prevResults;
}
async save(
prevResults: IDataContainerResult[],
data: DataUpdate
): Promise<IDataContainerResult[]> {
const executionContext = await this.ensureContextCreated();
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
resultsId: data.data.id,
// updatedRows: this.getRowsDiff(data),
});
this.requestInfo = {
requestDuration: response.result?.duration || 0,
requestMessage: 'Saved successfully',
};
throw new Error('Not implemented');
}
private async ensureContextCreated(): Promise<IExecutionContext> {
if (!this.executionContext) {
if (!this.options) {
throw new Error('Options must be provided');
}
this.executionContext = await this.createExecutionContext(this.options.connectionId);
}
return this.executionContext;
}
private async createExecutionContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<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,
};
}
}
@@ -8,10 +8,10 @@
import { injectable } from '@cloudbeaver/core-di';
import { TableViewerModel } from './TableViewer/TableViewerModel';
import { DataModelWrapper } from './TableViewer/DataModelWrapper';
export interface IDataPresentationProps {
tableModel: TableViewerModel;
tableModel: DataModelWrapper;
className?: string;
}
@@ -10,9 +10,11 @@ import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { GraphQLService } from '@cloudbeaver/core-sdk';
import { ContainerDataSource } from './ContainerDataSource';
import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataModel';
import { IExecutionContext } from './IExecutionContext';
import { RowDiff } from './TableViewer/TableDataModel/EditedRow';
import { IRequestDataResult, TableViewerModel, AccessMode } from './TableViewer/TableViewerModel';
import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
@injectable()
@@ -22,7 +24,7 @@ export class DataViewerTableService {
private graphQLService: GraphQLService) {
}
has(tableId: string) {
has(tableId: string): boolean {
return this.tableViewerStorageService.has(tableId);
}
@@ -33,7 +35,7 @@ export class DataViewerTableService {
async create(
tabId: string,
connectionId: string,
containerNodePath?: string,
containerNodePath = '',
): Promise<TableViewerModel> {
const connectionInfo = await this.connectionInfoResource.load(connectionId);
@@ -42,11 +44,19 @@ export class DataViewerTableService {
tableId: tabId,
connectionId,
containerNodePath,
access: connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default,
access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default,
requestDataAsync: this.requestDataAsync.bind(this),
saveChanges: this.saveChanges.bind(this),
}
);
},
new ContainerDataSource(this.graphQLService)
)
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.setOptions({
connectionId,
containerNodePath,
constraints: [],
whereFilter: '',
}).deprecatedModel;
}
private async createExecutionContext(
@@ -0,0 +1,13 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { DatabaseDataModel } from './DatabaseDataModel';
export interface DatabaseDataFormat<T> {
processResult(model: DatabaseDataModel<any, any>, data: T): void;
}
@@ -0,0 +1,55 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { uuid } from '@cloudbeaver/core-utils';
import { DatabaseDataAccessMode, IDatabaseDataModel } from './IDatabaseDataModel';
import { IDatabaseDataResult } from './IDatabaseDataResult';
import { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export class DatabaseDataModel<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataModel<TOptions, TResult> {
id: string;
results: TResult[];
source: IDatabaseDataSource<TOptions, TResult>;
access: DatabaseDataAccessMode;
get requestInfo(): IRequestInfo {
return this.source.requestInfo;
}
constructor(source: IDatabaseDataSource<TOptions, TResult>) {
this.id = uuid();
this.source = source;
this.results = [];
this.access = DatabaseDataAccessMode.Default;
}
isLoading(): boolean {
return this.source.isLoading();
}
setAccess(access: DatabaseDataAccessMode): this {
this.access = access;
return this;
}
setSlice(offset: number, count: number): this {
this.source.setSlice(offset, count);
return this;
}
setOptions(options: TOptions): this {
this.source.setOptions(options);
return this;
}
async requestData(): Promise<void> {
this.results = await this.source.requestData(this.results);
}
}
@@ -0,0 +1,98 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { IDatabaseDataResult } from './IDatabaseDataResult';
import { DataUpdate, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataSource<TOptions, TResult> {
offset: number;
count: number;
options: TOptions | null;
requestInfo: IRequestInfo;
private activeRequest: Promise<TResult[]> | null;
private activeSave: Promise<TResult[]> | null;
constructor() {
this.offset = 0;
this.count = 0;
this.options = null;
this.activeRequest = null;
this.activeSave = null;
this.requestInfo = {
requestDuration: 0,
requestMessage: '',
};
}
isLoading(): boolean {
return !!this.activeRequest;
}
setSlice(offset: number, count: number): void {
this.offset = offset;
this.count = count;
}
setOptions(options: TOptions): void {
this.options = options;
}
async requestData(prevResults: TResult[]): Promise<TResult[]> {
if (this.activeSave) {
try {
await this.activeSave;
} finally {}
}
if (this.activeRequest) {
return this.activeRequest;
}
try {
const promise = this.request(prevResults);
if (promise instanceof Promise) {
this.activeRequest = promise;
}
return promise;
} finally {
this.activeRequest = null;
}
}
async saveData(
prevResults: TResult[],
data: DataUpdate
): Promise<TResult[]> {
if (this.activeRequest) {
try {
await this.activeRequest;
} finally {}
}
if (this.activeSave) {
return this.activeSave;
}
try {
const promise = this.save(prevResults, data);
if (promise instanceof Promise) {
this.activeSave = promise;
}
return promise;
} finally {
this.activeSave = null;
}
}
abstract request(prevResults: TResult[]): TResult[] | Promise<TResult[]>;
abstract save(prevResults: TResult[], data: DataUpdate): Promise<TResult[]> | TResult[];
}
@@ -0,0 +1,14 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IDatabaseDataEditor {
isEdited(): boolean;
getChanges(): void;
applyChanges(): void;
cancelChanges(skipUpdate?: boolean): void;
}
@@ -0,0 +1,29 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { IDatabaseDataResult } from './IDatabaseDataResult';
import { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export enum DatabaseDataAccessMode {
Default,
Readonly
}
export interface IDatabaseDataModel<TOptions, TResult extends IDatabaseDataResult> {
readonly id: string;
readonly results: TResult[];
readonly source: IDatabaseDataSource<TOptions, TResult>;
readonly access: DatabaseDataAccessMode;
readonly requestInfo: IRequestInfo;
isLoading(): boolean;
setAccess(access: DatabaseDataAccessMode): this;
setSlice(offset: number, count: number): this;
setOptions(options: TOptions): this;
requestData(): Promise<void>;
}
@@ -0,0 +1,16 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
export interface IDatabaseDataResult {
id: string;
dataFormat: ResultDataFormat;
loadedFully: boolean;
data: any;
}
@@ -0,0 +1,46 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { SqlResultSet } from '@cloudbeaver/core-sdk';
import { IDatabaseDataResult } from './IDatabaseDataResult';
export enum DataUpdateType {
delete,
update,
add
}
export interface IRequestInfo {
readonly requestDuration: number;
readonly requestMessage: string;
}
export interface DataUpdate<T = any> {
data: SqlResultSet;
dataUpdate: SqlResultSet;
type: DataUpdateType;
}
export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResult> {
readonly offset: number;
readonly count: number;
readonly options: TOptions | null;
readonly requestInfo: IRequestInfo;
isLoading(): boolean;
setSlice(offset: number, count: number): void;
setOptions(options: TOptions): void;
requestData(
prevResults: TResult[]
): Promise<TResult[]> | TResult[];
saveData(
prevResults: TResult[],
data: DataUpdate
): Promise<TResult[]> | TResult[];
}
@@ -0,0 +1,57 @@
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel';
import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
import { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource';
import { ITableViewerModelOptions, TableViewerModel } from './TableViewerModel';
export class DataModelWrapper extends DatabaseDataModel<any, any> {
/**
* @deprecated Use getModel instead
*/
readonly deprecatedModel: TableViewerModel;
constructor(
commonDialogService: CommonDialogService,
options: ITableViewerModelOptions,
source: IDatabaseDataSource<any, any>
) {
super(source);
if (options.tableId) {
this.id = options.tableId;
}
this.deprecatedModel = new TableViewerModel(options, commonDialogService);
}
isLoading(): boolean {
return this.deprecatedModel.isLoaderVisible || this.source.isLoading();
}
setAccess(access: DatabaseDataAccessMode): this {
this.access = access;
this.deprecatedModel.access = access;
return this;
}
setSlice(offset: number, count: number): this {
this.source.setSlice(offset, count);
this.deprecatedModel.setChunkSize(count);
return this;
}
setOptions(options: any): this {
this.source.setOptions(options);
return this;
}
async requestData(): Promise<void> {
this.results = await this.source.requestData(this.results);
}
}
@@ -13,7 +13,7 @@ import styled, { css, use } from 'reshadow';
import { IconButton, SubmittingForm } from '@cloudbeaver/core-blocks';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { TableViewerModel } from '../TableViewerModel';
import { DataModelWrapper } from '../DataModelWrapper';
import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu';
const tableFooterStyles = composes(
@@ -67,7 +67,7 @@ const tableFooterStyles = composes(
);
type TableFooterProps = {
model: TableViewerModel;
model: DataModelWrapper;
}
export const TableFooter = observer(function TableFooter({
@@ -81,8 +81,8 @@ export const TableFooter = observer(function TableFooter({
}
const value = parseInt(ref.current.value, 10);
if (model.getChunkSize() !== value) {
model.setChunkSize(value);
if (model.deprecatedModel.getChunkSize() !== value) {
model.deprecatedModel.setChunkSize(value);
}
},
[model]
@@ -91,17 +91,17 @@ export const TableFooter = observer(function TableFooter({
return styled(useStyles(tableFooterStyles))(
<table-footer as="div">
<reload as="div">
<IconButton type="button" name='reload' onClick={model.refresh} viewBox="0 0 16 16"/>
<IconButton type="button" name='reload' onClick={model.deprecatedModel.refresh} viewBox="0 0 16 16"/>
</reload>
<count as="div">
<SubmittingForm onSubmit={handleChange}>
<input type="number" value={model.getChunkSize()} ref={ref} onBlur={handleChange} {...use({ mod: 'surface' })} />
<input type="number" value={model.deprecatedModel.getChunkSize()} ref={ref} onBlur={handleChange} {...use({ mod: 'surface' })} />
</SubmittingForm>
</count>
<TableFooterMenu model={model}/>
{model.requestStatusMessage.length > 0 && (
{model.deprecatedModel.requestStatusMessage.length > 0 && (
<time>
{model.requestStatusMessage} - {model.queryDuration}ms
{model.deprecatedModel.requestStatusMessage} - {model.deprecatedModel.queryDuration}ms
</time>
)}
</table-footer>
@@ -11,7 +11,7 @@ import styled, { css } from 'reshadow';
import { useService } from '@cloudbeaver/core-di';
import { TableViewerModel } from '../../TableViewerModel';
import { DataModelWrapper } from '../../DataModelWrapper';
import { TableFooterMenuItem } from './TableFooterMenuItem';
import { TableFooterMenuService } from './TableFooterMenuService';
@@ -27,7 +27,7 @@ const styles = css`
`;
type TableFooterMenuProps = {
model: TableViewerModel;
model: DataModelWrapper;
className?: string;
}
@@ -11,7 +11,7 @@ import {
ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem
} from '@cloudbeaver/core-dialogs';
import { TableViewerModel } from '../../TableViewerModel';
import { DataModelWrapper } from '../../DataModelWrapper';
@injectable()
export class TableFooterMenuService {
@@ -28,12 +28,12 @@ export class TableFooterMenuService {
return context.contextType === TableFooterMenuService.nodeContextType;
},
isDisabled(context) {
return !context.data.isEdited();
return !context.data.deprecatedModel.isEdited();
},
order: 1,
title: 'Save',
icon: 'table-save',
onClick: context => context.data.saveChanges(),
onClick: context => context.data.deprecatedModel.saveChanges(),
});
this.registerMenuItem({
id: 'cancel ',
@@ -41,26 +41,26 @@ export class TableFooterMenuService {
return context.contextType === TableFooterMenuService.nodeContextType;
},
isDisabled(context) {
return !context.data.isEdited();
return !context.data.deprecatedModel.isEdited();
},
order: 2,
title: 'Cancel',
icon: 'table-cancel',
onClick: context => context.data.cancelChanges(),
onClick: context => context.data.deprecatedModel.cancelChanges(),
});
}
constructMenuWithContext(model: TableViewerModel): IMenuItem[] {
const context: IMenuContext<TableViewerModel> = {
constructMenuWithContext(model: DataModelWrapper): IMenuItem[] {
const context: IMenuContext<DataModelWrapper> = {
menuId: this.tableFooterMenuToken,
contextId: model.tableId,
contextId: model.id,
contextType: TableFooterMenuService.nodeContextType,
data: model,
};
return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems;
}
registerMenuItem(options: IContextMenuItem<TableViewerModel>): void {
this.contextMenuService.addMenuItem<TableViewerModel>(this.tableFooterMenuToken, options);
registerMenuItem(options: IContextMenuItem<DataModelWrapper>): void {
this.contextMenuService.addMenuItem<DataModelWrapper>(this.tableFooterMenuToken, options);
}
}
@@ -15,10 +15,10 @@ import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { DataPresentationService } from '../DataPresentationService';
import { TableViewerModel } from './TableViewerModel';
import { DataModelWrapper } from './DataModelWrapper';
type TableGridProps = PropsWithChildren<{
model: TableViewerModel;
model: DataModelWrapper;
}>
const styles = css`
@@ -37,13 +37,14 @@ export const TableGrid = observer(function TableGrid({
const translate = useTranslate();
const dataPresentationService = useService(DataPresentationService);
if (model.errorMessage.length > 0) {
// TODO: probably must be implemented in presentation component
if (model.deprecatedModel.errorMessage.length > 0) {
return styled(styles)(
<error as="div">
{model.errorMessage}
{model.deprecatedModel.errorMessage}
<br/><br/>
{model.hasDetails && (
<Button type='button' mod={['outlined']} onClick={model.onShowDetails}>
{model.deprecatedModel.hasDetails && (
<Button type='button' mod={['outlined']} onClick={model.deprecatedModel.onShowDetails}>
{translate('ui_errors_details')}
</Button>
)}
@@ -53,7 +54,7 @@ export const TableGrid = observer(function TableGrid({
const Spreadsheet = dataPresentationService.default?.component;
if ((model.isFullyLoaded && model.isEmpty) || !Spreadsheet) {
if ((model.deprecatedModel.isFullyLoaded && model.deprecatedModel.isEmpty) || !Spreadsheet) {
return styled(styles)(<no-data as="div">{translate('data_viewer_nodata_message')}</no-data>);
}
@@ -13,7 +13,7 @@ import { Placeholder } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { TableViewerModel } from '../TableViewerModel';
import { DataModelWrapper } from '../DataModelWrapper';
import { TableHeaderService } from './TableHeaderService';
const styles = composes(
@@ -33,7 +33,7 @@ const styles = composes(
);
type Props = {
model: TableViewerModel;
model: DataModelWrapper;
}
export const TableHeader = observer(function TableHeader({
@@ -9,12 +9,12 @@
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { TableViewerModel } from '../TableViewerModel';
import { DataModelWrapper } from '../DataModelWrapper';
import { TableWhereFilter } from './TableWhereFilter';
@injectable()
export class TableHeaderService extends Bootstrap {
readonly tableHeaderPlaceholder = new PlaceholderContainer<TableViewerModel>()
readonly tableHeaderPlaceholder = new PlaceholderContainer<DataModelWrapper>()
register() {
this.tableHeaderPlaceholder.add(TableWhereFilter, 1);
@@ -15,7 +15,7 @@ import { SubmittingForm } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { TableViewerModel } from '../TableViewerModel';
import { DataModelWrapper } from '../DataModelWrapper';
const styles = composes(
css`
@@ -39,29 +39,29 @@ const styles = composes(
);
type Props = {
context: TableViewerModel;
context: DataModelWrapper;
}
export const TableWhereFilter = observer(function TableWhereFilter({
context,
}: Props) {
const translate = useTranslate();
const [filterValue, setValue] = useState(() => context.getQueryWhereFilter() || '');
const [filterValue, setValue] = useState(() => context.deprecatedModel.getQueryWhereFilter() || '');
const handleApply = useCallback(() => {
context.setQueryWhereFilter(filterValue);
context.refresh();
context.deprecatedModel.setQueryWhereFilter(filterValue);
context.deprecatedModel.refresh();
}, [context, filterValue]);
const resetFilter = useCallback(
() => {
const applyNeeded = context.getQueryWhereFilter() === filterValue;
const applyNeeded = context.deprecatedModel.getQueryWhereFilter() === filterValue;
setValue('');
if (applyNeeded) {
context.setQueryWhereFilter('');
context.refresh();
context.deprecatedModel.setQueryWhereFilter('');
context.deprecatedModel.refresh();
}
},
[context, filterValue]
@@ -49,7 +49,7 @@ export const TableViewer = observer(function TableViewer({
<TableHeader model={tableViewerModel} />
<TableGrid model={tableViewerModel} />
<TableFooter model={tableViewerModel} />
<Loader loading={tableViewerModel.isLoaderVisible} overlay/>
<Loader loading={tableViewerModel.isLoading()} overlay/>
</table-viewer>
);
});
@@ -14,6 +14,7 @@ import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import { uuid, MetadataMap } from '@cloudbeaver/core-utils';
import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
import { IExecutionContext } from '../IExecutionContext';
import { ErrorDialog } from './ErrorDialog';
import { RowDiff } from './TableDataModel/EditedRow';
@@ -28,11 +29,6 @@ export const fetchingSettings = {
fetchDefault: 200,
};
export enum AccessMode {
Default,
Readonly
}
export type AgGridRow = any[];
export type SortMode = 'asc' | 'desc' | null;
@@ -73,7 +69,7 @@ export interface ITableViewerModelOptions {
executionContext?: IExecutionContext | null; // will be filled before fist data fetch
sourceName?: string; // TODO: refactor it, used for showing sql query for export
noLoaderWhileRequestingDataAsync?: boolean;
access?: AccessMode;
access?: DatabaseDataAccessMode;
requestDataAsync(
model: TableViewerModel,
rowOffset: number,
@@ -99,7 +95,7 @@ export class TableViewerModel {
sourceName?: string;
noLoaderWhileRequestingDataAsync?: boolean;
@observable access: AccessMode;
@observable access: DatabaseDataAccessMode;
requestDataAsync: (
model: TableViewerModel,
@@ -156,7 +152,7 @@ export class TableViewerModel {
this.executionContext = options.executionContext || null;
this.sourceName = options.sourceName;
this.noLoaderWhileRequestingDataAsync = options.noLoaderWhileRequestingDataAsync;
this.access = options.access || AccessMode.Default;
this.access = options.access || DatabaseDataAccessMode.Default;
this.requestDataAsync = options.requestDataAsync;
this._saveChanges = options.saveChanges;
this.resetSubject = new Subject();
@@ -223,7 +219,7 @@ export class TableViewerModel {
}
isEdited(): boolean {
if (this.access === AccessMode.Readonly) {
if (this.access === DatabaseDataAccessMode.Readonly) {
return false;
}
@@ -243,7 +239,7 @@ export class TableViewerModel {
}
async saveChanges(): Promise<void> {
if (this.access === AccessMode.Readonly) {
if (this.access === DatabaseDataAccessMode.Readonly) {
return;
}
@@ -321,7 +317,7 @@ export class TableViewerModel {
}
onCellEditingStopped(rowNumber: number, column: string, value: any, editing: boolean): void {
if (this.access === AccessMode.Readonly) {
if (this.access === DatabaseDataAccessMode.Readonly) {
return;
}
@@ -11,31 +11,49 @@ import { observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { TableViewerModel, ITableViewerModelOptions } from './TableViewerModel';
import { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel';
import { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult';
import { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource';
import { DataModelWrapper } from './DataModelWrapper';
import { ITableViewerModelOptions } from './TableViewerModel';
@injectable()
export class TableViewerStorageService {
@observable private tableModelMap: Map<string, TableViewerModel> = new Map();
@observable private tableModelMap: Map<string, IDatabaseDataModel<any, any>> = new Map();
constructor(private commonDialogService: CommonDialogService) {}
has(tableId: string): boolean {
return this.tableModelMap.has(tableId);
}
get(tableId: string) {
return this.tableModelMap.get(tableId);
get(tableId: string): DataModelWrapper | null | undefined {
return this.tableModelMap.get(tableId) as DataModelWrapper;
}
/**
* @deprecated Use add method instead
*/
create(
options: ITableViewerModelOptions,
): TableViewerModel {
source: IDatabaseDataSource<any, any>
): DataModelWrapper {
const tableModel = new TableViewerModel(options, this.commonDialogService);
this.tableModelMap.set(tableModel.tableId, tableModel);
return tableModel;
return this.add(new DataModelWrapper(
this.commonDialogService,
options,
source
)) as DataModelWrapper;
}
remove(tableId: string) {
this.tableModelMap.delete(tableId);
add<TOptions, TResult extends IDatabaseDataResult>(
model: IDatabaseDataModel<TOptions, TResult>,
): IDatabaseDataModel<TOptions, TResult> {
this.tableModelMap.set(model.id, model);
return this.tableModelMap.get(model.id)!;
}
remove(id: string): void {
this.tableModelMap.delete(id);
}
}
@@ -2,12 +2,21 @@ import { manifest } from './manifest';
export default manifest;
export * from './DatabaseDataModel/DatabaseDataFormat';
export * from './DatabaseDataModel/DatabaseDataModel';
export * from './DatabaseDataModel/DatabaseDataSource';
export * from './DatabaseDataModel/IDatabaseDataEditor';
export * from './DatabaseDataModel/IDatabaseDataModel';
export * from './DatabaseDataModel/IDatabaseDataResult';
export * from './DatabaseDataModel/IDatabaseDataSource';
// All Services and Components that is provided by this plugin should be exported here
export * from './TableViewer/TableViewerStorageService';
export * from './TableViewer/TableViewer';
export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService';
export * from './TableViewer/TableViewerModel';
export * from './TableViewer/DataModelWrapper';
export * from './TableViewer/TableDataModel/TableColumn';
export * from './TableViewer/TableDataModel/TableRow';
@@ -0,0 +1,117 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import {
DatabaseDataSource, DataUpdate, IDatabaseDataResult, IExecutionContext
} from '@cloudbeaver/plugin-data-viewer';
export interface IDataContainerOptions {
sourceName: string;
connectionId: string;
whereFilter: string;
constraints: SqlDataFilterConstraint[];
}
export interface IDataContainerResult extends IDatabaseDataResult {
}
export class QueryDataSource extends DatabaseDataSource<IDataContainerOptions, IDataContainerResult> {
private executionContext: IExecutionContext | null;
constructor(private graphQLService: GraphQLService) {
super();
this.executionContext = null;
}
async request(
prevResults: IDataContainerResult[]
): Promise<IDataContainerResult[]> {
// if (!this.options?.containerNodePath) {
// throw new Error('containerNodePath must be provided for table');
// }
// const executionContext = await this.ensureContextCreated();
// const { readDataFromContainer } = await this.graphQLService.sdk.readDataFromContainer({
// connectionId: executionContext.connectionId,
// contextId: executionContext.contextId,
// containerNodePath: this.options.containerNodePath,
// filter: {
// offset: this.offset,
// limit: this.count,
// constraints: this.options.constraints,
// where: this.options.whereFilter || undefined,
// },
// });
// this.requestInfo = {
// requestDuration: readDataFromContainer?.duration || 0,
// requestMessage: readDataFromContainer?.statusMessage || '',
// };
// return readDataFromContainer?.results.map<IDataContainerResult>(result => ({
// id: result.resultSet!.id,
// dataFormat: result.dataFormat!,
// loadedFully: (result.resultSet?.rows?.length || 0) < this.count || !result.resultSet?.hasMoreData,
// data: result.resultSet,
// })) || prevResults;
return prevResults;
}
async save(
prevResults: IDataContainerResult[],
data: DataUpdate
): Promise<IDataContainerResult[]> {
const executionContext = await this.ensureContextCreated();
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
resultsId: data.data.id,
// updatedRows: this.getRowsDiff(data),
});
this.requestInfo = {
requestDuration: response.result?.duration || 0,
requestMessage: 'Saved successfully',
};
throw new Error('Not implemented');
}
private async ensureContextCreated(): Promise<IExecutionContext> {
if (!this.executionContext) {
if (!this.options) {
throw new Error('Options must be provided');
}
this.executionContext = await this.createExecutionContext(this.options.connectionId);
}
return this.executionContext;
}
private async createExecutionContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<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,
};
}
}
@@ -13,7 +13,7 @@ import { IDestructibleController, IInitializableController, injectable } from '@
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLError, ServerInternalError } from '@cloudbeaver/core-sdk';
import { GQLError, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk';
import { PromiseCancelledError } from '@cloudbeaver/core-utils';
import {
fetchingSettings,
@@ -21,10 +21,11 @@ import {
RowDiff,
TableViewerStorageService,
TableViewerModel,
AccessMode,
DatabaseDataAccessMode,
} from '@cloudbeaver/plugin-data-viewer';
import { IResultDataTab, IQueryTabGroup } from '../../ISqlEditorTabState';
import { QueryDataSource } from '../../QueryDataSource';
import { SqlEditorGroupMetadataService } from '../../SqlEditorGroupMetadataService';
import { SqlExecutionState } from '../../SqlExecutionState';
import { SQLQueryExecutionProcess } from '../SQLQueryExecutionProcess';
@@ -54,6 +55,7 @@ implements IInitializableController, IDestructibleController {
private sqlProcess: SQLQueryExecutionProcess | null = null;
constructor(
private sdk: GraphQLService,
private sqlResultService: SqlResultService,
private tableViewerStorageService: TableViewerStorageService,
private connectionInfoResource: ConnectionInfoResource,
@@ -101,17 +103,27 @@ implements IInitializableController, IDestructibleController {
this.tableViewerStorageService.remove(this.getTableId());
}
const tableModel = this.tableViewerStorageService.create({
tableId: this.getTableId(),
connectionId: this.group.sqlQueryParams.connectionId,
executionContext: this.group.sqlQueryParams,
resultId: dataSet.resultSet.id,
sourceName: this.group.sqlQueryParams.query,
access: connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default,
requestDataAsync: this.requestDataAsync.bind(this, sqlExecutionContext),
noLoaderWhileRequestingDataAsync: true,
saveChanges: this.saveChanges.bind(this),
});
const tableModel = this.tableViewerStorageService.create(
{
tableId: this.getTableId(),
connectionId: this.group.sqlQueryParams.connectionId,
executionContext: this.group.sqlQueryParams,
resultId: dataSet.resultSet.id,
sourceName: this.group.sqlQueryParams.query,
access: connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default,
requestDataAsync: this.requestDataAsync.bind(this, sqlExecutionContext),
noLoaderWhileRequestingDataAsync: true,
saveChanges: this.saveChanges.bind(this),
},
new QueryDataSource(this.sdk)
)
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.setOptions({
connectionId: this.group.sqlQueryParams.connectionId,
sourceName: this.group.sqlQueryParams.query,
constraints: [],
whereFilter: '',
}).deprecatedModel;
tableModel.insertRows(0, initialState.rows, !initialState.isFullyLoaded);
tableModel.setColumns(initialState.columns);
@@ -204,7 +216,7 @@ implements IInitializableController, IDestructibleController {
const connectionInfo = await this.connectionInfoResource.load(this.group.sqlQueryParams.connectionId);
model.resultId = resultId;
model.access = connectionInfo.readOnly ? AccessMode.Readonly : AccessMode.Default;
model.access = connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default;
model.sourceName = this.group.sqlQueryParams.query;
model.executionContext = this.group.sqlQueryParams;
model.connectionId = this.group.sqlQueryParams.connectionId;