CB-4561 te UI dynamic connection credentials based on the users team (#2381)

* CB-4598 npe fix

* CB-4561 refactor: connection info resource

* CB-4561 refactor: connection info resource

* CB-4561 load credentials from secret in initConnection

* CB-4561 merge devel

* CB-4561 allow initial team secrets test

* CB-4561 remove ability to use any secret in test connection

* CB-4561 feat: shared credentials management

* CB-4561 feat: connection secret authentication

* CB-4561 little fix

* CB-4561 feat: shared credentials

* CB-4561 chore: remove save credentials checkbox for shared credentials

* CB-4561 fix: send saved state for shared connection credentials

* CB-4561 feat: change shared secret authentication description

* CB-4561 fix: pr review comments

* CB-4561 fix: disable connection test for shared connections in distributed env

* CB-4561 fix: types

---------

Co-authored-by: Aleksandr Skoblikov <aleksandr.skoblikov@dbeaver.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
Alexey
2024-02-20 16:42:46 +01:00
committed by GitHub
co-authored by Aleksandr Skoblikov mr-anton-t
parent 662d991aca
commit 2b90eb038e
110 changed files with 1164 additions and 497 deletions
@@ -448,5 +448,12 @@ public class WebConnectionInfo {
return dataSourceContainer.getConnectionConfiguration().getKeepAliveInterval();
}
@Property
public List<WebSecretInfo> getSharedSecrets() throws DBException {
return dataSourceContainer.listSharedCredentials()
.stream()
.map(WebSecretInfo::new)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,38 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.model;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.secret.DBSSecretValue;
public class WebSecretInfo {
private final DBSSecretValue secretValue;
public WebSecretInfo(DBSSecretValue secretValue) {
this.secretValue = secretValue;
}
@Property
public String getDisplayName() {
return secretValue.getDisplayName();
}
@Property
public String getSecretId() {
return secretValue.getSubjectId();
}
}
@@ -1008,6 +1008,17 @@ public class WebSession extends BaseWebSession
return getWorkspace().getProjectById(projectId);
}
public WebProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException {
WebProjectImpl project = null;
if (projectId != null) {
project = getWorkspace().getProjectById(projectId);
}
if (project == null) {
throw new DBWebException("Project not found: " + projectId);
}
return project;
}
public List<WebProjectImpl> getAccessibleProjects() {
return getWorkspace().getProjects();
}
@@ -308,6 +308,11 @@ type NetworkHandlerConfig {
secureProperties: Object!
}
type SecretInfo {
displayName: String!
secretId: String!
}
# Connection instance
type ConnectionInfo {
id: ID!
@@ -337,6 +342,8 @@ type ConnectionInfo {
saveCredentials: Boolean!
# Shared credentials - the same for all users, stored in secure storage.
sharedCredentials: Boolean!
sharedSecrets: [SecretInfo!]! @since(version: "23.3.5")
# Determines that credentials were saved for current user.
# This field read is slow, it should be read only when it really needed
credentialsSaved: Boolean!
@@ -488,6 +495,7 @@ input ConnectionConfig {
saveCredentials: Boolean
sharedCredentials: Boolean
authModelId: ID
selectedSecretId: ID @since(version: "23.3.5")
credentials: Object
# Map of provider properties (name/value)
@@ -584,14 +592,14 @@ extend type Mutation {
copyConnectionFromNode( nodePath: String!, config: ConnectionConfig, projectId: ID ): ConnectionInfo!
# Test connection configuration. Returns remote server version
testConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo!
testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo!
# Test connection configuration. Returns remote server version
testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo!
# Initiate existing connection
initConnection( id: ID!, projectId: ID, credentials: Object, networkCredentials: [NetworkHandlerConfigInput!],
saveCredentials:Boolean, sharedCredentials: Boolean ): ConnectionInfo!
saveCredentials:Boolean, sharedCredentials: Boolean, selectedSecretId:String ): ConnectionInfo!
# Disconnect from database
closeConnection( id: ID!, projectId: ID ): ConnectionInfo!
@@ -16,6 +16,7 @@
*/
package io.cloudbeaver.model;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.meta.Property;
@@ -59,6 +60,7 @@ public class WebConnectionConfig {
private Map<String, Object> providerProperties;
private List<WebNetworkHandlerConfigInput> networkHandlersConfig;
private DBPDriverConfigurationType configurationType;
private String selectedSecretId;
public WebConnectionConfig() {
}
@@ -91,6 +93,7 @@ public class WebConnectionConfig {
properties = JSONUtils.getObjectOrNull(params, "properties");
userName = JSONUtils.getString(params, "userName");
userPassword = JSONUtils.getString(params, "userPassword");
selectedSecretId = JSONUtils.getString(params, "selectedSecretId");
authModelId = JSONUtils.getString(params, "authModelId");
credentials = JSONUtils.getObjectOrNull(params, "credentials");
@@ -231,4 +234,9 @@ public class WebConnectionConfig {
public Integer getKeepAliveInterval() {
return keepAliveInterval;
}
@Nullable
public String getSelectedSecretId() {
return selectedSecretId;
}
}
@@ -113,7 +113,8 @@ public interface DBWServiceCore extends DBWService {
@NotNull Map<String, Object> authProperties,
@Nullable List<WebNetworkHandlerConfigInput> networkCredentials,
@Nullable Boolean saveCredentials,
@Nullable Boolean sharedCredentials
@Nullable Boolean sharedCredentials,
@Nullable String selectedCredentials
) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
@@ -132,7 +132,8 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
env.getArgument("credentials"),
nhc,
env.getArgument("saveCredentials"),
env.getArgument("sharedCredentials")
env.getArgument("sharedCredentials"),
env.getArgument("selectedSecretId")
);
}
)
@@ -54,6 +54,7 @@ import org.jkiss.dbeaver.model.net.ssh.SSHImplementation;
import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.secret.DBSSecretController;
import org.jkiss.dbeaver.model.secret.DBSSecretValue;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
@@ -318,15 +319,32 @@ public class WebServiceCore implements DBWServiceCore {
@NotNull Map<String, Object> authProperties,
@Nullable List<WebNetworkHandlerConfigInput> networkCredentials,
@Nullable Boolean saveCredentials,
@Nullable Boolean sharedCredentials
@Nullable Boolean sharedCredentials,
@Nullable String selectedSecretId
) throws DBWebException {
WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId);
connectionInfo.setSavedCredentials(authProperties, networkCredentials);
DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer();
var dataSourceContainer = (DataSourceDescriptor) connectionInfo.getDataSourceContainer();
if (dataSourceContainer.isConnected()) {
throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected");
}
if (dataSourceContainer.isSharedCredentials() && selectedSecretId != null) {
List<DBSSecretValue> allSecrets;
try {
allSecrets = dataSourceContainer.listSharedCredentials();
} catch (DBException e) {
throw new DBWebException("Error loading connection secret", e);
}
DBSSecretValue selectedSecret =
allSecrets.stream()
.filter(secret -> selectedSecretId.equals(secret.getSubjectId()))
.findFirst().orElse(null);
if (selectedSecret == null) {
throw new DBWebException("Secret not found:" + selectedSecretId);
}
dataSourceContainer.setSelectedSharedCredentials(selectedSecret);
}
boolean oldSavePassword = dataSourceContainer.isSavePassword();
try {
@@ -642,12 +660,12 @@ public class WebServiceCore implements DBWServiceCore {
connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig
DBPDataSourceContainer dataSource = WebDataSourceUtils.getLocalOrGlobalDataSource(
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
CBApplication.getInstance(), webSession, projectId, connectionId);
WebProjectImpl project = getProjectById(webSession, projectId);
DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry();
DBPDataSourceContainer testDataSource;
DataSourceDescriptor testDataSource;
if (dataSource != null) {
try {
// Check that creds are saved to trigger secrets resolve
@@ -656,12 +674,27 @@ public class WebServiceCore implements DBWServiceCore {
throw new DBWebException("Can't determine whether datasource credentials are saved", e);
}
testDataSource = dataSource.createCopy(dataSource.getRegistry());
testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry());
WebServiceUtils.setConnectionConfiguration(
testDataSource.getDriver(),
testDataSource.getConnectionConfiguration(),
connectionConfig
);
if (connectionConfig.getSelectedSecretId() != null) {
try {
DBSSecretValue secretValue = dataSource.listSharedCredentials()
.stream()
.filter(secret -> connectionConfig.getSelectedSecretId().equals(secret.getSubjectId()))
.findFirst()
.orElse(null);
if (secretValue != null) {
testDataSource.setSelectedSharedCredentials(secretValue);
}
} catch (DBException e) {
throw new DBWebException("Failed to load secret value: " + connectionConfig.getSelectedSecretId());
}
}
WebServiceUtils.saveAuthProperties(
testDataSource,
testDataSource.getConnectionConfiguration(),
@@ -671,7 +704,8 @@ public class WebServiceCore implements DBWServiceCore {
true
);
} else {
testDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry);
testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig,
sessionRegistry);
}
webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration());
testDataSource.setSavePassword(true); // We need for test to avoid password callback
@@ -23,8 +23,8 @@ export class AppAuthService extends Bootstrap {
get loaders(): ILoadableState[] {
return [
getCachedDataResourceLoaderState(this.userInfoResource, undefined),
getCachedDataResourceLoaderState(this.serverConfigResource, undefined),
getCachedDataResourceLoaderState(this.userInfoResource, () => undefined),
getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined),
];
}
@@ -1,3 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32ZM10.9493 14.6232L9.5 16.5072L14.8623 21L22.5435 12.5942L20.8043 11L14.7174 17.8116L10.9493 14.6232Z" fill="#52C41A"/>
<path class="icon-fill" id="root" fill-rule="evenodd" clip-rule="evenodd" d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32ZM10.9493 14.6232L9.5 16.5072L14.8623 21L22.5435 12.5942L20.8043 11L14.7174 17.8116L10.9493 14.6232Z" fill="#52C41A"/>
</svg>

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 412 B

@@ -16,7 +16,7 @@ import { Button } from '../Button';
import { Container } from '../Containers/Container';
import { Fill } from '../Fill';
import { Form } from '../FormControls/Form';
import { InputField } from '../FormControls/InputField';
import { InputField } from '../FormControls/InputField/InputField';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { useFocus } from '../useFocus';
@@ -1,6 +1,18 @@
.group {
composes: theme-background-surface theme-text-on-surface from global;
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.secondary {
composes: theme-background-secondary theme-text-on-secondary from global;
}
.surface {
composes: theme-background-surface theme-text-on-surface from global;
}
.group {
align-content: baseline;
box-sizing: border-box;
padding: 24px;
@@ -17,13 +17,14 @@ import elementsSizeStyles from './shared/ElementsSize.m.css';
interface Props extends IContainerProps {
form?: boolean;
secondary?: boolean;
center?: boolean;
box?: boolean;
boxNoOverflow?: boolean;
}
export const Group = forwardRef<HTMLDivElement, Props & React.HTMLAttributes<HTMLDivElement>>(function Group(
{ form, center, box, boxNoOverflow, className, ...rest },
{ form, center, box, secondary, boxNoOverflow, className, ...rest },
ref,
) {
const styles = useS(style, containerStyles, elementsSizeStyles);
@@ -40,6 +41,8 @@ export const Group = forwardRef<HTMLDivElement, Props & React.HTMLAttributes<HTM
...containerProps,
group: true,
container: true,
secondary,
surface: !secondary,
form,
center,
boxNoOverflow,
@@ -51,6 +51,7 @@
.field.fill,
.container.fill,
.container > .fill {
flex-grow: 1;
max-width: none;
}
@@ -17,7 +17,7 @@ import { useErrorDetails } from './useErrorDetails';
import { useS } from './useS';
interface Props {
exception?: Error;
exception?: Error | null;
icon?: boolean;
inline?: boolean;
className?: string;
@@ -39,6 +39,10 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({ exce
};
}
if (!exception) {
return null;
}
return (
<div className={s(styles, { error: true, icon, inline }, className)}>
<div className={s(styles, { errorIcon: true })} title={error.message}>
@@ -51,7 +55,8 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({ exce
<span>{translate('core_blocks_exception_message_error_title')}</span>
</h2>
<div className={s(styles, { errorMessage: true })}>
{translate('core_blocks_exception_message_error_message')} {onRetry && translate('ui_please_retry')}
{(error.hasDetails && error.message) || translate('core_blocks_exception_message_error_message')}{' '}
{onRetry && translate('ui_please_retry')}
</div>
<div className={s(styles, { errorActions: true })}>
<Button type="button" mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
@@ -6,7 +6,6 @@
& .fieldLabel {
cursor: pointer;
user-select: none;
padding-left: 10px;
line-height: 16px;
}
}
@@ -12,6 +12,7 @@ import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps';
import { s } from '../../s';
import { useS } from '../../useS';
import { Field } from '../Field';
import { FieldDescription } from '../FieldDescription';
import { FieldLabel } from '../FieldLabel';
import { isControlPresented } from '../isControlPresented';
import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
@@ -13,7 +13,7 @@ import { s } from '../s';
import { useFocus } from '../useFocus';
import { useS } from '../useS';
import filterStyle from './Filter.m.css';
import { InputField } from './InputField';
import { InputField } from './InputField/InputField';
interface BaseProps {
placeholder?: string;
@@ -0,0 +1,17 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IFormStateControl<TState extends Record<string, any>, TKey extends keyof TState> {
name: TKey;
state: TState;
defaultState?: TState;
autoHide?: boolean;
mapState?: (value: TState[TKey]) => string;
mapValue?: (value: string) => TState[TKey];
onChange?: (value: TState[TKey], name: TKey) => any;
}
@@ -32,10 +32,13 @@
.customIconContainer {
composes: iconContainer;
right: 4px;
right: 6px;
width: 24px;
height: 24px;
cursor: auto;
display: flex;
align-items: center;
justify-content: center;
}
.input[disabled] + .iconContainer {
@@ -0,0 +1,35 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 React, { forwardRef, useContext } from 'react';
import { useCombinedHandler } from '../../useCombinedHandler';
import { FormContext } from '../FormContext';
import { isFormStateControl } from '../isFormStateControl';
import { InputFieldBase, type InputFieldBaseProps } from './InputFieldBase';
import { InputFieldState, type InputFieldStateProps } from './InputFieldState';
interface InputFieldType {
(props: InputFieldBaseProps): React.ReactElement<any, any> | null;
<TState extends Record<string, any>, TKey extends keyof TState>(props: InputFieldStateProps<TState, TKey>): React.ReactElement<any, any> | null;
}
export const InputField: InputFieldType = observer<InputFieldBaseProps | InputFieldStateProps<any, any>, HTMLInputElement>(
forwardRef(function InputField({ onChange, onKeyDown, ...rest }, ref) {
const context = useContext(FormContext);
const handleChange = useCombinedHandler(onChange, context?.change);
const handleKeyDown = useCombinedHandler(onKeyDown, context?.keyDown);
if (isFormStateControl(rest)) {
return <InputFieldState ref={ref} {...rest} onChange={handleChange} onKeyDown={handleKeyDown} />;
}
return <InputFieldBase ref={ref} {...rest} onChange={handleChange} onKeyDown={handleKeyDown} />;
}),
) as InputFieldType;
@@ -6,82 +6,53 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
import React, { forwardRef, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { isNotNullDefined } from '@cloudbeaver/core-utils';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import { Icon } from '../Icon';
import { Loader } from '../Loader/Loader';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { useCombinedHandler } from '../useCombinedHandler';
import { useCombinedRef } from '../useCombinedRef';
import { useS } from '../useS';
import { useStateDelay } from '../useStateDelay';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
import { filterLayoutFakeProps, getLayoutProps } from '../../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps';
import { Icon } from '../../Icon';
import { Loader } from '../../Loader/Loader';
import { useTranslate } from '../../localization/useTranslate';
import { s } from '../../s';
import { useCombinedHandler } from '../../useCombinedHandler';
import { useCombinedRef } from '../../useCombinedRef';
import { useS } from '../../useS';
import { useStateDelay } from '../../useStateDelay';
import { Field } from '../Field';
import { FieldDescription } from '../FieldDescription';
import { FieldLabel } from '../FieldLabel';
import { useCapsLockTracker } from '../useCapsLockTracker';
import inputFieldStyle from './InputField.m.css';
import { isControlPresented } from './isControlPresented';
import { useCapsLockTracker } from './useCapsLockTracker';
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value' | 'style'> &
export type InputFieldBaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'style'> &
ILayoutSizeProps & {
value?: string;
error?: boolean;
loading?: boolean;
description?: string;
labelTooltip?: string;
ref?: React.ForwardedRef<HTMLInputElement>;
canShowPassword?: boolean;
onCustomCopy?: () => void;
icon?: React.ReactElement;
onCustomCopy?: () => void;
onChange?: (value: string, name?: string) => any;
};
type ControlledProps = BaseProps & {
name?: string;
value?: string | number;
mapState?: (value: string | number) => string | number;
mapValue?: (value: string | number) => string | number;
onChange?: (value: string | number, name?: string) => any;
state?: never;
autoHide?: never;
};
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
mapState?: (value: TState[TKey]) => TState[TKey] | string | number;
mapValue?: (value: TState[TKey]) => TState[TKey];
onChange?: (value: TState[TKey], name: TKey) => any;
autoHide?: boolean;
value?: never;
};
interface InputFieldType {
(props: ControlledProps): React.ReactElement<any, any> | null;
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): React.ReactElement<any, any> | null;
}
export const InputField: InputFieldType = observer<ControlledProps | ObjectProps<any, any>, HTMLInputElement>(
forwardRef(function InputField(
export const InputFieldBase = observer<InputFieldBaseProps, HTMLInputElement>(
forwardRef(function InputFieldBase(
{
name,
value: valueControlled,
value,
defaultValue,
required,
state,
mapState,
mapValue,
children,
className,
error,
loading,
description,
labelTooltip,
autoHide,
canShowPassword = true,
onChange,
onCustomCopy,
@@ -98,7 +69,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const styles = useS(inputFieldStyle);
const context = useContext(FormContext);
loading = useStateDelay(loading ?? false, 300);
const revealPassword = useCallback(() => {
@@ -111,42 +81,25 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = mapValue?.(event.target.value) ?? event.target.value;
if (state) {
state[name] = value;
}
if (onChange) {
onChange(value, name);
}
if (context) {
context.change(value, name);
}
onChange?.(event.target.value, name);
},
[state, name, context, onChange],
[name, onChange],
);
const handleBlur = useCombinedHandler(rest.onBlur, capsLock.handleBlur);
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown, context?.keyDown);
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown);
const passwordType = rest.type === 'password';
let uncontrolled = passwordType && !canShowPassword;
let value: any = valueControlled ?? undefined;
if (state && name !== undefined && name in state) {
value = state[name];
}
if (mapState) {
value = mapState(value);
}
if (passwordType && !rest.readOnly && capsLock.warn) {
description = translate('ui_capslock_on');
}
uncontrolled ||= value === undefined;
if (!value) {
canShowPassword = false;
}
useLayoutEffect(() => {
if (uncontrolled && isNotNullDefined(value) && inputRef.current) {
@@ -154,10 +107,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
}
});
if (autoHide && !isControlPresented(name, state, defaultValue)) {
return null;
}
return (
<Field {...layoutProps} className={s(styles, {}, className)}>
<FieldLabel title={labelTooltip || rest.title} className={s(styles, { fieldLabel: true })} required={required}>
@@ -169,7 +118,7 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
{...rest}
type={passwordRevealed ? 'text' : rest.type}
name={name}
value={uncontrolled ? undefined : value ?? ''}
value={uncontrolled ? undefined : value}
defaultValue={defaultValue}
className={s(styles, { input: true })}
required={required}
@@ -198,4 +147,4 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
</Field>
);
}),
) as InputFieldType;
);
@@ -0,0 +1,40 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 React, { forwardRef } from 'react';
import type { IFormStateControl } from '../IFormStateControl';
import { useFormStateControl } from '../useFormStateControl';
import { InputFieldBase, InputFieldBaseProps } from './InputFieldBase';
export type InputFieldStateProps<TState extends Record<string, any>, TKey extends keyof TState> = Omit<InputFieldBaseProps, 'value'> &
IFormStateControl<TState, TKey>;
interface InputFieldType {
<TState extends Record<string, any>, TKey extends keyof TState>(props: InputFieldStateProps<TState, TKey>): React.ReactElement<any, any> | null;
}
export const InputFieldState: InputFieldType = observer<InputFieldStateProps<any, any>, HTMLInputElement>(
forwardRef(function InputFieldState({ name, state, defaultState, mapState, mapValue, autoHide, onChange, ...rest }, ref) {
const controlState = useFormStateControl({ name, defaultState, state, autoHide, mapState, mapValue, onChange });
if (controlState.hide) {
return null;
}
return (
<InputFieldBase
ref={ref}
name={name}
value={controlState.stringValue ?? controlState.defaultValue ?? ''}
onChange={controlState.onChange}
{...rest}
/>
);
}),
) as InputFieldType;
@@ -8,8 +8,6 @@
import { observer } from 'mobx-react-lite';
import { forwardRef, useContext, useEffect, useState } from 'react';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { Button } from '../Button';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
@@ -5,11 +5,12 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { isNotNullDefined } from '@cloudbeaver/core-utils';
export function isControlPresented(name: string | undefined, state: any, defaultValue?: string | number | readonly string[]): boolean {
if (state !== undefined && name !== undefined) {
export function isControlPresented(name: string | undefined, state: any, defaultValue?: any): boolean {
if (isNotNullDefined(state) && isNotNullDefined(name)) {
if (name in state) {
return state[name] !== null && state[name] !== undefined;
return isNotNullDefined(state[name]);
}
return defaultValue !== undefined;
}
@@ -0,0 +1,14 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { IFormStateControl } from './IFormStateControl';
export function isFormStateControl<TState extends Record<string, any>, TKey extends keyof TState>(
props: any,
): props is IFormStateControl<TState, TKey> {
return 'name' in props && 'state' in props;
}
@@ -0,0 +1,58 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { isNotNullDefined, isObject } from '@cloudbeaver/core-utils';
import { useCombinedHandler } from '../useCombinedHandler';
import type { IFormStateControl } from './IFormStateControl';
import { isControlPresented } from './isControlPresented';
export function useFormStateControl<TState extends Record<string, any>, TKey extends keyof TState>({
name,
state: value,
defaultState: defaultValue,
mapState: mapToString,
mapValue: mapToValue,
onChange,
...rest
}: IFormStateControl<TState, TKey>) {
const originalValue = value;
const handleChange = useCombinedHandler(function handleChange(inputValue: string) {
if (mapToValue) {
inputValue = mapToValue(inputValue);
}
if (isNotNullDefined(originalValue) && isObject(originalValue)) {
(originalValue as any)[name] = inputValue;
}
if (onChange) {
onChange(inputValue as any, name as any);
}
});
if (isNotNullDefined(defaultValue) && isObject(defaultValue)) {
defaultValue = defaultValue[name];
}
if (isNotNullDefined(value) && isObject(value)) {
value = value[name];
}
let stringValue: string | typeof value;
let defaultStringValue: string | typeof defaultValue;
if (mapToString) {
stringValue = mapToString(value as any);
defaultStringValue = mapToString(defaultValue as any);
} else {
stringValue = value;
defaultStringValue = defaultValue;
}
const hide = 'autoHide' in rest && !!rest.autoHide && !isControlPresented(String(name), stringValue, defaultStringValue);
return { name, value, stringValue, defaultValue, defaultStringValue, hide, onChange: handleChange };
}
@@ -18,14 +18,15 @@ export interface IconOrImageProps {
title?: string;
onClick?: () => void;
viewBox?: string;
width?: number;
}
export const IconOrImage: React.FC<IconOrImageProps> = function IconOrImage({ icon, className, title, onClick, viewBox }) {
export const IconOrImage: React.FC<IconOrImageProps> = function IconOrImage({ icon, className, title, onClick, viewBox, width }) {
const isStaticIcon = useMemo(() => icon && (icon.startsWith('platform:') || icon.startsWith('/')), [icon]);
if (isStaticIcon || isValidUrl(icon)) {
return <StaticImage title={title} icon={icon} className={className} onClick={onClick} />;
return <StaticImage title={title} icon={icon} width={width} className={className} onClick={onClick} />;
}
return <Icon name={icon} className={className} viewBox={viewBox || '0 0 32 32'} onClick={onClick} />;
return <Icon name={icon} className={className} viewBox={viewBox || '0 0 32 32'} width={width} height={width} onClick={onClick} />;
};
@@ -93,11 +93,14 @@
font-weight: 500;
min-width: 250px;
padding: 8px 24px 8px 8px;
border-right: 1px solid;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:not(:last-child) {
border-right: 1px solid;
}
}
.listItemDescription {
composes: theme-typography--body2 from global;
@@ -20,8 +20,9 @@ import { useS } from '../../useS';
import { RenderField } from './RenderField';
interface ObjectPropertyFormProps extends ILayoutSizeProps {
properties: ObjectPropertyInfo[];
properties: ReadonlyArray<ObjectPropertyInfo>;
state?: Record<string, any>;
defaultState?: Record<string, any>;
category?: string | null;
editable?: boolean;
autofillToken?: string;
@@ -41,6 +42,7 @@ interface ObjectPropertyFormProps extends ILayoutSizeProps {
export const ObjectPropertyInfoForm = observer<ObjectPropertyFormProps>(function ObjectPropertyInfoForm({
properties,
state,
defaultState,
category,
editable = true,
className,
@@ -86,6 +88,7 @@ export const ObjectPropertyInfoForm = observer<ObjectPropertyFormProps>(function
className={s(sizeStyles, { ...(geLayoutSize ? geLayoutSize(property) : layoutProps) }, className)}
property={property}
state={state}
defaultState={defaultState}
editable={editable}
autofillToken={autofillToken}
disabled={disabled}
@@ -13,7 +13,7 @@ import { removeMetadataFromBase64 } from '@cloudbeaver/core-utils';
import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox';
import { Combobox } from '../../FormControls/Combobox';
import { FormFieldDescription } from '../../FormControls/FormFieldDescription';
import { InputField } from '../../FormControls/InputField';
import { InputField } from '../../FormControls/InputField/InputField';
import { InputFileTextContent } from '../../FormControls/InputFileTextContent';
import { isControlPresented } from '../../FormControls/isControlPresented';
import { Textarea } from '../../FormControls/Textarea';
@@ -26,6 +26,7 @@ const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
interface RenderFieldProps {
property: ObjectPropertyInfo;
state?: Record<string, any>;
defaultState?: Record<string, any>;
editable?: boolean;
autofillToken?: string;
disabled?: boolean;
@@ -55,6 +56,7 @@ function getValue(value: any, controlType: ControlType) {
export const RenderField = observer<RenderFieldProps>(function RenderField({
property,
state,
defaultState,
editable = true,
autofillToken = '',
disabled,
@@ -74,26 +76,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
const value = getValue(property.value, controlType);
const defaultValue = getValue(property.defaultValue, controlType);
const passwordSaved = showRememberTip && ((password && !!property.value) || saved);
const description = passwordSaved ? translate('ui_processing_saved') : undefined;
if (controlType === 'file' && state) {
return (
<InputFileTextContent
required={required}
tooltip={property.description}
labelTooltip={property.displayName || property.description}
name={property.id!}
state={state}
disabled={disabled}
fileName={description}
className={className}
mapValue={removeMetadataFromBase64}
>
{property.displayName}
</InputFileTextContent>
);
}
if (controlType === 'link') {
return (
@@ -128,6 +110,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
title={property.description}
disabled={disabled || readOnly}
className={className}
groupGap
>
{property.displayName ?? ''}
</FieldCheckbox>
@@ -142,6 +125,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
title={property.description}
disabled={disabled || readOnly}
className={className}
groupGap
>
{property.displayName ?? ''}
</FieldCheckbox>
@@ -189,6 +173,27 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
);
}
const passwordSaved = showRememberTip && ((password && !!property.value) || saved);
const passwordSavedMessage = passwordSaved ? translate('core_blocks_object_property_info_password_saved') : undefined;
if (controlType === 'file' && state) {
return (
<InputFileTextContent
required={required}
tooltip={property.description}
labelTooltip={property.displayName || property.description}
name={property.id!}
state={state}
disabled={disabled}
fileName={passwordSavedMessage}
className={className}
mapValue={removeMetadataFromBase64}
>
{property.displayName}
</InputFileTextContent>
);
}
if (controlType === 'textarea') {
if (state !== undefined) {
return (
@@ -196,7 +201,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
required={required}
title={state[property.id!]}
labelTooltip={property.description || property.displayName}
description={description}
placeholder={passwordSavedMessage}
name={property.id!}
state={state}
disabled={disabled}
@@ -213,7 +218,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
required={required}
title={value}
labelTooltip={property.description || property.displayName}
description={description}
placeholder={passwordSavedMessage}
name={property.id!}
value={value}
disabled={disabled}
@@ -230,15 +235,17 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
<InputField
required={required}
type={password ? 'password' : 'text'}
title={password ? property.description || property.displayName : state[property.id!]}
title={password ? property.description || property.displayName : undefined}
labelTooltip={property.description || property.displayName}
name={property.id!}
state={state}
defaultState={defaultState}
autoHide={autoHide}
defaultValue={defaultValue}
description={description ?? property.hint}
description={property.hint}
placeholder={passwordSavedMessage}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
className={className}
canShowPassword={canShowPassword}
@@ -253,12 +260,13 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
<InputField
required={required}
type={password ? 'password' : 'text'}
title={password ? property.description || property.displayName : value}
title={password ? property.description || property.displayName : undefined}
labelTooltip={property.description || property.displayName}
name={property.id!}
value={value}
defaultValue={defaultValue}
description={description ?? property.hint}
description={property.hint}
placeholder={passwordSavedMessage}
disabled={disabled}
readOnly={readOnly}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
@@ -13,18 +13,18 @@ import { Button } from '../Button';
import ButtonStyles from '../Button.m.css';
import { Filter } from '../FormControls/Filter';
import FilterStyles from '../FormControls/Filter.m.css';
import InputFieldStyles from '../FormControls/InputField.m.css';
import InputFieldStyles from '../FormControls/InputField/InputField.m.css';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { SContext, StyleRegistry } from '../SContext';
import { useObjectRef } from '../useObjectRef';
import { useS } from '../useS';
import type { IProperty } from './IProperty';
import styles from './PropertiesTable.m.css';
import PropertiesTableAddButtonStyles from './PropertiesTableAddButtonStyles.m.css';
import PropertiesTableFilterStyles from './PropertiesTableFilterStyles.m.css';
import PropertiesTableInputStyles from './PropertiesTableInputStyles.m.css';
import { PropertyItem } from './PropertyItem';
import { useS } from '../useS';
import styles from './PropertiesTable.m.css';
import { s } from '../s';
type PropertiesState = Record<string, string | null>;
@@ -48,7 +48,7 @@ interface IActions<TResource extends IResource<any, any, any, any>, TKey, TInclu
active?: boolean;
forceSuspense?: boolean;
silent?: boolean;
onData?: (data: ResourceData<TResource, TKey, TIncludes>, resource: TResource) => Promise<any> | any;
onData?: (data: ResourceData<TResource, TKey, TIncludes>, resource: TResource) => any;
onError?: (exception: Error | Error[] | null) => void;
}
+2 -2
View File
@@ -155,8 +155,8 @@ export * from './Tags/Tags';
export * from './FormControls/Combobox';
export * from './FormControls/FormContext';
export * from './FormControls/FormFieldDescription';
export * from './FormControls/InputField';
export { default as InputFieldStyles } from './FormControls/InputField.m.css';
export * from './FormControls/InputField/InputField';
export { default as InputFieldStyles } from './FormControls/InputField/InputField.m.css';
export * from './FormControls/InputFiles';
export * from './FormControls/InputFileTextContent';
export * from './FormControls/Radio';
@@ -5,6 +5,7 @@ export default [
['core_block_properties_table_add', 'Add property'],
['core_blocks_properties_table_item_reset', 'Reset to default'],
['core_blocks_object_property_info_form_empty_placeholder', 'Properties empty'],
['core_blocks_object_property_info_password_saved', 'Field value is saved'],
['core_blocks_properties_table_item_remove', 'Remove property'],
['combobox_no_results_placeholder', 'No results'],
['core_blocks_exception_message_error_title', 'Unable to display content'],
@@ -0,0 +1,18 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { schema } from '@cloudbeaver/core-utils';
export const CONNECTION_INFO_PARAM_SCHEMA = schema
.object({
projectId: schema.string(),
connectionId: schema.string(),
})
.required()
.strict();
export type IConnectionInfoParams = schema.infer<typeof CONNECTION_INFO_PARAM_SCHEMA>;
@@ -11,6 +11,7 @@ import { ProjectsService } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, CachedMapResource, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { GraphQLService, SqlDialectInfo } from '@cloudbeaver/core-sdk';
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContext/ConnectionExecutionContextResource';
import {
ConnectionInfoActiveProjectKey,
@@ -18,7 +19,6 @@ import {
ConnectionInfoResource,
isConnectionInfoParamEqual,
} from './ConnectionInfoResource';
import type { IConnectionInfoParams } from './IConnectionsResource';
export type ConnectionDialect = SqlDialectInfo;
@@ -22,8 +22,8 @@ import {
import { GraphQLService, SqlContextInfo } from '@cloudbeaver/core-sdk';
import { flat } from '@cloudbeaver/core-utils';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '../ConnectionInfoResource';
import type { IConnectionInfoParams } from '../IConnectionsResource';
export const ConnectionExecutionContextProjectKey = resourceKeyAliasFactory('@connection-folder/project', (projectId: string) => ({ projectId }));
@@ -10,7 +10,7 @@ import { TaskScheduler } from '@cloudbeaver/core-executor';
import { CachedMapAllKey, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { MetadataMap } from '@cloudbeaver/core-utils';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionExecutionContext } from './ConnectionExecutionContext';
import { ConnectionExecutionContextResource } from './ConnectionExecutionContextResource';
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, makeObservable, observable, runInAction } from 'mobx';
import { action, makeObservable, observable, runInAction, toJS } from 'mobx';
import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
@@ -35,10 +35,11 @@ import {
TestConnectionMutation,
UserConnectionAuthPropertiesFragment,
} from '@cloudbeaver/core-sdk';
import { schemaValidationError } from '@cloudbeaver/core-utils';
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionInfoEventHandler, IConnectionInfoEvent } from './ConnectionInfoEventHandler';
import type { DatabaseConnection } from './DatabaseConnection';
import type { IConnectionInfoParams } from './IConnectionsResource';
export type Connection = DatabaseConnection & {
authProperties?: UserConnectionAuthPropertiesFragment[];
@@ -53,6 +54,7 @@ export type ConnectionInitConfig = Omit<
| 'includeCredentialsSaved'
| 'includeProperties'
| 'includeProviderProperties'
| 'includeSharedSecrets'
| 'customIncludeOptions'
>;
export type ConnectionInfoIncludes = Omit<GetUserConnectionsQueryVariables, 'id'>;
@@ -577,12 +579,17 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
includeCredentialsSaved: false,
includeProperties: false,
includeProviderProperties: false,
includeSharedSecrets: false,
customIncludeOptions: false,
};
}
protected validateKey(key: IConnectionInfoParams): boolean {
return typeof key === 'object' && typeof key.projectId === 'string' && typeof key.connectionId === 'string';
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
if (!parse.success) {
this.logger.warn(`Invalid resource key ${schemaValidationError(parse.error).toString()}`);
}
return parse.success;
}
}
@@ -15,9 +15,14 @@ import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-exec
import { ProjectInfo, projectInfoSortByName, ProjectsService } from '@cloudbeaver/core-projects';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
import { Connection, ConnectionInfoResource, createConnectionParam, isConnectionInfoParamEqual } from './ConnectionInfoResource';
import { ContainerResource, IStructContainers, ObjectContainer } from './ContainerResource';
import type { IConnectionInfoParams } from './IConnectionsResource';
export interface IRequireConnectionExecutorData {
key: IConnectionInfoParams;
resetCredentials?: boolean;
}
export interface IConnectionExecutorData {
connections: IConnectionInfoParams[];
@@ -32,7 +37,7 @@ export class ConnectionsManagerService {
get createConnectionProjects(): ProjectInfo[] {
return this.projectsService.activeProjects.filter(project => project.canEditDataSources).sort(projectInfoSortByName);
}
readonly connectionExecutor: IExecutor<IConnectionInfoParams>;
readonly connectionExecutor: IExecutor<IRequireConnectionExecutorData>;
readonly onDisconnect: IExecutor<IConnectionExecutorData>;
readonly onDelete: IExecutor<IConnectionExecutorData>;
@@ -47,11 +52,11 @@ export class ConnectionsManagerService {
) {
this.disconnecting = false;
this.connectionExecutor = new Executor<IConnectionInfoParams>(null, isConnectionInfoParamEqual);
this.connectionExecutor = new Executor<IRequireConnectionExecutorData>(null, (a, b) => isConnectionInfoParamEqual(a.key, b.key));
this.onDisconnect = new Executor();
this.onDelete = new Executor();
this.connectionExecutor.addHandler(key => connectionInfo.load(key));
this.connectionExecutor.addHandler(data => connectionInfo.load(data.key));
this.onDelete.before(this.onDisconnect);
makeObservable(this, {
@@ -64,8 +69,8 @@ export class ConnectionsManagerService {
});
}
async requireConnection(key: IConnectionInfoParams): Promise<Connection | null> {
const context = await this.connectionExecutor.execute(key);
async requireConnection(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
const context = await this.connectionExecutor.execute({ key, resetCredentials });
const connection = context.getContext(this.connectionContext);
return connection.connection;
@@ -12,8 +12,8 @@ import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList,
import { GraphQLService, NavNodeInfoFragment } from '@cloudbeaver/core-sdk';
import { isNull } from '@cloudbeaver/core-utils';
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from './ConnectionInfoResource';
import type { IConnectionInfoParams } from './IConnectionsResource';
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, createConnectionParam } from './ConnectionInfoResource';
export type ObjectContainer = NavNodeInfoFragment;
export interface ICatalogData {
@@ -47,7 +47,14 @@ export class ContainerResource extends CachedMapResource<ObjectContainerParams,
appAuthService.requireAuthentication(this);
this.preloadResource(connectionInfoResource, () => ConnectionInfoActiveProjectKey);
this.before(ExecutorInterrupter.interrupter(key => !connectionInfoResource.isConnected(key)));
this.before(
ExecutorInterrupter.interrupter(key => {
if (isResourceAlias(key)) {
return false;
}
return !connectionInfoResource.isConnected(ResourceKeyUtils.mapKey(key, key => createConnectionParam(key.projectId, key.connectionId)));
}),
);
this.connectionInfoResource.onItemDelete.addHandler(key =>
ResourceKeyUtils.forEach(key, key => this.delete({ projectId: key.projectId, connectionId: key.connectionId })),
@@ -11,7 +11,7 @@ import { AppAuthService } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { CachedMapAllKey, CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { DatabaseDriverFragment, DriverListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk';
import { DatabaseDriverFragment, DriverConfigurationType, DriverListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
export type DBDriver = DatabaseDriverFragment;
@@ -19,10 +19,10 @@ export type DBDriver = DatabaseDriverFragment;
export const NEW_DRIVER_SYMBOL = Symbol('new-driver');
export type NewDBDriver = DBDriver & { [NEW_DRIVER_SYMBOL]: boolean; timestamp: number };
type DriverResourceIncludes = Omit<DriverListQueryVariables, 'driverId'>;
export type DBDriverResourceIncludes = Omit<DriverListQueryVariables, 'driverId'>;
@injectable()
export class DBDriverResource extends CachedMapResource<string, DBDriver, DriverListQueryVariables> {
export class DBDriverResource extends CachedMapResource<string, DBDriver, DBDriverResourceIncludes> {
get enabledDrivers() {
return this.values.filter(driver => driver.enabled).sort(this.compare);
}
@@ -107,7 +107,7 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
}
}
getDefaultIncludes(): DriverResourceIncludes {
getDefaultIncludes(): DBDriverResourceIncludes {
return {
includeDriverLibraries: false,
includeDriverParameters: false,
@@ -140,3 +140,19 @@ export function compareNewDrivers(a: DBDriver, b: DBDriver): number {
return 0;
}
export function getDBDriverDefaultAuthModelId(driver: DBDriver | undefined): string | undefined {
return driver?.defaultAuthModel || driver?.applicableAuthModels[0];
}
export function getDBDriverDefaultConfigurationType(driver: DBDriver | undefined): DriverConfigurationType {
return driver?.configurationTypes[0] || DriverConfigurationType.Manual;
}
export function getDBDriverDefaultServerName(driver: DBDriver | undefined): string | undefined {
return driver?.requiresServerName ? '' : undefined;
}
export function getDBDriverDefaultServer(driver: DBDriver | undefined): string | undefined {
return driver?.defaultServer || 'localhost';
}
@@ -8,7 +8,9 @@
import { injectable } from '@cloudbeaver/core-di';
import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { type DatabaseAuthModel, GraphQLService } from '@cloudbeaver/core-sdk';
import { type DatabaseAuthModel as DatabaseAuthModelBase, GraphQLService } from '@cloudbeaver/core-sdk';
export type DatabaseAuthModel = DatabaseAuthModelBase;
@injectable()
export class DatabaseAuthModelsResource extends CachedMapResource<string, DatabaseAuthModel> {
@@ -20,11 +20,11 @@ import { getProjectNodeId } from '@cloudbeaver/core-projects';
import { isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { ServerEventId } from '@cloudbeaver/core-root';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionFolderEventHandler, IConnectionFolderEvent } from '../ConnectionFolderEventHandler';
import { Connection, ConnectionInfoActiveProjectKey, ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource';
import { ConnectionsManagerService } from '../ConnectionsManagerService';
import { ContainerResource } from '../ContainerResource';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import { getConnectionParentId } from './getConnectionParentId';
import { getFolderNodeParents } from './getFolderNodeParents';
@@ -10,11 +10,11 @@ import type { IExtension } from '@cloudbeaver/core-extensions';
import { NavNodeInfoResource, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { projectProvider } from '@cloudbeaver/core-projects';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
import { ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource';
import { connectionProvider } from '../extensions/IConnectionProvider';
import { objectCatalogProvider } from '../extensions/IObjectCatalogProvider';
import { objectSchemaProvider } from '../extensions/IObjectSchemaProvider';
import type { IConnectionInfoParams } from '../IConnectionsResource';
@injectable()
export class NavNodeExtensionsService {
@@ -7,7 +7,7 @@
*/
import { createExtension, IExtension, isExtension } from '@cloudbeaver/core-extensions';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
const connectionProviderSymbol = Symbol('@extension/ConnectionProvider');
@@ -7,7 +7,7 @@
*/
import { createExtension, IExtension, isExtension } from '@cloudbeaver/core-extensions';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
const connectionSetterSymbol = Symbol('@extension/ConnectionSetter');
@@ -33,7 +33,7 @@ export * from './createConnectionFolderParam';
export * from './DatabaseAuthModelsResource';
export * from './DatabaseConnection';
export * from './DBDriverResource';
export * from './IConnectionsResource';
export * from './CONNECTION_INFO_PARAM_SCHEMA';
export * from './isJDBCConnection';
export * from './NetworkHandlerResource';
export * from './useConnectionInfo';
@@ -10,9 +10,9 @@ import { computed, observable } from 'mobx';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
import { Connection, ConnectionInfoResource } from './ConnectionInfoResource';
import { ConnectionsManagerService } from './ConnectionsManagerService';
import type { IConnectionInfoParams } from './IConnectionsResource';
interface IPrivate extends IPublic {
manager: ConnectionsManagerService;
@@ -78,6 +78,7 @@ export default [
['ui_reconnect', 'Reconnect'],
['ui_forget', 'Forget'],
['ui_set', 'Set'],
['ui_restore', 'Restore'],
['ui_are_you_sure', 'Are you sure?'],
['ui_options', 'Options'],
['ui_changes_might_be_lost', 'Your changes might be lost'],
@@ -69,6 +69,13 @@ export default [
['ui_no_items_placeholder', 'Non ci sono ancora elementi.'],
['ui_search_no_result_placeholder', 'Nessun risultato trovato.'],
['ui_save_reminder', 'Ci sono modifiche non salvate.'],
['ui_yes', 'Yes'],
['ui_no', 'No'],
['ui_select_all', 'Select all'],
['ui_reconnect', 'Reconnect'],
['ui_forget', 'Forget'],
['ui_set', 'Set'],
['ui_restore', 'Restore'],
['ui_settings', 'Impostazioni'],
['ui_advanced_settings', 'Advanced settings'],
['ui_expand', 'Expand'],
@@ -74,6 +74,7 @@ export default [
['ui_reconnect', 'Переподключиться'],
['ui_forget', 'Забыть'],
['ui_set', 'Установить'],
['ui_restore', 'Восстановить'],
['ui_are_you_sure', 'Вы уверены?'],
['ui_options', 'Параметры'],
['ui_changes_might_be_lost', 'Ваши изменения могут быть потеряны'],
@@ -75,6 +75,7 @@ export default [
['ui_reconnect', '重新连接'],
['ui_forget', '忘记'],
['ui_set', '设置'],
['ui_restore', 'Restore'],
['ui_are_you_sure', '您确定吗?'],
['ui_options', '选项'],
['ui_changes_might_be_lost', '您的更改可能会丢失'],
@@ -58,32 +58,32 @@ export abstract class CachedDataResource<
export function getCachedDataResourceLoaderState<TData, TKey = void, TContext extends Record<string, any> = Record<string, never>>(
resource: CachedDataResource<TData, TKey, TContext>,
param: TKey,
context?: CachedResourceIncludeArgs<TData, TContext>,
getKey: () => TKey,
getIncludes?: () => CachedResourceIncludeArgs<TData, TContext> | undefined,
lazy?: boolean,
): ILoadableState {
return {
lazy,
get exception() {
return resource.getException(param);
return resource.getException(getKey());
},
isLoading() {
return resource.isLoading(param);
return resource.isLoading(getKey());
},
isError() {
return isContainsException(this.exception);
},
isLoaded() {
return resource.isLoaded(param, context);
return resource.isLoaded(getKey(), getIncludes?.());
},
isOutdated() {
return resource.isOutdated(param, context);
return resource.isOutdated(getKey(), getIncludes?.());
},
async load() {
await resource.load(param, context);
await resource.load(getKey(), getIncludes?.());
},
async reload() {
await resource.refresh(param, context);
await resource.refresh(getKey(), getIncludes?.());
},
};
}
@@ -265,32 +265,68 @@ export abstract class CachedMapResource<
export function getCachedMapResourceLoaderState<TKey, TValue, TContext extends Record<string, any> = Record<string, never>>(
resource: CachedMapResource<TKey, TValue, TContext>,
getKey: () => ResourceKey<TKey>,
getKey: () => ResourceKey<TKey> | null,
getIncludes?: () => CachedResourceIncludeArgs<TValue, TContext> | undefined,
lazy?: boolean,
): ILoadableState {
return {
lazy,
get exception() {
return resource.getException(getKey());
const key = getKey();
if (key === null) {
return null;
}
return resource.getException(key);
},
isLoading() {
return resource.isLoading(getKey());
const key = getKey();
if (key === null) {
return false;
}
return resource.isLoading(key);
},
isLoaded() {
return resource.isLoaded(getKey(), getIncludes?.());
const key = getKey();
if (key === null) {
return true;
}
return resource.isLoaded(key, getIncludes?.());
},
isError() {
return isContainsException(this.exception);
},
isOutdated() {
return resource.isOutdated(getKey(), getIncludes?.());
const key = getKey();
if (key === null) {
return false;
}
return resource.isOutdated(key, getIncludes?.());
},
async load() {
await resource.load(getKey(), getIncludes?.());
const key = getKey();
if (key === null) {
return;
}
await resource.load(key, getIncludes?.());
},
async reload() {
await resource.refresh(getKey(), getIncludes?.());
const key = getKey();
if (key === null) {
return;
}
await resource.refresh(key, getIncludes?.());
},
};
}
@@ -655,10 +655,14 @@ export abstract class CachedResource<
this.dataUpdate(key);
}
},
error: exception => {
this.markOutdatedSync(key);
this.markError(exception, key, include);
},
// TODO: must rethink how to handle exceptions form performUpdate
// probably we need to handle this exceptions at the place
// where performUpdate is called
// because performUpdate is like an transaction
// error: exception => {
// this.markOutdatedSync(key);
// this.markError(exception, key, include);
// },
after: () => {
this.flushOutdatedWaitList();
},
@@ -1,7 +1,6 @@
mutation closeConnection(
$projectId: ID!
$connectionId: ID!
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -10,9 +9,10 @@ mutation closeConnection(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: closeConnection(projectId: $projectId, id: $connectionId) {
...DatabaseConnection
}
}
}
@@ -1,7 +1,6 @@
mutation createConnection(
$projectId: ID!
$config: ConnectionConfig!
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -10,6 +9,7 @@ mutation createConnection(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: createConnection(projectId: $projectId, config: $config) {
@@ -2,7 +2,6 @@ mutation createConnectionFromNode(
$projectId: ID!
$nodePath: String!
$config: ConnectionConfig
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -11,6 +10,7 @@ mutation createConnectionFromNode(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: copyConnectionFromNode(projectId: $projectId, nodePath: $nodePath, config: $config) {
@@ -2,7 +2,6 @@ mutation createConnectionFromTemplate(
$projectId: ID!
$templateId: ID!
$connectionName: String!
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -11,6 +10,7 @@ mutation createConnectionFromTemplate(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: createConnectionFromTemplate(projectId: $projectId, templateId: $templateId, connectionName: $connectionName) {
@@ -8,9 +8,10 @@ query getTemplateConnections(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connections: templateConnections(projectId: $projectId) {
...DatabaseConnection
}
}
}
@@ -10,9 +10,10 @@ query getUserConnections(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
...DatabaseConnection
}
}
}
@@ -2,10 +2,10 @@ mutation initConnection(
$projectId: ID!
$connectionId: ID!
$credentials: Object
$networkCredentials: [NetworkHandlerConfigInput!]
$networkCredentials: [NetworkHandlerConfigInput!]
$saveCredentials: Boolean
$sharedCredentials: Boolean
$selectedSecretId: String
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -14,6 +14,7 @@ mutation initConnection(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: initConnection(
@@ -21,8 +22,9 @@ mutation initConnection(
id: $connectionId
credentials: $credentials
networkCredentials: $networkCredentials
saveCredentials: $saveCredentials,
sharedCredentials: $sharedCredentials,
saveCredentials: $saveCredentials
sharedCredentials: $sharedCredentials
selectedSecretId: $selectedSecretId
) {
...DatabaseConnection
}
@@ -2,7 +2,6 @@ mutation setConnectionNavigatorSettings(
$projectId: ID!
$connectionId: ID!
$settings: NavigatorSettingsInput!
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -11,6 +10,7 @@ mutation setConnectionNavigatorSettings(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: setConnectionNavigatorSettings(projectId: $projectId, id: $connectionId, settings: $settings) {
@@ -1,7 +1,6 @@
mutation updateConnection(
$projectId: ID!
$config: ConnectionConfig!
$includeOrigin: Boolean!
$customIncludeOriginDetails: Boolean!
$includeAuthProperties: Boolean!
@@ -10,6 +9,7 @@ mutation updateConnection(
$includeAuthNeeded: Boolean!
$includeProperties: Boolean!
$includeProviderProperties: Boolean!
$includeSharedSecrets: Boolean!
$customIncludeOptions: Boolean!
) {
connection: updateConnection(projectId: $projectId, config: $config) {
@@ -37,6 +37,10 @@ fragment DatabaseConnection on ConnectionInfo {
...ObjectOriginInfo
}
sharedSecrets @include(if: $includeSharedSecrets) {
displayName
secretId
}
authNeeded @include(if: $includeAuthNeeded)
authModel
+3 -1
View File
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { Tab as BaseTab } from 'reakit/Tab';
import styled, { use } from 'reshadow';
@@ -27,6 +27,7 @@ import { useTab } from './useTab';
export const Tab = observer<TabProps>(function Tab({ tabId, title, menuContext, disabled, className, children, style, onOpen, onClose, onClick }) {
const translate = useTranslate();
const ref = useRef<HTMLButtonElement>(null);
const tabContext = useMemo(() => ({ tabId }), [tabId]);
const tab = useTab(tabId, onOpen, onClose, onClick);
const info = tab.getInfo();
@@ -46,6 +47,7 @@ export const Tab = observer<TabProps>(function Tab({ tabId, title, menuContext,
<TabMenu tabId={tabId} state={tab.state} menuContext={menuContext} style={style} />
</tab-actions>
<BaseTab
ref={ref}
{...tab.state.state}
type="button"
title={translate(title ?? info?.title)}
+19 -37
View File
@@ -13,7 +13,7 @@ import { useTabState } from 'reakit/Tab';
import { useAutoLoad, useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { useDataContext } from '@cloudbeaver/core-data-context';
import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor';
import { isDefined, isNull, isUndefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import { isDefined, isNotNullDefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer';
import { ITabsContext, type TabDirection, TabsContext } from './TabsContext';
@@ -65,12 +65,6 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
displayed = tabList;
}
if (!selectedId && (currentTabId === undefined || currentTabId === null) && autoSelect) {
if (displayed.length > 0) {
selectedId = displayed[0];
}
}
const closable = !!onClose;
const [localTabsState] = useState(() => new MetadataMap<string, any>());
@@ -86,7 +80,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
const dynamic = useObjectRef(
() => ({
selectedId: selectedId || currentTabId,
selectedId: state.selectedId,
}),
{
canClose,
@@ -100,30 +94,22 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
},
);
if ((!isNull(currentTabId) && !isUndefined(currentTabId)) || !autoSelect) {
if (isNotNullDefined(currentTabId)) {
state.selectedId = currentTabId;
dynamic.selectedId = currentTabId;
}
if (
displayed.length > 0 &&
!isNull(dynamic.selectedId) &&
!isUndefined(dynamic.selectedId) &&
!isNull(selectedId) &&
!isUndefined(selectedId) &&
autoSelect
) {
const tabExists = displayed.includes(dynamic.selectedId);
if (displayed.length > 0 && autoSelect) {
const tabExists = isNotNullDefined(state.selectedId) && displayed.includes(state.selectedId);
if (!tabExists) {
if (displayed.includes(selectedId)) {
state.selectedId = selectedId;
} else {
state.selectedId = displayed[0];
}
state.selectedId = displayed[0];
}
}
if (isNotNullDefined(currentTabId)) {
dynamic.selectedId = state.selectedId;
}
useExecutor({
executor: openExecutor,
handlers: [
@@ -134,7 +120,10 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
return;
}
dynamic.selectedId = data.tabId;
dynamic.state.setSelectedId(data.tabId);
if (dynamic.state.selectedId !== data.tabId) {
dynamic.state.setCurrentId(data.tabId);
dynamic.state.setSelectedId(data.tabId);
}
},
],
});
@@ -148,25 +137,18 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
],
});
const currentSelectedId = state.selectedId;
useEffect(() => {
if ((!isNull(currentTabId) && !isUndefined(currentTabId)) || !autoSelect) {
if (!isNotNullDefined(currentSelectedId) || dynamic.selectedId === currentSelectedId) {
return;
}
openExecutor.execute({
tabId: state.selectedId!,
tabId: currentSelectedId,
props,
});
}, [currentTabId, state.selectedId, autoSelect]);
useEffect(() => {
if (!isNull(state.selectedId) && !isUndefined(state.selectedId)) {
openExecutor.execute({
tabId: state.selectedId,
props,
});
}
}, [!isNull(state.selectedId) && !isUndefined(state.selectedId)]);
}, [currentSelectedId]);
const value = useObservableRef<ITabsContext<T>>(
() => ({
@@ -0,0 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 const EMPTY_ARRAY: ReadonlyArray<any> = Object.freeze([]);
export const EMPTY_OBJECT: Readonly<Record<any, any>> = Object.freeze({});
+1
View File
@@ -8,6 +8,7 @@ export * from './Quadtree/index';
export * from './underscore';
export * from './emptyConstants';
export * from './base64ToBlob';
export * from './blobToBase64';
export * from './base64ToHex';
@@ -42,8 +42,8 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
name="sessionExpireTime"
state={state.serverConfig}
min={1}
mapState={v => (v === 0 ? 60000 : v ?? 1800000) / 1000 / 60}
mapValue={v => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60}
mapState={(v: number | undefined) => String((v === 0 ? 60000 : v ?? 1800000) / 1000 / 60)}
mapValue={(v?: string) => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60}
required
tiny
>
@@ -143,8 +143,8 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
const loadableStateContext = this.formState.dataContext.get(DATA_CONTEXT_LOADABLE_STATE);
loadableStateContext.getState('user-info', () => [
getCachedDataResourceLoaderState(this.serverConfigResource, undefined),
getCachedDataResourceLoaderState(this.authRolesResource, undefined),
getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined),
getCachedDataResourceLoaderState(this.authRolesResource, () => undefined),
]);
}
@@ -6,6 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { AppAuthService } from '@cloudbeaver/core-authentication';
import { importLazyComponent } from '@cloudbeaver/core-blocks';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
@@ -14,10 +15,11 @@ import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core
import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections';
import { ACTION_CONNECTION_TEMPLATE } from './Actions/ACTION_CONNECTION_TEMPLATE';
import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog';
import { TemplateConnectionsResource } from './TemplateConnectionsResource';
import { TemplateConnectionsService } from './TemplateConnectionsService';
const ConnectionDialog = importLazyComponent(() => import('./ConnectionDialog/ConnectionDialog').then(m => m.ConnectionDialog));
@injectable()
export class TemplateConnectionPluginBootstrap extends Bootstrap {
constructor(
@@ -46,13 +48,15 @@ export class TemplateConnectionPluginBootstrap extends Bootstrap {
!this.appAuthService.authenticated ||
!this.projectsService.userProject?.canEditDataSources ||
!this.templateConnectionsService.projectTemplates.length,
getLoader: (context, action) => {
return [
...this.appAuthService.loaders,
getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey),
getCachedDataResourceLoaderState(this.templateConnectionsResource, undefined, undefined),
];
},
getLoader: () => [
...this.appAuthService.loaders,
getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey),
getCachedDataResourceLoaderState(
this.templateConnectionsResource,
() => undefined,
() => undefined,
),
],
handler: async (context, action) => {
switch (action) {
case ACTION_CONNECTION_TEMPLATE: {
@@ -68,6 +68,7 @@ export class TemplateConnectionsResource extends CachedDataResource<Connection[]
includeCredentialsSaved: false,
includeProperties: false,
includeProviderProperties: false,
includeSharedSecrets: false,
customIncludeOptions: false,
});
return connections;
@@ -12,7 +12,7 @@ import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams }
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects';
import { PermissionsService, EAdminPermission } from '@cloudbeaver/core-root';
import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root';
import { formStateContext } from '@cloudbeaver/core-ui';
import type { MetadataValueGetter } from '@cloudbeaver/core-utils';
import {
@@ -6,20 +6,21 @@
* you may not use this file except in compliance with the License.
*/
import { AuthProviderService } from '@cloudbeaver/core-authentication';
import { importLazyComponent } from '@cloudbeaver/core-blocks';
import {
Connection,
ConnectionInfoResource,
ConnectionsManagerService,
createConnectionParam,
IConnectionInfoParams,
IRequireConnectionExecutorData,
} from '@cloudbeaver/core-connections';
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { AuthenticationService } from '@cloudbeaver/plugin-authentication';
import { DatabaseAuthDialog } from './DatabaseAuthDialog/DatabaseAuthDialog';
const DatabaseAuthDialog = importLazyComponent(() => import('./DatabaseAuthDialog/DatabaseAuthDialog').then(m => m.DatabaseAuthDialog));
@injectable()
export class ConnectionAuthService extends Dependency {
@@ -28,7 +29,6 @@ export class ConnectionAuthService extends Dependency {
private readonly commonDialogService: CommonDialogService,
private readonly authProviderService: AuthProviderService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly notificationService: NotificationService,
private readonly authenticationService: AuthenticationService,
) {
super();
@@ -40,10 +40,10 @@ export class ConnectionAuthService extends Dependency {
}));
}
private async connectionDialog(connectionKey: IConnectionInfoParams, context: IExecutionContextProvider<IConnectionInfoParams | null>) {
private async connectionDialog(data: IRequireConnectionExecutorData, context: IExecutionContextProvider<IRequireConnectionExecutorData | null>) {
const connection = context.getContext(this.connectionsManagerService.connectionContext);
const newConnection = await this.auth(connectionKey);
const newConnection = await this.auth(data.key, data.resetCredentials);
if (!newConnection?.connected) {
return;
@@ -51,7 +51,7 @@ export class ConnectionAuthService extends Dependency {
connection.connection = newConnection;
}
async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
private async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
if (!this.connectionInfoResource.has(key)) {
return null;
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
export const ConnectionAuthenticationDialogLoader = importLazyComponent(() =>
import('./ConnectionAuthenticationDialog').then(module => module.ConnectionAuthenticationDialog),
);
@@ -1,13 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 React from 'react';
export const ConnectionAuthenticationDialogLoader = React.lazy(async () => {
const { ConnectionAuthenticationDialog } = await import('./ConnectionAuthenticationDialog');
return { default: ConnectionAuthenticationDialog };
});
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
export const ConnectionAuthenticationFormLoader = importLazyComponent(() =>
import('./ConnectionAuthenticationForm').then(m => m.ConnectionAuthenticationForm),
);
@@ -1,13 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 React from 'react';
export const ConnectionAuthenticationFormLoader = React.lazy(async () => {
const { ConnectionAuthenticationForm } = await import('./ConnectionAuthenticationForm');
return { default: ConnectionAuthenticationForm };
});
@@ -0,0 +1,44 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { Container, ObjectPropertyInfoForm } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { isSafari } from '@cloudbeaver/core-utils';
interface Props {
credentials?: Record<string, any>;
defaultCredentials?: Record<string, any>;
properties: ReadonlyArray<ObjectPropertyInfo>;
readonly?: boolean;
disabled?: boolean;
}
export const ConnectionAuthModelCredentialsForm = observer<Props>(function ConnectionAuthModelCredentialsForm({
credentials,
defaultCredentials,
properties,
readonly,
disabled,
}) {
return (
<Container wrap gap hideEmpty>
<ObjectPropertyInfoForm
autofillToken={isSafari ? 'section-connection-authentication section-secrets' : 'new-password'}
properties={properties}
state={credentials}
defaultState={defaultCredentials}
disabled={disabled}
readOnly={readonly}
showRememberTip
hideEmptyPlaceholder
tiny
/>
</Container>
);
});
@@ -0,0 +1,63 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { Combobox, usePermission, useResource } from '@cloudbeaver/core-blocks';
import { DatabaseAuthModelsResource } from '@cloudbeaver/core-connections';
import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource';
import { EAdminPermission } from '@cloudbeaver/core-root';
import { isNotNullDefined } from '@cloudbeaver/core-utils';
interface Props {
authModelCredentialsState: { authModelId?: string };
applicableAuthModels: string[];
readonlyAuthModelId?: boolean;
readonly?: boolean;
disabled?: boolean;
onAuthModelChange?: (authModelId: string | undefined) => void;
}
export const ConnectionAuthModelSelector = observer<Props>(function ConnectionAuthModelSelector({
authModelCredentialsState,
applicableAuthModels,
onAuthModelChange,
readonlyAuthModelId,
readonly,
disabled,
}) {
const adminPermission = usePermission(EAdminPermission.admin);
const authModelsLoader = useResource(
ConnectionAuthModelSelector,
DatabaseAuthModelsResource,
applicableAuthModels.length ? resourceKeyList(applicableAuthModels) : CachedResourceListEmptyKey,
);
const availableAuthModels = authModelsLoader.data.filter(isNotNullDefined).filter(model => adminPermission || !model.requiresLocalConfiguration);
if (availableAuthModels.length <= 1) {
return null;
}
return (
<Combobox
name="authModelId"
state={authModelCredentialsState}
items={availableAuthModels}
keySelector={model => model.id}
valueSelector={model => model.displayName}
titleSelector={model => model.description}
searchable={availableAuthModels.length > 10}
readOnly={readonly || readonlyAuthModelId}
disabled={disabled}
tiny
fill
onSelect={onAuthModelChange}
/>
);
});
@@ -15,11 +15,11 @@ import { ENotificationType } from '@cloudbeaver/core-events';
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
import { ConnectionFormActionsContext, IConnectionFormActionsContext } from './ConnectFormActionsContext';
import connectionFormStyles from './ConnectionForm.m.css';
import { ConnectionFormService } from './ConnectionFormService';
import { connectionConfigContext } from './Contexts/connectionConfigContext';
import type { IConnectionFormState } from './IConnectionFormProps';
import connectionFormStyles from './ConnectionForm.m.css';
import { ConnectionFormActionsContext, IConnectionFormActionsContext } from './ConnectFormActionsContext';
const tabsStyles = css`
TabList {
@@ -108,7 +108,7 @@ export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionF
const style = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES];
const styles = useStyles(style, topBarStyles, formStyles);
const service = useService(ConnectionFormService);
const form = useForm({
onSubmit: event => {
if (event?.type === 'test') {
@@ -11,6 +11,7 @@ import { useContext } from 'react';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks';
import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
import { ConnectionFormActionsContext } from './ConnectFormActionsContext';
@@ -30,6 +31,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, state.config.driverId || null);
const driver = driverMap.data;
const serverConfigResource = useResource(ConnectionFormBaseActions, ServerConfigResource, undefined);
const { data: authModel } = useResource(
ConnectionFormBaseActions,
DatabaseAuthModelsResource,
@@ -40,6 +42,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
});
const authorized = authentication.providerId === AUTH_PROVIDER_LOCAL_ID || authentication.authorized;
const disableTest = serverConfigResource.data?.distributed && !!state.config.sharedCredentials;
return (
<>
@@ -48,9 +51,11 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
{translate('ui_processing_cancel')}
</Button>
)}
<Button type="button" disabled={state.disabled || !authorized} mod={['outlined']} loader onClick={actions.test}>
{translate('connections_connection_test')}
</Button>
{!disableTest && (
<Button type="button" disabled={state.disabled || !authorized} mod={['outlined']} loader onClick={actions.test}>
{translate('connections_connection_test')}
</Button>
)}
<Button type="button" disabled={state.disabled || state.readonly} mod={['unelevated']} loader onClick={actions.save}>
{translate(state.mode === 'edit' ? 'ui_processing_save' : 'ui_processing_create')}
</Button>
@@ -316,8 +316,8 @@ export class ConnectionOptionsTabService extends Bootstrap {
if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) {
tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel;
tempConfig.saveCredentials = state.config.saveCredentials;
tempConfig.sharedCredentials = state.config.sharedCredentials;
tempConfig.saveCredentials = state.config.saveCredentials || tempConfig.sharedCredentials;
const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info);
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useRef } from 'react';
import { useContext, useRef } from 'react';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import {
@@ -20,29 +20,29 @@ import {
Group,
GroupTitle,
InputField,
ObjectPropertyInfoForm,
Link,
Radio,
RadioGroup,
s,
Textarea,
useAdministrationSettings,
useFormValidator,
usePermission,
useResource,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource';
import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { DriverConfigurationType } from '@cloudbeaver/core-sdk';
import { type TabContainerPanelComponent, useAuthenticationAction } from '@cloudbeaver/core-ui';
import { isSafari } from '@cloudbeaver/core-utils';
import { type TabContainerPanelComponent, TabsContext, useAuthenticationAction } from '@cloudbeaver/core-ui';
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm';
import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector';
import { ConnectionFormService } from '../ConnectionFormService';
import type { IConnectionFormProps } from '../IConnectionFormProps';
import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID';
import { ConnectionOptionsTabService } from './ConnectionOptionsTabService';
import styles from './Options.m.css';
import { ParametersForm } from './ParametersForm';
@@ -77,26 +77,15 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
const translate = useTranslate();
const { info, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state;
const style = useS(styles);
const tabsState = useContext(TabsContext);
//@TODO it's here until the profile implementation in the CloudBeaver
const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID;
const adminPermission = usePermission(EAdminPermission.admin);
useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current);
const optionsHook = useOptions(state);
const { credentialsSavingEnabled } = useAdministrationSettings();
const handleAuthModelSelect = useCallback(async (value?: string, name?: string, prev?: string) => {
const model = applicableAuthModels.find(model => model?.id === value);
if (!model) {
return;
}
optionsHook.setAuthModel(model);
}, []);
const driverMap = useResource(
Options,
DBDriverResource,
@@ -119,25 +108,11 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
if (config.template) {
config.folder = undefined;
}
if (name === 'sharedCredentials' && value) {
config.saveCredentials = true;
for (const handler of config.networkHandlersConfig ?? []) {
if (!handler.savePassword) {
handler.savePassword = true;
}
}
}
}
const { data: applicableAuthModels } = useResource(
Options,
DatabaseAuthModelsResource,
driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey,
);
const applicableAuthModels = driver?.applicableAuthModels ?? [];
const { data: authModel } = useResource(
const authModelLoader = useResource(
Options,
DatabaseAuthModelsResource,
getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null),
@@ -146,6 +121,22 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
},
);
const authModel = authModelLoader.data;
async function handleAuthModelSelect(id: string | undefined) {
if (!id) {
return;
}
const model = await authModelLoader.resource.load(id);
if (!model) {
return;
}
optionsHook.setAuthModel(model);
}
const authentication = useAuthenticationAction({
providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
});
@@ -154,7 +145,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
const edit = state.mode === 'edit';
const originLocal = !info || isLocalConnection(info);
const availableAuthModels = applicableAuthModels.filter(model => !!model && (adminPermission || !model.requiresLocalConfiguration));
const drivers = driverMap.resource.enabledDrivers.filter(({ id }) => availableDrivers.includes(id));
let properties = authModel?.properties;
@@ -163,6 +153,13 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
properties = info.authProperties;
}
const sharedCredentials = config.sharedCredentials && serverConfigResource.data?.distributed;
function openCredentialsTab(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID);
}
return (
<Form ref={formRef} className={s(style, { form: true })} disabled={driverMap.isLoading()} onChange={handleFormChange}>
<ColoredContainer wrap overflow parent gap>
@@ -285,63 +282,56 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
{!driver?.anonymousAccess && (authentication.authorized || !edit) && (
<Group form gap>
<GroupTitle>{translate('connections_connection_edit_authentication')}</GroupTitle>
{availableAuthModels.length > 1 && (
<Combobox
name="authModelId"
{serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && (
<FieldCheckbox
id={config.connectionId + 'isShared'}
name="sharedCredentials"
title={translate('connections_connection_share_credentials_tooltip')}
state={config}
items={availableAuthModels}
keySelector={model => model!.id}
valueSelector={model => model!.displayName}
titleSelector={model => model?.description}
searchable={availableAuthModels.length > 10}
readOnly={readonly || !originLocal}
disabled={disabled}
tiny
fill
onSelect={handleAuthModelSelect}
/>
disabled={disabled || readonly}
keepSize
>
{translate('connections_connection_share_credentials')}
</FieldCheckbox>
)}
{authModel && properties && (
<ConnectionAuthModelSelector
authModelCredentialsState={config}
applicableAuthModels={applicableAuthModels}
readonlyAuthModelId={!originLocal}
readonly={readonly}
disabled={disabled}
onAuthModelChange={handleAuthModelSelect}
/>
{!sharedCredentials ? (
<>
<Container wrap gap hideEmpty>
<ObjectPropertyInfoForm
autofillToken={isSafari ? 'section-connection-authentication section-options' : 'new-password'}
{properties && (
<ConnectionAuthModelCredentialsForm
credentials={config.credentials}
properties={properties}
state={config.credentials}
readonly={readonly}
disabled={disabled}
readOnly={readonly}
showRememberTip
hideEmptyPlaceholder
tiny
/>
</Container>
{credentialsSavingEnabled && !config.template && (
<Container wrap gap>
<FieldCheckbox
id={config.connectionId + 'authNeeded'}
name="saveCredentials"
state={config}
disabled={disabled || readonly || config.sharedCredentials}
mod={['primary']}
keepSize
>
{translate('connections_connection_edit_save_credentials')}
</FieldCheckbox>
{serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && (
<FieldCheckbox
id={config.connectionId + 'isShared'}
name="sharedCredentials"
title={translate('connections_connection_share_credentials_tooltip')}
state={config}
disabled={disabled || readonly}
keepSize
>
{translate('connections_connection_share_credentials')}
</FieldCheckbox>
)}
</Container>
)}
</>
) : (
<FormFieldDescription>
{translate('plugin_connections_connection_form_shared_credentials_manage_info')}
<Link inline onClick={openCredentialsTab}>
{translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')}
</Link>
</FormFieldDescription>
)}
{!sharedCredentials && authModel && credentialsSavingEnabled && !config.template && (
<FieldCheckbox
id={config.connectionId + 'authNeeded'}
name="saveCredentials"
state={config}
disabled={disabled || readonly || config.sharedCredentials}
mod={['primary']}
keepSize
>
{translate('connections_connection_edit_save_credentials')}
</FieldCheckbox>
)}
</Group>
)}
@@ -176,6 +176,7 @@ export class ConnectionSSHTabService extends Bootstrap {
if (this.isChanged(handler, initial) || passwordChanged || keyChanged) {
handlerConfig = {
...handler,
savePassword: handler.savePassword || config.sharedCredentials,
key: handler.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? handler.key : undefined,
password: passwordChanged ? handler.password : undefined,
};
@@ -147,13 +147,8 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
</InputField>
{keyAuth && <SSHKeyUploader state={handlerState} saved={keySaved} disabled={disabled || !enabled} readonly={readonly} />}
</Container>
{credentialsSavingEnabled && !formState.config.template && (
<FieldCheckbox
id={SSH_TUNNEL_ID + '_savePassword'}
name="savePassword"
state={handlerState}
disabled={disabled || !enabled || readonly || formState.config.sharedCredentials}
>
{credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && (
<FieldCheckbox id={SSH_TUNNEL_ID + '_savePassword'} name="savePassword" state={handlerState} disabled={disabled || !enabled || readonly}>
{translate('connections_connection_edit_save_credentials')}
</FieldCheckbox>
)}
@@ -136,6 +136,7 @@ export class ConnectionSSLTabService extends Bootstrap {
const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id);
const handlerConfig: NetworkHandlerConfigInput = toJS(handler);
handlerConfig.savePassword = handler.savePassword || config.sharedCredentials;
const changed = this.isChanged(handlerConfig, initial);
@@ -102,7 +102,7 @@ export const SSL: TabContainerPanelComponent<Props> = observer(function SSL({ st
</React.Fragment>
))}
{credentialsSavingEnabled && !formState.config.template && (
{credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && (
<FieldCheckbox
id={handler.id + '_savePassword'}
name="savePassword"
@@ -6,7 +6,4 @@
* you may not use this file except in compliance with the License.
*/
export interface IConnectionInfoParams {
projectId: string;
connectionId: string;
}
export const CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID = 'shared_credentials';
@@ -28,7 +28,6 @@ import {
import { ACTION_DELETE, ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view';
import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar';
import { ConnectionAuthService } from '../ConnectionAuthService';
import { PluginConnectionsSettingsService } from '../PluginConnectionsSettingsService';
import { PublicConnectionFormService } from '../PublicConnectionForm/PublicConnectionFormService';
import { ACTION_CONNECTION_CHANGE_CREDENTIALS } from './Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS';
@@ -54,7 +53,6 @@ export class ConnectionMenuBootstrap extends Bootstrap {
private readonly connectionsSettingsService: ConnectionsSettingsService,
private readonly pluginConnectionsSettingsService: PluginConnectionsSettingsService,
private readonly permissionsService: PermissionsService,
private readonly connectionAuthService: ConnectionAuthService,
private readonly serverConfigResource: ServerConfigResource,
) {
super();
@@ -192,7 +190,7 @@ export class ConnectionMenuBootstrap extends Bootstrap {
}
if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) {
return this.serverConfigResource.distributed;
return this.serverConfigResource.distributed && !connection.sharedCredentials;
}
return false;
@@ -245,7 +243,7 @@ export class ConnectionMenuBootstrap extends Bootstrap {
break;
}
case ACTION_CONNECTION_CHANGE_CREDENTIALS: {
await this.connectionAuthService.auth({ connectionId: connection.id, projectId: connection.projectId }, true);
await this.connectionsManagerService.requireConnection({ connectionId: connection.id, projectId: connection.projectId }, true);
break;
}
}
@@ -7,29 +7,12 @@
*/
import { observer } from 'mobx-react-lite';
import {
CommonDialogBody,
CommonDialogFooter,
CommonDialogHeader,
CommonDialogWrapper,
ErrorMessage,
Form,
Loader,
s,
useAdministrationSettings,
useAutoLoad,
useErrorDetails,
useFocus,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import type { IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { CommonDialogHeader, CommonDialogWrapper, useResource, useTranslate } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, DBDriverResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
import type { DialogComponent } from '@cloudbeaver/core-dialogs';
import { ConnectionAuthenticationFormLoader } from '../ConnectionAuthentication/ConnectionAuthenticationFormLoader';
import style from './DatabaseAuthDialog.m.css';
import { DBAuthDialogFooter } from './DBAuthDialogFooter';
import { useDatabaseAuthDialog } from './useDatabaseAuthDialog';
import { DatabaseCredentialsAuthDialog } from './DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog';
import { DatabaseSecretAuthDialog } from './DatabaseSecretAuthDialog/DatabaseSecretAuthDialog';
interface Payload {
connection: IConnectionInfoParams;
@@ -38,53 +21,38 @@ interface Payload {
}
export const DatabaseAuthDialog: DialogComponent<Payload> = observer(function DatabaseAuthDialog({ payload, options, rejectDialog, resolveDialog }) {
const styles = useS(style);
const connectionInfoLoader = useResource(DatabaseAuthDialog, ConnectionInfoResource, {
key: payload.connection,
includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'],
});
const translate = useTranslate();
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const driverLoader = useResource(DatabaseAuthDialog, DBDriverResource, connectionInfoLoader.data?.driverId || null);
const useSharedCredentials = (connectionInfoLoader.data?.sharedSecrets?.length || 0) > 1;
const { credentialsSavingEnabled } = useAdministrationSettings();
const dialog = useDatabaseAuthDialog(payload.connection, payload.networkHandlers, payload.resetCredentials, resolveDialog);
const errorDetails = useErrorDetails(dialog.authException);
let subtitle = connectionInfoLoader.data?.name;
useAutoLoad(DatabaseAuthDialog, dialog);
if (useSharedCredentials) {
subtitle = [subtitle, translate('plugin_connections_connection_auth_secret_description')].join(' | ');
}
return (
<CommonDialogWrapper size="large">
<CommonDialogWrapper size="large" fixedSize={useSharedCredentials}>
<CommonDialogHeader
title="connections_database_authentication"
subTitle={dialog.connection?.name}
icon={dialog.driver?.icon}
subTitle={subtitle}
icon={driverLoader.data?.icon}
onReject={options?.persistent ? undefined : rejectDialog}
/>
<CommonDialogBody>
<Form ref={focusedRef} className={s(styles, { submittingForm: true })} onSubmit={dialog.login}>
<Loader state={dialog} inlineException>
<ConnectionAuthenticationFormLoader
config={dialog.config}
authModelId={dialog.authModelId}
authProperties={dialog.connection?.authProperties}
networkHandlers={payload.networkHandlers}
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
className={s(styles, { connectionAuthenticationFormLoader: true })}
disabled={dialog.authenticating}
hideFeatures={['nonSecuredProperty']}
/>
</Loader>
</Form>
</CommonDialogBody>
<CommonDialogFooter>
<DBAuthDialogFooter isAuthenticating={dialog.authenticating} onLogin={dialog.login}>
{dialog.authException && (
<ErrorMessage
text={errorDetails.message ?? translate('core_blocks_exception_message_error_message')}
hasDetails={errorDetails.hasDetails}
className={style.errorMessage}
onShowDetails={errorDetails.open}
/>
)}
</DBAuthDialogFooter>
</CommonDialogFooter>
{useSharedCredentials ? (
<DatabaseSecretAuthDialog connectionKey={payload.connection} onLogin={resolveDialog} />
) : (
<DatabaseCredentialsAuthDialog
connection={payload.connection}
networkHandlers={payload.networkHandlers}
resetCredentials={payload.resetCredentials}
onLogin={resolveDialog}
/>
)}
</CommonDialogWrapper>
);
});
@@ -0,0 +1,87 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 {
CommonDialogBody,
CommonDialogFooter,
ErrorMessage,
Form,
Loader,
s,
useAdministrationSettings,
useAutoLoad,
useErrorDetails,
useFocus,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import type { IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { ConnectionAuthenticationFormLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationFormLoader';
import style from './DatabaseCredentialsAuthDialog.m.css';
import { DatabaseCredentialsAuthDialogFooter } from './DatabaseCredentialsAuthDialogFooter';
import { useDatabaseCredentialsAuthDialog } from './useDatabaseCredentialsAuthDialog';
interface Props {
connection: IConnectionInfoParams;
networkHandlers: string[];
resetCredentials?: boolean;
onLogin?: () => void;
}
export const DatabaseCredentialsAuthDialog = observer<Props>(function DatabaseCredentialsAuthDialog({
connection,
networkHandlers,
resetCredentials,
onLogin,
}) {
const styles = useS(style);
const translate = useTranslate();
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const { credentialsSavingEnabled } = useAdministrationSettings();
const dialog = useDatabaseCredentialsAuthDialog(connection, networkHandlers, resetCredentials, onLogin);
const errorDetails = useErrorDetails(dialog.authException);
useAutoLoad(DatabaseCredentialsAuthDialog, dialog);
return (
<>
<CommonDialogBody>
<Form ref={focusedRef} className={s(styles, { submittingForm: true })} onSubmit={dialog.login}>
<Loader state={dialog} inlineException>
<ConnectionAuthenticationFormLoader
config={dialog.config}
authModelId={dialog.authModelId}
authProperties={dialog.connection?.authProperties}
networkHandlers={networkHandlers}
formId={`${connection.projectId}:${connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
className={s(styles, { connectionAuthenticationFormLoader: true })}
disabled={dialog.authenticating}
hideFeatures={['nonSecuredProperty']}
/>
</Loader>
</Form>
</CommonDialogBody>
<CommonDialogFooter>
<DatabaseCredentialsAuthDialogFooter isAuthenticating={dialog.authenticating} onLogin={dialog.login}>
{dialog.authException && (
<ErrorMessage
text={errorDetails.message ?? translate('core_blocks_exception_message_error_message')}
hasDetails={errorDetails.hasDetails}
className={style.errorMessage}
onShowDetails={errorDetails.open}
/>
)}
</DatabaseCredentialsAuthDialogFooter>
</CommonDialogFooter>
</>
);
});
@@ -32,7 +32,7 @@ export interface Props {
className?: string;
}
export const DBAuthDialogFooter = observer<React.PropsWithChildren<Props>>(function DBAuthDialogFooter({
export const DatabaseCredentialsAuthDialogFooter = observer<React.PropsWithChildren<Props>>(function DatabaseCredentialsAuthDialogFooter({
isAuthenticating,
onLogin,
className,
@@ -21,7 +21,7 @@ import { useService } from '@cloudbeaver/core-di';
import { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk';
import type { ILoadableState } from '@cloudbeaver/core-utils';
import type { IConnectionAuthenticationConfig } from '../ConnectionAuthentication/IConnectionAuthenticationConfig';
import type { IConnectionAuthenticationConfig } from '../../ConnectionAuthentication/IConnectionAuthenticationConfig';
interface IState extends ILoadableState {
readonly authModelId: string | null;
@@ -38,7 +38,12 @@ interface IState extends ILoadableState {
getConfig: () => ConnectionInitConfig;
}
export function useDatabaseAuthDialog(key: IConnectionInfoParams, networkHandlers: string[], resetCredentials?: boolean, onInit?: () => void) {
export function useDatabaseCredentialsAuthDialog(
key: IConnectionInfoParams,
networkHandlers: string[],
resetCredentials?: boolean,
onInit?: () => void,
) {
const connectionInfoResource = useService(ConnectionInfoResource);
const dbDriverResource = useService(DBDriverResource);
@@ -0,0 +1,88 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import {
CommonDialogBody,
CommonDialogFooter,
ExceptionMessage,
Group,
ItemList,
ListItem,
ListItemName,
Loader,
useObservableRef,
useResource,
} from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
interface Props {
connectionKey: IConnectionInfoParams;
onLogin?: () => void;
}
export const DatabaseSecretAuthDialog = observer<Props>(function DatabaseSecretAuthDialog({ connectionKey, onLogin }) {
const connectionInfoLoader = useResource(DatabaseSecretAuthDialog, ConnectionInfoResource, {
key: connectionKey,
includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'],
});
const state = useObservableRef(
() => ({
exception: null as Error | null,
authenticating: false,
}),
{
exception: observable.ref,
authenticating: observable.ref,
},
false,
);
const secrets = connectionInfoLoader.data?.sharedSecrets || [];
async function handleSecretSelect(secretId: string) {
try {
state.authenticating = true;
await connectionInfoLoader.resource.init({
...connectionKey,
selectedSecretId: secretId,
});
state.exception = null;
onLogin?.();
} catch (exception: any) {
state.exception = exception;
} finally {
state.authenticating = false;
}
}
if (state.authenticating) {
return <Loader loader />;
}
return (
<>
<CommonDialogBody noBodyPadding>
<ItemList>
{secrets.map(secret => (
<ListItem key={secret.secretId} onClick={() => handleSecretSelect(secret.secretId)}>
<ListItemName>{secret.displayName}</ListItemName>
</ListItem>
))}
</ItemList>
</CommonDialogBody>
{state.exception && (
<CommonDialogFooter>
<Group secondary vertical dense>
<ExceptionMessage exception={state.exception} inline />
</Group>
</CommonDialogFooter>
)}
</>
);
});
@@ -8,8 +8,8 @@
import { action, makeObservable, observable } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { ConfirmationDialog } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -20,11 +20,11 @@ import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { AuthenticationService } from '@cloudbeaver/plugin-authentication';
import { ConnectionAuthService } from '../ConnectionAuthService';
import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService';
import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState';
import type { IConnectionFormState } from '../ConnectionForm/IConnectionFormProps';
import { PublicConnectionForm } from './PublicConnectionForm';
const PublicConnectionForm = importLazyComponent(() => import('./PublicConnectionForm').then(m => m.PublicConnectionForm));
const formGetter = () => PublicConnectionForm;
@@ -38,7 +38,7 @@ export class PublicConnectionFormService {
private readonly optionsPanelService: OptionsPanelService,
private readonly connectionFormService: ConnectionFormService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly connectionAuthService: ConnectionAuthService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly userInfoResource: UserInfoResource,
private readonly authenticationService: AuthenticationService,
private readonly projectsService: ProjectsService,
@@ -203,7 +203,7 @@ export class PublicConnectionFormService {
try {
await this.connectionInfoResource.close(connectionKey);
await this.connectionAuthService.auth(connectionKey);
await this.connectionsManagerService.requireConnection(connectionKey);
} catch (exception: any) {
this.notificationService.logException(exception, 'connections_public_connection_edit_reconnect_failed');
}

Some files were not shown because too many files have changed in this diff Show More