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

This commit is contained in:
serge-rider
2020-09-02 11:33:49 +03:00
25 changed files with 129 additions and 73 deletions
@@ -20,6 +20,7 @@
"@cloudbeaver/core-root": "^0.1.0",
"@cloudbeaver/core-routing": "^0.1.0",
"@cloudbeaver/core-theming": "^0.1.0",
"@cloudbeaver/core-executor": "^0.1.0",
"react": "^16.x.x",
"mobx": "^5.x.x",
"mobx-react": "^6.x.x",
@@ -54,7 +54,7 @@ export interface IAdministrationItemSubItem {
export interface IConfigurationWizardItemOptions {
description: string;
onValidate?: () => boolean;
onValidate?: () => Promise<boolean> | boolean;
isDisabled?: () => boolean;
isHidden?: () => boolean;
isDone?: () => boolean;
@@ -55,15 +55,6 @@ export class ConfigurationWizardService {
});
}
@computed get canNext() {
if (this.currentStep?.configurationWizardOptions?.isDone
&& !this.currentStep?.configurationWizardOptions?.isDone()) {
return false;
}
return true;
}
@computed get nextStep() {
return this.steps.find((item, index) => {
if (index <= this.currentStepIndex) {
@@ -112,27 +103,37 @@ export class ConfigurationWizardService {
const step = this.getStep(name);
if (!step) {
return;
return false;
}
if (step.configurationWizardOptions?.onValidate) {
const isValid = await step.configurationWizardOptions.onValidate();
if (!isValid) {
return false;
}
}
if (step.configurationWizardOptions?.onFinish) {
await step.configurationWizardOptions.onFinish();
}
return true;
}
next() {
if (!this.currentStep || !this.canNext) {
async next() {
if (!this.currentStep) {
return;
}
this.finishStep(this.currentStep.name);
if (!await this.finishStep(this.currentStep.name)) {
return;
}
if (this.currentStepIndex + 1 < this.steps.length) {
if (this.nextStep) {
this.administrationScreenService.navigateToItem(this.nextStep.name);
}
} else {
this.finish();
await this.finish();
}
}
@@ -7,15 +7,19 @@
*/
import { observer } from 'mobx-react';
import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import {
SubmittingForm, InputGroup, InputField, useFocus, Switch
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ServerConfigInput } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { ServerConfigurationService } from './ServerConfigurationService';
export const formStyles = css`
SubmittingForm {
flex: 1;
@@ -51,11 +55,22 @@ export const ServerConfigurationForm = observer(function ServerConfigurationForm
onChange,
onSave,
}: Props) {
const service = useService(ServerConfigurationService);
const translate = useTranslate();
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
useEffect(() => {
const validate = () => {
focusedRef.current?.checkValidity();
focusedRef.current?.reportValidity();
};
service.validationTask.addHandler(validate);
return () => service.validationTask.removeHandler(validate);
}, [service]);
return styled(useStyles(formStyles, boxStyles))(
<SubmittingForm onSubmit={onSave} name='server_config' ref={focusedRef as React.RefObject<HTMLFormElement>}>
<SubmittingForm onSubmit={onSave} name='server_config' ref={focusedRef}>
<box as="div">
<box-element as='div'>
<group as="div">
@@ -8,6 +8,7 @@
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { IExecutor, Executor, IContextProvider } from '@cloudbeaver/core-executor';
import { GraphQLService } from '@cloudbeaver/core-sdk';
import { AdministrationScreenService } from '../../../AdministrationScreenService';
@@ -17,6 +18,7 @@ import { IServerConfigurationPageState } from './IServerConfigurationPageState';
export class ServerConfigurationService {
readonly state: IServerConfigurationPageState;
readonly validationTask: IExecutor<boolean>;
constructor(
private administrationScreenService: AdministrationScreenService,
@@ -42,18 +44,35 @@ export class ServerConfigurationService {
showUtilityObjects: false,
},
}));
this.validationTask = new Executor();
}
isDone() {
return this.isFormFilled();
}
validate() {
return this.isFormFilled();
async validate() {
const context = await this.validationTask.execute(true);
const state = await context.getContext(this.validationStatusContext);
return state.getState();
}
validationStatusContext = (context: IContextProvider<boolean>) => {
let state = this.isFormFilled();
const invalidate = () => {
state = false;
};
const getState = () => state;
return {
getState,
invalidate,
};
}
private isFormFilled() {
return !!(
this.state?.serverConfig.serverName
&& this.state.serverConfig.adminName
@@ -79,7 +79,6 @@ export const WizardStepper = observer(function WizardStepper() {
type="button"
mod={['unelevated']}
onClick={() => service.next()}
disabled={!service.canNext}
>
{translate(service.currentStepIndex === service.steps.length - 1 ? 'ui_stepper_finish' : 'ui_stepper_next')}
</Button>
@@ -47,7 +47,7 @@ const navigationTreeStyles = css`
export const NavigationTree = observer(function NavigationTree() {
const navTreeService = useService(NavigationTreeService);
const [onFocus, onBlur] = useActiveView(navTreeService.getView);
const [ref] = useFocus({ onFocus, onBlur });
const [ref] = useFocus<HTMLDivElement>({ onFocus, onBlur });
const nodeChildren = useChildren();
const isEnabled = usePermission(EPermission.public);
@@ -69,7 +69,7 @@ export const NavigationTree = observer(function NavigationTree() {
}
return styled(navigationTreeStyles)(
<tree as="div" tabIndex={0} ref={ref as React.RefObject<HTMLDivElement>}>
<tree as="div" tabIndex={0} ref={ref}>
{nodeChildren.children.map(id => (
<NavigationTreeNode key={id} id={id} parentId={ROOT_NODE_PATH}/>
))}
@@ -41,7 +41,7 @@ const stylesArray = [styles];
export const NavigationTabsBar = observer(function NavigationTabsBar() {
const navigation = useService(NavigationTabsService);
const [onFocus, onBlur] = useActiveView(navigation.getView);
const [ref] = useFocus({ onFocus, onBlur });
const [ref] = useFocus<HTMLDivElement>({ onFocus, onBlur });
const handleSelect = useCallback((tabId: string) => navigation.selectTab(tabId), [navigation]);
const handleClose = useCallback((tabId: string) => navigation.closeTab(tabId), [navigation]);
@@ -62,7 +62,7 @@ export const NavigationTabsBar = observer(function NavigationTabsBar() {
))}
style={stylesArray}
tabIndex={0}
ref={ref as React.RefObject<HTMLDivElement>}
ref={ref}
>
{navigation.tabIdList.map(tabId => (
<TabPanel key={tabId} tabId={tabId}>
+4 -4
View File
@@ -16,18 +16,18 @@ type FocusOptions = {
onBlur?: () => void;
}
export function useFocus({
export function useFocus<T extends HTMLElement>({
focusFirstChild,
onFocus,
onBlur,
}: FocusOptions): [React.RefObject<HTMLElement>, boolean] {
}: FocusOptions): [React.RefObject<T>, boolean] {
const [focus, setFocus] = useState(false);
const reference = useRef<HTMLElement>(null);
const reference = useRef<T>(null);
useLayoutEffect(() => {
if (reference.current !== null && focusFirstChild) {
const firstFocusable = reference.current
.querySelector<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
.querySelector<T>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) {
firstFocusable.focus();
@@ -39,10 +39,10 @@ export const ConnectionForm = observer(function ConnectionForm({
controller,
}: ConnectionFormProps) {
const translate = useTranslate();
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLDivElement>({ focusFirstChild: true });
return styled(useStyles(styles, formStyles))(
<connection-form as='div' ref={focusedRef as React.RefObject<HTMLDivElement>}>
<connection-form as='div' ref={focusedRef}>
<box as="div">
<box-element as='div'>
<group as="div">
@@ -61,7 +61,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({
rejectDialog,
}: DialogComponentProps<string, null>) {
const connection = useConnectionInfo(payload);
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const { driver } = useDBDriver(connection.connectionInfo?.driverId || '');
const controller = useController(DBAuthDialogController, payload, rejectDialog);
@@ -81,7 +81,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({
{(!connection.isLoaded() || connection.isLoading())
? <Loader />
: (
<SubmittingForm onSubmit={controller.login} ref={focusedRef as React.RefObject<HTMLFormElement>}>
<SubmittingForm onSubmit={controller.login} ref={focusedRef}>
<ObjectPropertyInfoForm
prefix={`auth_${connection.connectionInfo?.id || ''} `}
autofillToken={`section-${connection.connectionInfo?.id || ''} section-auth`}
+12 -3
View File
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { ExecutionContext } from './ExecutionContext';
import { ExecutionContext, IContextProvider } from './ExecutionContext';
import { IExecutor } from './IExecutor';
import { IExecutorHandler } from './IExecutorHandler';
@@ -18,7 +18,7 @@ export class Executor<T> implements IExecutor<T> {
private defaultData?: T | null
) { }
async execute(data: T) {
async execute(data: T): Promise<IContextProvider<T>> {
if ((data === undefined || data === null) && this.defaultData !== undefined && this.defaultData !== null) {
data = this.defaultData;
}
@@ -29,13 +29,14 @@ export class Executor<T> implements IExecutor<T> {
const result = await handler(context, data);
if (result === false) {
return;
return context;
}
}
for (const handler of this.postHandlers) {
await handler(context, data);
}
return context;
}
addHandler(handler: IExecutorHandler<T>) {
@@ -43,8 +44,16 @@ export class Executor<T> implements IExecutor<T> {
return this;
}
removeHandler(handler: IExecutorHandler<T>) {
this.handlers = this.handlers.filter(h => h === handler);
}
addPostHandler(handler: IExecutorHandler<T>) {
this.postHandlers.push(handler);
return this;
}
removePostHandler(handler: IExecutorHandler<T>) {
this.postHandlers = this.postHandlers.filter(h => h === handler);
}
}
@@ -6,9 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { IContextProvider } from './ExecutionContext';
import { IExecutorHandler } from './IExecutorHandler';
export interface IExecutor<T> {
execute(data: T): Promise<void>;
execute(data: T): Promise<IContextProvider<T>>;
addHandler(handler: IExecutorHandler<T>): void;
removeHandler(handler: IExecutorHandler<T>): void;
addPostHandler(handler: IExecutorHandler<T>): void;
removePostHandler(handler: IExecutorHandler<T>): void;
}
@@ -138,7 +138,7 @@ export const UserForm = observer(function UserForm({
}: Props) {
const translate = useTranslate();
const controller = useController(UserFormController, userId, onCancel);
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const handleLoginChange = useCallback(
(value: string) => controller.credentials.login = value,
@@ -186,7 +186,7 @@ export const UserForm = observer(function UserForm({
</Button>
</TabList>
<content-box as='div'>
<SubmittingForm onSubmit={controller.save} ref={focusedRef as React.RefObject<HTMLFormElement>}>
<SubmittingForm onSubmit={controller.save} ref={focusedRef}>
<TabPanel tabId='info'>
<flex-box as="div">
<flex-box-element as='div'>
@@ -27,13 +27,13 @@ export const AuthProviderForm = observer(function AuthProviderForm({
credentials,
authenticate,
}: Props) {
const [elementRef] = useFocus({ focusFirstChild: true });
const [elementRef] = useFocus<HTMLDivElement>({ focusFirstChild: true });
const handleChange = useCallback((key: string, value: string) => {
credentials[key] = value;
}, [credentials]);
return styled(useStyles(formStyles))(
<login-form as='div' ref={elementRef as React.RefObject<HTMLDivElement>}>
<login-form as='div' ref={elementRef}>
{provider.credentialParameters.map(parameter => parameter.user && (
<group as="div" key={parameter.id}>
<InputField
@@ -31,11 +31,11 @@ export const ConnectionForm = observer(function ConnectionForm({
controller,
}: ConnectionFormProps) {
const translate = useTranslate();
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLDivElement>({ focusFirstChild: true });
return styled(useStyles(formStyles))(
<connection-form as='div'>
<connection-type as="div" ref={focusedRef as React.RefObject<HTMLDivElement>}>
<connection-type as="div" ref={focusedRef}>
<Radio
name="type"
id="custom"
@@ -45,7 +45,7 @@ const styles = css`
export const ConnectionDialog = observer(function ConnectionDialog({
rejectDialog,
}: DialogComponentProps<null, null>) {
const [focusedRef] = useFocus({ focusFirstChild: true });
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const controller = useController(ConnectionController, rejectDialog);
const translate = useTranslate();
let title = translate('basicConnection_connectionDialog_newConnection');
@@ -81,7 +81,7 @@ export const ConnectionDialog = observer(function ConnectionDialog({
{controller.isConnecting && translate('basicConnection_connectionDialog_connecting_message')}
</center>
) : (
<SubmittingForm onSubmit={controller.onConnect} ref={focusedRef as React.RefObject<HTMLFormElement>}>
<SubmittingForm onSubmit={controller.onConnect} ref={focusedRef}>
<ObjectPropertyInfoForm
prefix={`auth_${controller.template?.id || ''}`}
autofillToken={`section-${controller.template?.id || ''} section-auth`}
@@ -12,6 +12,7 @@ export type AgGridContext = {
selection: TableSelection;
isReadonly(): boolean;
isCellEdited(rowId: number, column: string): boolean;
editCellValue(rowNumber: number, column: string, value: any, editing: boolean): void;
revertCellValue(rowNumber: number, column: string): void;
onEditSave(): void;
onEditCancel(): void;
@@ -18,7 +18,7 @@ import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import { agGridStyles } from '../styles/styles';
import { AgGridTableController } from './AgGridTableController';
import { LoadingCell } from './LoadingCell';
import { IndexCellRenderer } from './IndexCellRenderer';
import { PlainTextEditor } from './PlainTextEditor/PlainTextEditor';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
@@ -40,7 +40,7 @@ async function loader() {
const agGridComponents = {
agColumnHeader: TableColumnHeader,
plainTextEditor: PlainTextEditor,
loadingCellRenderer: LoadingCell,
indexCellRenderer: IndexCellRenderer,
};
export const AgGridTable = observer(function AgGridTable({
@@ -48,6 +48,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
selection: this.selection,
isReadonly: () => this.gridModel.access === AccessMode.Readonly,
isCellEdited: this.isCellEdited.bind(this),
editCellValue: this.editCellValue.bind(this),
revertCellValue: this.revertCellValue.bind(this),
onEditSave: this.onEditSave.bind(this),
onEditCancel: this.onEditCancel.bind(this),
@@ -158,8 +159,12 @@ export class AgGridTableController implements IInitializableController, IDestruc
this.gridModel.revertCellValue(rowIndex, colId);
}
private editCellValue(rowIndex: number, colId: string, value: any, editing: boolean) {
this.gridModel.onCellEditingStopped(rowIndex, colId, value, editing);
}
private handleCellEditingStopped(event: CellEditingStoppedEvent) {
this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value);
this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value, false);
}
private isCellEdited(rowIndex: number, column: string) {
@@ -247,12 +252,7 @@ export const INDEX_COLUMN_DEF: ColDef = {
suppressMenu: true,
editable: false,
sortable: false,
cellRenderer: (params) => {
if (!params.data) {
return 'Loading...';
}
return params.rowIndex + 1;
},
cellRenderer: 'indexCellRenderer',
};
function mapDataToColumns(columns?: IAgGridCol[]): ColDef[] {
@@ -0,0 +1,19 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { CellClassParams } from '@ag-grid-community/core';
import { Loader } from '@cloudbeaver/core-blocks';
export function IndexCellRenderer(props: CellClassParams) {
return (
<div style={{ display: 'flex', width: '100%' }}>
{!props.data && <Loader small/>}
{props.data && props.rowIndex + 1}
</div>
);
}
@@ -1,15 +0,0 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export function LoadingCell() {
return (
<div style={{ paddingLeft: '10px', lineHeight: '25px' }}>
<span>Loading...</span>
</div>
);
}
@@ -71,6 +71,7 @@ export const PlainTextEditor = forwardRef<Partial<ICellEditorComp>, ICellEditorP
const handleChange = useCallback((newValue: string) => {
value.current = newValue;
context.editCellValue(props.rowIndex, props.column.getColId(), newValue, true);
forceUpdate(value.current);
}, []);
@@ -30,7 +30,7 @@ export class TableEditor {
/**
* this method doesn't modify TableRow but create a diff that can be applied later
*/
editCellValue(rowId: number, columnKey: string, value: any) {
editCellValue(rowId: number, columnKey: string, value: any, editing: boolean) {
const column = this.dataModel
.getColumns()
.find(column => column.name === columnKey);
@@ -42,7 +42,9 @@ export class TableEditor {
const newValue = this.getOrCreateEditedRow(rowId)
.setValue(column.position, value);
this.rowsUpdateSubject.next([rowId]);
if (!editing) {
this.rowsUpdateSubject.next([rowId]);
}
return newValue;
}
@@ -322,12 +322,12 @@ export class TableViewerModel {
}
}
onCellEditingStopped(rowNumber: number, column: string, value: any) {
onCellEditingStopped(rowNumber: number, column: string, value: any, editing: boolean) {
if (this.access === AccessMode.Readonly) {
return;
}
this.tableEditor.editCellValue(rowNumber, column, value);
this.tableEditor.editCellValue(rowNumber, column, value, editing);
}
onSortChanged(sorting: SortModel) {