Merge branch 'fix-server-configuration' into devel

This commit is contained in:
Wroud
2021-03-30 20:04:29 +03:00
24 changed files with 206 additions and 167 deletions
@@ -256,7 +256,7 @@ export class AdministrationScreenService {
return true;
}
private async isAccessProvided(state: RouterState) {
private async isAccessProvided(state: RouterState): Promise<boolean> {
await this.serverConfigResource.load();
if (this.isConfigurationMode) {
@@ -264,8 +264,7 @@ export class AdministrationScreenService {
}
if (this.screenService.isActive(state.name, AdministrationScreenService.setupName)) {
this.navigateToRoot();
return;
return false;
}
await this.ensurePermissions.execute();
@@ -77,7 +77,7 @@ export function useCheckboxState<TKey extends string>(options: CheckboxStateOpti
}
onChange?.(checked, name as TKey);
context?.onChange(checked, name);
context?.change(checked, name);
refresh(count + 1);
},
@@ -177,7 +177,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
onSelect(null, name, value);
}
if (context) {
context.onChange(null, name);
context.change(null, name);
}
setSearchValue('');
},
@@ -195,7 +195,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
onSelect(id, name, value);
}
if (context) {
context.onChange(id, name);
context.change(id, name);
}
},
[value, state, name, menu, context, onSelect]
@@ -195,7 +195,7 @@ export const ComboboxNew: ComboboxType = observer(function ComboboxNew({
onSelect(null, name, value);
}
if (context) {
context.onChange(null, name);
context.change(null, name);
}
setSearchValue('');
},
@@ -213,7 +213,7 @@ export const ComboboxNew: ComboboxType = observer(function ComboboxNew({
onSelect(id, name, value);
}
if (context) {
context.onChange(id, name);
context.change(id, name);
}
setSearchValue('');
},
@@ -8,8 +8,18 @@
import { createContext } from 'react';
import type { IExecutor } from '@cloudbeaver/core-executor';
type ChangeHandler = (value: string | number | boolean | null | undefined, name: string | undefined) => void;
export interface IChangeData {
value: string | number | boolean | null | undefined;
name: string | undefined;
}
export interface IFormContext {
onChange: (value: string | number | boolean | null | undefined, name: string | undefined) => void;
changeExecutor: IExecutor<IChangeData>;
change: ChangeHandler;
}
export const FormContext = createContext<IFormContext | null>(null);
@@ -72,7 +72,7 @@ export const InputField: InputFieldType = observer(function InputField({
onChange(event.target.value, name);
}
if (context) {
context.onChange(event.target.value, name);
context.change(event.target.value, name);
}
}, [state, name, context, onChange]);
@@ -92,7 +92,7 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({
onChange(value, name);
}
if (context) {
context.onChange(value, name);
context.change(value, name);
}
}, [state, name, context, onChange]);
@@ -144,7 +144,7 @@ export const Radio: RadioType = observer(function Radio({
if (context) {
context.onChange(value);
} else if (formContext) {
formContext.onChange(value, name);
formContext.change(value, name);
}
if (onChange) {
@@ -60,7 +60,7 @@ export const RadioGroup: RadioGroupType = observer(function RadioGroup({
}
if (formContext) {
formContext.onChange(value, name);
formContext.change(value, name);
}
}, [name, state, formContext, onChange]);
@@ -6,9 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { useCallback, forwardRef, useMemo } from 'react';
import { useCallback, forwardRef, useState, useContext } from 'react';
import { FormContext } from './FormContext';
import { Executor } from '@cloudbeaver/core-executor';
import { useObjectRef } from '../useObjectRef';
import { FormContext, IChangeData, IFormContext } from './FormContext';
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange'> & {
disabled?: boolean;
@@ -25,14 +28,23 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
},
ref
) {
const parentContext = useContext(FormContext);
const props = useObjectRef({ parentContext, onChange, onSubmit });
const [changeExecutor] = useState(() => new Executor<IChangeData>());
const handleSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (onSubmit) {
onSubmit(e);
}
}, [onSubmit]);
props.onSubmit?.(e);
}, []);
const context = useMemo(() => ({ onChange }), [onChange]);
const context = useObjectRef<IFormContext>({
changeExecutor,
change(value, name) {
props.onChange(value, name);
props.parentContext?.change(value, name);
changeExecutor.execute({ value, name });
},
}, {});
return (
<form {...rest} ref={ref} onSubmit={handleSubmit}>
@@ -68,7 +68,7 @@ export const Textarea: TextareaType = observer(function Textarea({
onChange(event.target.value, name);
}
if (context) {
context.onChange(event.target.value, name);
context.change(event.target.value, name);
}
}, [state, name, onChange]);
@@ -93,7 +93,7 @@ export const TextareaNew: TextareaType = observer(function TextareaNew({
onChange(event.target.value, name);
}
if (context) {
context.onChange(event.target.value, name);
context.change(event.target.value, name);
}
}, [state, name, onChange]);
@@ -6,11 +6,9 @@
* you may not use this file except in compliance with the License.
*/
import { useContext, useEffect } from 'react';
import { useContext } from 'react';
import type { IExecutorHandler } from '@cloudbeaver/core-executor';
import { useObjectRef } from '../../useObjectRef';
import { useExecutor } from '../../useExecutor';
import type { ITabData } from '../TabsContainer/ITabsContainer';
import { TabsContext } from '../TabsContext';
@@ -24,34 +22,25 @@ export function useTab(
throw new Error('TabsContext not provided');
}
const dynamic = useObjectRef({
tabId,
open: onOpen,
close: onClose,
useExecutor({
executor: state.openExecutor,
handlers: [function openHandler(data) {
if (tabId !== data.tabId) {
return;
}
onOpen?.(data);
}],
});
useEffect(() => {
const openHandler: IExecutorHandler<ITabData<any>> = data => {
if (dynamic.tabId !== data.tabId) {
useExecutor({
executor: state.closeExecutor,
handlers: [function closeHandler(data) {
if (tabId !== data.tabId) {
return;
}
dynamic.open?.(data);
};
const closeHandler: IExecutorHandler<ITabData<any>> = data => {
if (dynamic.tabId !== data.tabId) {
return;
}
dynamic.close?.(data);
};
state.openExecutor.addHandler(openHandler);
state.closeExecutor.addHandler(closeHandler);
return () => {
state.openExecutor.removeHandler(openHandler);
state.closeExecutor.removeHandler(closeHandler);
};
}, [state.openExecutor, state.closeExecutor]);
onClose?.(data);
}],
});
const handleOpen = () => state.open(tabId);
+2
View File
@@ -71,7 +71,9 @@ export * from './StaticImage';
export * from './TextPlaceholder';
export * from './useClipboard';
export * from './useDataResource';
export * from './useExecutor';
export * from './useFocus';
export * from './useFormValidator';
export * from './useMapResource';
export * from './useObjectRef';
export * from './useStateDelay';
@@ -0,0 +1,56 @@
/*
* 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 { useEffect } from 'react';
import type { IExecutor, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
import { useObjectRef } from './useObjectRef';
interface IUseExecutorOptions<T> {
executor: IExecutor<T> | IExecutorHandlersCollection<T>;
handlers?: Array<IExecutorHandler<T>>;
postHandlers?: Array<IExecutorHandler<T>>;
}
export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
const props = useObjectRef(options);
useEffect(() => {
if (!props.executor) {
return;
}
const handlers: Array<IExecutorHandler<T>> = [];
const postHandlers: Array<IExecutorHandler<T>> = [];
if (props.handlers) {
for (let i = 0; i < props.handlers.length; i++) {
const handler: IExecutorHandler<T> = (data, contexts) => props.handlers?.[i](data, contexts);
props.executor.addHandler(handler);
handlers.push(handler);
}
}
if (props.postHandlers) {
for (let i = 0; i < props.postHandlers.length; i++) {
const handler: IExecutorHandler<T> = (data, contexts) => props.postHandlers?.[i](data, contexts);
props.executor.addPostHandler(handler);
postHandlers.push(handler);
}
}
return () => {
for (const handler of handlers) {
props.executor.removeHandler(handler);
}
for (const handler of postHandlers) {
props.executor.removePostHandler(handler);
}
};
}, [props.executor, props.handlers?.length, props.postHandlers?.length]);
}
@@ -0,0 +1,28 @@
/*
* 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 { IExecutor, IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
import { useExecutor } from './useExecutor';
export function useFormValidator(
validationTask: IExecutor<any> | IExecutorHandlersCollection<any>,
ref: React.RefObject<HTMLFormElement>,
callback?: () => void
): void {
useExecutor({
executor: validationTask,
handlers: [function validate() {
ref.current?.checkValidity();
ref.current?.reportValidity();
callback?.();
}],
});
}
@@ -26,9 +26,9 @@ import {
TextareaNew,
ComboboxNew,
Container,
useFormValidator,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useFormValidator } from '@cloudbeaver/core-executor';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -8,4 +8,3 @@ export * from './IExecutionContext';
export * from './IExecutor';
export * from './IExecutorHandler';
export * from './IExecutorHandlersCollection';
export * from './useFormValidator';
@@ -1,44 +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 { useEffect, useRef } from 'react';
import type { IExecutor } from './IExecutor';
import type { IExecutorHandlersCollection } from './IExecutorHandlersCollection';
export function useFormValidator(
validationTask: IExecutor<any> | IExecutorHandlersCollection<any>,
ref: React.RefObject<HTMLFormElement>,
callback?: () => void
): void {
const callbackRef = useRef({
callback,
ref,
});
callbackRef.current.callback = callback;
callbackRef.current.ref = ref;
useEffect(() => {
if (!validationTask) {
return;
}
function validate() {
if (callbackRef.current) {
callbackRef.current.ref.current?.checkValidity();
callbackRef.current.ref.current?.reportValidity();
callbackRef.current.callback?.();
}
}
validationTask.addHandler(validate);
return () => validationTask.removeHandler(validate);
}, [validationTask]);
}
@@ -10,9 +10,8 @@ import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { AdministrationItemContentComponent, AdministrationTools, ADMINISTRATION_TOOLS_STYLES } from '@cloudbeaver/core-administration';
import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupItem, GroupTitle, IconButton, Loader, Placeholder, SubmittingForm, useFocus } from '@cloudbeaver/core-blocks';
import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupItem, GroupTitle, IconButton, Loader, Placeholder, SubmittingForm, useFocus, useFormValidator } from '@cloudbeaver/core-blocks';
import { useController, useService } from '@cloudbeaver/core-di';
import { useFormValidator } from '@cloudbeaver/core-executor';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -37,13 +37,6 @@ export class ServerConfigurationPageController {
change = (): void => {
this.serverConfigurationService.changed();
if (this.state.serverConfig.enabledAuthProviders?.length === 0) {
this.state.serverConfig.authenticationEnabled = false;
}
if (!this.state.serverConfig.authenticationEnabled) {
this.state.serverConfig.anonymousAccessEnabled = true;
}
if (!this.state.serverConfig.adminCredentialsSaveEnabled) {
this.state.serverConfig.publicCredentialsSaveEnabled = false;
@@ -7,10 +7,11 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled from 'reshadow';
import { AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { BASE_CONTAINERS_STYLES, Container, Group, GroupTitle, Loader, PlaceholderComponent, SwitchNew, useMapResource } from '@cloudbeaver/core-blocks';
import { BASE_CONTAINERS_STYLES, Container, FormContext, Group, GroupTitle, Loader, PlaceholderComponent, SwitchNew, useExecutor, useMapResource } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IConfigurationPlaceholderProps } from '@cloudbeaver/plugin-administration';
@@ -24,8 +25,33 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
const providers = useMapResource(AuthProvidersResource, AuthProvidersResource.keyAll);
const translate = useTranslate();
const styles = useStyles(BASE_CONTAINERS_STYLES);
const formContext = useContext(FormContext);
const disabled = providers.data.length === 1 && !providers.resource.has(AUTH_PROVIDER_LOCAL_ID);
if (formContext === null) {
throw new Error('Form state should be provided');
}
const localExists = providers.resource.has(AUTH_PROVIDER_LOCAL_ID);
const externalAuthentication = providers.data.length === 1 && !localExists;
const providersSelectable = providers.data.length > 1;
useExecutor({
executor: formContext.changeExecutor,
handlers: [function switchControls() {
if (externalAuthentication) {
serverConfig.enabledAuthProviders = [...providers.resource.keys];
serverConfig.authenticationEnabled = true;
}
if (serverConfig.enabledAuthProviders?.length === 0) {
serverConfig.authenticationEnabled = false;
}
if (!serverConfig.authenticationEnabled) {
serverConfig.anonymousAccessEnabled = true;
}
}],
});
return styled(styles)(
<Container wrap gap>
@@ -42,21 +68,19 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
>
{translate('administration_configuration_wizard_configuration_anonymous_access')}
</SwitchNew>
{!disabled && (
<SwitchNew
name="authenticationEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_authentication_description')}
mod={['primary']}
disabled={serverConfig.enabledAuthProviders?.length === 0}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_authentication')}
</SwitchNew>
)}
<SwitchNew
name="authenticationEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_authentication_description')}
mod={['primary']}
disabled={serverConfig.enabledAuthProviders?.length === 0}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_authentication')}
</SwitchNew>
<Loader state={providers}>
{() => !disabled && styled(styles)(
{() => providersSelectable && styled(styles)(
<>
<GroupTitle>{translate('administration_configuration_wizard_configuration_authentication_provider')}</GroupTitle>
{providers.data.map(provider => provider && (
@@ -77,7 +101,7 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
)}
</Loader>
</Group>
{configurationWizard ? (
{configurationWizard && localExists ? (
<ServerConfigurationAdminForm serverConfig={serverConfig} />
) : (
<Container medium />
@@ -10,7 +10,6 @@
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { BASE_CONTAINERS_STYLES, Group, GroupTitle, InputFieldNew } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { ServerConfigInput } from '@cloudbeaver/core-sdk';
@@ -26,10 +25,6 @@ export const ServerConfigurationAdminForm: React.FC<Props> = observer(function S
const translate = useTranslate();
const style = useStyles(BASE_CONTAINERS_STYLES);
if (!serverConfig.enabledAuthProviders?.includes(AUTH_PROVIDER_LOCAL_ID)) {
return null;
}
return styled(style)(
<Group form gap medium>
<GroupTitle>{translate('administration_configuration_wizard_configuration_admin')}</GroupTitle>
@@ -26,26 +26,18 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap {
register(): void {
this.serverConfigurationService.validationTask.addHandler(this.validateForm);
this.serverConfigurationService.prepareConfigTask.addHandler(this.prepareConfig);
this.serverConfigurationService.loadConfigTask.addHandler(this.loadServerConfig);
}
load(): void { }
private loadServerConfig: IExecutorHandler<ILoadConfigData> = async (data, contexts) => {
const providers = await this.authProvidersResource.loadAll();
const disabled = providers.length === 1 && !this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
if (disabled) {
data.state.serverConfig.enabledAuthProviders = [...this.authProvidersResource.keys];
data.state.serverConfig.authenticationEnabled = true;
}
if (!data.reload) {
return;
}
try {
await this.authProvidersResource.loadAll();
const config = await this.serverConfigResource.load();
if (!config) {
@@ -53,16 +45,17 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap {
}
if (config.configurationMode) {
data.state.serverConfig.adminName = 'cbadmin';
data.state.serverConfig.adminPassword = '';
data.state.serverConfig.anonymousAccessEnabled = false;
data.state.serverConfig.authenticationEnabled = true;
if (this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID)) {
data.state.serverConfig.adminName = 'cbadmin';
data.state.serverConfig.adminPassword = '';
}
} else {
data.state.serverConfig.anonymousAccessEnabled = config.anonymousAccessEnabled;
data.state.serverConfig.authenticationEnabled = config.authenticationEnabled;
data.state.serverConfig.adminName = undefined;
data.state.serverConfig.adminPassword = undefined;
}
data.state.serverConfig.anonymousAccessEnabled = config.anonymousAccessEnabled;
data.state.serverConfig.authenticationEnabled = config.authenticationEnabled;
data.state.serverConfig.enabledAuthProviders = config.enabledAuthProviders;
} catch (exception) {
ExecutorInterrupter.interrupt(contexts);
@@ -70,37 +63,21 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap {
}
};
private prepareConfig: IExecutorHandler<IServerConfigSaveData> = async (data, contexts) => {
const providers = await this.authProvidersResource.loadAll();
const disabled = providers.length === 1 && !this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
private validateForm: IExecutorHandler<IServerConfigSaveData> = async (data, contexts) => {
await this.authProvidersResource.loadAll();
const administratorPresented = data.configurationWizard && this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
if (disabled) {
data.state.serverConfig.enabledAuthProviders = [...this.authProvidersResource.keys];
data.state.serverConfig.authenticationEnabled = true;
if (!administratorPresented) {
return;
}
if (
!data.configurationWizard
|| !data.state.serverConfig.enabledAuthProviders?.includes(AUTH_PROVIDER_LOCAL_ID)
) {
data.state.serverConfig.adminName = undefined;
data.state.serverConfig.adminPassword = undefined;
}
};
private validateForm: IExecutorHandler<IServerConfigSaveData> = (data, contexts) => {
const validation = contexts.getContext(serverConfigValidationContext);
if (
data.configurationWizard
&& data.state.serverConfig.enabledAuthProviders?.includes(AUTH_PROVIDER_LOCAL_ID)
) {
if (!data.state.serverConfig.adminName
if (!data.state.serverConfig.adminName
|| data.state.serverConfig.adminName.length < 6
|| !data.state.serverConfig.adminPassword
) {
validation.invalidate();
}
) {
validation.invalidate();
}
};
}