Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
Serge Rider
2021-12-15 19:32:41 +03:00
49 changed files with 723 additions and 207 deletions
@@ -12,7 +12,7 @@
<extension point="org.eclipse.core.runtime.products" id="product">
<product application="io.cloudbeaver.product.ce.application" description="Cloudbeaver Web UI Application" name="CloudBeaver CE Server">
<property name="appName" value="CloudBeaver CE Server"/>
<property name="versionUpdateURL" value="https://cloudbeaver.io/product/cloudbeaver-ce-version.xml"/>
<property name="versionUpdateURL" value="https://cloudbeaver.io/product/cloudbeaver-ce-version.json"/>
</product>
</extension>
+2 -1
View File
@@ -25,6 +25,7 @@ interface IState {
export interface IMouseHook<T> {
state: IState;
ref: T | null;
reference: (obj: T) => void;
}
@@ -73,5 +74,5 @@ export function useMouse<T extends HTMLElement>(options: IOptions = {}): IMouseH
};
}, [reference]);
return { state, reference: setReference };
return { ref: reference, state, reference: setReference };
}
@@ -14,17 +14,17 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const package = require(resolve('package.json'));
const certPath = resolve(__dirname, '../../../../../server-cert.pem')
const keyPath = resolve(__dirname, '../../../../../server-key.pem')
const certPath = resolve(__dirname, '../../../../../certs/private.pem')
const keyPath = resolve(__dirname, '../../../../../certs/private.key')
let https = false;
// if(fs.existsSync(certPath) && fs.existsSync(keyPath)) {
// https = {
// key: fs.readFileSync(keyPath),
// cert: fs.readFileSync(certPath)
// }
// }
if(fs.existsSync(certPath) && fs.existsSync(keyPath)) {
https = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
}
}
module.exports = (env, argv) => merge(commonConfig(env, argv), {
entry: {
@@ -11,7 +11,7 @@ export default [
['administration_configuration_wizard_configuration', 'Server configuration'],
['administration_configuration_wizard_configuration_step_description', 'Main server configuration'],
['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'],
['administration_configuration_wizard_configuration_message', 'You can allow integration to external services such as SSO once the easy configuration is completed.\n\r Administrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'],
['administration_configuration_wizard_configuration_message', 'You will be able to add additional services after the server configuration.\n\r Administrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'],
['administration_configuration_tools_save_tooltip', 'Save configuration'],
['administration_configuration_tools_cancel_tooltip', 'Reset changes'],
@@ -43,14 +43,19 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
});
const localProvider = providers.resource.get(AUTH_PROVIDER_LOCAL_ID);
const primaryProvider = providers.resource.get(providers.resource.getPrimary());
const externalAuthentication = localProvider === undefined && providerList.length === 1;
const authenticationDisabled = serverConfig.enabledAuthProviders?.length === 0;
useExecutor({
executor: formContext.changeExecutor,
handlers: [function switchControls() {
if (serverConfig.enabledAuthProviders?.length === 0 && localProvider) {
serverConfig.anonymousAccessEnabled = true;
if (serverConfig.enabledAuthProviders?.length === 0) {
if (localProvider) {
serverConfig.anonymousAccessEnabled = true;
} else if (primaryProvider) {
serverConfig.enabledAuthProviders.push(primaryProvider.id);
}
}
if (serverConfig.enabledAuthProviders?.length) {
@@ -91,9 +96,18 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
<>
{providerList.map(provider => {
const links = authProviderService.getServiceDescriptionLinks(provider);
const disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat));
let disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat));
const tooltip = disabled ? `Following services need to be enabled: "${provider.requiredFeatures.join(', ')}"` : '';
if (
!localProvider
&& primaryProvider?.id === provider.id
&& serverConfig.enabledAuthProviders?.length === 1
&& serverConfig.enabledAuthProviders.includes(provider.id)
) {
disabled = true;
}
if (
configurationWizard
&& (
@@ -41,7 +41,7 @@ export interface IEditorRef {
focus: () => void;
}
const lockNavigation = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
const lockNavigation = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'];
export const CellEditor = observer<Pick<EditorProps<IResultSetRowKey>, 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({
row,
@@ -8,11 +8,11 @@
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useMemo } from 'react';
import type { CellRendererProps } from 'react-data-grid';
import { useContext, useEffect } from 'react';
import type { CalculatedColumn, CellRendererProps } from 'react-data-grid';
import { Cell } from 'react-data-grid';
import { useMouse, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { getComputed, useMouse, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { IResultSetElementKey, IResultSetRowKey, isBooleanValuePresentationAvailable, DatabaseEditChangeType } from '@cloudbeaver/plugin-data-viewer';
@@ -65,7 +65,7 @@ export const CellRenderer = observer<CellRendererProps<IResultSetRowKey>>(functi
editionState: computed,
}, { row, column, rowIdx });
const classes = useMemo(() => computed(() => {
const classes = getComputed(() => {
let classes = '';
if (cellContext.isSelected) {
classes += ' rdg-cell-custom-selected';
@@ -88,7 +88,31 @@ export const CellRenderer = observer<CellRendererProps<IResultSetRowKey>>(functi
}
}
return classes;
}), []).get();
});
function isEditable(column: CalculatedColumn<IResultSetRowKey, unknown>): boolean {
if (
!editingContext
|| !tableDataContext
|| !cellContext.cell
) {
return false;
}
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
const value = tableDataContext.getCellValue(cellContext.cell);
if (!resultColumn || value === undefined) {
return false;
}
const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn);
return !(
handleByBooleanFormatter
|| tableDataContext.isCellReadonly(cellContext.cell)
);
}
const state = useObjectRef(() => ({
mouseUp(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
@@ -112,27 +136,8 @@ export const CellRenderer = observer<CellRendererProps<IResultSetRowKey>>(functi
},
doubleClick(event: React.MouseEvent<HTMLDivElement>) {
if (
!this.editingContext
|| !this.tableDataContext
!this.isEditable(this.column)
|| EventContext.has(event, EventStopPropagationFlag)
|| !cellContext.cell
) {
return;
}
const resultColumn = this.tableDataContext.getColumnInfo(cellContext.cell.column);
const value = this.tableDataContext.getCellValue(cellContext.cell);
if (!resultColumn || value === undefined) {
return;
}
const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn);
if (
!this.column.editable
|| handleByBooleanFormatter
|| this.tableDataContext.format.isReadOnly(cellContext.cell)
) {
return;
}
@@ -147,6 +152,7 @@ export const CellRenderer = observer<CellRendererProps<IResultSetRowKey>>(functi
dataGridContext,
editingContext,
tableDataContext,
isEditable,
}, ['doubleClick', 'mouseUp']);
useEffect(() => () => editingContext?.closeEditor(cellContext.position), []);
@@ -25,6 +25,7 @@ export interface IDataGridContext {
isGridInFocus: () => boolean;
getEditorPortal: () => HTMLDivElement | null;
getDataGridApi: () => DataGridHandle | null;
focus: () => void;
}
export const DataGridContext = createContext<IDataGridContext>(undefined as any);
@@ -7,7 +7,7 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { isBooleanValuePresentationAvailable, DatabaseEditChangeType, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, DatabaseSelectAction, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
import { isBooleanValuePresentationAvailable, DatabaseEditChangeType, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuService } from './DataGridContextMenuService';
@@ -124,7 +124,7 @@ export class DataGridContextMenuCellEditingService {
},
onClick(context) {
const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction);
editor.addRowCopy(context.data.key.row);
editor.duplicateRow(context.data.key.row);
},
}
);
@@ -62,7 +62,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.setFilter(columnLabel, operator, filterValue);
await model.refresh(true);
await model.request(true);
});
}
@@ -160,7 +160,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteData();
await model.refresh(true);
await model.request(true);
});
},
}
@@ -403,7 +403,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteFilter(columnLabel);
await model.refresh(true);
await model.request(true);
});
},
}
@@ -430,7 +430,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteDataFilters();
await model.refresh(true);
await model.request(true);
});
},
}
@@ -35,7 +35,7 @@ export class DataGridContextMenuOrderService {
await model.requestDataAction(async () => {
constraints.setOrder(columnLabel, order, true);
await model.refresh(true);
await model.request(true);
});
}
@@ -143,7 +143,7 @@ export class DataGridContextMenuOrderService {
const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction);
await context.data.model.requestDataAction(async () => {
constraints.deleteOrders();
await context.data.model.refresh(true);
await context.data.model.request(true);
});
},
}
@@ -13,9 +13,12 @@ import type { DataGridHandle } from 'react-data-grid';
import styled from 'reshadow';
import { TextPlaceholder, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { Executor } from '@cloudbeaver/core-executor';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { ClipboardService } from '@cloudbeaver/core-ui';
import {
DatabaseDataSelectActionsData, DatabaseEditChangeType, IDatabaseResultSet, IDataPresentationProps,
IResultSetEditActionData, IResultSetElementKey, IResultSetPartialKey, ResultSetDataKeysUtils, ResultSetSelectAction
@@ -51,6 +54,7 @@ const headerHeight = 28;
export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResultSet>>(function DataGridTable({ model, actions, resultIndex, className }) {
const translate = useTranslate();
const clipboardService = useService(ClipboardService);
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<HTMLDivElement>(null);
const dataGridDivRef = useRef<HTMLDivElement | null>(null);
@@ -64,13 +68,11 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction);
const tableData = useTableData(model, resultIndex, dataGridDivRef);
const focusSyncRef = useRef<CellPosition | null>(null);
const gridSelectionContext = useGridSelectionContext(tableData, selectionAction);
const editingContext = useEditing({
readonly: model.isReadonly() || model.isDisabled(resultIndex),
onEdit: (position, key) => {
onEdit: (position, code, key) => {
const column = tableData.getColumn(position.idx);
const row = tableData.getRow(position.rowIdx);
@@ -80,22 +82,42 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
const cellKey: IResultSetElementKey = { row, column: column.columnDataIndex };
// TODO: not works yet
switch (key) {
case 'Delete':
if (tableData.isCellReadonly(cellKey)) {
return false;
}
switch (code) {
case 'Backspace':
tableData.editor.set(cellKey, '');
break;
case 'Enter':
break;
default:
if (key) {
tableData.editor.set(cellKey, key);
if (/^[\d\p{L}]$/iu.test(key) && key.length === 1) {
tableData.editor.set(cellKey, key);
} else {
return false;
}
}
}
return true;
},
onCloseEditor: () => {
restoreFocus();
},
});
const tableData = useTableData(model, resultIndex, dataGridDivRef);
const gridSelectionContext = useGridSelectionContext(tableData, selectionAction);
function restoreFocus() {
const gridDiv = gridContainerRef.current;
const focusSink = gridDiv?.querySelector<HTMLDivElement>('.rdg-focus-sink');
focusSink?.focus();
}
function isGridInFocus(): boolean {
const gridDiv = gridContainerRef.current;
const focusSink = gridDiv?.querySelector('.rdg-focus-sink');
@@ -121,7 +143,7 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
}
}
const { onKeydownHandler } = useGridSelectedCellsCopy(tableData, selectionAction, gridSelectionContext);
const gridSelectedCellCopy = useGridSelectedCellsCopy(tableData, selectionAction, gridSelectionContext);
const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({
onDragStart: startPosition => {
dataGridRef.current?.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx });
@@ -134,6 +156,99 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
},
});
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
gridSelectedCellCopy.onKeydownHandler(event);
if (EventContext.has(event, EventStopPropagationFlag)) {
return;
}
const cell = selectionAction.getFocusedElement();
const activeElements = selectionAction.getActiveElements();
const activeRows = selectionAction.getActiveRows();
if (!cell) {
return;
}
const idx = tableData.getColumnIndexFromColumnKey(cell.column);
const rowIdx = tableData.getRowIndexFromKey(cell.row);
const position: CellPosition = { idx, rowIdx };
if (editingContext.isEditing(position)) {
return;
}
switch (event.nativeEvent.code) {
case 'Escape': {
tableData.editor.revert(...activeElements);
return;
}
case 'Insert': {
if (event.altKey) {
if (event.ctrlKey || event.metaKey) {
tableData.editor.duplicate(...activeRows);
} else {
tableData.editor.add(cell);
}
return;
}
}
}
const editingState = tableData.editor.getElementState(cell);
switch (event.nativeEvent.code) {
case 'Delete': {
const filteredRows = activeRows
.filter(cell => tableData.editor.getElementState(cell) !== DatabaseEditChangeType.delete);
if (filteredRows.length > 0) {
const editor = tableData.editor;
const firstRow = filteredRows[0];
const editingState = tableData.editor.getElementState(firstRow);
editor.delete(...filteredRows);
if (editingState === DatabaseEditChangeType.add) {
if (rowIdx - 1 > 0) {
dataGridRef.current?.selectCell({ idx, rowIdx: rowIdx - 1 });
}
} else {
if (rowIdx + 1 < tableData.rows.length) {
dataGridRef.current?.selectCell({ idx, rowIdx: rowIdx + 1 });
}
}
}
return;
}
case 'KeyV': {
if (editingState === DatabaseEditChangeType.delete) {
return;
}
if (event.ctrlKey || event.metaKey) {
if (!clipboardService.clipboardAvailable || clipboardService.state === 'denied' || tableData.isCellReadonly(cell)) {
return;
}
clipboardService
.read()
.then(value => tableData.editor.set(cell, value))
.catch();
return;
}
}
}
if (editingState === DatabaseEditChangeType.delete) {
return;
}
editingContext.edit({ idx, rowIdx }, event.nativeEvent.code, event.key);
}
useEffect(() => {
function syncEditor(data: IResultSetEditActionData) {
const editor = tableData.editor;
@@ -234,7 +349,7 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
selectionAction.focus({
row,
column: column.columnDataIndex ?? { index: 0 },
column: { index: 0, ...column.columnDataIndex },
});
};
@@ -267,7 +382,8 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
isGridInFocus,
getEditorPortal: () => editorRef.current,
getDataGridApi: () => dataGridRef.current,
}), [model, actions, resultIndex, editorRef, dataGridRef, gridContainerRef]);
focus: restoreFocus,
}), [model, actions, resultIndex, editorRef, dataGridRef, gridContainerRef, restoreFocus]);
if (!tableData.columns.length) {
return <TextPlaceholder>{translate('data_grid_table_empty_placeholder')}</TextPlaceholder>;
@@ -282,7 +398,7 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
ref={setContainersRef}
className="cb-react-grid-container"
tabIndex={-1}
onKeyDown={onKeydownHandler}
onKeyDown={handleKeyDown}
onMouseDown={onMouseDownHandler}
onMouseMove={onMouseMoveHandler}
>
@@ -290,7 +406,6 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
ref={dataGridRef}
className={`cb-react-grid-theme ${className}`}
columns={tableData.columns}
cellNavigationMode={editingContext.isEditorActive() ? 'NONE' : 'CHANGE_ROW'}
defaultColumnOptions={{
minWidth: 50,
resizable: true,
@@ -70,15 +70,20 @@ export const OrderButton = observer<Props>(function OrderButton({
const nextOrder = getNextOrder(currentOrder);
await model.requestDataAction(async () => {
constraints.setOrder(attribute, nextOrder, e.ctrlKey || e.metaKey);
await model.refresh(true);
await model.request(true);
});
};
function preventFocus(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
event.preventDefault();
}
return styled(styles)(
<order-button
as='div'
title={translate('data_grid_table_tooltip_column_header_order')}
className={className}
onMouseDown={preventFocus}
onClick={handleSort}
{...use({ disabled: loading })}
>
@@ -93,13 +93,18 @@ export const TableColumnHeader = observer<HeaderRendererProps<any>>(function Tab
}
}
}
const handleColumnSelection = (e: React.MouseEvent<HTMLDivElement>) => {
gridSelectionContext.selectColumn(calculatedColumn.idx, e.ctrlKey || e.metaKey);
};
function preventFocus(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
event.preventDefault();
}
function handleClick(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
gridSelectionContext.selectColumn(calculatedColumn.idx, event.ctrlKey || event.metaKey);
}
return styled(useStyles(headerStyles))(
<table-header>
<shrink-container as='div' title={columnTooltip} onClick={handleColumnSelection}>
<shrink-container as='div' title={columnTooltip} onMouseDown={preventFocus} onClick={handleClick}>
<icon>
{icon && <StaticImage icon={icon} />}
{!dataReadonly && columnReadOnly && <readonly-status className='rdg-table-header__readonly-status' />}
@@ -43,12 +43,21 @@ export const TableIndexColumnHeader = observer<HeaderRendererProps<any>>(functio
throw new Error('Contexts required');
}
const readonly = getComputed(
() => tableDataContext.isReadOnly() || dataGridContext.model.isReadonly()
);
const readonly = getComputed(() => (
tableDataContext.isReadOnly()
|| dataGridContext.model.isReadonly()
));
function preventFocus(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
event.preventDefault();
}
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
selectionContext.selectTable();
}
return styled(styles)(
<container title={translate('data_grid_table_index_column_tooltip')} onClick={() => selectionContext.selectTable()}>
<container title={translate('data_grid_table_index_column_tooltip')} onMouseDown={preventFocus} onClick={handleClick}>
{readonly && <IconOrImage title={translate('data_grid_table_readonly_tooltip')} icon='/icons/lock.png' />}
{props.column.name}
</container>
@@ -49,6 +49,7 @@ export interface ITableData {
isIndexColumn: (columnKey: string) => boolean;
isIndexColumnInRange: (columnsRange: Array<Column<IResultSetRowKey, any>>) => boolean;
isReadOnly: () => boolean;
isCellReadonly: (key: Partial<IResultSetElementKey>) => boolean;
}
export const TableDataContext = createContext<ITableData>(undefined as any);
@@ -9,6 +9,7 @@
import { useCallback } from 'react';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { copyToClipboard } from '@cloudbeaver/core-utils';
import { IResultSetColumnKey, IResultSetElementKey, ResultSetDataKeysUtils, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
@@ -65,9 +66,6 @@ function getSelectedCellsValue(
return rowsValues.join('\r\n');
}
// needed for event.code
type IKeyboardEvent = React.KeyboardEvent<HTMLDivElement> & KeyboardEvent;
export function useGridSelectedCellsCopy(
tableData: ITableData,
resultSetSelectAction: ResultSetSelectAction,
@@ -75,8 +73,10 @@ export function useGridSelectedCellsCopy(
) {
const props = useObjectRef({ tableData, selectionContext, resultSetSelectAction });
const onKeydownHandler = useCallback((event: IKeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) {
const onKeydownHandler = useCallback((event: React.KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.nativeEvent.code === EVENT_KEY_CODE.C) {
EventContext.set(event, EventStopPropagationFlag);
const focusedElement = props.resultSetSelectAction.getFocusedElement();
let value: string | null = null;
@@ -11,7 +11,7 @@ import type { Column } from 'react-data-grid';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { TextTools, uuid } from '@cloudbeaver/core-utils';
import { IDatabaseDataModel, IDatabaseResultSet, IResultSetColumnKey, IResultSetRowKey, ResultSetDataAction, ResultSetDataKeysUtils, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction } from '@cloudbeaver/plugin-data-viewer';
import { IDatabaseDataModel, IDatabaseResultSet, IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey, ResultSetDataAction, ResultSetDataKeysUtils, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction } from '@cloudbeaver/plugin-data-viewer';
import { IndexFormatter } from './Formatters/IndexFormatter';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
@@ -33,7 +33,8 @@ export const indexColumn: Column<IResultSetRowKey, any> = {
export function useTableData(
model: IDatabaseDataModel<any, IDatabaseResultSet>,
resultIndex: number,
gridDIVElement: React.RefObject<HTMLDivElement | null>
gridDIVElement: React.RefObject<HTMLDivElement | null>,
onCellKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void
): ITableData {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
@@ -77,6 +78,9 @@ export function useTableData(
editable: true,
width: Math.min(300, measuredCells[index]),
headerRenderer: TableColumnHeader,
editorOptions: {
onCellKeyDown,
},
}));
columns.unshift(indexColumn);
@@ -152,6 +156,18 @@ export function useTableData(
isReadOnly() {
return this.columnKeys.every(column => this.getColumnInfo(column)?.readOnly);
},
isCellReadonly(key: Partial<IResultSetElementKey>) {
if (!key.column) {
return true;
}
const column = this.getColumnByDataIndex(key.column);
return (
!column.editable
|| this.format.isReadOnly(key)
);
},
}), {
columns: computed,
rows: computed,
@@ -10,7 +10,7 @@ import { createContext } from 'react';
export interface IEditingContext {
readonly readonly: boolean;
edit: (position: CellPosition, key?: string) => void;
edit: (position: CellPosition, code?: string, key?: string) => void;
closeEditor: (position: CellPosition) => void;
close: () => void;
isEditorActive: () => boolean;
@@ -24,7 +24,8 @@ function getPositionHash(position: CellPosition): string {
interface IEditingOptions {
readonly?: boolean;
onEdit: (position: CellPosition, key?: string) => boolean;
onEdit: (position: CellPosition, code?: string, key?: string) => boolean;
onCloseEditor?: (position: CellPosition) => void;
}
export function useEditing(options: IEditingOptions): IEditingContext {
@@ -40,18 +41,12 @@ export function useEditing(options: IEditingOptions): IEditingContext {
get readonly() {
return state.readonly;
},
edit(position: CellPosition, key?: string) {
edit(position: CellPosition, code?: string, key?: string) {
if (state.options.readonly) {
return;
}
// TODO: not works yet
switch (key) {
case 'Escape':
state.editingCells.delete(getPositionHash(position));
break;
}
if (!state.options.onEdit(position, key)) {
if (!state.options.onEdit(position, code, key)) {
return;
}
@@ -62,8 +57,15 @@ export function useEditing(options: IEditingOptions): IEditingContext {
},
closeEditor(position: CellPosition) {
const info = state.editingCells.get(getPositionHash(position));
if (!info.editing) {
return;
}
info.editing = false;
state.editorOpened = false;
state.options.onCloseEditor?.(position);
},
close() {
state.editingCells.clear();
@@ -126,7 +126,7 @@ export class DataViewerTabService {
// TODO: used for initial data fetch, but can repeat request each time data tab is selected,
// so probably should be refactored and managed by presentation
if (model.source.error === null && model.source.results.length === 0) {
model.refresh();
model.request();
}
}
@@ -143,7 +143,7 @@ export class DataViewerTabService {
await model.requestDataAction(() => {
canClose = true;
});
} catch {}
} catch { }
return canClose;
}
@@ -42,7 +42,7 @@ export abstract class DatabaseEditAction<TKey, TValue, TResult extends IDatabase
abstract get(key: TKey): TValue | undefined;
abstract set(key: TKey, value: TValue): void;
abstract add(key?: TKey): void;
abstract addCopy(key: TKey): void;
abstract duplicate(...key: TKey[]): void;
abstract delete(...key: TKey[]): void;
abstract applyUpdate(result: TResult): void;
abstract revert(...key: TKey[]): void;
@@ -31,6 +31,7 @@ export abstract class DatabaseSelectAction<TKey, TResult extends IDatabaseDataRe
abstract isElementSelected(key: TKey): boolean;
abstract getFocusedElement(): TKey | null;
abstract getSelectedElements(): TKey[];
abstract getActiveElements(): TKey[];
abstract set(key: TKey, selected: boolean): void;
abstract clear(): void;
}
@@ -96,7 +96,7 @@ export class DocumentEditAction
throw new Error('Not implemented');
}
addCopy(key: IDocumentElementKey): void {
duplicate(key: IDocumentElementKey): void {
throw new Error('Not implemented');
}
@@ -54,7 +54,7 @@ export interface IDatabaseDataEditAction<TKey, TValue, TResult extends IDatabase
get: (key: TKey) => TValue | undefined;
set: (key: TKey, value: TValue) => void;
add: (key?: TKey) => void;
addCopy: (key: TKey) => void;
duplicate: (...key: TKey[]) => void;
delete: (key: TKey) => void;
applyUpdate: (result: TResult) => void;
revert: (key: TKey) => void;
@@ -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.
*/
import { createAction } from '@cloudbeaver/core-view';
export const DATA_VIEWER_CONSTRAINTS_DELETE_ACTION = createAction('data-viewer-constraints-delete', {
label: 'data_grid_table_delete_filters_and_orders',
tooltip: 'data_grid_table_delete_filters_and_orders',
icon: 'erase',
});
@@ -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.
*/
import { createAction } from '@cloudbeaver/core-view';
export const DATA_VIEWER_REFRESH_ACTION = createAction('data-viewer-refresh', {
label: 'ui_refresh',
tooltip: 'data_viewer_refresh_result_set',
icon: 'reload',
});
@@ -208,21 +208,35 @@ export class ResultSetEditAction
}
}
addCopy(key: IResultSetElementKey): void {
this.addRowCopy(key.row);
}
duplicate(...keys: IResultSetElementKey[]): void {
const rows: IResultSetRowKey[] = [];
const rowKeys = new Set<string>();
addRowCopy(row: IResultSetRowKey): void {
let value = this.data.getRowValue(row);
for (const key of keys) {
const serialized = ResultSetDataKeysUtils.serialize(key.row);
const editedValue = this.editorData
.get(ResultSetDataKeysUtils.serialize(row));
if (editedValue) {
value = editedValue.update;
if (!rowKeys.has(serialized)) {
rows.push(key.row);
rowKeys.add(serialized);
}
}
this.addRow(row, JSON.parse(JSON.stringify(value)));
this.duplicateRow(...rows);
}
duplicateRow(...rows: IResultSetRowKey[]): void {
for (const row of rows) {
let value = this.data.getRowValue(row);
const editedValue = this.editorData
.get(ResultSetDataKeysUtils.serialize(row));
if (editedValue) {
value = editedValue.update;
}
this.addRow(row, JSON.parse(JSON.stringify(value)));
}
}
delete(...keys: IResultSetElementKey[]): void {
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, toJS } from 'mobx';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
@@ -160,6 +160,46 @@ export class ResultSetSelectAction extends DatabaseSelectAction<any, IDatabaseRe
return Array.from(this.selectedElements.values()).flat();
}
getActiveElements(): IResultSetElementKey[] {
const elements = this.getSelectedElements();
const focus = this.getFocusedElement();
if (elements.length === 0 && focus) {
return [focus];
}
return elements;
}
getSelectedRows(): IResultSetElementKey[] {
const cells: IResultSetElementKey[] = [];
const rowsKeys = new Set<string>();
const elements = this.getSelectedElements();
for (const cell of elements) {
const key = ResultSetDataKeysUtils.serialize(cell.row);
if (!rowsKeys.has(key)) {
cells.push(cell);
rowsKeys.add(key);
}
}
return cells;
}
getActiveRows(): IResultSetElementKey[] {
const elements = this.getSelectedRows();
const focus = this.getFocusedElement();
if (elements.length === 0 && focus) {
return [focus];
}
return elements;
}
set(key: IResultSetPartialKey, selected: boolean, silent?: boolean): void {
if (key.row === undefined) {
for (const row of this.view.rowKeys) {
@@ -233,7 +273,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction<any, IDatabaseRe
return;
}
this.focusedElement = key;
this.focusedElement = toJS(key);
this.actions.execute({
type: 'focus',
resultId: this.result.id,
@@ -118,6 +118,14 @@ implements IDatabaseDataModel<TOptions, TResult> {
}
async refresh(concurrent?: boolean): Promise<void> {
if (concurrent) {
await this.source.refreshData();
return;
}
await this.requestDataAction(() => this.source.refreshData());
}
async request(concurrent?: boolean): Promise<void> {
if (concurrent) {
await this.source.requestData();
return;
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observable, makeObservable, action } from 'mobx';
import { observable, makeObservable, action, toJS } from 'mobx';
import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
@@ -26,6 +26,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
results: TResult[];
offset: number;
count: number;
prevOptions: Readonly<TOptions> | null;
options: TOptions | null;
requestInfo: IRequestInfo;
error: Error | null;
@@ -45,6 +46,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
this.results = [];
this.offset = 0;
this.count = 0;
this.prevOptions = null;
this.options = null;
this.disabled = false;
this.activeRequest = null;
@@ -69,6 +71,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
results: observable,
offset: observable,
count: observable,
prevOptions: observable,
options: observable,
requestInfo: observable,
error: observable.ref,
@@ -276,6 +279,13 @@ implements IDatabaseDataSource<TOptions, TResult> {
}
}
async refreshData(): Promise<void> {
if (this.prevOptions) {
this.options = toJS(this.prevOptions);
}
await this.requestData();
}
async saveData(): Promise<void> {
if (this.activeRequest) {
try {
@@ -318,6 +328,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
abstract dispose(): Promise<void>;
async requestDataAction(): Promise<TResult[] | null> {
this.prevOptions = toJS(this.options);
return this.request(this.results);
}
}
@@ -45,6 +45,7 @@ export interface IDatabaseDataModel<TOptions = any, TResult extends IDatabaseDat
retry: () => Promise<void>;
save: () => Promise<void>;
refresh: (concurrent?: boolean) => Promise<void>;
request: (concurrent?: boolean) => Promise<void>;
reload: () => Promise<void>;
requestDataPortion: (offset: number, count: number) => Promise<void>;
cancel: () => Promise<void> | void;
@@ -33,6 +33,7 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
readonly results: TResult[];
readonly offset: number;
readonly count: number;
readonly prevOptions: Readonly<TOptions> | null;
readonly options: TOptions | null;
readonly requestInfo: IRequestInfo;
readonly error: Error | null;
@@ -80,6 +81,7 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
retry: () => Promise<void>;
runTask: <T>(task: () => Promise<T>) => Promise<T>;
requestData: () => Promise<void> | void;
refreshData: () => Promise<void> | void;
saveData: () => Promise<void> | void;
cancel: () => Promise<void> | void;
clearError: () => void;
@@ -101,19 +101,17 @@ export class TableFooterMenuService {
return !editor?.hasFeature('add');
},
isDisabled(context) {
const select = context.data.model.source.getActionImplementation(
context.data.resultIndex,
DatabaseSelectAction
);
const focus = select?.getFocusedElement();
return (
if (
context.data.model.isLoading()
|| context.data.model.isDisabled(context.data.resultIndex)
|| !context.data.model.source.hasResult(context.data.resultIndex)
|| !focus
);
) {
return true;
}
const selectedElements = getActiveElements(context.data.model, context.data.resultIndex);
return selectedElements.length === 0;
},
onClick(context) {
const editor = context.data.model.source.getActionImplementation(
@@ -125,16 +123,9 @@ export class TableFooterMenuService {
return;
}
const select = context.data.model.source.getActionImplementation(
context.data.resultIndex,
DatabaseSelectAction
);
const selectedElements = getActiveElements(context.data.model, context.data.resultIndex);
const focus = select?.getFocusedElement();
if (focus) {
editor.addCopy(focus);
}
editor.duplicate(...selectedElements);
},
});
this.registerMenuItem({
@@ -375,12 +366,5 @@ function getActiveElements(model: IDatabaseDataModel<any, IDatabaseDataResult>,
DatabaseSelectAction
);
const selectedElements = select?.getSelectedElements() || [];
const focus = select?.getFocusedElement();
if (selectedElements.length === 0 && focus) {
selectedElements.push(focus);
}
return selectedElements;
return select?.getActiveElements() ?? [];
}
@@ -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.
*/
import { createDataContext } from '@cloudbeaver/core-view';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions';
export const DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL = createDataContext<IDatabaseDataModel<IDatabaseDataOptions, any>>('data-viewer-database-data-model');
@@ -0,0 +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 { createDataContext } from '@cloudbeaver/core-view';
export const DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX = createDataContext<number>('data-viewer-database-data-model-result-index');
@@ -0,0 +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 { createMenu } from '@cloudbeaver/core-view';
export const DATA_VIEWER_DATA_MODEL_TOOLS_MENU = createMenu('data-viewer-data-model-tools', 'Data viewer data model tools menu');
@@ -27,16 +27,18 @@ const styles = css`
interface Props {
model: IDatabaseDataModel<any, any>;
resultIndex: number;
className?: string;
}
export const TableHeader = observer<Props>(function TableHeader({
model,
resultIndex,
className,
}) {
const service = useService(TableHeaderService);
return styled(styles)(
<table-header as="div">
<table-header className={className}>
<Placeholder container={service.tableHeaderPlaceholder} model={model} resultIndex={resultIndex} />
</table-header>
);
@@ -0,0 +1,98 @@
/*
* 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 { css } from 'reshadow';
import type { PlaceholderComponent } from '@cloudbeaver/core-blocks';
import { composes } from '@cloudbeaver/core-theming';
import { MenuBar } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL';
import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX';
import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU';
import type { ITableHeaderPlaceholderProps } from './TableHeaderService';
const TABLE_HEADER_MENU_BAR_STYLES = composes(
css`
menu-bar {
composes: theme-border-color-background theme-background-surface theme-text-on-surface from global;
}
menu-bar-item {
composes: theme-ripple from global;
}
MenuSeparator {
composes: theme-border-color-background from global;
}
`,
css`
menu-bar {
composes: theme-typography--body2 from global;
display: flex;
margin-left: 8px;
box-sizing: border-box;
border: 1px solid;
height: 24px;
}
menu-bar-item {
padding: 4px;
display: flex;
align-items: center;
cursor: pointer;
background: transparent;
outline: none;
color: inherit;
&[use|hidden] {
display: none;
}
& IconOrImage {
display: block;
width: 16px;
}
& Loader {
width: 16px;
}
& item-label {
display: block;
text-transform: uppercase;
font-weight: 700;
}
& IconOrImage + item-label, & Loader + item-label {
padding-left: 8px
}
}
MenuSeparator {
height: 100%;
margin: 0;
border: 0;
border-right: 1px solid;
}
`
);
export const TableHeaderMenu: PlaceholderComponent<ITableHeaderPlaceholderProps> = observer(function TableHeaderMenu({
model,
resultIndex,
}) {
const menu = useMenu(DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
menu.context.set(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL, model);
menu.context.set(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX, resultIndex);
return (
<MenuBar menu={menu} style={TABLE_HEADER_MENU_BAR_STYLES} />
);
});
@@ -8,8 +8,15 @@
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { MenuService, ActionService, DATA_CONTEXT_MENU } from '@cloudbeaver/core-view';
import { DATA_VIEWER_CONSTRAINTS_DELETE_ACTION } from '../../DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION';
import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL';
import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX';
import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU';
import { TableHeaderMenu } from './TableHeaderMenu';
import { TableWhereFilter } from './TableWhereFilter';
export interface ITableHeaderPlaceholderProps {
@@ -21,8 +28,79 @@ export interface ITableHeaderPlaceholderProps {
export class TableHeaderService extends Bootstrap {
readonly tableHeaderPlaceholder = new PlaceholderContainer<ITableHeaderPlaceholderProps>();
constructor(
private readonly menuService: MenuService,
private readonly actionService: ActionService
) {
super();
}
register(): void {
this.tableHeaderPlaceholder.add(TableWhereFilter, 1);
this.tableHeaderPlaceholder.add(TableHeaderMenu, 2);
this.actionService.addHandler({
id: 'table-header-menu-base-handler',
isActionApplicable(context, action) {
const menu = context.find(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
const model = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
const resultIndex = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
if (!menu || !model || resultIndex === undefined) {
return false;
}
return true;
},
handler: async (context, action) => {
switch (action) {
case DATA_VIEWER_CONSTRAINTS_DELETE_ACTION: {
const model = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
const resultIndex = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
if (constraints) {
constraints.deleteData();
await model.request();
}
break;
}
}
},
getActionInfo: (context, action) => {
if (context.get(DATA_CONTEXT_MENU) === DATA_VIEWER_DATA_MODEL_TOOLS_MENU) {
return { ...action.info, label: '' };
}
return action.info;
},
isDisabled: (context, action) => {
const model = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
const resultIndex = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
if (model.isLoading() || model.isDisabled(resultIndex)) {
return true;
}
if (action === DATA_VIEWER_CONSTRAINTS_DELETE_ACTION) {
const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
if (constraints) {
return constraints.filterConstraints.length === 0 && constraints.orderConstraints.length === 0;
}
}
return true;
},
});
this.menuService.addCreator({
isApplicable: context => context.get(DATA_CONTEXT_MENU) === DATA_VIEWER_DATA_MODEL_TOOLS_MENU,
getItems: (context, items) => [
...items,
DATA_VIEWER_CONSTRAINTS_DELETE_ACTION,
],
});
}
load(): void | Promise<void> { }
@@ -7,16 +7,15 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { InlineEditor } from '@cloudbeaver/core-app';
import { PlaceholderComponent, useObjectRef } from '@cloudbeaver/core-blocks';
import type { PlaceholderComponent } 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';
import { useWhereFilter } from './useWhereFilter';
const styles = composes(
css`
@@ -37,65 +36,20 @@ export const TableWhereFilter: PlaceholderComponent<ITableHeaderPlaceholderProps
resultIndex,
}) {
const translate = useTranslate();
const hasResult = model.source.hasResult(resultIndex);
let filterValue = model.source.options?.whereFilter || '';
let supported = false;
if (hasResult) {
const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
supported = constraints?.supported || false;
if (constraints && constraints.filterConstraints.length > 0 && model.source.requestInfo.requestFilter) {
filterValue = model.source.requestInfo.requestFilter;
}
}
const setValue = useCallback((filterValue: string) => {
model.source.options.whereFilter = filterValue;
const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
constraints?.deleteFilters();
}, [model.source.options]);
const props = useObjectRef({ model, resultIndex, filterValue });
const handleApply = useCallback(() => {
const { model, resultIndex } = props;
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
model.refresh();
}, []);
const resetFilter = useCallback(async () => {
const { model, resultIndex } = props;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
await model.requestDataAction(async () => {
constraints.deleteDataFilters();
const applyNeeded = !!model.requestInfo.requestFilter;
if (applyNeeded) {
await model.refresh(true);
}
});
}, []);
const state = useWhereFilter(model, resultIndex);
return styled(useStyles(styles))(
<InlineEditor
name="data_where"
value={filterValue}
value={state.filter}
placeholder={translate('table_header_sql_expression')}
controlsPosition='inside'
edited={!!filterValue}
disableSave={model.source.options?.whereFilter === model.source.requestInfo.requestFilter}
disabled={!supported || model.isLoading() || model.isDisabled(resultIndex)}
edited={!!state.filter}
disableSave={!state.applicableFilter}
disabled={state.disabled}
simple
onSave={handleApply}
onUndo={resetFilter}
onChange={setValue}
onSave={state.apply}
onChange={state.set}
/>
);
});
@@ -0,0 +1,81 @@
/*
* 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 { action, computed } from 'mobx';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions';
interface IState {
model: IDatabaseDataModel<IDatabaseDataOptions, any>;
resultIndex: number;
readonly filter: string;
readonly constraints: ResultSetConstraintAction | null;
readonly disabled: boolean;
readonly applicableFilter: boolean;
set: (value: string) => void;
apply: () => Promise<void>;
}
export function useWhereFilter(
model: IDatabaseDataModel<IDatabaseDataOptions, any>,
resultIndex: number
): Readonly<IState> {
return useObservableRef(() => ({
get filter() {
if (this.constraints?.filterConstraints.length && this.model.source.requestInfo.requestFilter) {
return this.model.requestInfo.requestFilter;
}
return this.model.source.options?.whereFilter ?? '';
},
get constraints() {
if (!this.model.source.hasResult(this.resultIndex)) {
return null;
}
return this.model.source.tryGetAction(this.resultIndex, ResultSetConstraintAction) ?? null;
},
get disabled() {
const supported = this.constraints?.supported ?? false;
return !supported || this.model.isLoading() || this.model.isDisabled(resultIndex);
},
get applicableFilter() {
return this.model.source.prevOptions?.whereFilter !== this.model.source.options?.whereFilter
|| this.model.source.options?.whereFilter !== this.model.source.requestInfo.requestFilter;
},
set(value: string) {
if (this.constraints) {
this.constraints.deleteFilters();
}
if (this.model.source.options) {
this.model.source.options.whereFilter = value;
}
},
async apply() {
if (!this.applicableFilter || this.model.isLoading() || this.model.isDisabled(this.resultIndex)) {
return;
}
await this.model.request();
},
}),
{
filter: computed,
constraints: computed,
disabled: computed,
applicableFilter: computed,
set: action.bound,
apply: action.bound,
},
{ model, resultIndex });
}
@@ -77,6 +77,7 @@ const viewerStyles = composes(
margin: 0;
}
TablePresentationBar {
padding-top: 40px;
&:first-child {
margin-right: 4px;
}
@@ -235,7 +236,6 @@ export const TableViewer = observer<Props>(function TableViewer({
return styled(styles)(
<table-viewer className={className}>
<TableHeader model={dataModel} resultIndex={resultIndex} />
<table-content>
<TablePresentationBar
type={DataPresentationType.main}
@@ -247,6 +247,7 @@ export const TableViewer = observer<Props>(function TableViewer({
onPresentationChange={dataTableActions.setPresentation}
/>
<table-data>
<TableHeader model={dataModel} resultIndex={resultIndex} />
<Split sticky={30} mode={valuePanelDisplayed ? undefined : 'minimize'} keepRatio>
<Pane>
<pane-content>
@@ -257,6 +258,13 @@ export const TableViewer = observer<Props>(function TableViewer({
presentation={presentation}
resultIndex={resultIndex}
/>
<TableError model={dataModel} loading={loading} />
<Loader
loading={loading}
cancelDisabled={!dataModel.source.canCancel}
overlay={overlay}
onCancel={() => dataModel.source.cancel()}
/>
</pane-content>
</Pane>
{valuePanelDisplayed && <ResizerControls />}
@@ -274,13 +282,7 @@ export const TableViewer = observer<Props>(function TableViewer({
</pane-content>
</Pane>
</Split>
<TableError model={dataModel} loading={loading} />
<Loader
loading={loading}
cancelDisabled={!dataModel.source.canCancel}
overlay={overlay}
onCancel={() => dataModel.source.cancel()}
/>
</table-data>
<TablePresentationBar
type={DataPresentationType.toolsPanel}
@@ -28,4 +28,5 @@ export default [
['data_viewer_script_preview', 'Script'],
['data_viewer_script_preview_dialog_title', 'Preview changes'],
['data_viewer_script_preview_error_title', "Can't get the script"],
['data_viewer_refresh_result_set', 'Refresh result set'],
];
@@ -21,4 +21,5 @@ export default [
['data_viewer_presentation_value_image_fit', 'Adatta alla Finestra'],
['data_viewer_presentation_value_image_original_size', 'Dimensioni Originali'],
['data_viewer_presentation_value_boolean_placeholder', 'Non posso rappresentare il valore corrente come booleano'],
['data_viewer_refresh_result_set', 'Refresh result set'],
];
@@ -24,4 +24,5 @@ export default [
['data_viewer_script_preview', 'Скрипт'],
['data_viewer_script_preview_dialog_title', 'Предпросмотр изменений'],
['data_viewer_script_preview_error_title', 'Не удалось получить скрипт'],
['data_viewer_refresh_result_set', 'Обновить резалт сет'],
];
@@ -28,4 +28,5 @@ export default [
['data_viewer_script_preview', '脚本'],
['data_viewer_script_preview_dialog_title', '预览更改'],
['data_viewer_script_preview_error_title', '无法获取脚本'],
['data_viewer_refresh_result_set', 'Refresh result set'],
];
@@ -105,7 +105,7 @@ export class SqlQueryService {
await model
.setCountGain(getDefaultRowsCount())
.setSlice(0)
.refresh();
.request();
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
} catch (exception) {
@@ -179,7 +179,7 @@ export class SqlQueryService {
await model
.setCountGain(getDefaultRowsCount())
.setSlice(0)
.refresh();
.request();
statistics.executedQueries++;
statistics.executeTime += source.requestInfo.requestDuration;
+4 -9
View File
@@ -4542,15 +4542,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
version "1.0.30001243"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz#d9250155c91e872186671c523f3ae50cfc94a3aa"
integrity sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==
caniuse-lite@^1.0.30001274:
version "1.0.30001280"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7"
integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001274:
version "1.0.30001286"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001286.tgz"
integrity sha512-zaEMRH6xg8ESMi2eQ3R4eZ5qw/hJiVsO/HlLwniIwErij0JDr9P+8V4dtx1l+kLq6j3yy8l8W4fst1lBnat5wQ==
capital-case@^1.0.4:
version "1.0.4"