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

This commit is contained in:
serge-rider
2021-02-04 21:21:21 +03:00
41 changed files with 496 additions and 62 deletions
+4 -1
View File
@@ -36,7 +36,10 @@
anonymousAccessEnabled: true,
authenticationEnabled: true,
anonymousUserRole: "user",
supportsCustomConnections: false
supportsCustomConnections: false,
publicCredentialsSaveEnabled: true,
adminCredentialsSaveEnabled: true
/*
enabledDrivers: [
@@ -24,7 +24,8 @@ Require-Bundle: org.eclipse.core.runtime;visibility:=reexport,
org.eclipse.jetty.util;visibility:=reexport,
com.google.gson;visibility:=reexport,
org.jkiss.bundle.graphql.java;visibility:=reexport,
org.jkiss.bundle.apache.dbcp
org.jkiss.bundle.apache.dbcp,
org.jkiss.dbeaver.net.ssh
Export-Package: io.cloudbeaver,
io.cloudbeaver.auth.provider,
io.cloudbeaver.auth.provider.local,
@@ -84,6 +84,10 @@ type ServerConfig {
supportsCustomConnections: Boolean
supportsConnectionBrowser: Boolean
supportsWorkspaces: Boolean
publicCredentialsSaveEnabled: Boolean
adminCredentialsSaveEnabled: Boolean
sessionExpireTime: Int
localHostAddress: String
@@ -251,6 +255,12 @@ type ConnectionInfo {
supportedDataFormats: [ ResultDataFormat! ]!
}
type NetworkEndpointInfo {
message: String
clientVersion: String
serverVersion: String
}
type ObjectOrigin {
type: ID!
subType: ID
@@ -409,6 +419,9 @@ extend type Mutation {
# Test connection configuration. Returns remote server version
testConnection( config: ConnectionConfig! ): ConnectionInfo!
# Test connection configuration. Returns remote server version
testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo!
# Initiate existing connection
initConnection( id: ID!, credentials: Object, saveCredentials: Boolean ): ConnectionInfo!
@@ -0,0 +1,57 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2020 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;
/**
* WebNetworkEndpointInfo
*/
public class WebNetworkEndpointInfo {
private String message;
private String clientVersion;
private String serverVersion;
public WebNetworkEndpointInfo(String message) {
this.message = message;
}
public WebNetworkEndpointInfo(String message, String clientVersion, String serverVersion) {
this.message = message;
this.clientVersion = clientVersion;
this.serverVersion = serverVersion;
}
public String getMessage() {
return message;
}
public String getClientVersion() {
return clientVersion;
}
public void setClientVersion(String clientVersion) {
this.clientVersion = clientVersion;
}
public String getServerVersion() {
return serverVersion;
}
public void setServerVersion(String serverVersion) {
this.serverVersion = serverVersion;
}
}
@@ -77,6 +77,16 @@ public class WebServerConfig {
return application.getAppConfiguration().isSupportsUserWorkspaces();
}
@Property
public boolean isPublicCredentialsSaveEnabled() {
return application.getAppConfiguration().isPublicCredentialsSaveEnabled();
}
@Property
public boolean isAdminCredentialsSaveEnabled() {
return application.getAppConfiguration().isAdminCredentialsSaveEnabled();
}
@Property
public boolean isConfigurationMode() {
return application.isConfigurationMode();
@@ -33,6 +33,8 @@ public class CBAppConfig {
private boolean supportsCustomConnections = true;
private boolean supportsConnectionBrowser = false;
private boolean supportsUserWorkspaces = false;
private boolean publicCredentialsSaveEnabled = true;
private boolean adminCredentialsSaveEnabled = true;
private String[] enabledDrivers = new String[0];
private String[] disabledDrivers = new String[0];
private String defaultAuthProvider = LocalAuthProvider.PROVIDER_ID;
@@ -78,6 +80,22 @@ public class CBAppConfig {
return supportsUserWorkspaces;
}
public boolean isPublicCredentialsSaveEnabled() {
return publicCredentialsSaveEnabled;
}
public void setPublicCredentialsSaveEnabled(boolean publicCredentialsSaveEnabled) {
this.publicCredentialsSaveEnabled = publicCredentialsSaveEnabled;
}
public boolean isAdminCredentialsSaveEnabled() {
return adminCredentialsSaveEnabled;
}
public void setAdminCredentialsSaveEnabled(boolean adminCredentialsSaveEnabled) {
this.adminCredentialsSaveEnabled = adminCredentialsSaveEnabled;
}
public String[] getEnabledDrivers() {
return enabledDrivers;
}
@@ -573,6 +573,8 @@ public class CBApplication extends BaseApplicationImpl {
JSONUtils.field(json, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled());
JSONUtils.field(json, "authenticationEnabled", appConfig.isAuthenticationEnabled());
JSONUtils.field(json, "supportsCustomConnections", appConfig.isSupportsCustomConnections());
JSONUtils.field(json, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled());
JSONUtils.field(json, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled());
if (!CommonUtils.isEmpty(appConfig.getPlugins())) {
JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins());
@@ -104,6 +104,9 @@ public interface DBWServiceCore extends DBWService {
@WebAction
WebConnectionInfo testConnection(WebSession webSession, WebConnectionConfig connectionConfig) throws DBWebException;
@WebAction
WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException;
@WebAction
WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException;
@@ -22,6 +22,7 @@ import graphql.schema.idl.TypeRuntimeWiring;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.model.WebConnectionConfig;
import io.cloudbeaver.model.WebNetworkHandlerConfigInput;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.session.WebSessionManager;
import io.cloudbeaver.server.CBPlatform;
@@ -89,6 +90,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
new WebConnectionConfig(env.getArgument("config"))))
.dataFetcher("initConnection", env -> getService(env).initConnection(getWebSession(env), env.getArgument("id"), env.getArgument("credentials"), env.getArgument("saveCredentials")))
.dataFetcher("testConnection", env -> getService(env).testConnection(getWebSession(env), getConnectionConfig(env)))
.dataFetcher("testNetworkHandler", env -> getService(env).testNetworkHandler(getWebSession(env), new WebNetworkHandlerConfigInput(env.getArgument("config"))))
.dataFetcher("closeConnection", env -> getService(env).closeConnection(getWebSession(env), env.getArgument("id")))
.dataFetcher("deleteConnection", env -> getService(env).deleteConnection(getWebSession(env), env.getArgument("id")))
@@ -38,9 +38,16 @@ import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.model.navigator.DBNDataSource;
import org.jkiss.dbeaver.model.navigator.DBNModel;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
import org.jkiss.dbeaver.model.net.DBWNetworkHandler;
import org.jkiss.dbeaver.model.net.DBWTunnel;
import org.jkiss.dbeaver.model.net.ssh.SSHImplementation;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
@@ -366,6 +373,59 @@ public class WebServiceCore implements DBWServiceCore {
}
}
@Override
public WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException {
DBRProgressMonitor monitor = webSession.getProgressMonitor();
monitor.beginTask("Instantiate SSH tunnel", 2);
NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance().getDescriptor(nhConfig.getId());
if (handlerDescriptor == null) {
throw new DBWebException("Network handler '" + nhConfig.getId() + "' not found");
}
try {
DBWNetworkHandler handler = handlerDescriptor.createHandler(DBWNetworkHandler.class);
if (handler instanceof DBWTunnel) {
DBWTunnel tunnel = (DBWTunnel)handler;
DBPConnectionConfiguration connectionConfig = new DBPConnectionConfiguration();
connectionConfig.setHostName("localhost");
connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties().get(DBWHandlerConfiguration.PROP_PORT)));
try {
monitor.subTask("Initialize tunnel");
DBWHandlerConfiguration configuration = new DBWHandlerConfiguration(handlerDescriptor, null);
configuration.setUserName(nhConfig.getUserName());
configuration.setPassword(nhConfig.getPassword());
configuration.setSavePassword(true);
configuration.setEnabled(true);
configuration.setProperties(nhConfig.getProperties());
tunnel.initializeHandler(monitor, DBWorkbench.getPlatform(), configuration, connectionConfig);
monitor.worked(1);
// Get info
Object implementation = tunnel.getImplementation();
if (implementation instanceof SSHImplementation) {
return new WebNetworkEndpointInfo(
"Connected",
((SSHImplementation) implementation).getClientVersion(),
((SSHImplementation) implementation).getServerVersion());
} else {
return new WebNetworkEndpointInfo("Connected");
}
} finally {
monitor.subTask("Close tunnel");
tunnel.closeTunnel(monitor);
monitor.worked(1);
}
} else {
return new WebNetworkEndpointInfo(nhConfig.getId() + " is not a tunnel");
}
} catch (Exception e) {
throw new DBWebException("Error testing network handler endpoint", e);
} finally {
// Close it
monitor.done();
}
}
@Override
public WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException {
return closeAndDeleteConnection(webSession, connectionId, false);
@@ -130,7 +130,13 @@ public class WebServiceNavigator implements DBWServiceNavigator {
if (node == null) {
throw new DBWebException("Navigator node '" + nodePath + "' not found");
}
node.refreshNode(monitor, this);
if (node instanceof DBNDataSource) {
// Do not refresh entire tree - just clear child nodes
// Otherwise refresh may fail if navigator settings were changed.
((DBNDataSource) node).cleanupNode();
} else {
node.refreshNode(monitor, this);
}
return true;
} catch (DBException e) {
throw new DBWebException("Error refreshing navigator node '" + nodePath + "'", e);
@@ -54,6 +54,8 @@ input ServerConfigInput {
anonymousAccessEnabled: Boolean
authenticationEnabled: Boolean
customConnectionsEnabled: Boolean
publicCredentialsSaveEnabled: Boolean
adminCredentialsSaveEnabled: Boolean
sessionExpireTime: Int
}
@@ -35,6 +35,8 @@ public class AdminServerConfig {
private boolean anonymousAccessEnabled;
private boolean authenticationEnabled;
private boolean customConnectionsEnabled;
private boolean publicCredentialsSaveEnabled;
private boolean adminCredentialsSaveEnabled;
private long sessionExpireTime;
@@ -47,6 +49,8 @@ public class AdminServerConfig {
this.anonymousAccessEnabled = JSONUtils.getBoolean(params, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled());
this.authenticationEnabled = JSONUtils.getBoolean(params, "authenticationEnabled", appConfig.isAuthenticationEnabled());
this.customConnectionsEnabled = JSONUtils.getBoolean(params, "customConnectionsEnabled", appConfig.isSupportsCustomConnections());
this.publicCredentialsSaveEnabled = JSONUtils.getBoolean(params, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled());
this.adminCredentialsSaveEnabled = JSONUtils.getBoolean(params, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled());
this.sessionExpireTime = JSONUtils.getLong(params, "sessionExpireTime", -1);
}
@@ -79,24 +83,20 @@ public class AdminServerConfig {
return anonymousAccessEnabled;
}
public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) {
this.anonymousAccessEnabled = anonymousAccessEnabled;
}
public boolean isAuthenticationEnabled() {
return authenticationEnabled;
}
public void setAuthenticationEnabled(boolean authenticationEnabled) {
this.authenticationEnabled = authenticationEnabled;
}
public boolean isCustomConnectionsEnabled() {
return customConnectionsEnabled;
}
public void setCustomConnectionsEnabled(boolean customConnectionsEnabled) {
this.customConnectionsEnabled = customConnectionsEnabled;
public boolean isPublicCredentialsSaveEnabled() {
return publicCredentialsSaveEnabled;
}
public boolean isAdminCredentialsSaveEnabled() {
return adminCredentialsSaveEnabled;
}
public long getSessionExpireTime() {
@@ -350,6 +350,8 @@ public class WebServiceAdmin implements DBWServiceAdmin {
appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled());
appConfig.setAuthenticationEnabled(config.isAuthenticationEnabled());
appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled());
appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled());
appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled());
String adminName = config.getAdminName();
String adminPassword = config.getAdminPassword();
if (CommonUtils.isEmpty(adminName)) {
+5
View File
@@ -30,6 +30,11 @@
<artifactId>derbyshared</artifactId>
<version>10.15.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.15.1.3</version>
</dependency>
</dependencies>
</project>
@@ -53,8 +53,8 @@ export const InlineEditorStyles = composes(
letter-spacing: inherit;
outline: none;
border: none;
padding-left: 11px; /* because of left border */
padding-right: 12px;
padding-left: 8px;
padding-right: 8px;
padding-top: 1px; /* because of -1px top */
line-height: 24px;
width: 100%;
@@ -89,7 +89,15 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
return null;
}
let checked = state ? state[name] : checkedControlled;
let checked = checkedControlled;
if (state) {
if (typeof state[name] === 'string') {
checked = state[name] === 'true';
} else {
checked = state[name];
}
}
if (Array.isArray(checked)) {
checked = checked.includes(value);
@@ -78,10 +78,12 @@ interface ICheckboxMarkupProps extends React.InputHTMLAttributes<HTMLInputElemen
indeterminate?: boolean;
}
export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxMarkup({ label, className, ...rest }) {
export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxMarkup({
label, className, title, ...rest
}) {
return styled(useStyles(checkboxStyles, checkboxMod.primary, rest.disabled
&& checkboxState.disabled, rest.checked && checkboxState.checked))(
<checkbox-container className={className} as='div'>
<checkbox-container className={className} title={title} as='div'>
<checkbox as='div'>
<checkbox-input as='input' type='checkbox' {...rest} />
<checkbox-background as='div'>
@@ -14,6 +14,7 @@ import { InputField } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox';
import { FormFieldDescription } from '../FormControls/FormFieldDescription';
import { FormGroup } from '../FormControls/FormGroup';
import { isControlPresented } from '../FormControls/isControlPresented';
@@ -48,6 +49,7 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
}) {
const href = property.features.includes('href');
const password = property.features.includes('password');
const checkbox = property.dataType === 'Boolean';
let description: string | undefined;
if (href) {
@@ -73,6 +75,19 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
description = 'Password saved';
}
if (checkbox) {
return (
<FieldCheckbox
name={property.id!}
state={state}
checkboxLabel={property.displayName}
title={property.description}
disabled={disabled}
mod='surface'
/>
);
}
return (
<InputField
type={password ? 'password' : 'text'}
@@ -93,7 +108,7 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
interface ObjectPropertyFormProps {
properties: ObjectPropertyInfo[] | undefined;
credentials: Record<string, string | number>;
state: Record<string, string | number>;
editable?: boolean;
autofillToken?: string;
className?: string;
@@ -106,7 +121,7 @@ interface ObjectPropertyFormProps {
export const ObjectPropertyInfoForm: React.FC<ObjectPropertyFormProps> = observer(function ObjectPropertyInfoForm({
properties,
credentials,
state,
editable = true,
autofillToken = '',
className,
@@ -134,7 +149,7 @@ export const ObjectPropertyInfoForm: React.FC<ObjectPropertyFormProps> = observe
<FormGroup key={property.id}>
<RenderField
property={property}
state={credentials}
state={state}
editable={editable}
autofillToken={autofillToken}
disabled={disabled}
@@ -26,7 +26,7 @@ interface IValidationStatus {
}
@injectable()
export class ConnectionFormController
implements IInitializableController {
implements IInitializableController {
isSaving: boolean;
readonly afterSave: IExecutor<string>;
@@ -80,7 +80,10 @@ implements IInitializableController {
const connectionConfig = this.getConnectionConfig();
const validation = this.validate(connectionConfig);
if (!validation.status) {
this.notificationService.logError({ title: this.model.editing ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', message: validation.errorMessage });
this.notificationService.logError({
title: this.model.editing ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error',
message: validation.errorMessage,
});
return;
}
@@ -133,7 +136,7 @@ implements IInitializableController {
if (!config.name?.length) {
validationStatus.errorMessage = "Field 'name' can't be empty";
} else if (this.model.editing && this.isConnectionNameAlreadyExists(config.name)) {
validationStatus.errorMessage = 'Connection with this name already exists';
validationStatus.errorMessage = `Connection with ${config.name} name already exists`;
}
validationStatus.status = !validationStatus.errorMessage;
@@ -173,6 +176,10 @@ implements IInitializableController {
}
config.networkHandlersConfig = this.model.networkHandlersState;
if (Object.keys(this.model.connection.providerProperties).length > 0) {
config.providerProperties = this.model.connection.providerProperties;
}
return config;
}
@@ -140,7 +140,7 @@ export const Options = observer(function Options({
<ObjectPropertyInfoForm
autofillToken='new-password'
properties={controller.properties}
credentials={model.credentials}
state={model.credentials}
disabled={disabled}
showRememberTip
/>
@@ -157,6 +157,18 @@ export const Options = observer(function Options({
</>
)}
</FormBoxElement>
{controller.providerProperties && controller.providerProperties.length > 0 && (
<FormBoxElement>
<FormGroup>
<InputGroup>{translate('connections_connection_edit_settings')}</InputGroup>
</FormGroup>
<ObjectPropertyInfoForm
properties={controller.providerProperties}
state={model.connection.providerProperties}
disabled={disabled}
/>
</FormBoxElement>
)}
</FormBox>
</SubmittingForm>
);
@@ -18,7 +18,7 @@ import type { IConnectionFormModel } from '../IConnectionFormModel';
@injectable()
export class OptionsController
implements IInitializableController {
implements IInitializableController {
get drivers(): DBDriver[] {
return Array.from(this.dbDriverResource.data.values())
.filter(({ id }) => this.model.availableDrivers.includes(id));
@@ -46,6 +46,10 @@ implements IInitializableController {
return this.authModel?.properties;
}
get providerProperties(): ObjectPropertyInfo[] | undefined {
return this.dbDriverResource.get(this.model.connection.driverId)?.providerProperties;
}
private model!: IConnectionFormModel;
/** we want to save prev generated connection name to detect if user changed "name" field to turn off autofill */
private prevGeneratedName: string | null = null;
@@ -87,6 +91,7 @@ implements IInitializableController {
this.setDefaultParameters(prevDriverId);
this.cleanCredentials();
this.model.connection.properties = {};
this.model.connection.providerProperties = {};
this.model.connection.authModel = this.driver?.defaultAuthModel;
}
@@ -8,7 +8,7 @@
import { observer } from 'mobx-react-lite';
import { TextPlaceholder, useTab, ObjectPropertyInfoForm, FormBox, FormBoxElement, FormGroup, InputGroup, Loader, useTabState, ExceptionMessage } from '@cloudbeaver/core-blocks';
import { TextPlaceholder, useTab, ObjectPropertyInfoForm, FormBox, FormBoxElement, FormGroup, Loader, useTabState, ExceptionMessage } from '@cloudbeaver/core-blocks';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
@@ -94,7 +94,7 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
<FormGroup><br /></FormGroup>
<ObjectPropertyInfoForm
properties={state.properties}
credentials={state.state}
state={state.state}
editable={false}
autoHide
/>
@@ -71,6 +71,7 @@ export class ConnectionsResource extends CachedMapResource<string, AdminConnecti
useUrl: false,
authProperties: [],
properties: {},
providerProperties: {},
origin: {
type: 'local',
displayName: 'Local',
@@ -15,21 +15,22 @@ import {
import { MetadataMap } from '@cloudbeaver/core-utils';
export type DBDriver = Pick<
DriverInfo,
| 'id'
| 'name'
| 'icon'
| 'description'
| 'defaultPort'
| 'defaultDatabase'
| 'defaultServer'
| 'defaultUser'
| 'sampleURL'
| 'embedded'
| 'anonymousAccess'
| 'promotedScore'
| 'defaultAuthModel'
| 'applicableNetworkHandlers'
DriverInfo,
| 'id'
| 'name'
| 'icon'
| 'description'
| 'defaultPort'
| 'defaultDatabase'
| 'defaultServer'
| 'defaultUser'
| 'sampleURL'
| 'embedded'
| 'anonymousAccess'
| 'promotedScore'
| 'defaultAuthModel'
| 'applicableNetworkHandlers'
| 'providerProperties'
>;
@injectable()
@@ -101,7 +101,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({
<ObjectPropertyInfoForm
autofillToken={`section-${connection.connectionInfo?.id || ''} section-auth`}
properties={connection.connectionInfo?.authProperties}
credentials={controller.config.credentials}
state={controller.config.credentials}
disabled={controller.isAuthenticating}
/>
<FormGroup>
@@ -16,6 +16,7 @@ export default [
['connections_connection_create_search_database', 'Search'],
['connections_connection_edit_save_credentials', 'Save credentials'],
['connections_connection_edit_authentication', 'Authentication'],
['connections_connection_edit_settings', 'Settings'],
['connections_connection_edit_access', 'Access'],
['connections_connection_edit_access_load_failed', 'Fail to get connection access'],
['connections_connection_edit_access_role', 'Role'],
@@ -15,6 +15,7 @@ export default [
['connections_connection_create_search_database', 'Найти'],
['connections_connection_edit_save_credentials', ' Запомнить данные'],
['connections_connection_edit_authentication', 'Авторизация'],
['connections_connection_edit_settings', 'Настройки'],
['connections_connection_edit_access', 'Доступ'],
['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'],
['connections_connection_edit_access_role', 'Роль'],
@@ -15,6 +15,18 @@ query driverList {
defaultAuthModel
# applicableAuthModel
providerProperties {
id
displayName
description
category
dataType
defaultValue
validValues
features
order
}
applicableNetworkHandlers
}
}
}
@@ -15,6 +15,7 @@ fragment AdminConnection on ConnectionInfo {
databaseName
url
properties
providerProperties
features
+34 -2
View File
@@ -268,6 +268,7 @@ export interface Mutation {
sqlContextSetDefaults: Scalars['Boolean'];
sqlResultClose: Scalars['Boolean'];
testConnection: ConnectionInfo;
testNetworkHandler: NetworkEndpointInfo;
touchSession?: Maybe<Scalars['Boolean']>;
updateResultsData?: Maybe<SqlExecuteInfo>;
updateResultsDataBatch?: Maybe<SqlExecuteInfo>;
@@ -374,6 +375,10 @@ export interface MutationTestConnectionArgs {
config: ConnectionConfig;
}
export interface MutationTestNetworkHandlerArgs {
config: NetworkHandlerConfigInput;
}
export interface MutationUpdateResultsDataArgs {
connectionId: Scalars['ID'];
contextId: Scalars['ID'];
@@ -448,6 +453,8 @@ export interface ServerConfig {
supportsCustomConnections?: Maybe<Scalars['Boolean']>;
supportsConnectionBrowser?: Maybe<Scalars['Boolean']>;
supportsWorkspaces?: Maybe<Scalars['Boolean']>;
publicCredentialsSaveEnabled?: Maybe<Scalars['Boolean']>;
adminCredentialsSaveEnabled?: Maybe<Scalars['Boolean']>;
sessionExpireTime?: Maybe<Scalars['Int']>;
localHostAddress?: Maybe<Scalars['String']>;
configurationMode?: Maybe<Scalars['Boolean']>;
@@ -569,6 +576,12 @@ export interface ConnectionInfo {
supportedDataFormats: ResultDataFormat[];
}
export interface NetworkEndpointInfo {
message?: Maybe<Scalars['String']>;
clientVersion?: Maybe<Scalars['String']>;
serverVersion?: Maybe<Scalars['String']>;
}
export interface ObjectOrigin {
type: Scalars['ID'];
subType?: Maybe<Scalars['ID']>;
@@ -844,6 +857,8 @@ export interface ServerConfigInput {
anonymousAccessEnabled?: Maybe<Scalars['Boolean']>;
authenticationEnabled?: Maybe<Scalars['Boolean']>;
customConnectionsEnabled?: Maybe<Scalars['Boolean']>;
publicCredentialsSaveEnabled?: Maybe<Scalars['Boolean']>;
adminCredentialsSaveEnabled?: Maybe<Scalars['Boolean']>;
sessionExpireTime?: Maybe<Scalars['Int']>;
}
@@ -1097,7 +1112,12 @@ export type DeleteConnectionMutation = Pick<Mutation, 'deleteConnection'>;
export type DriverListQueryVariables = Exact<{ [key: string]: never }>;
export interface DriverListQuery { driverList: Array<Pick<DriverInfo, 'id' | 'name' | 'icon' | 'description' | 'defaultPort' | 'defaultDatabase' | 'defaultServer' | 'defaultUser' | 'sampleURL' | 'embedded' | 'anonymousAccess' | 'promotedScore' | 'defaultAuthModel' | 'applicableNetworkHandlers'>> }
export interface DriverListQuery {
driverList: Array<(
Pick<DriverInfo, 'id' | 'name' | 'icon' | 'description' | 'defaultPort' | 'defaultDatabase' | 'defaultServer' | 'defaultUser' | 'sampleURL' | 'embedded' | 'anonymousAccess' | 'promotedScore' | 'defaultAuthModel' | 'applicableNetworkHandlers'>
& { providerProperties: Array<Pick<ObjectPropertyInfo, 'id' | 'displayName' | 'description' | 'category' | 'dataType' | 'defaultValue' | 'validValues' | 'features' | 'order'>> }
)>;
}
export type DriverPropertiesQueryVariables = Exact<{
driverId: Scalars['ID'];
@@ -1216,7 +1236,7 @@ export type NavGetStructContainersQueryVariables = Exact<{
export interface NavGetStructContainersQuery { navGetStructContainers: { catalogList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>>; schemaList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>> } }
export type AdminConnectionFragment = (
Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'useUrl' | 'readOnly' | 'saveCredentials' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel' | 'supportedDataFormats'>
Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'useUrl' | 'readOnly' | 'saveCredentials' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'providerProperties' | 'features' | 'authNeeded' | 'authModel' | 'supportedDataFormats'>
& { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[]; networkHandlersConfig: Array<Pick<NetworkHandlerConfig, 'id' | 'enabled' | 'userName' | 'password' | 'savePassword' | 'properties'>> }
);
@@ -1540,6 +1560,7 @@ export const AdminConnectionFragmentDoc = `
databaseName
url
properties
providerProperties
features
origin {
...ObjectOriginInfo
@@ -1907,6 +1928,17 @@ export const DriverListDocument = `
anonymousAccess
promotedScore
defaultAuthModel
providerProperties {
id
displayName
description
category
dataType
defaultValue
validValues
features
order
}
applicableNetworkHandlers
}
}
@@ -98,7 +98,7 @@ export const OriginInfo: TabContainerPanelComponent<IUserFormProps> = observer(f
<FormGroup><br /></FormGroup>
<ObjectPropertyInfoForm
properties={state.properties}
credentials={state.state}
state={state.state}
editable={false}
autoHide
/>
@@ -89,7 +89,7 @@ export const ConnectionForm = observer(function ConnectionForm({
<ObjectPropertyInfoForm
autofillToken='new-password'
properties={controller.authModel.properties}
credentials={controller.config.credentials}
state={controller.config.credentials}
disabled={controller.isConnecting}
/>
<FormGroup>
@@ -85,7 +85,7 @@ export const ConnectionDialog = observer(function ConnectionDialog({
<ObjectPropertyInfoForm
autofillToken={`section-${controller.template?.id || ''} section-auth`}
properties={controller.authModel.properties}
credentials={controller.credentials}
state={controller.credentials}
disabled={controller.isConnecting}
/>
</SubmittingForm>
@@ -15,7 +15,8 @@
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"dependencies": {
"react-data-grid": "^7.0.0-canary.30"
"react-data-grid": "^7.0.0-canary.30",
"react-popper": "^2.2.4"
},
"peerDependencies": {
"@cloudbeaver/plugin-data-viewer": "^0.1.0",
@@ -0,0 +1,95 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useContext, useLayoutEffect, useState } from 'react';
import type { EditorProps } from 'react-data-grid';
import { createPortal } from 'react-dom';
import { usePopper } from 'react-popper';
import styled, { css } from 'reshadow';
import { InlineEditor, InlineEditorControls } from '@cloudbeaver/core-app';
import { DataGridContext } from '../DataGridContext';
const styles = css`
editor {
composes: theme-typography--body2 from global;
}
InlineEditor {
font-size: 12px;
left: -1px;
}
`;
export const CellEditor: React.FC<EditorProps<any, any>> = observer(function CellEditor({
rowIdx,
row,
column,
editorPortalTarget,
onClose,
}) {
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
const [popperRef, setPopperRef] = useState<HTMLDivElement | null>(null);
const popper = usePopper(elementRef, popperRef, {
placement: 'right',
});
const context = useContext(DataGridContext);
useLayoutEffect(() => {
if (elementRef && popperRef) {
const size = elementRef.parentElement?.getBoundingClientRect();
if (size) {
popperRef.style.width = (size.width + 2) + 'px';
popperRef.style.height = (size.height + 2) + 'px';
}
}
});
if (!context) {
throw new Error('DataGridContext should be provided');
}
const modelResultData = context.model?.getResult(context.resultIndex);
const value = row[column.key];
const isLastColumn = column.isLastFrozenColumn; // TODO: do not work
const isLastRow = rowIdx > 0 && ((modelResultData?.data.rows.length || 0) - 1 === rowIdx);
let controlsPosition: InlineEditorControls = 'right';
if (isLastColumn) {
controlsPosition = isLastRow ? 'top' : 'bottom';
}
const handleSave = () => onClose(false);
const handleReject = () => onClose(false);
const handleChange = () => {};
const handleUndo = () => onClose(false);
return styled(styles)(
<box ref={setElementRef} as='div'>
{createPortal((
<editor ref={setPopperRef} as="div" style={popper.styles.popper} {...popper.attributes.popper}>
<InlineEditor
type={typeof value === 'number' ? 'number' : 'text'}
value={value}
controlsPosition={controlsPosition}
edited
hideSave
hideCancel
autofocus
onSave={handleSave}
onReject={handleReject}
onChange={handleChange}
onUndo={handleUndo}
/>
</editor>
), context.getEditorPortal() || editorPortalTarget)}
</box>
);
});
@@ -10,9 +10,10 @@ import { createContext } from 'react';
import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
interface IDataGridContext {
model: IDatabaseDataModel<any, any>;
export interface IDataGridContext {
model: IDatabaseDataModel<any>;
resultIndex: number;
getEditorPortal: () => HTMLDivElement | null;
}
export const DataGridContext = createContext<IDataGridContext | null>(null);
@@ -8,7 +8,7 @@
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import DataGrid from 'react-data-grid';
import type { Column } from 'react-data-grid';
import styled from 'reshadow';
@@ -21,7 +21,8 @@ import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
import { ResultSetTools } from '../ResultSetTools';
import baseStyles from '../styles/base.scss';
import { reactGridStyles } from '../styles/styles';
import { DataGridContext } from './DataGridContext';
import { CellEditor } from './CellEditor/CellEditor';
import { DataGridContext, IDataGridContext } from './DataGridContext';
import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext';
import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext';
import { DataGridSortingContext } from './DataGridSorting/DataGridSortingContext';
@@ -30,7 +31,7 @@ import { DataGridTableContainer } from './DataGridTableContainer';
import { CellFormatter } from './Formatters/CellFormatter';
import { IndexFormatter } from './Formatters/IndexFormatter';
import { RowRenderer } from './RowRenderer/RowRenderer';
import { TableColumnHeaderNew } from './TableColumnHeader/TableColumnHeader-new';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
interface Props {
model: IDatabaseDataModel<any>;
@@ -54,6 +55,7 @@ const indexColumn: Column<any[], any> = {
};
export const DataGridTable: React.FC<Props> = observer(function DataGridTable({ model, resultIndex, className }) {
const editorRef = useRef<HTMLDivElement>(null);
const styles = useStyles(reactGridStyles, baseStyles);
const modelResultData = model?.getResult(resultIndex);
@@ -114,16 +116,36 @@ export const DataGridTable: React.FC<Props> = observer(function DataGridTable({
width: Math.min(300, measuredCells[i]),
minWidth: 40,
resizable: true,
headerRenderer: TableColumnHeaderNew,
editable: true,
headerRenderer: TableColumnHeader,
formatter: CellFormatter,
editor: CellEditor,
editorOptions: {
onCellKeyDown: event => {
event.preventDefault();
event.stopPropagation();
},
onNavigation: event => {
event.preventDefault();
event.stopPropagation();
return false;
},
},
}));
columns.unshift(indexColumn);
return { rows, columns };
}), [modelResultData]).get();
const gridContext = useMemo<IDataGridContext>(() => ({
model,
resultIndex,
getEditorPortal: () => editorRef.current,
}), [model, resultIndex, editorRef]);
return styled(styles)(
<DataGridContext.Provider value={{ model, resultIndex }}>
<DataGridContext.Provider value={gridContext}>
<DataGridSortingContext.Provider value={gridSortingContext}>
<DataGridSelectionContext.Provider value={gridSelectionContext}>
<DataGridTableContainer modelResultData={modelResultData}>
@@ -136,6 +158,7 @@ export const DataGridTable: React.FC<Props> = observer(function DataGridTable({
rowRenderer={RowRenderer}
onScroll={handleScroll}
/>
<div ref={editorRef} />
</DataGridTableContainer>
</DataGridSelectionContext.Provider>
</DataGridSortingContext.Provider>
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useMemo } from 'react';
import { useContext } from 'react';
import type { HeaderRendererProps } from 'react-data-grid';
import styled, { css, use } from 'reshadow';
@@ -79,7 +78,7 @@ function getColumn(colIdx: number, source: SqlResultSet) {
return source.columns?.[colIdx];
}
export const TableColumnHeaderNew: React.FC<HeaderRendererProps<any>> = observer(function TableColumnHeaderNew({
export const TableColumnHeader: React.FC<HeaderRendererProps<any>> = observer(function TableColumnHeader({
column: calculatedColumn,
}) {
const dataGridContext = useContext(DataGridContext);
@@ -91,7 +90,7 @@ export const TableColumnHeaderNew: React.FC<HeaderRendererProps<any>> = observer
const model = dataGridContext.model;
const columnName = calculatedColumn.name as string;
const column = getColumn(Number(calculatedColumn.key), model.getResult(dataGridContext.resultIndex).data);
const column = getColumn(Number(calculatedColumn.key), model.getResult(dataGridContext.resultIndex)?.data);
const loading = model.isLoading();
@@ -24,4 +24,17 @@
.rdg-cell {
border-color: transparent;
}
.rdg-cell-editing {
overflow: visible;
height: 24px;
box-shadow: none !important;
}
.rdg-editor-container {
position: relative;
display: flex;
width: 100%;
height: 100%;
}
}
+20
View File
@@ -11092,11 +11092,24 @@ react-dom@^16.13.1, react-dom@^16.x.x:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-fast-compare@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-popper@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22"
integrity sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew==
dependencies:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-sanitized-html@^2.x.x:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-sanitized-html/-/react-sanitized-html-2.0.0.tgz#2ac300ae60e8e7cb28378b1233b9630e415d0241"
@@ -13350,6 +13363,13 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.0.tgz#e63194736bf3aa22026f7b191cd57907b0f9f696"