Merge pull request #370 from dbeaver/feat/context-filter

This commit is contained in:
Alexey
2021-05-28 12:12:58 +03:00
committed by GitHub
50 changed files with 1181 additions and 237 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

@@ -177,10 +177,13 @@
</g>
</symbol>
<symbol id="sort-arrow" viewBox="0 0 6 6">
<g>
<path d="M2.564 0L0 2.383 1.832 2.383 1.832 4.582 3.297 4.582 3.297 2.383 5.128 2.383z" transform="translate(-5 -.222) translate(5 .389)"/>
</g>
<symbol id="sort-arrow-up" viewBox="0 0 16 16">
<path fill="#338ECC" fill-rule="evenodd" d="M1 7l7-6 7 6h-5v8H6V7H1z" clip-rule="evenodd"/>
</symbol>
<symbol id="sort-arrow-down" viewBox="0 0 16 16">
<path fill="#338ECC" fill-rule="evenodd" d="M15 9l-7 6-7-6h5V1h4v8h5z" clip-rule="evenodd"/>
</symbol>
<symbol id="sort-arrow-unknown" viewBox="0 0 16 16">
<path fill="#C4C4C4" fill-rule="evenodd" d="M11.5 2L8 6h2.333v8h2.334V6H15l-3.5-4zm-7 12L8 10H5.667V2H3.333v8H1l3.5 4z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -94,7 +94,7 @@ import { RouterService, ScreenService } from '@cloudbeaver/core-routing';
import { EnvironmentService, GraphQLService } from '@cloudbeaver/core-sdk';
import { LocalStorageSaveService, SettingsService } from '@cloudbeaver/core-settings';
import { ThemeService } from '@cloudbeaver/core-theming';
import { NavigationService, OptionsPanelService } from '@cloudbeaver/core-ui';
import { NavigationService, OptionsPanelService, ClipboardBootstrap, ClipboardService } from '@cloudbeaver/core-ui';
import { ActiveViewService } from '@cloudbeaver/core-view';
export const coreManifest: PluginManifest = {
@@ -135,6 +135,8 @@ export const coreManifest: PluginManifest = {
PermissionsService,
CoreSettingsService,
CommonDialogService,
ClipboardService,
ClipboardBootstrap,
SessionExpireService,
SessionExpiredDialogService,
ConnectionsLocaleService,
@@ -25,9 +25,12 @@ export const defaultENLocale = [
['ui_data_delete_confirmation', 'Delete confirmation'],
['ui_no_matches_placeholder', 'Your search returned no matches.'],
['ui_information', 'Information'],
['ui_clipboard', 'Clipboard'],
['ui_copy_to_clipboard', 'Copy'],
['ui_copy_to_clipboard_copied', 'Copied'],
['ui_copy_to_clipboard_failed_to_copy', 'Failed to copy'],
['ui_clipboard_access_denied_title', 'Clipboard access denied'],
['ui_clipboard_access_denied_message', 'You need to give access to clipboard to use some of the app features'],
['root_permission_denied', 'You have no permissions'],
['app_root_session_expired_title', 'Session expired'],
['app_root_session_expired_message', 'Session has been expired. Do you want to reload?'],
@@ -23,9 +23,12 @@ export const defaultRULocale = [
['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'],
['ui_data_delete_confirmation', 'Подтвердите удаление'],
['ui_information', 'Информация'],
['ui_clipboard', 'Буфер обмена'],
['ui_copy_to_clipboard', 'Копировать'],
['ui_copy_to_clipboard_copied', 'Скопировано'],
['ui_copy_to_clipboard_failed_to_copy', 'Не удалось скопировать'],
['ui_clipboard_access_denied_title', 'Доступ к буферу обмена запрещен'],
['ui_clipboard_access_denied_message', 'Чтобы использовать некоторые функции приложения, необходим доступ к буферу обмена'],
['root_permission_denied', 'Отказано в доступе'],
['app_root_session_expired_title', 'Сессия истекла'],
['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'],
@@ -6,6 +6,7 @@ mutation getSqlExecuteTaskResults(
) {
duration
statusMessage
filterText
results {
title
updateRowCount
@@ -27,6 +28,11 @@ mutation getSqlExecuteTaskResults(
readOnlyStatus
scale
typeName
supportedOperations {
id
expression
argumentCount
}
}
rows
hasMoreData
@@ -15,6 +15,7 @@ mutation updateResultsData(
updateValues: $values
) {
duration
filterText
results {
updateRowCount
resultSet {
@@ -16,6 +16,7 @@ mutation updateResultsDataBatch(
addedRows: $addedRows
) {
duration
filterText
results {
updateRowCount
resultSet {
+32 -5
View File
@@ -65,6 +65,7 @@ export interface Query {
sqlCompletionProposals?: Maybe<Array<Maybe<SqlCompletionProposal>>>;
sqlDialectInfo?: Maybe<SqlDialectInfo>;
sqlListContexts?: Maybe<Array<Maybe<SqlContextInfo>>>;
sqlSupportedOperations: DataTypeLogicalOperation[];
templateConnections: ConnectionInfo[];
updateConnectionConfiguration: ConnectionInfo;
userConnections: ConnectionInfo[];
@@ -247,6 +248,13 @@ export interface QuerySqlListContextsArgs {
connectionId: Scalars['ID'];
}
export interface QuerySqlSupportedOperationsArgs {
connectionId: Scalars['ID'];
contextId: Scalars['ID'];
resultsId: Scalars['ID'];
attributeIndex: Scalars['Int'];
}
export interface QueryUpdateConnectionConfigurationArgs {
id: Scalars['ID'];
config: ConnectionConfig;
@@ -810,6 +818,7 @@ export interface SqlResultColumn {
precision?: Maybe<Scalars['Int']>;
readOnly: Scalars['Boolean'];
readOnlyStatus?: Maybe<Scalars['String']>;
supportedOperations: DataTypeLogicalOperation[];
}
export interface DatabaseDocument {
@@ -836,7 +845,8 @@ export interface SqlQueryResults {
export interface SqlExecuteInfo {
statusMessage?: Maybe<Scalars['String']>;
duration?: Maybe<Scalars['Int']>;
duration: Scalars['Int'];
filterText?: Maybe<Scalars['String']>;
results: SqlQueryResults[];
}
@@ -845,6 +855,12 @@ export interface SqlResultRow {
updateValues?: Maybe<Scalars['Object']>;
}
export interface DataTypeLogicalOperation {
id: Scalars['ID'];
expression: Scalars['String'];
argumentCount?: Maybe<Scalars['Int']>;
}
export enum AdminSubjectType {
User = 'user',
Role = 'role'
@@ -1423,12 +1439,15 @@ export type GetSqlExecuteTaskResultsMutationVariables = Exact<{
export interface GetSqlExecuteTaskResultsMutation {
result: (
Pick<SqlExecuteInfo, 'duration' | 'statusMessage'>
Pick<SqlExecuteInfo, 'duration' | 'statusMessage' | 'filterText'>
& { results: Array<(
Pick<SqlQueryResults, 'title' | 'updateRowCount' | 'sourceQuery' | 'dataFormat'>
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows' | 'hasMoreData'>
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'scale' | 'typeName'>>>> }
& { columns?: Maybe<Array<Maybe<(
Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'scale' | 'typeName'>
& { supportedOperations: Array<Pick<DataTypeLogicalOperation, 'id' | 'expression' | 'argumentCount'>> }
)>>>; }
)>; }
)>; }
);
@@ -1444,7 +1463,7 @@ export type UpdateResultsDataMutationVariables = Exact<{
export interface UpdateResultsDataMutation {
result?: Maybe<(
Pick<SqlExecuteInfo, 'duration'>
Pick<SqlExecuteInfo, 'duration' | 'filterText'>
& { results: Array<(
Pick<SqlQueryResults, 'updateRowCount'>
& { resultSet?: Maybe<Pick<SqlResultSet, 'id' | 'rows'>> }
@@ -1463,7 +1482,7 @@ export type UpdateResultsDataBatchMutationVariables = Exact<{
export interface UpdateResultsDataBatchMutation {
result?: Maybe<(
Pick<SqlExecuteInfo, 'duration'>
Pick<SqlExecuteInfo, 'duration' | 'filterText'>
& { results: Array<(
Pick<SqlQueryResults, 'updateRowCount'>
& { resultSet?: Maybe<Pick<SqlResultSet, 'id' | 'rows'>> }
@@ -2308,6 +2327,7 @@ export const GetSqlExecuteTaskResultsDocument = `
result: asyncSqlExecuteResults(taskId: $taskId) {
duration
statusMessage
filterText
results {
title
updateRowCount
@@ -2329,6 +2349,11 @@ export const GetSqlExecuteTaskResultsDocument = `
readOnlyStatus
scale
typeName
supportedOperations {
id
expression
argumentCount
}
}
rows
hasMoreData
@@ -2347,6 +2372,7 @@ export const UpdateResultsDataDocument = `
updateValues: $values
) {
duration
filterText
results {
updateRowCount
resultSet {
@@ -2368,6 +2394,7 @@ export const UpdateResultsDataBatchDocument = `
addedRows: $addedRows
) {
duration
filterText
results {
updateRowCount
resultSet {
+2
View File
@@ -21,6 +21,8 @@
"@cloudbeaver/core-blocks": "^0.1.0",
"@cloudbeaver/core-sdk": "^0.1.0",
"@cloudbeaver/core-dialogs": "^0.1.0",
"@cloudbeaver/core-events": "^0.1.0",
"@cloudbeaver/core-utils": "^0.1.0",
"mobx": "^5.x.x",
"mobx-react-lite": "^3.x.x"
}
@@ -0,0 +1,26 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ClipboardService } from './ClipboardService';
@injectable()
export class ClipboardBootstrap extends Bootstrap {
constructor(
private clipboardService: ClipboardService,
) {
super();
}
register(): void { }
async load(): Promise<void> {
await this.clipboardService.tryActivateClipboard();
}
}
@@ -0,0 +1,95 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
import { copyToClipboard } from '@cloudbeaver/core-utils';
@injectable()
export class ClipboardService {
private readPermission: PermissionStatus | null;
private permissionUpdate: number;
clipboardAvailable: boolean;
clipboardValue: string | null;
get state(): PermissionState {
if (this.permissionUpdate > 0 && this.readPermission) {
return this.readPermission.state;
}
return 'prompt';
}
constructor(
private notificationService: NotificationService
) {
makeObservable<ClipboardService, 'permissionUpdate'>(this, {
clipboardValue: observable,
permissionUpdate: observable,
});
this.permissionUpdate = 1;
this.readPermission = null;
this.clipboardValue = null;
this.clipboardAvailable = false;
}
async read(): Promise<string | null> {
if (!this.clipboardAvailable || this.state === 'denied') {
return null;
}
try {
const value = await navigator.clipboard.readText();
this.clipboardValue = value;
} catch (exeption) {
this.clipboardAvailable = false;
this.clipboardValue = null;
this.notificationService.logException(exeption, 'Failed to read from clipboard', '', true);
this.notificationService.logInfo({ title: 'ui_clipboard_access_denied_title', message: 'ui_clipboard_access_denied_message' });
}
return this.clipboardValue;
}
write(value: string, notify: boolean): void {
try {
copyToClipboard(value);
if (notify) {
this.notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Info);
}
} catch (exception) {
this.notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy');
}
}
async tryActivateClipboard(): Promise<void> {
if (!navigator.clipboard) {
this.clipboardAvailable = false;
return;
}
this.clipboardAvailable = true;
await this.updatePermissionStatus();
if (this.readPermission) {
this.readPermission.addEventListener('change', () => {
this.permissionUpdate++;
});
}
}
async updatePermissionStatus(): Promise<void> {
try {
this.readPermission = await navigator.permissions.query({ name: 'clipboard-read' });
} catch (exeption) {
this.notificationService.logException(exeption, 'Failed to get permission status', '', true);
this.readPermission = null;
}
}
}
+2
View File
@@ -2,3 +2,5 @@ export * from './Screens/AppScreen/NavigationService';
export * from './Screens/AppScreen/OptionsPanelService';
export * from './AuthenticationProvider';
export * from './useAuthenticationAction';
export * from './Clipboard/ClipboardBootstrap';
export * from './Clipboard/ClipboardService';
+1
View File
@@ -12,6 +12,7 @@ export * from './md5';
export * from './TextTools';
export * from './copyToClipboard';
export * from './declensionOfNumber';
export * from './replaceMiddle';
export * from './Promises/CancellablePromise';
export * from './Promises/Deferred';
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 function replaceMiddle(value: string, newMiddle: string, sideLength: number, limiter: number): string {
if (value.length < limiter) {
return value;
}
return value.substr(0, sideLength) + newMiddle + value.substr(value.length - sideLength);
}
@@ -0,0 +1,407 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult, IContextMenuItem, IMenuContext } from '@cloudbeaver/core-dialogs';
import { ComputedContextMenuModel } from '@cloudbeaver/core-dialogs';
import { ClipboardService } from '@cloudbeaver/core-ui';
import { replaceMiddle } from '@cloudbeaver/core-utils';
import {
wrapOperationArgument,
IDatabaseDataModel,
nullOperationsFilter,
ResultSetConstraintAction,
IS_NOT_NULL_ID,
IS_NULL_ID,
ResultSetDataAction,
IDatabaseResultSet,
IDatabaseDataOptions,
ResultSetFormatAction,
isFilterConstraint
} from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuService, IDataGridCellMenuContext } from '../DataGridContextMenuService';
import { FilterCustomValueDialog } from './FilterCustomValueDialog';
@injectable()
export class DataGridContextMenuFilterService {
private static menuFilterToken = 'menuFilter';
constructor(
private dataGridContextMenuService: DataGridContextMenuService,
private commonDialogService: CommonDialogService,
private clipboardService: ClipboardService,
) {
this.dataGridContextMenuService.onRootMenuOpen.addHandler(this.getClipboardValue.bind(this));
}
getMenuFilterToken(): string {
return DataGridContextMenuFilterService.menuFilterToken;
}
private async applyFilter(
model: IDatabaseDataModel<IDatabaseDataOptions, IDatabaseResultSet>,
resultIndex: number,
columnIndex: number,
operator: string,
filterValue?: any,
) {
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(columnIndex)?.label || '';
constraints.setFilter(columnLabel, operator, filterValue);
await model.refresh();
}
private async getClipboardValue() {
if (this.clipboardService.state === 'granted') {
await this.clipboardService.read();
}
}
private getGeneralizedMenuItems(
context: IMenuContext<IDataGridCellMenuContext>,
value: any | (() => any),
icon: string,
): Array<IContextMenuItem<IDataGridCellMenuContext>> {
const { model, resultIndex, column } = context.data;
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const supportedOperations = data.getColumnOperations(column);
const columnLabel = data.getColumn(column)?.label || '';
return supportedOperations
.filter(operation => !nullOperationsFilter(operation))
.map(operation => ({
id: operation.id,
isPresent: () => true,
isDisabled(context) {
return context.data.model.isLoading();
},
titleGetter() {
const val = typeof value === 'function' ? value() : value;
const stringifyValue = format.toString(val);
const wrappedValue = wrapOperationArgument(operation.id, stringifyValue);
const clippeddValue = replaceMiddle(wrappedValue, ' ... ', 8, 30);
return `${columnLabel} ${operation.expression} ${clippeddValue}`;
},
icon,
onClick: async () => {
const val = typeof value === 'function' ? value() : value;
await this.applyFilter(model, resultIndex, column, operation.id, val);
},
}));
}
register(): void {
this.dataGridContextMenuService.add(
this.dataGridContextMenuService.getMenuToken(),
{
id: this.getMenuFilterToken(),
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden(context) {
return context.data.model.isDisabled(context.data.resultIndex)
|| context.data.model.source.results.length > 1;
},
order: 1,
title: 'data_grid_table_filter',
icon: '/icons/filter.png',
isPanel: true,
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'clipboardValue',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
if (!this.clipboardService.clipboardAvailable || this.clipboardService.state === 'denied') {
return true;
}
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(context.data.column);
return supportedOperations.length === 0;
},
order: 0,
title: 'ui_clipboard',
icon: '/icons/filter_clipboard.png',
panel: new ComputedContextMenuModel<IDataGridCellMenuContext>({
id: 'clipboardValuePanel',
menuItemsGetter: context => {
if (context.contextType !== DataGridContextMenuService.cellContext) {
return [];
}
if (this.clipboardService.state === 'prompt') {
return [{
id: 'permission',
isPresent: () => true,
isDisabled(context) {
return context.data.model.isLoading();
},
title: 'data_grid_table_context_menu_filter_clipboard_permission',
icon: '/icons/permissions.png',
onClick: async () => {
await this.clipboardService.read();
},
}];
}
const valueGetter = () => this.clipboardService.clipboardValue || '';
const items = this.getGeneralizedMenuItems(context, valueGetter, '/icons/filter_clipboard.png');
return items;
},
}),
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'cellValue',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const { model, resultIndex, column, row } = context.data;
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const supportedOperations = data.getColumnOperations(column);
return supportedOperations.length === 0 || format.isNull(data.getCellValue({ column, row }));
},
order: 1,
title: 'data_grid_table_filter_cell_value',
icon: '/icons/filter_value.png',
panel: new ComputedContextMenuModel<IDataGridCellMenuContext>({
id: 'cellValuePanel',
menuItemsGetter: context => {
const { model, resultIndex, column, row } = context.data;
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const cellValue = data.getCellValue({ column, row });
const items = this.getGeneralizedMenuItems(context, cellValue, '/icons/filter_value.png');
return items;
},
}),
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'customValue',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(context.data.column);
return supportedOperations.length === 0;
},
order: 2,
title: 'data_grid_table_filter_custom_value',
icon: '/icons/filter_custom.png',
separator: true,
panel: new ComputedContextMenuModel<IDataGridCellMenuContext>({
id: 'customValuePanel',
menuItemsGetter: context => {
const { model, resultIndex, column, row } = context.data;
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(column);
const cellValue = data.getCellValue({ column, row });
const columnLabel = data.getColumn(column)?.label || '';
return supportedOperations
.filter(operation => !nullOperationsFilter(operation))
.map(operation => {
const title = `${columnLabel} ${operation.expression}`;
return {
id: operation.id,
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isDisabled(context) {
return context.data.model.isLoading();
},
title: title + ' ..',
icon: '/icons/filter_custom.png',
onClick: async () => {
const isNull = format.isNull(cellValue);
const stringifyCellValue = format.toString(cellValue);
const customValue = await this.commonDialogService.open(
FilterCustomValueDialog,
{
defaultValue: isNull ? '' : stringifyCellValue,
inputTitle: title + ':',
}
);
if (customValue === DialogueStateResult.Rejected || customValue === DialogueStateResult.Resolved) {
return;
}
await this.applyFilter(
model,
resultIndex,
column,
operation.id,
customValue
);
},
};
});
},
}),
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'isNullValue',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(context.data.column);
return !supportedOperations.some(operation => operation.id === IS_NULL_ID);
},
order: 3,
icon: '/icons/filter_value.png',
titleGetter: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(context.data.column)?.label || '';
return `${columnLabel} IS NULL`;
},
onClick: async context => {
await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NULL_ID);
},
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'isNotNullValue',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(context.data.column);
return !supportedOperations.some(operation => operation.id === IS_NOT_NULL_ID);
},
order: 4,
icon: '/icons/filter_value.png',
separator: true,
titleGetter: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(context.data.column)?.label || '';
return `${columnLabel} IS NOT NULL`;
},
onClick: async context => {
await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NOT_NULL_ID);
},
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'deleteFilter',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const { model, resultIndex, column } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(column)?.label || '';
const currentConstraint = constraints.get(columnLabel);
return !currentConstraint || !isFilterConstraint(currentConstraint);
},
order: 5,
icon: '/icons/filter_reset.png',
titleGetter: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(context.data.column)?.name || '';
return `Delete filter for ${columnLabel}`;
},
onClick: async context => {
const { model, resultIndex, column } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(column)?.label || '';
constraints.deleteFilter(columnLabel);
await model.refresh();
},
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'deleteAllFilters',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const { model, resultIndex } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const filterConstraints = constraints.getFilterConstraints();
return filterConstraints.length === 0 && !model.requestInfo.requestFilter;
},
order: 6,
title: 'data_grid_table_filter_reset_all_filters',
onClick: async context => {
const { model, resultIndex } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
constraints.deleteDataFilters();
await model.refresh();
},
}
);
this.dataGridContextMenuService.add(
this.getMenuFilterToken(),
{
id: 'deleteFiltersAndSorting',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
order: 7,
title: 'data_grid_table_delete_filters_and_sorting',
icon: '/icons/erase.png',
onClick: async context => {
const { model, resultIndex } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
constraints.deleteAll();
model.source.options.whereFilter = '';
await model.refresh();
},
}
);
}
}
@@ -0,0 +1,99 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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-lite';
import { useState, useCallback, useRef, useEffect } from 'react';
import styled, { css } from 'reshadow';
import { Button, InputFieldNew } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ClipboardService } from '@cloudbeaver/core-ui';
const styles = css`
CommonDialogWrapper {
min-width: 400px;
}
controls {
display: flex;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
flex-direction: row-reverse;
}
Button {
margin-left: 24px;
}
`;
interface IPayload {
inputTitle: string;
defaultValue: string | number;
}
export const FilterCustomValueDialog: DialogComponent<IPayload, string | number> = observer(
function FilterCustomValueDialog({
payload,
resolveDialog,
rejectDialog,
}: DialogComponentProps<IPayload, string | number>) {
const clipboardService = useService(ClipboardService);
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<string | number>(payload.defaultValue);
const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]);
const translate = useTranslate();
const getValueFromClipboard = useCallback(async () => {
const value = await clipboardService.read();
if (value) {
setValue(value);
}
if (inputRef.current) {
inputRef.current.focus();
}
}, [clipboardService]);
useEffect(() => {
inputRef.current?.focus();
}, []);
return styled(styles)(
<CommonDialogWrapper
title={translate('data_grid_table_context_menu_filter_dialog_title')}
footer={(
<controls as="div">
<Button type="button" mod={['unelevated']} onClick={handleApply}>
{translate('ui_processing_ok')}
</Button>
<Button type="button" mod={['outlined']} onClick={rejectDialog}>
{translate('ui_processing_cancel')}
</Button>
{clipboardService.clipboardAvailable && clipboardService.state !== 'denied' && (
<Button type="button" mod={['outlined']} onClick={getValueFromClipboard}>
{translate('ui_clipboard')}
</Button>
)}
</controls>
)}
onReject={rejectDialog}
>
<InputFieldNew
ref={inputRef}
name='customValue'
value={value}
onChange={setValue}
>
{payload.inputTitle}
</InputFieldNew>
</CommonDialogWrapper>
);
}
);
@@ -7,22 +7,20 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { ContextMenuService } from '@cloudbeaver/core-dialogs';
import { ESortMode, getNextSortMode, IDatabaseDataModel, ResultSetDataAction, ResultSetSortAction, SortMode } from '@cloudbeaver/plugin-data-viewer';
import { EOrder, IDatabaseDataModel, ResultSetConstraintAction, ResultSetDataAction, Order } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuService } from './DataGridContextMenuService';
@injectable()
export class DataGridContextMenuSortingService {
private static menuSortingToken = 'menuSorting';
export class DataGridContextMenuOrderService {
private static menuOrderToken = 'menuOrder';
constructor(
private dataGridContextMenuService: DataGridContextMenuService,
private contextMenuService: ContextMenuService,
) { }
getMenuSortingToken(): string {
return DataGridContextMenuSortingService.menuSortingToken;
getMenuOrderToken(): string {
return DataGridContextMenuOrderService.menuOrderToken;
}
private getColumnName(model: IDatabaseDataModel<any>, resultIndex: number, columnIndex: number) {
@@ -30,43 +28,43 @@ export class DataGridContextMenuSortingService {
return data.getColumn(columnIndex)?.name;
}
private async changeSortMode(
private async changeOrder(
model: IDatabaseDataModel<any>,
resultIndex: number,
columnIndex: number,
sortMode: SortMode
order: Order
) {
const columnName = this.getColumnName(model, resultIndex, columnIndex)!;
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
sorting.setSortMode(columnName, sortMode, true);
constraints.setOrder(columnName, order, true);
await model.refresh();
}
private getSortMode(model: IDatabaseDataModel<any>, resultIndex: number, columnIndex: number) {
private getOrder(model: IDatabaseDataModel<any>, resultIndex: number, columnIndex: number) {
const columnName = this.getColumnName(model, resultIndex, columnIndex)!;
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
return sorting.getSortMode(columnName);
return constraints.getOrder(columnName);
}
private getSortingConstraints(model: IDatabaseDataModel<any>, resultIndex: number) {
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
return sorting.getSortingConstraints();
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
return constraints.getOrderConstraints();
}
private async removeSortingConstraints(model: IDatabaseDataModel<any>, resultIndex: number) {
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
private async removeSortingFromConstraints(model: IDatabaseDataModel<any>, resultIndex: number) {
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
sorting.removeSortingConstraints();
constraints.deleteOrders();
await model.refresh();
}
register(): void {
this.dataGridContextMenuService.add(
this.contextMenuService.getRootMenuToken(),
this.dataGridContextMenuService.getMenuToken(),
{
id: this.getMenuSortingToken(),
id: this.getMenuOrderToken(),
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
@@ -77,7 +75,7 @@ export class DataGridContextMenuSortingService {
}
);
this.dataGridContextMenuService.add(
this.getMenuSortingToken(),
this.getMenuOrderToken(),
{
id: 'asc',
isPresent(context) {
@@ -85,18 +83,18 @@ export class DataGridContextMenuSortingService {
},
isDisabled: context => context.data.model.isLoading(),
onClick: async context => {
await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.asc);
await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.asc);
},
type: 'radio',
isChecked: context => this.getSortMode(
isChecked: context => this.getOrder(
context.data.model,
context.data.resultIndex,
context.data.column) === ESortMode.asc,
context.data.column) === EOrder.asc,
title: 'ASC',
}
);
this.dataGridContextMenuService.add(
this.getMenuSortingToken(),
this.getMenuOrderToken(),
{
id: 'desc',
isPresent(context) {
@@ -104,29 +102,29 @@ export class DataGridContextMenuSortingService {
},
isDisabled: context => context.data.model.isLoading(),
onClick: async context => {
await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.desc);
await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.desc);
},
type: 'radio',
isChecked: context => this.getSortMode(
isChecked: context => this.getOrder(
context.data.model,
context.data.resultIndex,
context.data.column) === ESortMode.desc,
context.data.column) === EOrder.desc,
title: 'DESC',
}
);
this.dataGridContextMenuService.add(
this.getMenuSortingToken(),
this.getMenuOrderToken(),
{
id: 'disableSorting',
id: 'disableOrder',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isDisabled: context => context.data.model.isLoading(),
onClick: async context => {
await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, null);
await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, null);
},
type: 'radio',
isChecked: context => this.getSortMode(
isChecked: context => this.getOrder(
context.data.model,
context.data.resultIndex,
context.data.column) === null,
@@ -134,16 +132,19 @@ export class DataGridContextMenuSortingService {
}
);
this.dataGridContextMenuService.add(
this.getMenuSortingToken(),
this.getMenuOrderToken(),
{
id: 'disableAllSorting',
isPresent: context => {
id: 'disableOrders',
isPresent(context) {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const sortingConstraints = this.getSortingConstraints(context.data.model, context.data.resultIndex);
return context.contextType === DataGridContextMenuService.cellContext && sortingConstraints.length > 1;
return sortingConstraints.length === 0;
},
isDisabled: context => context.data.model.isLoading(),
onClick: async context => {
await this.removeSortingConstraints(context.data.model, context.data.resultIndex);
await this.removeSortingFromConstraints(context.data.model, context.data.resultIndex);
},
title: 'data_grid_table_disable_all_sorting',
}
@@ -8,6 +8,7 @@
import { injectable } from '@cloudbeaver/core-di';
import { ContextMenuService, IContextMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs';
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
export interface IDataGridCellMenuContext {
@@ -19,12 +20,15 @@ export interface IDataGridCellMenuContext {
@injectable()
export class DataGridContextMenuService {
onRootMenuOpen: IExecutor<IDataGridCellMenuContext>;
static cellContext = 'data-grid-cell-context-menu';
private static menuToken = 'dataGridCell';
constructor(
private contextMenuService: ContextMenuService,
) { }
) {
this.onRootMenuOpen = new Executor();
}
getMenuToken(): string {
return DataGridContextMenuService.menuToken;
@@ -40,7 +44,16 @@ export class DataGridContextMenuService {
menuId: this.getMenuToken(),
contextType: DataGridContextMenuService.cellContext,
data: { model, resultIndex, row, column },
});
}, this.getMenuToken());
}
openMenu(
model: IDatabaseDataModel<any>,
resultIndex: number,
row: number,
column: number
): void {
this.onRootMenuOpen.execute({ model, resultIndex, row, column });
}
add(panelId: string, menuItem: IContextMenuItem<IDataGridCellMenuContext>): void {
@@ -53,7 +53,11 @@ export const CellMenu: React.FC<Props> = observer(function TreeNodeMenu({
return styled(cellMenuStyles)(
<cell-menu as='div' onClick={e => e.stopPropagation()} onDoubleClick={e => e.stopPropagation()}>
<MenuTrigger panel={panel} onVisibleSwitch={onStateSwitch}>
<MenuTrigger
panel={panel}
onClick={() => dataGridContextMenuService.openMenu(model, resultIndex, row, column)}
onVisibleSwitch={onStateSwitch}
>
<Icon name="snack" viewBox="0 0 16 10" />
</MenuTrigger>
</cell-menu>
@@ -11,10 +11,10 @@ import styled, { css, use } from 'reshadow';
import { IconOrImage } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ESortMode, getNextSortMode, IDatabaseDataModel, IDatabaseDataResult, ResultSetSortAction } from '@cloudbeaver/plugin-data-viewer';
import { EOrder, getNextOrder, IDatabaseDataModel, IDatabaseDataResult, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer';
const styles = css`
sort-button {
order-button {
margin-left: 4px;
display: flex;
padding: 2px 4px;
@@ -25,13 +25,13 @@ const styles = css`
box-sizing: border-box;
cursor: pointer;
}
sort-button > IconOrImage {
width: 8px;
order-button > IconOrImage {
width: 12px;
}
sort-button:hover > IconOrImage {
width: 9px;
order-button:hover > IconOrImage {
width: 13px;
}
sort-button[|disabled] {
order-button[|disabled] {
opacity: 0.7;
cursor: default;
}
@@ -44,22 +44,22 @@ interface Props {
className?: string;
}
export const SortButton: React.FC<Props> = observer(function SortButtton({
export const OrderButton: React.FC<Props> = observer(function OrderButtton({
model,
resultIndex,
columnName,
className,
}) {
const translate = useTranslate();
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
const currentSortMode = sorting.getSortMode(columnName);
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const currentOrder = constraints.getOrder(columnName);
const loading = model.isLoading();
let icon = '/icons/sort_unknown.png';
if (currentSortMode === ESortMode.asc) {
icon = '/icons/sort_increase.png';
} else if (currentSortMode === ESortMode.desc) {
icon = '/icons/sort_decrease.png';
let icon = 'sort-arrow-unknown';
if (currentOrder === EOrder.asc) {
icon = 'sort-arrow-up';
} else if (currentOrder === EOrder.desc) {
icon = 'sort-arrow-down';
}
const handleSort = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -68,20 +68,20 @@ export const SortButton: React.FC<Props> = observer(function SortButtton({
return;
}
const nextSortMode = getNextSortMode(currentSortMode);
sorting.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey);
const nextOrder = getNextOrder(currentOrder);
constraints.setOrder(columnName, nextOrder, e.ctrlKey || e.metaKey);
model.refresh();
};
return styled(styles)(
<sort-button
<order-button
as='div'
title={translate('data_grid_table_tooltip_column_header_sort')}
className={className}
onClick={handleSort}
{...use({ disabled: loading })}
>
<IconOrImage icon={icon} />
</sort-button>
<IconOrImage icon={icon} viewBox='0 0 16 16' />
</order-button>
);
});
@@ -18,7 +18,7 @@ import { ResultSetDataAction } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContext } from '../DataGridContext';
import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext';
import { TableDataContext } from '../TableDataContext';
import { SortButton } from './SortButton';
import { OrderButton } from './OrderButton';
const headerStyles = composes(
css`
@@ -101,7 +101,7 @@ export const TableColumnHeader: React.FC<HeaderRendererProps<any>> = observer(fu
<name>{columnName}</name>
</shrink-container>
{sortable && column?.name && (
<SortButton model={model} resultIndex={resultIndex} columnName={column.name} />
<OrderButton model={model} resultIndex={resultIndex} columnName={column.name} />
)}
</table-header>
);
@@ -11,7 +11,8 @@ import { ExceptionsCatcherService } from '@cloudbeaver/core-events';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService';
import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService';
import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService';
import { DataGridSettingsService } from './DataGridSettingsService';
import { SpreadsheetGrid } from './SpreadsheetGrid';
@@ -20,7 +21,8 @@ export class SpreadsheetBootstrap extends Bootstrap {
constructor(
private dataPresentationService: DataPresentationService,
private dataGridSettingsService: DataGridSettingsService,
private dataGridContextMenuSortingService: DataGridContextMenuSortingService,
private dataGridContextMenuSortingService: DataGridContextMenuOrderService,
private dataGridContextMenuFilterService: DataGridContextMenuFilterService,
exceptionsCatcherService: ExceptionsCatcherService
) {
super();
@@ -37,6 +39,7 @@ export class SpreadsheetBootstrap extends Bootstrap {
icon: '/icons/grid.png',
});
this.dataGridContextMenuSortingService.register();
this.dataGridContextMenuFilterService.register();
}
load(): void | Promise<void> { }
@@ -1,7 +1,14 @@
export default [
['data_grid_table_sorting', 'Sorting'],
['data_grid_table_filter', 'Filters'],
['data_grid_table_filter_cell_value', 'Cell value'],
['data_grid_table_filter_custom_value', 'Custom'],
['data_grid_table_filter_reset_all_filters', 'Reset all filters'],
['data_grid_table_disable_sorting', 'Disabled'],
['data_grid_table_disable_all_sorting', 'Disable all'],
['data_grid_table_delete_filters_and_sorting', 'Reset filters / sorting'],
['data_grid_table_tooltip_column_header_sort', 'Sort by column'],
['data_grid_table_context_menu_filter_dialog_title', 'Edit value'],
['data_grid_table_context_menu_filter_clipboard_permission', 'Give access to clipboard'],
['data_grid_table_index_column_tooltip', 'Select whole table'],
];
@@ -1,7 +1,14 @@
export default [
['data_grid_table_sorting', 'Сортировка'],
['data_grid_table_filter', 'Фильтры'],
['data_grid_table_filter_cell_value', 'Значение ячейки'],
['data_grid_table_filter_custom_value', 'Произвольный'],
['data_grid_table_filter_reset_all_filters', 'Сбросить все фильтры'],
['data_grid_table_disable_sorting', 'Отключена'],
['data_grid_table_disable_all_sorting', 'Отменить сортировку всех колонок'],
['data_grid_table_delete_filters_and_sorting', 'Отменить фильтры / сортировки'],
['data_grid_table_tooltip_column_header_sort', 'Сортировка по колонке'],
['data_grid_table_context_menu_filter_dialog_title', 'Редактировать значение'],
['data_grid_table_context_menu_filter_clipboard_permission', 'Дать доступ к буферу обмена'],
['data_grid_table_index_column_tooltip', 'Выбрать всю таблицу'],
];
@@ -8,8 +8,9 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService';
import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService';
import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService';
import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService';
import { DataGridSettingsService } from './DataGridSettingsService';
import { LocaleService } from './LocaleService';
import { SpreadsheetBootstrap } from './SpreadsheetBootstrap';
@@ -21,6 +22,7 @@ export const manifest: PluginManifest = {
DataGridSettingsService,
LocaleService,
DataGridContextMenuService,
DataGridContextMenuSortingService,
DataGridContextMenuOrderService,
DataGridContextMenuFilterService,
],
};
@@ -92,6 +92,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
this.requestInfo = {
requestDuration: response?.duration || 0,
requestMessage: response?.statusMessage || '',
requestFilter: response.filterText || '',
source: null,
};
@@ -145,6 +146,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
this.requestInfo = {
requestDuration: response.result?.duration || 0,
requestMessage: 'Saved successfully',
requestFilter: response.result?.filterText || '',
source: null,
};
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { SqlResultColumn } from '@cloudbeaver/core-sdk';
import type { DataTypeLogicalOperation, SqlResultColumn } from '@cloudbeaver/core-sdk';
import type { IDatabaseDataAction } from '../IDatabaseDataAction';
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
@@ -16,4 +16,5 @@ export interface IDatabaseResultDataAction<TKey, TResult extends IDatabaseDataRe
extends IDatabaseDataAction<any, TResult> {
getCellValue: (cell: IResultSetElementKey) => any;
getColumn: (columnIndex: number) => SqlResultColumn | undefined;
getColumnOperations: (columnIndex: number) => DataTypeLogicalOperation[];
}
@@ -0,0 +1,29 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import type { IDatabaseDataAction } from '../IDatabaseDataAction';
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
import type { Order } from '../Order';
export interface IDatabaseDataConstraintAction<TKey, TResult extends IDatabaseDataResult>
extends IDatabaseDataAction<any, TResult> {
setFilter: (attribute: string, operator: string, value?: any) => void;
setOrder: (attribute: string, order: Order, multiple: boolean) => void;
deleteAll: () => void;
deleteFilter: (attribute: string) => void;
deleteFilters: () => void;
deleteOrder: (attribute: string) => void;
deleteOrders: () => void;
deleteDataFilters: () => void;
get: (attribute: string) => SqlDataFilterConstraint | undefined;
getOrder: (attribute: string) => Order;
getFilterConstraints: () => SqlDataFilterConstraint[];
getOrderConstraints: () => SqlDataFilterConstraint[];
}
@@ -13,5 +13,6 @@ export interface IDatabaseDataFormatAction<TKey, TResult extends IDatabaseDataRe
extends IDatabaseDataAction<any, TResult> {
isReadOnly: (key: TKey) => boolean;
get: (value: any) => any;
isNull: (value: any) => boolean;
toString: (value: any) => string;
}
@@ -1,17 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 type { IDatabaseDataAction } from '../IDatabaseDataAction';
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
import type { SortMode } from './ResultSet/ResultSetSortAction';
export interface IDatabaseDataSortAction<TKey, TResult extends IDatabaseDataResult>
extends IDatabaseDataAction<any, TResult> {
setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void;
getSortMode: (columnName: string) => SortMode;
}
@@ -0,0 +1,269 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { DataTypeLogicalOperation, ResultDataFormat, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import { DatabaseDataAction } from '../../DatabaseDataAction';
import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions';
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { EOrder, Order } from '../../Order';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { IDatabaseDataConstraintAction } from '../IDatabaseDataConstraintAction';
import type { IResultSetElementKey } from './IResultSetElementKey';
export const IS_NULL_ID = 'IS_NULL';
export const IS_NOT_NULL_ID = 'IS_NOT_NULL';
@databaseDataAction()
export class ResultSetConstraintAction extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
implements IDatabaseDataConstraintAction<IResultSetElementKey, IDatabaseResultSet> {
static dataFormat = ResultDataFormat.Resultset;
private deleteConstraint(attribute: string) {
if (!this.source.options) {
return;
}
this.source.options.constraints = this.source.options.constraints
.filter(constraint => constraint.attribute !== attribute);
}
private reorderConstraints() {
if (!this.source.options) {
return;
}
this.source.options.constraints = this.source.options.constraints
.map((constaint, idx) => ({ ...constaint, orderPosition: idx }));
}
private deleteEmptyConstraint(attribute: string) {
const constraint = this.get(attribute);
if (constraint && !isFilterConstraint(constraint) && !isOrderConstraint(constraint)) {
this.deleteConstraint(attribute);
this.reorderConstraints();
}
}
get(attribute: string): SqlDataFilterConstraint | undefined {
if (!this.source.options) {
throw new Error('Options must be provided');
}
return this.source.options.constraints.find(constaint => constaint.attribute === attribute);
}
deleteAll(): void {
if (!this.source.options) {
return;
}
this.source.options.constraints = [];
}
deleteFilter(attribute: string): void {
const constraint = this.get(attribute);
if (constraint) {
deleteLogicalOperationFromConstraint(constraint);
this.deleteEmptyConstraint(attribute);
}
}
deleteFilters(): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const newConstraints: SqlDataFilterConstraint[] = [];
for (const constraint of this.source.options.constraints) {
deleteLogicalOperationFromConstraint(constraint);
if (isOrderConstraint(constraint)) {
newConstraints.push({ ...constraint, orderPosition: newConstraints.length });
}
}
this.source.options.constraints = newConstraints;
}
deleteOrders(): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const newConstraints: SqlDataFilterConstraint[] = [];
for (const constraint of this.source.options.constraints) {
deleteOrderFromConstraint(constraint);
if (isFilterConstraint(constraint)) {
newConstraints.push({ ...constraint, orderPosition: newConstraints.length });
}
}
this.source.options.constraints = newConstraints;
}
deleteOrder(attribute: string): void {
const constraint = this.get(attribute);
if (constraint) {
deleteOrderFromConstraint(constraint);
this.deleteEmptyConstraint(attribute);
}
}
deleteDataFilters(): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
this.deleteFilters();
this.source.options.whereFilter = '';
}
setFilter(attribute: string, operator: string, value?: any): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const currentConstraint = this.get(attribute);
if (currentConstraint) {
currentConstraint.operator = operator;
if (value !== undefined) {
currentConstraint.value = value;
} else if (currentConstraint.value !== undefined) {
delete currentConstraint.value;
}
return;
}
const constraint: SqlDataFilterConstraint = {
attribute,
operator,
orderPosition: this.source.options.constraints.length,
};
if (value !== undefined) {
constraint.value = value;
}
this.source.options.constraints.push(constraint);
}
getFilterConstraints(): SqlDataFilterConstraint[] {
if (!this.source.options) {
throw new Error('Options must be provided');
}
return this.source.options.constraints.filter(isFilterConstraint);
}
getOrderConstraints(): SqlDataFilterConstraint[] {
if (!this.source.options) {
throw new Error('Options must be provided');
}
return this.source.options.constraints.filter(isOrderConstraint);
}
setOrder(attribute: string, order: Order, multiple: boolean): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const resetOrder = order === null;
if (!multiple) {
this.deleteOrders();
}
const currentConstraint = this.get(attribute);
if (!currentConstraint) {
if (!resetOrder) {
this.source.options.constraints.push({
attribute,
orderPosition: this.source.options.constraints.length,
orderAsc: order === EOrder.asc,
});
}
return;
}
if (!resetOrder) {
currentConstraint.orderAsc = order === EOrder.asc;
} else {
if (isFilterConstraint(currentConstraint)) {
deleteOrderFromConstraint(currentConstraint);
} else {
this.deleteConstraint(currentConstraint.attribute);
this.reorderConstraints();
}
}
}
getOrder(attribute: string): Order {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const currentConstraint = this.get(attribute);
if (!currentConstraint || !isOrderConstraint(currentConstraint)) {
return null;
}
return currentConstraint.orderAsc ? EOrder.asc : EOrder.desc;
}
}
export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean {
return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID;
}
export function getNextOrder(order: Order): Order {
switch (order) {
case EOrder.asc:
return EOrder.desc;
case EOrder.desc:
return null;
default:
return EOrder.asc;
}
}
export function wrapOperationArgument(operationId: string, argument: any): string {
let result = argument;
if (operationId === 'LIKE') {
result = `%${result} %`;
}
result = `'${result}'`;
return result;
}
export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean {
return constraint.operator !== undefined;
}
function isOrderConstraint(constraint: SqlDataFilterConstraint) {
return constraint.orderAsc !== undefined;
}
function deleteOrderFromConstraint(constraint: SqlDataFilterConstraint) {
delete constraint.orderAsc;
return constraint;
}
function deleteLogicalOperationFromConstraint(constraint: SqlDataFilterConstraint) {
delete constraint.operator;
delete constraint.value;
return constraint;
}
@@ -1,3 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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';
import { DatabaseDataAction } from '../../DatabaseDataAction';
@@ -16,7 +24,7 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
return undefined;
}
return this.result.data.rows[cell.row][cell.column];
return this.result.data.rows?.[cell.row]?.[cell.column];
}
getColumn(columnIndex: number) {
@@ -26,4 +34,14 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
return this.result.data.columns[columnIndex];
}
getColumnOperations(columnIndex: number) {
const column = this.getColumn(columnIndex);
if (!column) {
return [];
}
return column.supportedOperations
.filter(operation => operation.argumentCount === 1 || operation.argumentCount === 0);
}
}
@@ -39,12 +39,16 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
return value !== null && typeof value === 'object';
}
isNull(value: any): boolean {
return this.get(value) === null;
}
get(value: any): any {
if (value !== null && typeof value === 'object') {
if ('text' in value) {
return value.text;
} else if ('value' in value) {
if (typeof value.value === 'object') {
if (value.value !== null && typeof value.value === 'object') {
return JSON.stringify(value.value);
}
return value.value;
@@ -1,129 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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';
import { DatabaseDataAction } from '../../DatabaseDataAction';
import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions';
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { IDatabaseDataSortAction } from '../IDatabaseDataSortAction';
import type { IResultSetElementKey } from './IResultSetElementKey';
export enum ESortMode {
'asc' = 'asc',
'desc' = 'desc'
}
export type SortMode = ESortMode | null;
@databaseDataAction()
export class ResultSetSortAction extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
implements IDatabaseDataSortAction<IResultSetElementKey, IDatabaseResultSet> {
static dataFormat = ResultDataFormat.Resultset;
private clear() {
if (!this.source.options) {
return;
}
this.source.options.constraints = [];
}
private deleteConstraint(columnName: string) {
if (!this.source.options) {
return;
}
this.source.options.constraints = this.source.options.constraints
.filter(constraint => constraint.attribute !== columnName);
}
private reorderConstraints() {
if (!this.source.options) {
return;
}
this.source.options.constraints = this.source.options.constraints
.map((constaint, idx) => ({ ...constaint, orderPosition: idx }));
}
getSortingConstraints() {
if (!this.source.options) {
throw new Error('Options must be provided');
}
return this.source.options.constraints
.filter(constraint => constraint.orderAsc !== undefined);
}
removeSortingConstraints() {
if (!this.source.options) {
throw new Error('Options must be provided');
}
this.source.options.constraints = this.source.options.constraints
.filter(constaint => constaint.orderAsc === undefined);
}
setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const resetSortMode = sortMode === null;
const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName);
if (currentConstraint && multiple) {
if (resetSortMode) {
this.deleteConstraint(currentConstraint.attribute);
this.reorderConstraints();
} else {
currentConstraint.orderAsc = sortMode === ESortMode.asc;
}
return;
}
if (!multiple) {
this.clear();
}
if (!resetSortMode) {
this.source.options.constraints.push({
attribute: columnName,
orderPosition: this.source.options.constraints.length,
orderAsc: sortMode === ESortMode.asc,
});
}
}
getSortMode(columnName: string): SortMode {
if (!this.source.options) {
throw new Error('Options must be provided');
}
const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName);
if (!currentConstraint) {
return null;
}
return currentConstraint.orderAsc ? ESortMode.asc : ESortMode.desc;
}
}
export function getNextSortMode(sortMode: SortMode): SortMode {
switch (sortMode) {
case ESortMode.asc:
return ESortMode.desc;
case ESortMode.desc:
return null;
default:
return ESortMode.asc;
}
}
@@ -19,7 +19,7 @@ import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDa
import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext';
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataSource<TOptions, TResult> {
implements IDatabaseDataSource<TOptions, TResult> {
access: DatabaseDataAccessMode;
dataFormat: ResultDataFormat;
supportedDataFormats: ResultDataFormat[];
@@ -73,6 +73,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
this.requestInfo = {
requestDuration: 0,
requestMessage: '',
requestFilter: '',
source: null,
};
this.error = null;
@@ -17,6 +17,7 @@ import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext';
export interface IRequestInfo {
readonly requestDuration: number;
readonly requestMessage: string;
readonly requestFilter: string;
readonly source: string | null;
}
@@ -0,0 +1,14 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 enum EOrder {
'asc' = 'asc',
'desc' = 'desc'
}
export type Order = EOrder | null;
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { InlineEditor } from '@cloudbeaver/core-app';
@@ -15,6 +15,7 @@ import { PlaceholderComponent, useObjectRef } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
import type { ITableHeaderPlaceholderProps } from './TableHeaderService';
const styles = composes(
@@ -36,29 +37,44 @@ export const TableWhereFilter: PlaceholderComponent<ITableHeaderPlaceholderProps
resultIndex,
}) {
const translate = useTranslate();
const [filterValue, setValue] = useState(() => model.source.options?.whereFilter || '');
const hasResult = model.source.hasResult(resultIndex);
let filterValue = model.source.options?.whereFilter || '';
if (hasResult) {
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
if (constraints.getFilterConstraints().length > 0 && model.source.requestInfo.requestFilter) {
filterValue = model.source.requestInfo.requestFilter;
}
}
const setValue = useCallback((filterValue: string) => {
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
model.source.options.whereFilter = filterValue;
constraints?.deleteFilters();
}, [model.source.options]);
const props = useObjectRef({ model, resultIndex, filterValue });
const handleApply = useCallback(() => {
const { model, resultIndex, filterValue } = props;
const { model, resultIndex } = props;
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
model.source.options!.whereFilter = filterValue;
model.refresh();
}, []);
const resetFilter = useCallback(() => {
const { model, resultIndex, filterValue } = props;
const { model, resultIndex } = props;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
const applyNeeded = model.source.options?.whereFilter === filterValue;
setValue('');
constraints.deleteDataFilters();
const applyNeeded = !!model.requestInfo.requestFilter;
if (applyNeeded) {
model.source.options!.whereFilter = '';
model.refresh();
}
}, []);
@@ -6,10 +6,11 @@ export * from './DatabaseDataModel/Actions/ResultSet/IResultSetElementKey';
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction';
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction';
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction';
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSortAction';
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator';
export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction';
export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction';
export * from './DatabaseDataModel/Actions/IDatabaseDataConstraintAction';
export * from './DatabaseDataModel/DatabaseDataAction';
export * from './DatabaseDataModel/DatabaseDataActions';
export * from './DatabaseDataModel/DatabaseDataEditor';
@@ -25,6 +26,7 @@ export * from './DatabaseDataModel/IDatabaseDataResult';
export * from './DatabaseDataModel/IDatabaseDataSource';
export * from './DatabaseDataModel/IDatabaseExecutionContext';
export * from './DatabaseDataModel/IDatabaseResultSet';
export * from './DatabaseDataModel/Order';
// All Services and Components that is provided by this plugin should be exported here
export * from './TableViewer/TableViewerStorageService';
@@ -87,6 +87,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
this.requestInfo = {
requestDuration: response.result?.duration || 0,
requestMessage: 'Saved successfully',
requestFilter: response.result?.filterText || '',
source: this.options.query || null,
};
@@ -120,6 +121,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
this.requestInfo = {
requestDuration: response.duration || 0,
requestMessage: response.statusMessage || '',
requestFilter: response.filterText || '',
source: this.options?.query || null,
};