Merge remote-tracking branch 'origin/devel' into CB-92-builder-package

# Conflicts:
#	webapp/package.json
#	webapp/packages/builder/configs/webpack/common.js
#	webapp/packages/builder/src/fix-gql.js
#	webapp/packages/core/package.json
#	webapp/packages/core/src/AppBootstrap.ts
#	webapp/packages/core/src/root/index.ts
#	webapp/packages/dbeaver/configs/webpack/common-exp.js
#	webapp/packages/dbeaver/package.json
#	webapp/yarn.lock
This commit is contained in:
Dmitry Krupenya
2020-05-18 12:42:46 +03:00
119 changed files with 3072 additions and 2405 deletions
@@ -60,7 +60,7 @@ type ServerConfig {
supportsConnectionBrowser: Boolean
supportsWorkspaces: Boolean
supportedLanguages: [ ServerLanguage ]!
supportedLanguages: [ ServerLanguage! ]!
services: [ WebServiceConfig ]
productConfiguration: Object!
}
@@ -73,7 +73,7 @@ type SessionInfo {
cacheExpired: Boolean!
serverMessages: [ ServerMessage ]
connections: [ ConnectionInfo ]!
connections: [ ConnectionInfo! ]!
}
####################################################
@@ -183,7 +183,7 @@ type ConnectionInfo {
clientVersion: String
# Supported features (provided etc)
features: [ String! ]
features: [ String! ]!
}
type LogEntry {
@@ -199,30 +199,30 @@ type LogEntry {
extend type Query {
# Returns server config
serverConfig: ServerConfig
serverConfig: ServerConfig!
# Returns session state ( initialize if not )
sessionState: SessionInfo
sessionState: SessionInfo!
# Session permissions
sessionPermissions: [ID]!
# Get driver info
driverList( id: ID ): [ DriverInfo! ]
driverList( id: ID ): [ DriverInfo! ]!
# Get list of predefined data sources
dataSourceList: [ DataSourceInfo! ]
dataSourceList: [ DataSourceInfo! ]!
# Return connection state
connectionState( id: ID! ): ConnectionInfo
connectionState( id: ID! ): ConnectionInfo!
readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]
readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]!
}
extend type Mutation {
# Initialize session
openSession: SessionInfo
openSession: SessionInfo!
# Destroy session
closeSession: Boolean
@@ -234,13 +234,13 @@ extend type Mutation {
changeSessionLanguage(locale: String): Boolean
# Create new connection
createConnection( config: ConnectionConfig! ): ConnectionInfo
createConnection( config: ConnectionConfig! ): ConnectionInfo!
# Test connection configuration. Returns remote server version
testConnection( config: ConnectionConfig! ): ConnectionInfo
testConnection( config: ConnectionConfig! ): ConnectionInfo!
# Connect to database
openConnection( config: ConnectionConfig! ): ConnectionInfo
openConnection( config: ConnectionConfig! ): ConnectionInfo!
# Disconnect from database
closeConnection( id: ID! ): Boolean!
@@ -29,7 +29,7 @@ type ObjectPropertyInfo {
# Default property value
defaultValue: Object
# Supported features (system, hidden, inherited, foreign, expensive, etc)
features: [ String! ]
features: [ String! ]!
}
input ObjectPropertyFilter {
@@ -94,10 +94,8 @@ type NavigatorNodeInfo {
}
type DatabaseStructContainers {
catalogList: [ DatabaseObjectInfo! ]!
schemaList: [ DatabaseObjectInfo! ]!
}
####################################################
@@ -111,9 +109,9 @@ extend type Query {
parentPath: ID!,
offset: Int,
limit: Int,
onlyFolders: Boolean): [ NavigatorNodeInfo! ]
onlyFolders: Boolean): [ NavigatorNodeInfo! ]!
navNodeInfo( nodePath: ID! ): NavigatorNodeInfo
navNodeInfo( nodePath: ID! ): NavigatorNodeInfo!
navRefreshNode( nodePath: ID! ): Boolean
@@ -99,7 +99,7 @@ type SQLExecuteInfo {
statusMessage: String
duration: Int
results: [ SQLQueryResults! ]
results: [ SQLQueryResults! ]!
}
extend type Query {
@@ -17,10 +17,14 @@
package io.cloudbeaver.model.user;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebAuthProviderPropertyDescriptor;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.dbeaver.Log;
import java.util.List;
/**
* Web user.
* WebAuthProviderInfo.
*/
public class WebAuthProviderInfo {
@@ -48,6 +52,14 @@ public class WebAuthProviderInfo {
return descriptor.getDescription();
}
public boolean isDefaultProvider() {
return descriptor.getId().equals(CBPlatform.getInstance().getApplication().getAppConfiguration().getDefaultAuthProvider());
}
public List<WebAuthProviderPropertyDescriptor> getCredentialParameters() {
return descriptor.getCredentialParameters();
}
@Override
public String toString() {
return getId();
@@ -16,6 +16,8 @@
*/
package io.cloudbeaver.server;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
/**
* Application configuration
*/
@@ -30,6 +32,7 @@ public class CBAppConfig {
private boolean supportsUserWorkspaces = false;
private String[] enabledDrivers = new String[0];
private String[] disabledDrivers = new String[0];
private String defaultAuthProvider = LocalAuthProvider.PROVIDER_ID;
public boolean isAuthenticationEnabled() {
return authenticationEnabled;
@@ -70,4 +73,8 @@ public class CBAppConfig {
public String[] getDisabledDrivers() {
return disabledDrivers;
}
public String getDefaultAuthProvider() {
return defaultAuthProvider;
}
}
@@ -23,11 +23,11 @@ type AuthProviderInfo {
icon: ID
description: String
isDefault: Boolean
defaultProvider: Boolean
#configurationParameters: [ObjectPropertyInfo]!
credentialParameters: [AuthCredentialInfo]!
credentialParameters: [AuthCredentialInfo!]!
}
type UserAuthInfo {
@@ -46,12 +46,12 @@ type UserAuthInfo {
extend type Query {
authLogin(provider: ID!, credentials: Object!): UserAuthInfo
authLogin(provider: ID!, credentials: Object!): UserAuthInfo!
authLogout: Boolean
sessionUser: UserAuthInfo
authProviders: [AuthProviderInfo]!
authProviders: [AuthProviderInfo!]!
}
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWService;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebAction;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebAuthProviderInfo;
import java.util.Map;
@@ -36,4 +37,7 @@ public interface DBWServiceAuth extends DBWService {
@WebAction(requirePermissions = {})
WebAuthInfo sessionUser(WebSession webSession) throws DBWebException;
@WebAction(requirePermissions = {})
WebAuthProviderInfo[] getAuthProviders(WebSession webSession);
}
@@ -17,7 +17,6 @@
package io.cloudbeaver.service.auth;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.registry.WebServiceRegistry;
import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.WebServiceBindingBase;
import io.cloudbeaver.service.auth.impl.WebServiceAuthImpl;
@@ -45,7 +44,7 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
return true;
})
.dataFetcher("sessionUser", env -> getService(env).sessionUser(getWebSession(env)))
.dataFetcher("authProviders", env -> WebServiceRegistry.getInstance().getAuthProviders())
.dataFetcher("authProviders", env -> getService(env).getAuthProviders(getWebSession(env)))
;
}
@@ -21,6 +21,7 @@ import io.cloudbeaver.DBWAuthProviderExternal;
import io.cloudbeaver.DBWSecurityController;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebAuthProviderInfo;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebServiceRegistry;
@@ -126,4 +127,11 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
return webSession.getAttribute(ATTR_USER_AUTH);
}
@Override
public WebAuthProviderInfo[] getAuthProviders(WebSession webSession) {
return WebServiceRegistry.getInstance().getAuthProviders()
.stream().map(WebAuthProviderInfo::new)
.toArray(WebAuthProviderInfo[]::new);
}
}
@@ -29,7 +29,7 @@ input DataTransferParameters {
extend type Query {
# Available transfer processors
dataTransferAvailableStreamProcessors: [ DataTransferProcessorInfo ]
dataTransferAvailableStreamProcessors: [ DataTransferProcessorInfo! ]!
dataTransferExportDataFromContainer(
connectionId: ID!,
+1 -1
View File
@@ -11,7 +11,7 @@
</parent>
<properties>
<deps.output.dir>mysql</deps.output.dir>
<deps.output.dir>mysql/mysql8</deps.output.dir>
</properties>
<dependencies>
@@ -34,11 +34,11 @@
<setEntry value="org.jkiss.dbeaver.db.feature:default"/>
<setEntry value="org.jkiss.dbeaver.runtime.feature:default"/>
</setAttribute>
<stringAttribute key="selected_target_plugins" value="com.github.jsqlparser@default:default,com.google.gson@default:default,com.google.guava*27.1.0.v20190517-1946@default:default,com.ibm.icu.base@default:default,com.jcraft.jsch@default:default,com.jcraft.jzlib@default:default,com.sun.jna*4.5.1.v20190425-1842@default:default,com.sun.jna.platform*4.5.1.v20190425-1842@default:default,javax.annotation@default:default,javax.inject@default:default,javax.servlet*3.1.0.v201410161800@default:default,javax.xml@default:default,net.sf.opencsv@default:default,org.apache.batik.constants@default:default,org.apache.batik.css@default:default,org.apache.batik.i18n@default:default,org.apache.batik.util@default:default,org.apache.commons.io@default:default,org.apache.commons.jexl@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging@default:default,org.apache.felix.gogo.runtime@default:default,org.apache.felix.scr@1:true,org.apache.log4j@default:default,org.apache.lucene.analyzers-common*8.0.0.v20190404-1858@default:default,org.apache.lucene.analyzers-smartcn@default:default,org.apache.lucene.core*8.0.0.v20190404-1858@default:default,org.apache.xerces@default:default,org.apache.xml.resolver@default:default,org.apache.xml.serializer@default:default,org.apache.xmlgraphics@default:default,org.bouncycastle.bcpkix@default:default,org.bouncycastle.bcprov@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filebuffers@default:default,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions.supplier@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.edit@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.emf.edit@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.bidi@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.event@default:default,org.eclipse.equinox.http.jetty@default:default,org.eclipse.equinox.http.servlet@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.jdt.annotation*2.2.300.v20190328-1431@default:default,org.eclipse.jetty.http@default:default,org.eclipse.jetty.io@default:default,org.eclipse.jetty.security@default:default,org.eclipse.jetty.server@default:default,org.eclipse.jetty.servlet@default:default,org.eclipse.jetty.util@default:default,org.eclipse.ltk.core.refactoring@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi.util@default:default,org.eclipse.osgi@-1:true,org.eclipse.team.core@default:default,org.eclipse.text@default:default,org.jkiss.bundle.apache.dbcp@default:default,org.jkiss.bundle.aws@default:default,org.jkiss.bundle.gis@default:default,org.jkiss.bundle.graphql.java@default:default,org.jkiss.bundle.sshj@default:default,org.objectweb.asm*7.1.0.v20190412-2143@default:default,org.slf4j.api*1.7.10.v20170428-1633@default:default,org.tukaani.xz@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default,stax.api@default:default"/>
<stringAttribute key="selected_workspace_plugins" value="com.dbeaver.db.sqlite.crypt@default:default,io.cloudbeaver.connect.provider.sample@default:default,io.cloudbeaver.resources.drivers.base@default:default,io.cloudbeaver.resources.drivers.ee@default:default,io.cloudbeaver.server@default:default,io.cloudbeaver.service.admin@default:default,io.cloudbeaver.service.auth.aws@default:default,io.cloudbeaver.service.auth@default:default,io.cloudbeaver.service.data.transfer@default:default,io.cloudbeaver.service.metadata@default:default,org.jkiss.dbeaver.cmp.simple@default:default,org.jkiss.dbeaver.data.gis@default:default,org.jkiss.dbeaver.data.transfer@default:default,org.jkiss.dbeaver.ext.clickhouse@default:default,org.jkiss.dbeaver.ext.db2.i@default:default,org.jkiss.dbeaver.ext.db2.zos@default:default,org.jkiss.dbeaver.ext.derby@default:default,org.jkiss.dbeaver.ext.firebird@default:default,org.jkiss.dbeaver.ext.generic@default:default,org.jkiss.dbeaver.ext.greenplum@default:default,org.jkiss.dbeaver.ext.h2@default:default,org.jkiss.dbeaver.ext.h2gis@default:default,org.jkiss.dbeaver.ext.hana@default:default,org.jkiss.dbeaver.ext.hive@default:default,org.jkiss.dbeaver.ext.hsqldb@default:default,org.jkiss.dbeaver.ext.informix@default:default,org.jkiss.dbeaver.ext.kognitio@default:default,org.jkiss.dbeaver.ext.mssql@default:default,org.jkiss.dbeaver.ext.mysql@default:default,org.jkiss.dbeaver.ext.netezza@default:default,org.jkiss.dbeaver.ext.opendistro@default:default,org.jkiss.dbeaver.ext.oracle@default:default,org.jkiss.dbeaver.ext.phoenix@default:default,org.jkiss.dbeaver.ext.postgresql@default:default,org.jkiss.dbeaver.ext.snowflake.core@default:default,org.jkiss.dbeaver.ext.sqlite@default:default,org.jkiss.dbeaver.ext.teradata@default:default,org.jkiss.dbeaver.ext.vertica@default:default,org.jkiss.dbeaver.model.sql@default:default,org.jkiss.dbeaver.model@default:default,org.jkiss.dbeaver.net.ssh.sshj@default:default,org.jkiss.dbeaver.net.ssh@default:default,org.jkiss.dbeaver.registry@default:default,org.jkiss.dbeaver.slf4j@default:false,org.jkiss.dbeaver.tasks.native@default:default,org.jkiss.utils@default:default,org.jkiss.wmi@default:default"/>
<stringAttribute key="selected_target_plugins" value="com.github.jsqlparser@default:default,com.google.gson@default:default,com.google.guava*27.1.0.v20190517-1946@default:default,com.ibm.icu.base@default:default,com.jcraft.jsch@default:default,com.jcraft.jzlib@default:default,com.sun.jna*4.5.1.v20190425-1842@default:default,com.sun.jna.platform*4.5.1.v20190425-1842@default:default,javax.annotation@default:default,javax.inject@default:default,javax.servlet*3.1.0.v201410161800@default:default,javax.xml@default:default,net.sf.opencsv@default:default,org.apache.batik.constants@default:default,org.apache.batik.css@default:default,org.apache.batik.i18n@default:default,org.apache.batik.util@default:default,org.apache.commons.io@default:default,org.apache.commons.jexl@default:default,org.apache.commons.jxpath@default:default,org.apache.commons.logging@default:default,org.apache.felix.gogo.runtime@default:default,org.apache.felix.scr@1:true,org.apache.log4j@default:default,org.apache.xerces@default:default,org.apache.xml.resolver@default:default,org.apache.xml.serializer@default:default,org.apache.xmlgraphics@default:default,org.bouncycastle.bcpkix@default:default,org.bouncycastle.bcprov@default:default,org.eclipse.ant.core@default:default,org.eclipse.compare.core@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filebuffers@default:default,org.eclipse.core.filesystem.win32.x86_64@default:false,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.net.win32.x86_64@default:false,org.eclipse.core.net@default:default,org.eclipse.core.resources.win32.x86_64@default:false,org.eclipse.core.resources@default:default,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.e4.core.commands@default:default,org.eclipse.e4.core.contexts@default:default,org.eclipse.e4.core.di.annotations@default:default,org.eclipse.e4.core.di.extensions.supplier@default:default,org.eclipse.e4.core.di.extensions@default:default,org.eclipse.e4.core.di@default:default,org.eclipse.e4.core.services@default:default,org.eclipse.e4.emf.xpath@default:default,org.eclipse.emf.common@default:default,org.eclipse.emf.ecore.change@default:default,org.eclipse.emf.ecore.edit@default:default,org.eclipse.emf.ecore.xmi@default:default,org.eclipse.emf.ecore@default:default,org.eclipse.emf.edit@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.bidi@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.event@default:default,org.eclipse.equinox.http.jetty@default:default,org.eclipse.equinox.http.servlet@default:default,org.eclipse.equinox.p2.core@default:default,org.eclipse.equinox.p2.engine@default:default,org.eclipse.equinox.p2.metadata.repository@default:default,org.eclipse.equinox.p2.metadata@default:default,org.eclipse.equinox.p2.repository@default:default,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security.win32.x86_64@default:false,org.eclipse.equinox.security@default:default,org.eclipse.jdt.annotation*2.2.300.v20190328-1431@default:default,org.eclipse.jetty.http@default:default,org.eclipse.jetty.io@default:default,org.eclipse.jetty.security@default:default,org.eclipse.jetty.server@default:default,org.eclipse.jetty.servlet@default:default,org.eclipse.jetty.util@default:default,org.eclipse.osgi.compatibility.state@default:false,org.eclipse.osgi.services@default:default,org.eclipse.osgi.util@default:default,org.eclipse.osgi@-1:true,org.eclipse.team.core@default:default,org.eclipse.text@default:default,org.jkiss.bundle.apache.dbcp@default:default,org.jkiss.bundle.apache.poi@default:default,org.jkiss.bundle.aws@default:default,org.jkiss.bundle.gis@default:default,org.jkiss.bundle.google.cloud@default:default,org.jkiss.bundle.graphql.java@default:default,org.jkiss.bundle.influxdb@default:default,org.jkiss.bundle.jaxb@default:default,org.jkiss.bundle.jedis@default:default,org.jkiss.bundle.onelogin.saml@default:default,org.jkiss.bundle.sshj@default:default,org.objectweb.asm*7.1.0.v20190412-2143@default:default,org.slf4j.api*1.7.10.v20170428-1633@default:default,org.tukaani.xz@default:default,org.w3c.css.sac@default:default,org.w3c.dom.events@default:default,org.w3c.dom.smil@default:default,org.w3c.dom.svg@default:default,stax.api@default:default"/>
<stringAttribute key="selected_workspace_plugins" value="com.dbeaver.db.sqlite.crypt@default:default,com.dbeaver.resources.clients.mysql.win32@default:default,com.dbeaver.resources.clients.postgresql.win32@default:default,com.dbeaver.resources.drivers.jdbc@default:default,io.cloudbeaver.connect.provider.sample@default:default,io.cloudbeaver.product.ce@default:default,io.cloudbeaver.resources.drivers.base@default:default,io.cloudbeaver.resources.drivers.ee@default:default,io.cloudbeaver.server@default:default,io.cloudbeaver.service.admin@default:default,io.cloudbeaver.service.auth.aws@default:default,io.cloudbeaver.service.auth@default:default,io.cloudbeaver.service.data.transfer@default:default,io.cloudbeaver.service.metadata@default:default,org.jkiss.dbeaver.cmp.simple@default:default,org.jkiss.dbeaver.data.gis@default:default,org.jkiss.dbeaver.data.office@default:default,org.jkiss.dbeaver.data.transfer@default:default,org.jkiss.dbeaver.ext.clickhouse@default:default,org.jkiss.dbeaver.ext.db2.i@default:default,org.jkiss.dbeaver.ext.db2.zos@default:default,org.jkiss.dbeaver.ext.derby@default:default,org.jkiss.dbeaver.ext.firebird@default:default,org.jkiss.dbeaver.ext.generic@default:default,org.jkiss.dbeaver.ext.greenplum@default:default,org.jkiss.dbeaver.ext.h2@default:default,org.jkiss.dbeaver.ext.h2gis@default:default,org.jkiss.dbeaver.ext.hana@default:default,org.jkiss.dbeaver.ext.hive@default:default,org.jkiss.dbeaver.ext.hsqldb@default:default,org.jkiss.dbeaver.ext.informix@default:default,org.jkiss.dbeaver.ext.kognitio@default:default,org.jkiss.dbeaver.ext.mssql@default:default,org.jkiss.dbeaver.ext.mysql@default:default,org.jkiss.dbeaver.ext.netezza@default:default,org.jkiss.dbeaver.ext.opendistro@default:default,org.jkiss.dbeaver.ext.oracle@default:default,org.jkiss.dbeaver.ext.phoenix@default:default,org.jkiss.dbeaver.ext.postgresql@default:default,org.jkiss.dbeaver.ext.snowflake.core@default:default,org.jkiss.dbeaver.ext.sqlite@default:default,org.jkiss.dbeaver.ext.teradata@default:default,org.jkiss.dbeaver.ext.vertica@default:default,org.jkiss.dbeaver.model.sql@default:default,org.jkiss.dbeaver.model@default:default,org.jkiss.dbeaver.net.ssh.sshj@default:default,org.jkiss.dbeaver.net.ssh@default:default,org.jkiss.dbeaver.registry@default:default,org.jkiss.dbeaver.slf4j@default:false,org.jkiss.dbeaver.tasks.native@default:default,org.jkiss.utils@default:default,org.jkiss.wmi@default:default"/>
<booleanAttribute key="show_selected_only" value="false"/>
<booleanAttribute key="tracing" value="false"/>
<booleanAttribute key="useCustomFeatures" value="true"/>
<booleanAttribute key="useCustomFeatures" value="false"/>
<booleanAttribute key="useDefaultConfig" value="true"/>
<booleanAttribute key="useDefaultConfigArea" value="true"/>
<booleanAttribute key="useProduct" value="true"/>
+4 -5
View File
@@ -5,15 +5,12 @@ module.exports = {
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
// 'plugin:prettier/recommended',
// 'prettier/@typescript-eslint',
],
plugins: [
'import',
'@typescript-eslint',
'eslint-plugin-import-helpers',
// 'prettier',
'jest',
],
@@ -37,7 +34,6 @@ module.exports = {
},
rules: {
// 'prettier/prettier': 'warn',
'arrow-parens': ['error', 'as-needed', { 'requireForBlockBody': true }],
'curly': ['error', 'all'],
'class-methods-use-this': 'off',
@@ -76,12 +72,15 @@ module.exports = {
'no-restricted-globals': 'off',
'no-useless-constructor': 'off',
'max-classes-per-file': 'off',
'arrow-body-style': ['error', 'as-needed'],
'function-call-argument-newline': ['error', 'consistent'],
'max-len': [
'error',
{
code: 120,
ignoreTrailingComments: true,
ignoreStrings: true
ignoreStrings: true,
ignoreTemplateLiterals: true
}
],
'lines-between-class-members': 'off',
-9
View File
@@ -1,9 +0,0 @@
// we use prettier only for graphQL-codegen
module.exports = {
useTabs: false,
tabWidth: 2,
trailingComma: 'all',
singleQuote: false,
semi: true,
}
-1
View File
@@ -33,7 +33,6 @@
"@typescript-eslint/parser": "^2.8.0",
"eslint": "^6.6.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-import": "^2.18.2",
@@ -15,14 +15,6 @@
},
"peerDependencies": {
"@dbeaver/core": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
"@material/ripple": "^4.0.0",
"@material/theme": "^4.0.0",
"@material/typography": "^4.0.0",
"graphql": "^14.5.8",
"graphql-request": "^1.8.2",
"graphql-tag": "^2.10.1",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
@@ -22,14 +22,6 @@
},
"peerDependencies": {
"@dbeaver/core": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
"@material/ripple": "^4.0.0",
"@material/theme": "^4.0.0",
"@material/typography": "^4.0.0",
"graphql": "^14.5.8",
"graphql-request": "^1.8.2",
"graphql-tag": "^2.10.1",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
+18
View File
@@ -0,0 +1,18 @@
# dependencies
/node_modules
# testing
/coverage
# production
/dist
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,24 @@
{
"name": "@dbeaver/authentication",
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/types",
"scripts": {
"build-plugin": "npm run clean-dist && webpack -p --config=../../configs/webpack/build-plugin.js",
"clean-dist": "rimraf dist/*",
"rollup": "npm run clean-dist && rollup --config ../../configs/rollup.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"@dbeaver/core": "^0.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"reakit": "^1.0.0-beta.11",
"reshadow": "0.0.1-alpha.75",
"rxjs": "^6.5.5"
}
}
@@ -0,0 +1,72 @@
/*
* 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 { observable } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { SessionService } from '@dbeaver/core/root';
import { GraphQLService, UserAuthInfo } from '@dbeaver/core/sdk';
import { AuthProviderService } from './AuthProviderService';
@injectable()
export class AuthInfoService {
@observable private user: UserAuthInfo | null = null;
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private authProviderService: AuthProviderService,
private sessionService: SessionService,
) { }
get userInfo() {
return this.user;
}
async login(provider: string, credentials: object): Promise<UserAuthInfo> {
if (this.user) {
throw new Error('User already logged in');
}
const processedCredentials = this.authProviderService.processCredentials(provider, credentials);
const { user } = await this.graphQLService.gql.authLogin({
provider,
credentials: processedCredentials,
});
this.user = user;
await this.updateSession();
return this.user;
}
async logout() {
if (this.user) {
await this.graphQLService.gql.authLogout();
this.user = null;
await this.updateSession();
}
}
async updateAuthInfo(): Promise<UserAuthInfo | null> {
try {
const { user } = await this.graphQLService.gql.getSessionUser();
this.user = user || null;
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load session user');
}
return this.user;
}
private async updateSession() {
await this.sessionService.update();
}
}
@@ -0,0 +1,59 @@
/*
* 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 { SettingsMenuService, TopNavService } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { ServerService } from '@dbeaver/core/root';
import { AuthenticationService } from './AuthenticationService';
import { AuthInfoService } from './AuthInfoService';
import { AuthDialogService } from './Dialog/AuthDialogService';
import { UserInfo } from './UserInfo';
@injectable()
export class AuthMenuService {
constructor(
private serverService: ServerService,
private authDialogService: AuthDialogService,
private authInfoService: AuthInfoService,
private settingsMenuService: SettingsMenuService,
private authenticationService: AuthenticationService,
private topNavService: TopNavService,
) { }
register() {
this.settingsMenuService.addMenuItem(
SettingsMenuService.settingsMenuToken,
{
id: 'login',
order: 0,
isHidden: () => !this.serverService.config.data?.authenticationEnabled || !!this.authInfoService.userInfo,
title: 'authentication_login',
onClick: () => this.authDialogService.showLoginForm(),
}
);
this.settingsMenuService.addMenuItem(
SettingsMenuService.settingsMenuToken,
{
id: 'logout',
order: Number.MAX_SAFE_INTEGER,
isHidden: () => !this.serverService.config.data?.authenticationEnabled || !this.authInfoService.userInfo,
title: 'authentication_logout',
onClick: this.logout.bind(this),
}
);
this.topNavService.placeholder.add(UserInfo, 4);
}
private async logout() {
await this.authInfoService.logout();
await this.authenticationService.auth();
}
}
@@ -0,0 +1,52 @@
/*
* 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 { createHash } from 'crypto';
import { injectable } from '@dbeaver/core/di';
import {
GraphQLService, CachedResource, AuthProviderInfo
} from '@dbeaver/core/sdk';
export type AuthProvider = Omit<AuthProviderInfo, 'configurationParameters'>
@injectable()
export class AuthProviderService {
readonly providers = new CachedResource([], this.refreshAsync.bind(this), data => !!data.length)
constructor(
private graphQLService: GraphQLService,
) { }
processCredentials(providerId: string, credentials: any) {
const provider = this.providers.data.find(provider => provider.id === providerId);
if (!provider) {
return credentials;
}
const credentialsProcessed = { ...credentials };
for (const parameter of provider.credentialParameters) {
if (parameter.encryption === 'hash' && parameter.id in credentialsProcessed) {
const md5Hash = createHash('md5')
.update(credentialsProcessed[parameter.id])
.digest('hex')
.toUpperCase();
credentialsProcessed[parameter.id] = md5Hash;
}
}
return credentialsProcessed;
}
private async refreshAsync(data: AuthProvider[]): Promise<AuthProvider[]> {
const { providers } = await this.graphQLService.gql.getAuthProviders();
return providers;
}
}
@@ -0,0 +1,42 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { ServerService } from '@dbeaver/core/root';
import { AuthInfoService } from './AuthInfoService';
import { AuthDialogService } from './Dialog/AuthDialogService';
@injectable()
export class AuthenticationService {
constructor(
private serverService: ServerService,
private authDialogService: AuthDialogService,
private authInfoService: AuthInfoService,
) { }
async auth() {
const config = await this.serverService.config.load();
if (!config) {
throw new Error('Can\'t configure Authentication');
}
if (!config.authenticationEnabled) {
return;
}
const userInfo = await this.authInfoService.updateAuthInfo();
if (userInfo) {
return;
}
if (!config.anonymousAccessEnabled) {
await this.authDialogService.showLoginForm(true);
}
}
}
@@ -0,0 +1,25 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { AuthenticationService } from './AuthenticationService';
import { AuthMenuService } from './AuthMenuService';
@injectable()
export class Bootstrap {
constructor(
private authenticationService: AuthenticationService,
private authMenuService: AuthMenuService
) { }
async bootstrap() {
this.authMenuService.register();
await this.authenticationService.auth();
}
}
@@ -0,0 +1,134 @@
/*
* 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';
import styled, { css } from 'reshadow';
import {
SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle
} from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { DialogComponent, CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { composes, useStyles } from '@dbeaver/core/theming';
import { AuthDialogController } from './AuthDialogController';
import { AuthDialogFooter } from './AuthDialogFooter';
import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
const styles = composes(
css`
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
ErrorMessage {
composes: theme-background-secondary from global;
}
`,
css`
custom-connection {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
CommonDialogWrapper {
min-height: 400px;
min-width: 600px;
}
SubmittingForm {
overflow: auto;
margin: auto;
margin-top: 20px;
margin-bottom: 20px;
}
SubmittingForm, AuthProviderForm {
flex: 1;
display: flex;
flex-direction: column;
}
TabList {
box-sizing: border-box;
display: inline-flex;
width: 100%;
padding-left: 24px;
}
Tab {
composes: theme-typography--body2 from global;
text-transform: uppercase;
font-weight: normal;
&:global([aria-selected=true]) {
font-weight: normal !important;
}
}
AuthProviderForm {
flex-direction: column;
padding: 18px 24px;
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
`
);
export const AuthDialog: DialogComponent<null, null> = observer(
function AuthDialog(props) {
const controller = useController(AuthDialogController, props.rejectDialog);
const translate = useTranslate();
return styled(useStyles(styles))(
<TabsState currentTabId={controller.provider?.id || null}>
<CommonDialogWrapper
title={translate('authentication_login_dialog_title')}
noBodyPadding
header={(
<TabList>
{controller.providers.map(provider => (
<Tab
key={provider.id}
tabId={provider.id}
onOpen={() => controller.selectProvider(provider.id)}
>
<TabTitle title={provider.label} />
</Tab>
))}
</TabList>
)}
footer={(
<AuthDialogFooter
isAuthenticating={controller.isAuthenticating}
onLogin={controller.login}
/>
)}
onReject={props.options?.persistent ? undefined : props.rejectDialog}
>
<SubmittingForm onSubmit={controller.login}>
{controller.provider && (
<AuthProviderForm
provider={controller.provider}
credentials={controller.credentials}
authenticate={controller.isAuthenticating}
/>
)}
{!controller.provider && <>Select available provider</>}
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.showDetails}
/>
)}
</CommonDialogWrapper>
</TabsState>
);
}
);
@@ -0,0 +1,113 @@
/*
* 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 { observable, computed } from 'mobx';
import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GQLErrorCatcher } from '@dbeaver/core/sdk';
import { ErrorDetailsDialog } from '@dbeaver/core/src/app';
import { AuthInfoService } from '../AuthInfoService';
import { AuthProviderService, AuthProvider } from '../AuthProviderService';
@injectable()
export class AuthDialogController implements IInitializableController, IDestructibleController {
@observable provider: AuthProvider | null = null
@observable isAuthenticating = false;
@observable credentials = {};
get isLoading() {
return this.authProviderService.providers.isLoading();
}
@computed get providers(): AuthProvider[] {
return this.authProviderService
.providers
.data
.concat()
.sort(this.compareProviders);
}
readonly error = new GQLErrorCatcher();
private isDistructed = false;
private close!: () => void;
constructor(
private notificationService: NotificationService,
private authProviderService: AuthProviderService,
private authInfoService: AuthInfoService,
private commonDialogService: CommonDialogService,
) { }
init(onClose: () => void) {
this.close = onClose;
this.loadProviders();
}
destruct(): void {
this.isDistructed = true;
}
login = async () => {
if (!this.provider || this.isAuthenticating) {
return;
}
this.isAuthenticating = true;
try {
await this.authInfoService.login(this.provider.id, this.credentials);
this.close();
} catch (exception) {
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, 'Login failed');
}
} finally {
this.isAuthenticating = false;
}
}
selectProvider = (providerId: string) => {
if (providerId === this.provider?.id) {
return;
}
this.provider = this.authProviderService
.providers.data.find(provider => provider.id === providerId) || null;
this.credentials = {};
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
private async loadProviders() {
try {
await this.authProviderService.providers.load();
if (this.providers.length > 0) {
this.provider = this.providers[0];
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load auth providers');
}
}
private compareProviders = (providerA: AuthProvider, providerB: AuthProvider): number => {
if (providerA.defaultProvider === providerB.defaultProvider)
{
return providerA.label.localeCompare(providerB.label);
}
if (providerA.defaultProvider) {
return -1;
}
return 1;
}
}
@@ -0,0 +1,51 @@
/*
* 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';
import styled, { css } from 'reshadow';
import { Button } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
const styles = css`
controls {
display: flex;
height: 100%;
flex: 1;
align-items: center;
margin: auto;
justify-content: flex-end;
}
`;
export type Props = {
isAuthenticating: boolean;
onLogin(): void;
}
export const AuthDialogFooter = observer(
function AuthDialogFooter({
isAuthenticating,
onLogin,
}: Props) {
const translate = useTranslate();
return styled(styles)(
<controls as="div">
<Button
type="button"
mod={['unelevated']}
onClick={onLogin}
disabled={isAuthenticating}
>
{translate('authentication_login')}
</Button>
</controls>
);
}
);
@@ -0,0 +1,23 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { AuthDialog } from './AuthDialog';
@injectable()
export class AuthDialogService {
constructor(
private commonDialogService: CommonDialogService,
) { }
async showLoginForm(persistent = false) {
await this.commonDialogService.open(AuthDialog, null, { persistent });
}
}
@@ -0,0 +1,52 @@
/*
* 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';
import { useCallback } from 'react';
import styled from 'reshadow';
import { InputField } from '@dbeaver/core/blocks';
import { useStyles } from '@dbeaver/core/theming';
import { AuthProvider } from '../../AuthProviderService';
import { formStyles } from './formStyles';
type Props = {
provider: AuthProvider;
credentials: any;
authenticate: boolean;
}
export const AuthProviderForm = observer(function AuthProviderForm({
provider,
credentials,
authenticate,
}: Props) {
const handleChange = useCallback((key: string, value: string) => {
credentials[key] = value;
}, [credentials]);
return styled(useStyles(formStyles))(
<login-form as='div'>
{provider.credentialParameters.map(parameter => parameter.user && (
<group as="div" key={parameter.id}>
<InputField
type={parameter.encryption === 'none' ? 'text' : 'password'}
name={parameter.id}
value={credentials[parameter.id]}
onChange={value => handleChange(parameter.id, value)}
disabled={authenticate}
mod='surface'
>
{parameter.displayName}
</InputField>
</group>
))}
</login-form>
);
});
@@ -0,0 +1,25 @@
/*
* 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 { css } from 'reshadow';
export const formStyles = css`
login-form {
flex: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
group {
box-sizing: border-box;
display: flex;
margin: 0 12px;
}
`;
@@ -0,0 +1,58 @@
/*
* 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';
import styled, { css } from 'reshadow';
import { IconOrImage } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { composes, useStyles } from '@dbeaver/core/theming';
import { AuthInfoService } from './AuthInfoService';
const styles = composes(
css`
user {
composes: theme-ripple from global;
}
`,
css`
user {
height: 100%;
padding: 0 16px;
display: flex;
align-items: center;
}
IconOrImage {
display: block;
width: 24px;
}
user-name {
display: block;
line-height: initial;
margin-left: 8px;
}
`
);
export const UserInfo = observer(function UserInfo() {
const authInfoService = useService(AuthInfoService);
if (!authInfoService.userInfo) {
return null;
}
return styled(useStyles(styles))(
<user as='div'>
<user-icon as="div">
<IconOrImage icon='user' viewBox='0 0 28 28' />
</user-icon>
<user-name as='div'>{authInfoService.userInfo.userId}</user-name>
</user>
);
});
@@ -0,0 +1,8 @@
import { manifest } from './manifest';
export default manifest;
export * from './AuthenticationService';
export * from './AuthInfoService';
export * from './AuthProviderService';
export * from './Dialog/AuthDialogService';
@@ -0,0 +1,36 @@
/*
* 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 { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { AuthenticationService } from './AuthenticationService';
import { AuthInfoService } from './AuthInfoService';
import { AuthMenuService } from './AuthMenuService';
import { AuthProviderService } from './AuthProviderService';
import { Bootstrap } from './Bootstrap';
import { AuthDialogService } from './Dialog/AuthDialogService';
export const manifest: PluginManifest = {
info: {
name: 'Authentication',
},
providers: [
AuthenticationService,
AuthInfoService,
AuthProviderService,
AuthDialogService,
AuthMenuService,
],
async initialize(services: IServiceInjector) {
services
.resolveServiceByClass(Bootstrap)
.bootstrap();
},
};
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "dist/types",
"declaration": true,
"noEmit": false,
"outDir": "dist/js"
},
"exclude": [
"node_modules",
"@dbeaver/**"
],
"include": ["src/**/*"]
}
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"@dbeaver/**"
]
}
@@ -17,14 +17,6 @@
},
"peerDependencies": {
"@dbeaver/core": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
"@material/ripple": "^4.0.0",
"@material/theme": "^4.0.0",
"@material/typography": "^4.0.0",
"graphql": "^14.5.8",
"graphql-request": "^1.8.2",
"graphql-tag": "^2.10.1",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
@@ -10,6 +10,7 @@ import { MainMenuService, ConnectionDialogsService } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { PermissionsService, EPermission } from '@dbeaver/core/root';
import { BasicConnectionService } from './BasicConnectionService';
import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog';
@@ -17,13 +18,14 @@ import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog';
@injectable()
export class BasicConnectionPluginBootstrap {
private hasPreconfiguredConnection = false;
constructor(private connectionDialogsService: ConnectionDialogsService,
private mainMenuService: MainMenuService,
private basicConnectionService: BasicConnectionService,
private notificationService: NotificationService,
private commonDialogService: CommonDialogService) {
constructor(
private connectionDialogsService: ConnectionDialogsService,
private mainMenuService: MainMenuService,
private basicConnectionService: BasicConnectionService,
private commonDialogService: CommonDialogService,
private notificationService: NotificationService,
private permissionsService: PermissionsService
) {
}
bootstrap() {
@@ -33,9 +35,10 @@ export class BasicConnectionPluginBootstrap {
{
id: 'mainMenuConnect',
order: 2,
title: 'Preconfigured',
title: 'basicConnection_main_menu_item',
onClick: () => this.openConnectionsDialog(),
isDisabled: () => !this.hasPreconfiguredConnection,
isHidden: () => !this.permissionsService.has(EPermission.public),
isDisabled: () => !this.basicConnectionService.dbSources.data.length,
}
);
}
@@ -46,11 +49,9 @@ export class BasicConnectionPluginBootstrap {
private async loadDbSources() {
try {
const sources = await this.basicConnectionService.loadDBSourcesAsync();
this.hasPreconfiguredConnection = !!sources.length;
await this.basicConnectionService.dbSources.load();
} catch (error) {
this.notificationService.logException(error, 'DBSources loading failed');
}
}
}
@@ -6,30 +6,35 @@
* you may not use this file except in compliance with the License.
*/
import { Connection, DBSource, ConnectionsManagerService } from '@dbeaver/core/app';
import {
Connection, DBSource, ConnectionsManagerService
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { ConnectionConfig, GraphQLService } from '@dbeaver/core/sdk';
import { PermissionsService, EPermission } from '@dbeaver/core/root';
import { ConnectionConfig, GraphQLService, CachedResource } from '@dbeaver/core/sdk';
@injectable()
export class BasicConnectionService {
private dbSourcesCache: DBSource[] = [];
readonly dbSources = new CachedResource([], this.loadDBSourcesAsync.bind(this), data => !!data.length)
constructor(private graphQLService: GraphQLService,
private connectionsManagerService: ConnectionsManagerService) {
constructor(
private graphQLService: GraphQLService,
private connectionsManagerService: ConnectionsManagerService,
private permissionsService: PermissionsService
) {
this.permissionsService.onUpdate.subscribe(() => this.dbSources.refresh());
}
public getDBSources(): DBSource[] {
return this.dbSourcesCache;
return this.dbSources.data;
}
async loadDBSourcesAsync(): Promise<DBSource[]> {
if (this.dbSourcesCache.length > 0) {
return this.dbSourcesCache;
private async loadDBSourcesAsync(data: DBSource[]): Promise<DBSource[]> {
if (!await this.permissionsService.hasAsync(EPermission.public)) {
return [];
}
const { dataSourceList } = await this.graphQLService.gql.dataSourceList();
this.dbSourcesCache = dataSourceList;
return this.dbSourcesCache;
return dataSourceList;
}
async openConnectionAsync(config: ConnectionConfig): Promise<Connection> {
@@ -153,7 +153,7 @@ implements IInitializableController, IDestructibleController, IConnectionControl
private async loadDBSources() {
try {
await this.basicConnectionService.loadDBSourcesAsync();
await this.basicConnectionService.dbSources.load();
await this.connectionsManagerService.loadDriversAsync();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load database sources');
@@ -22,10 +22,8 @@ import { DBSourceSelector } from './DBSourceSelector/DBSourceSelector';
const styles = css`
CommonDialogWrapper {
display: flex;
flex-direction: column;
max-height: 330px;
min-height: 330px;
max-height: 500px;
min-height: 500px;
}
SubmittingForm, center {
display: flex;
@@ -15,8 +15,10 @@ import { useTranslate } from '@dbeaver/core/localization';
const styles = css`
controls {
display: flex;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
}
fill {
@@ -50,10 +50,6 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), {
"@dbeaver/core/product": path.resolve(__dirname, "../../../core/src/product"),
"@dbeaver/core/plugin": path.resolve(__dirname, "../../../core/src/plugin"),
// rewrite imports for unnecessary work in `web/src/libs/sdk.ts`
'graphql-tag': path.resolve(__dirname, '../../src/fix-gql.js'),
graphql: path.resolve(__dirname, '../../src/fix-gql.js'),
react: 'preact/compat',
react$: 'preact/compat',
'react-dom': 'preact/compat',
-5
View File
@@ -1,5 +0,0 @@
/**
* rewrite imports for unnecessary work in `webapp/packages/core/src/libs/sdk.ts`
*/
module.exports = strings => strings[0]
module.exports.print = v => v
@@ -12,6 +12,7 @@ module.exports = {
basicConnection_connectionDialog_connecting: 'Connecting...',
basicConnection_connectionDialog_connect: 'Connect',
basicConnection_connectionDialog_connecting_message: 'Connecting to database...',
basicConnection_main_menu_item: 'Preconfigured',
customConnection_connectionType_custom: 'Parameters',
customConnection_connectionType_url: 'URL',
@@ -42,6 +43,10 @@ module.exports = {
administration_menu_enter: 'Administration',
authentication_login_dialog_title: 'Authentication',
authentication_login: 'Login',
authentication_logout: 'Logout',
ui_stepper_back: 'Back',
ui_processing_loading: 'Loading...',
ui_processing_cancel: 'Cancel',
@@ -11,6 +11,7 @@ module.exports = {
basicConnection_connectionDialog_connecting: 'Подключение...',
basicConnection_connectionDialog_connect: 'Подключиться',
basicConnection_connectionDialog_connecting_message: 'Подключение к базе...',
basicConnection_main_menu_item: 'Подключиться',
customConnection_connectionType_custom: 'Параметры',
customConnection_connectionType_url: 'URL',
@@ -41,6 +42,10 @@ module.exports = {
administration_menu_enter: 'Администрирование',
authentication_login_dialog_title: 'Авторизация',
authentication_login: 'Войти',
authentication_logout: 'Выйти',
ui_stepper_back: 'Назад',
ui_processing_loading: 'Загрузка...',
ui_processing_cancel: 'Отменить',
+49 -41
View File
@@ -36,19 +36,15 @@
.__Tab {
composes: theme-typography--body2 from global;
position: relative;
display: flex;
flex-shrink: 0;
text-align: left;
align-items: center;
outline: none;
font-weight: normal;
cursor: pointer;
padding: 0;
height: 48px;
padding: 0 24px 0 12px;
border-top: solid 2px transparent;
&:global([aria-selected=true]) {
&:global([aria-selected="true"]) {
font-weight: 500;
cursor: auto;
border-top-color: $color-negative;
@@ -58,52 +54,64 @@
}
}
&:not(:global([aria-selected=true])) {
&:not(:global([aria-selected="true"])) {
background-color: transparent !important;
}
}
tab-icon {
height: 22px;
padding-right: 8px;
tab-container {
height: 100%;
display: flex;
flex-shrink: 0;
text-align: left;
align-items: center;
& StaticImage,
& img {
width: 22px;
}
& placeholder {
width: 22px;
tab-icon {
height: 22px;
padding-left: 12px;
StaticImage,
img {
width: 22px;
}
placeholder {
width: 22px;
height: 22px;
}
}
}
tab-title placeholder {
width: 80px;
height: 16px;
}
tab-title {
padding: 0 12px;
}
tab-action:before {
content: "";
display: block;
left: -2px;
top: -2px;
height: 12px;
width: 12px;
position: absolute;
cursor: pointer;
}
tab-title placeholder {
width: 80px;
height: 16px;
}
tab-action {
height: 8px;
width: 8px;
align-self: flex-start;
position: absolute;
top: 4px;
right: 4px;
& Icon {
tab-action:before {
content: "";
display: block;
left: -4px;
top: 0;
height: 16px;
width: 16px;
position: absolute;
cursor: pointer;
}
tab-action {
position: relative;
height: 8px;
width: 8px;
align-self: flex-start;
padding: 4px;
padding-left: 0;
Icon {
display: block;
}
}
}
+9 -2
View File
@@ -5,21 +5,28 @@ schema:
- ../../../server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls
- ../../../server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls
- ../../../server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls
- ../../../server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls
documents:
- ./src/**/*.gql
generates:
src/sdk/sdk.ts:
plugins:
- add: '/* eslint-disable */'
- add: '/* eslint-disable max-len */'
- typescript
- typescript-operations
- typescript-graphql-request
config:
skipTypename: true
documentMode : 'string'
avoidOptionals:
field: false
inputValue: false
object: false
maybeValue: T
scalars:
Object: any
JSON: "{ [key: string]: any }"
DateTime: any
hooks:
afterAllFileWrite:
- prettier --write --config ../../.prettierrc.js
- eslint --fix
@@ -4,14 +4,6 @@ const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = (env, argv) => merge(commonConfig(env, argv), {
resolve: {
alias: {
// rewrite imports for unnecessary work in `web/src/libs/sdk.ts`
'graphql-tag': path.join(__dirname, '../../fix-gql.js'),
graphql: path.join(__dirname, '../../fix-gql.js'),
},
},
context: path.resolve(__dirname, '../../src'),
plugins: [
new ForkTsCheckerWebpackPlugin({
-5
View File
@@ -1,5 +0,0 @@
/**
* rewrite imports for unnecessary work in `webapp/packages/core/src/libs/sdk.ts`
*/
module.exports = strings => strings[0]
module.exports.print = v => v
-2
View File
@@ -35,8 +35,6 @@
},
"peerDependencies": {
"go-split": "^1.3.2",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
+14 -20
View File
@@ -8,8 +8,6 @@
import {
ConnectionDialogsService,
ConnectionsManagerService,
NodesManagerService,
NavigationTabsService, NavigationTreeContextMenuService, LogViewerMenuService, ConnectionSchemaManagerService,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
@@ -19,6 +17,9 @@ import { LocalizationService } from '@dbeaver/core/localization';
import { PermissionsService } from '@dbeaver/core/root';
import { ThemeService } from '@dbeaver/core/theming';
import { SessionExpireService } from './dialogs';
import { LocalizationService } from './localization';
/**
* AppBootstrap.init() will be executed between first and second phase of App initialization,
* when application has registered all dependencies but plugins did'nt initialized.
@@ -28,19 +29,17 @@ import { ThemeService } from '@dbeaver/core/theming';
@injectable()
export class AppBootstrap {
constructor(private exceptionsCatcherService: ExceptionsCatcherService,
private localizationService: LocalizationService,
private themeService: ThemeService,
private connectionDialogService: ConnectionDialogsService,
private connectionsManager: ConnectionsManagerService,
private logViewerMenuService: LogViewerMenuService,
private sessionExpireService: SessionExpireService,
private navigationTreeContextMenuService: NavigationTreeContextMenuService,
private nodesManagerService: NodesManagerService,
private navigationTabsService: NavigationTabsService,
private connectionSchemaManagerService: ConnectionSchemaManagerService,
private permissionsService: PermissionsService) {
}
constructor(
private exceptionsCatcherService: ExceptionsCatcherService,
private localizationService: LocalizationService,
private themeService: ThemeService,
private connectionDialogService: ConnectionDialogsService,
private logViewerMenuService: LogViewerMenuService,
private sessionExpireService: SessionExpireService,
private navigationTreeContextMenuService: NavigationTreeContextMenuService,
private navigationTabsService: NavigationTabsService,
private connectionSchemaManagerService: ConnectionSchemaManagerService,
) { }
async init() {
this.exceptionsCatcherService.subscribe();
@@ -48,16 +47,11 @@ export class AppBootstrap {
await this.localizationService.init();
await this.themeService.init();
await this.permissionsService.update();
this.connectionSchemaManagerService.registerCallbacks();
this.navigationTreeContextMenuService.registerMenuItems();
this.connectionDialogService.registerMenuItems();
this.logViewerMenuService.registerMenuItems();
await this.connectionsManager.restoreConnections();
await this.nodesManagerService.updateRootChildren();
}
async doAfterPluginsInit() {
@@ -9,6 +9,8 @@
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { usePermission, EPermission } from '@dbeaver/core/root';
import { useChildren } from '../shared/useChildren';
import { NavigationTreeNode } from './NavigationTreeNode/NavigationTreeNode';
@@ -38,6 +40,11 @@ const navigationTreeStyles = css`
export const NavigationTree = observer(function NavigationTree() {
const nodeChildren = useChildren();
const isEnabled = usePermission(EPermission.public);
if (!isEnabled) {
return null;
}
if (!nodeChildren || nodeChildren.children.length === 0) {
return styled(navigationTreeStyles)(
@@ -15,8 +15,10 @@ export const styles = css`
}
controls {
display: flex;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
flex-direction: row-reverse;
}
Button {
@@ -0,0 +1,19 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
const styles = css`
fill {
flex: 1;
}
`;
export function Fill() {
return styled(styles)(<fill as='div' />);
}
@@ -0,0 +1,43 @@
/*
* 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';
import styled, { css } from 'reshadow';
import { Icon } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { ServerService } from '@dbeaver/core/root';
declare const version: string; // declared in webpack DefinePlugin // todo move to enviroment?
const styles = css`
logo {
height: 100%;
display: flex;
align-items: center;
margin-right: 16px;
width: 250px;
}
Icon {
height: 24px;
width: auto;
margin-bottom: 2px;
}
`;
export const Logo = observer(function Logo() {
const serverService = useService(ServerService);
const title = `Frontend: ${version}\nBackend: ${serverService.config.data?.version}`;
return styled(styles)(
<logo as="div" title={title}>
<Icon name="logo" viewBox="0 0 361 73" />
</logo>
);
});
@@ -25,7 +25,6 @@ export const settingsMenuStyles = css`
}
menu-panel-item {
flex-direction: row-reverse;
& menu-item-text {
text-align: right;
}
@@ -6,34 +6,21 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import styled from 'reshadow';
import { Icon } from '@dbeaver/core/blocks';
import { Placeholder } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { ServerService } from '@dbeaver/core/root';
import { useStyles } from '@dbeaver/core/theming';
import { ConnectionSelector } from './ConnectionSchemaManager/ConnectionSelector/ConnectionSelector';
import { MainMenu } from './MainMenu/MainMenu';
import { SettingsMenu } from './SettingsMenu/SettingsMenu';
import { TopNavService } from './TopNavBarService';
import { topNavBarStyles } from './topNavBarStyles';
declare const version: string; // declared in webpack DefinePlugin // todo move to enviroment?
export const TopNavBar = observer(function TopNavBar() {
const serverService = useService(ServerService);
const title = `Frontend: ${version}\nBackend: ${serverService.config.data?.version}`;
export function TopNavBar() {
const topNavBarService = useService(TopNavService);
return styled(useStyles(topNavBarStyles))(
<header>
<logo as="div" title={title}>
<Icon name="logo" viewBox="0 0 361 73" />
</logo>
<MainMenu />
<ConnectionSelector/>
<SettingsMenu />
<Placeholder container={topNavBarService.placeholder} />
</header>
);
});
}
@@ -0,0 +1,29 @@
/*
* 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 { PlaceholderContainer } from '@dbeaver/core/blocks';
import { injectable } from '@dbeaver/core/di';
import { ConnectionSelector } from './ConnectionSchemaManager/ConnectionSelector/ConnectionSelector';
import { Fill } from './Fill';
import { Logo } from './Logo';
import { MainMenu } from './MainMenu/MainMenu';
import { SettingsMenu } from './SettingsMenu/SettingsMenu';
@injectable()
export class TopNavService {
readonly placeholder = new PlaceholderContainer();
constructor() {
this.placeholder.add(Logo, 0);
this.placeholder.add(MainMenu, 1);
this.placeholder.add(ConnectionSelector, 2);
this.placeholder.add(Fill, 3);
this.placeholder.add(SettingsMenu, 4);
}
}
@@ -25,18 +25,5 @@ export const topNavBarStyles = composes(
padding: 0 16px;
z-index: 1;
}
logo {
height: 100%;
display: flex;
align-items: center;
margin-right: 16px;
width: 250px;
& Icon {
height: 24px;
width: auto;
margin-bottom: 2px;
}
}
`
);
+2 -1
View File
@@ -1,7 +1,8 @@
// Services
export * from './TopNavBar/MainMenu/MainMenuService';
export * from './TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService';
export * from './TopNavBar/SettingsMenu/SettingsMenuService';
export * from './TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService';
export * from './TopNavBar/TopNavBarService';
export * from './shared/ConnectionsManager/ConnectionsManagerService';
export * from './shared/ConnectionsManager/ConnectionDialogsService';
export * from './shared/ConnectionsManager/extensions/IConnectionProvider';
@@ -65,9 +65,12 @@ export class ConnectionsManagerService {
onOpenConnection = new Subject<Connection>();
onCloseConnection = new Subject<string>();
constructor(private graphQLService: GraphQLService,
private nodesManagerService: NodesManagerService,
private sessionService: SessionService) {
constructor(
private graphQLService: GraphQLService,
private nodesManagerService: NodesManagerService,
private sessionService: SessionService
) {
this.sessionService.onUpdate.subscribe(this.restoreConnections.bind(this));
}
getDBDrivers(): Map<string, DBDriver> {
@@ -114,9 +117,8 @@ export class ConnectionsManagerService {
}
async closeConnectionAsync(id: string, skipNodesRefresh?: boolean): Promise<void> {
await this.nodesManagerService.closeConnection(id);
this.onCloseConnection.next(id);
await this.graphQLService.gql.closeConnection({ id });
await this.afterConnectionClose(id);
this.connectionsMap.delete(id);
if (!skipNodesRefresh) {
@@ -129,16 +131,29 @@ export class ConnectionsManagerService {
return data.get(connectionId)!;
}
async restoreConnections() {
private async afterConnectionClose(id: string) {
await this.nodesManagerService.closeConnection(id);
this.onCloseConnection.next(id);
}
private async restoreConnections() {
const config = await this.sessionService.session.load();
if (!config) {
return;
}
let connectionsToRemove = this.connections.concat();
// TODO: connections must be string[]
for (const connection of config.connections) {
await this.restoreConnection(connection);
connectionsToRemove = connectionsToRemove.filter(({ id }) => id !== connection.id);
}
for (const connection of connectionsToRemove) {
await this.afterConnectionClose(connection.id);
this.connectionsMap.delete(connection.id);
}
await this.nodesManagerService.updateRootChildren();
}
@@ -187,6 +202,9 @@ export class ConnectionsManagerService {
// TODO: Must be loaded based on connection id
// const { connection } = await this.graphQLService.gql.connectionState({ id });
if (this.connectionsMap.has(connection.id)) {
return;
}
this.connectionsMap.set(connection.id, connection);
this.onOpenConnection.next(connection);
}
@@ -20,8 +20,10 @@ import { useStyles } from '@dbeaver/core/theming';
const styles = css`
controls {
display: flex;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
flex-direction: row-reverse;
}
Button {
@@ -7,6 +7,7 @@
*/
import { injectable } from '@dbeaver/core/di';
import { PermissionsService, EPermission } from '@dbeaver/core/root';
import { DatabaseObjectInfo, GraphQLService } from '@dbeaver/core/sdk';
import { INavigator } from '../Navigation/INavigator';
@@ -51,7 +52,8 @@ export class NodesManagerService {
private nodesStore = new NodesStore();
constructor(private graphQLService: GraphQLService,
private navigationService: NavigationService) {
private navigationService: NavigationService,
private permissionsService: PermissionsService) {
this.navigator = this.navigationService.createNavigator<INodeNavigationData>(
data => data.nodeId,
@@ -134,7 +136,10 @@ export class NodesManagerService {
}
async updateRootChildren() {
return this.updateChildren(ROOT_NODE_PATH);
if (!await this.permissionsService.hasAsync(EPermission.public)) {
return;
}
this.updateChildren(ROOT_NODE_PATH);
}
async updateChildrenDatabaseObjectInfo(nodeId: string) {
@@ -10,6 +10,7 @@ import { action, observable } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { PermissionsService, EPermission } from '@dbeaver/core/root';
import { GraphQLService } from '@dbeaver/core/sdk';
import { CoreSettingsService } from '@dbeaver/core/settings';
import { uuid } from '@dbeaver/core/utils';
@@ -19,23 +20,29 @@ import { ILogEntry } from './ILogEntry';
@injectable()
export class LogViewerService {
@observable isActive = false;
@observable _isActive = false;
get isActive() {
return this._isActive;
}
@observable private log: ILogEntry[] = [];
private interval: any = null;
constructor(private graphQLService: GraphQLService,
private coreSettingsService: CoreSettingsService,
private notificationService: NotificationService) {
constructor(
private graphQLService: GraphQLService,
private coreSettingsService: CoreSettingsService,
private notificationService: NotificationService,
private permissionsService: PermissionsService,
) {
this.permissionsService.onUpdate.subscribe(this.stopIfHasNoPermission.bind(this));
}
toggle() {
if (this.isActive) {
if (this._isActive) {
this.stopLog();
this.isActive = false;
} else {
this.startLog();
this.isActive = true;
}
}
@@ -44,9 +51,13 @@ export class LogViewerService {
}
async startLog() {
if (this.interval) {
if (this._isActive) {
return;
}
if (!this.isLogViewerAvailable()) {
throw new Error('Access denied');
}
this._isActive = true;
await this.updateLog();
const refreshInterval = this.coreSettingsService.settings.getValue('app.logViewer.refreshInterval');
this.interval = setInterval(() => {
@@ -59,6 +70,7 @@ export class LogViewerService {
clearInterval(this.interval);
this.interval = null;
}
this._isActive = false;
}
@action
@@ -66,24 +78,6 @@ export class LogViewerService {
this.log = [];
}
private async updateLog() {
try {
const newEntries = await this.loadLog();
this.addNewEntries(newEntries);
} catch (e) {
this.notificationService.logException(e, 'Failed to load log');
}
}
@action
addNewEntries(entries: ILogEntry[]) {
this.log.unshift(...entries.reverse());
const maxLogEntries = this.coreSettingsService.settings.getValue('app.logViewer.maxLogEntries');
if (this.log.length > maxLogEntries) {
this.log.splice(maxLogEntries, this.log.length - maxLogEntries);
}
}
async loadLog(): Promise<ILogEntry[]> {
const maxLogEntries = this.coreSettingsService.settings.getValue('app.logViewer.maxLogEntries');
const { log } = await this.graphQLService.gql.readSessionLog({
@@ -96,4 +90,36 @@ export class LogViewerService {
}));
return entries;
}
@action
private addNewEntries(entries: ILogEntry[]) {
this.log.unshift(...entries.reverse());
const maxLogEntries = this.coreSettingsService.settings.getValue('app.logViewer.maxLogEntries');
if (this.log.length > maxLogEntries) {
this.log.splice(maxLogEntries, this.log.length - maxLogEntries);
}
}
private async updateLog() {
if (!this.isLogViewerAvailable()) {
return;
}
try {
const newEntries = await this.loadLog();
this.addNewEntries(newEntries);
} catch (e) {
this.notificationService.logException(e, 'Failed to load log');
}
}
private stopIfHasNoPermission() {
if (this._isActive && !this.isLogViewerAvailable()) {
this.stopLog();
}
}
private isLogViewerAvailable() {
return this.permissionsService.has(EPermission.public);
}
}
@@ -0,0 +1,27 @@
/*
* 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';
import { PlaceholderContainer } from './PlaceholderContainer';
type Props<T = unknown> = T extends unknown ? {
container: PlaceholderContainer<T>;
context?: T;
} : {
container: PlaceholderContainer<T>;
context: T;
}
export const Placeholder = observer(function Placeholder<T = unknown>({ container, context }: Props<T>) {
return (
<>
{container.get().map(({ id, component: Component }) => <Component key={id} context={context!} />)}
</>
);
});
@@ -0,0 +1,57 @@
/*
* 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 { observable } from 'mobx';
import { uuid } from '@dbeaver/core/utils';
export type PlaceholderProps<T = unknown> = {
context: T;
}
export type PlaceholderComponent<T = unknown> = React.FunctionComponent<PlaceholderProps<T>>
export type PlaceholderElement<T = unknown> = {
id: string;
component: PlaceholderComponent<T>;
order?: number;
}
export class PlaceholderContainer<T = unknown> {
private placeholders = observable<PlaceholderElement<T>>([], { deep: false });
get() {
return this.placeholders;
}
add(component: PlaceholderComponent<T>, order?: number) {
const placeholder: PlaceholderElement<T> = {
id: uuid(),
component,
order,
};
if (!order) {
this.placeholders.push(placeholder);
return;
}
this.placeholders.splice(this.findPosition(order), 0, placeholder);
}
private findPosition(order: number) {
const position = this.placeholders
.findIndex(placeholder => placeholder.order === undefined || order <= placeholder.order);
if (position === -1) {
return this.placeholders.length;
}
return position;
}
}
@@ -8,7 +8,8 @@
export interface IProperty {
id: string;
name: string;
key: string;
displayName?: string;
defaultValue?: string;
description?: string;
validValues?: string[];
@@ -26,7 +26,7 @@ type PropertiesState = {
type PropertiesTableProps = {
properties: IProperty[];
propertiesState?: PropertiesState;
onNameChange?(id: string, name: string): void;
onKeyChange?(id: string, name: string): void;
onChange?(state: PropertiesState): void;
onAdd?(): void;
onRemove?(id: string): void;
@@ -36,7 +36,7 @@ type PropertiesTableProps = {
export const PropertiesTable = observer(function PropertiesTable({
propertiesState,
properties,
onNameChange,
onKeyChange,
onChange,
onAdd,
onRemove,
@@ -44,22 +44,22 @@ export const PropertiesTable = observer(function PropertiesTable({
}: PropertiesTableProps) {
const translate = useTranslate();
const state = useLocalStore<PropertiesState>(() => (propertiesState || {}));
const changeName = useCallback((id: string, name: string) => {
const changeName = useCallback((id: string, key: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
if (state[property.name] !== undefined) {
state[name] = state[property.name];
delete state[property.name];
if (state[property.key] !== undefined) {
state[key] = state[property.key];
delete state[property.key];
}
if (onNameChange) {
onNameChange(property.name, name);
if (onKeyChange) {
onKeyChange(property.key, key);
}
property.name = name;
property.key = key;
}, [properties]);
const changeValue = useCallback((id: string, value: string) => {
@@ -69,7 +69,7 @@ export const PropertiesTable = observer(function PropertiesTable({
return;
}
state[property.name] = value;
state[property.key] = value;
if (onChange) {
onChange(state);
@@ -83,8 +83,8 @@ export const PropertiesTable = observer(function PropertiesTable({
return;
}
if (state[property.name] !== undefined) {
delete state[property.name];
if (state[property.key] !== undefined) {
delete state[property.key];
}
if (onRemove) {
@@ -93,8 +93,8 @@ export const PropertiesTable = observer(function PropertiesTable({
properties.splice(properties.indexOf(property), 1);
}, [properties, onRemove]);
const isNameUnique = useCallback(
(name: string) => properties.filter(property => property.name === name).length === 1,
const isKeyUnique = useCallback(
(key: string) => properties.filter(property => property.key === key).length === 1,
[]
);
@@ -116,11 +116,11 @@ export const PropertiesTable = observer(function PropertiesTable({
<PropertyItem
key={property.id}
property={property}
value={state[property.name]}
value={state[property.key]}
onNameChange={changeName}
onValueChange={changeValue}
onRemove={removeProperty}
error={!isNameUnique(property.name)}
error={!isKeyUnique(property.key)}
/>
))}
</properties-list>
@@ -80,7 +80,7 @@ const styles = composes(
&[|edited] {
font-weight: 600;
}
&:not(:focus):not([|focus]) {
&:global([readonly]), &:not(:focus):not([|focus]) {
background: transparent;
border: solid 1px transparent;
}
@@ -105,7 +105,7 @@ const styles = composes(
`
);
type PropertyItemProps = {
type Props = {
property: IProperty;
value?: string;
onNameChange(staticId: string, newId: string): void;
@@ -121,12 +121,12 @@ export const PropertyItem = observer(function PropertyItem({
onValueChange,
onRemove,
error,
}: PropertyItemProps) {
const isEditable = property.name !== property.id;
}: Props) {
const isKeyEditable = !property.displayName;
const edited = value !== undefined && value !== property.defaultValue;
const [focus, setFocus] = useState(false);
const nameInputRef = useRef<HTMLInputElement>(null);
const handleNameChange = useCallback((value: string) => onNameChange(property.id, value), [property]);
const keyInputRef = useRef<HTMLInputElement>(null);
const handleKeyChange = useCallback((key: string) => onNameChange(property.id, key), [property]);
const handleValueChange = useCallback(
(value: string) => onValueChange(property.id, value),
[property]
@@ -134,8 +134,8 @@ export const PropertyItem = observer(function PropertyItem({
const handleRemove = useCallback(() => onRemove(property.id), [property]);
useEffect(() => {
if (nameInputRef.current && isEditable) {
nameInputRef.current.focus();
if (keyInputRef.current && isKeyEditable) {
keyInputRef.current.focus();
}
}, []);
@@ -145,12 +145,12 @@ export const PropertyItem = observer(function PropertyItem({
<ShadowInput
type='text'
name={property.id}
onChange={handleNameChange}
ref={nameInputRef}
readOnly={!isEditable}
onChange={handleKeyChange}
ref={keyInputRef}
readOnly={!isKeyEditable}
autoComplete='none'
>
{property.name || property.id}
{property.displayName || property.key}
</ShadowInput>
</property-name>
<property-value as='div'>
@@ -176,7 +176,7 @@ export const PropertyItem = observer(function PropertyItem({
</DriverPropertyValueSelector>
</property-select>
)}
{isEditable && (
{isKeyEditable && (
<property-remove as="div">
<button type="button" onClick={handleRemove}><Icon name="reject" viewBox="0 0 11 11" /></button>
</property-remove>
@@ -18,7 +18,7 @@ import { TabsContext } from '../TabsContext';
type TabProps = PropsWithChildren<{
tabId: string;
className?: string;
onOpen: (tabId: string) => void;
onOpen?: (tabId: string) => void;
onClose?: (tabId: string) => void;
}>
@@ -36,8 +36,10 @@ export function Tab({
const handleOpen = useCallback((e: React.MouseEvent<any>) => {
e.preventDefault();
onOpen(tabId);
}, []);
if (onOpen) {
onOpen(tabId);
}
}, [onOpen]);
const handleClose = useCallback((e: React.MouseEvent<any>) => {
e.preventDefault();
@@ -54,12 +56,14 @@ export function Tab({
className={className}
onClick={handleOpen}
>
{children}
{onClose && (
<tab-action as="div" onClick={handleClose}>
<Icon name="cross-bold" viewBox="0 0 7 8"/>
</tab-action>
)}
<tab-container as='div'>
{children}
{onClose && (
<tab-action as="div" onClick={handleClose}>
<Icon name="cross-bold" viewBox="0 0 7 8"/>
</tab-action>
)}
</tab-container>
</BaseTab>
);
}
@@ -0,0 +1,22 @@
/*
* 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 { useContext } from 'react';
import { TabList as BaseTabList, TabListOptions } from 'reakit/Tab';
import { TabsContext } from './TabsContext';
export function TabList(props: React.PropsWithChildren<TabListOptions>) {
const state = useContext(TabsContext);
if (!state) {
throw new Error('Tabs context was not provided');
}
return <BaseTabList {...props} {...state} />;
}
@@ -6,15 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import {
useMemo, PropsWithChildren, ReactNode, useEffect,
} from 'react';
import { useTabState } from 'reakit/Tab';
import { PropsWithChildren, ReactNode } from 'react';
import styled from 'reshadow';
import { useStyles, Style } from '@dbeaver/core/theming';
import { TabsContext } from '../TabsContext';
import { TabsState } from '../TabsState';
type TabsBoxProps = PropsWithChildren<{
currentTabId: string;
@@ -30,16 +27,9 @@ export function TabsBox({
className,
style = [],
}: TabsBoxProps) {
const state = useTabState({
selectedId: currentTabId,
manual: true,
});
state.selectedId = currentTabId;
useEffect(() => state.select(currentTabId), [currentTabId]); // hack currentId and selectedId not works
const value = useMemo(() => state, Object.values(state));
return styled(useStyles(...style))(
<TabsContext.Provider value={value}>
<TabsState currentTabId={currentTabId}>
<tabs-box as="div" className={className}>
{tabs && (
<tabs as="div">
@@ -50,6 +40,6 @@ export function TabsBox({
{children}
</tab-panels>
</tabs-box>
</TabsContext.Provider>
</TabsState>
);
}
@@ -0,0 +1,40 @@
/*
* 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 { useEffect, useMemo } from 'react';
import { useTabState } from 'reakit/Tab';
import { TabsContext } from './TabsContext';
type Props = React.PropsWithChildren<{
selectedId?: string;
currentTabId?: string | null;
}>
export function TabsState({ selectedId, currentTabId, children }: Props) {
const state = useTabState({
selectedId: selectedId || currentTabId,
manual: true,
});
if (currentTabId) {
state.selectedId = currentTabId;
}
useEffect(() => {
if (!currentTabId) {
return;
}
state.select(currentTabId);
}, [currentTabId]); // hack currentId and selectedId not works
const value = useMemo(() => state, Object.values(state));
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}
@@ -1,6 +1,8 @@
export * from './Tab';
export * from './TabPanel';
export * from './TabsContext';
export * from './TabsState';
export * from './TabList';
export * from './ITab';
+2
View File
@@ -18,3 +18,5 @@ export * from './SanitizedHTML/SanitizedHTML';
export * from './ItemList';
export * from './PropertiesTable';
export * from './IconButton';
export * from './Placeholder/Placeholder';
export * from './Placeholder/PlaceholderContainer';
@@ -10,8 +10,13 @@ import { observable } from 'mobx';
import { injectable } from '@dbeaver/core/di';
export type DialogOptions = {
persistent?: boolean;
}
export type DialogComponentProps<TPayload, TResult> = {
payload: TPayload;
options?: DialogOptions;
resolveDialog(result: TResult | null): void;
rejectDialog(): void; // the dialog was closed by cancel button or backdrop click
}
@@ -23,6 +28,7 @@ export type DialogComponent<TPayload, TResult> = React.ElementType<
export interface DialogInternal {
component: DialogComponent<any, any>;
payload: any;
options?: DialogOptions;
resolve(result: any): void;
}
@@ -32,14 +38,16 @@ export class CommonDialogService {
// note that if dialog is closed by user it will be resolved with null
async open<TPayload, TResult>(
dialog: DialogComponent<TPayload, TResult>,
payload: TPayload
component: DialogComponent<TPayload, TResult>,
payload: TPayload,
options?: DialogOptions,
): Promise<TResult | null> {
return new Promise<TResult>((resolve, reject) => {
const dialogInternal: DialogInternal = {
component: dialog,
component,
payload,
resolve,
options,
};
this.dialogs.push(dialogInternal);
});
@@ -13,7 +13,7 @@ import { composes, useStyles } from '@dbeaver/core/theming';
export type CommonDialogWrapperProps = {
title: string;
onReject: () => any;
onReject?: () => void;
className?: string;
noBodyPadding?: boolean;
footer?: JSX.Element | boolean;
@@ -39,6 +39,7 @@ const style = composes(
padding: 0;
margin: 0;
border: none;
height: auto;
min-width: 748px;
max-height: 100%;
border-radius: 0.25rem;
@@ -54,6 +55,8 @@ const style = composes(
}
dialog-body {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
padding: 18px 24px;
@@ -80,6 +83,7 @@ const style = composes(
}
footer {
composes: theme-elevation-z10 from global;
display: flex;
z-index: 0;
box-sizing: border-box;
min-height: 72px;
@@ -99,17 +103,19 @@ export function CommonDialogWrapper({
}: CommonDialogWrapperProps) {
return styled(useStyles(style))(
<dialog>
<dialog className={className}>
<header>
<header-title as="div">
<h1>{title}</h1>
<reject as="div">
<Icon name="cross" viewBox="0 0 16 16" onClick={onReject} />
</reject>
{onReject && (
<reject as="div">
<Icon name="cross" viewBox="0 0 16 16" onClick={onReject} />
</reject>
)}
</header-title>
{header}
</header>
<dialog-body as="div" className={className} {...use({ noPadding: noBodyPadding })}>{children}</dialog-body>
<dialog-body as="div" {...use({ noPadding: noBodyPadding })}>{children}</dialog-body>
<footer>
{footer}
</footer>
@@ -13,6 +13,7 @@ import {
DialogBackdrop,
useDialogState,
} from 'reakit/Dialog';
import { Portal } from 'reakit/Portal';
import styled from 'reshadow';
import { useService } from '@dbeaver/core/di';
@@ -63,18 +64,26 @@ function NestedDialog(props: NestedDialogType) {
(result: any) => props.resolveDialog(props.dialog, result),
[props.dialog, props.resolveDialog]
);
const backdropClickCallback = useCallback(() => {
if (!props.dialog.options?.persistent) {
handleReject();
}
}, [props.dialog.options?.persistent, handleReject]);
const DialogComponent = props.dialog.component;
// TODO: place Dialog inside CommonDialogWrapper, so we can pass aria-label
return styled(styles)(
<Dialog {...dialogState} aria-label="can't be provided" tabIndex={0}>
<DialogBackdrop {...dialogState} onClick={handleReject} />
<DialogComponent
payload={props.dialog.payload}
resolveDialog={handleResolve}
rejectDialog={handleReject}
/>
</Dialog>
<>
<Portal><DialogBackdrop {...dialogState} onClick={backdropClickCallback} /></Portal>
<Dialog {...dialogState} aria-label="can't be provided" tabIndex={0} >
<DialogComponent
payload={props.dialog.payload}
options={props.dialog.options}
resolveDialog={handleResolve}
rejectDialog={handleReject}
/>
</Dialog>
</>
);
}
@@ -11,24 +11,23 @@ import { css } from 'reshadow';
export const dialogStyles = css`
DialogBackdrop {
box-sizing: border-box;
background-color: #0000007a;
background-color: rgba(0,0,0,0.48);
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
z-index: 999;
}
Dialog {
box-sizing: border-box;
display: flex;
position: fixed;
align-items: center;
justify-content: center;
top: 0;
padding: 54px;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
max-height: calc(100vh);
transform: translate(-50%, -50%);
z-index: 999;
}
`;
@@ -26,9 +26,11 @@ const styles = css`
}
controls {
display: flex;
justify-content: flex-end;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
justify-content: flex-end;
}
`;
+2 -1
View File
@@ -11,7 +11,7 @@ import {
MainMenuService, NavigationService, NavigationTreeService, NodesManagerService,
ConnectionSchemaManagerService, NavigationTabsService,
NavigationTreeContextMenuService,
SettingsMenuService, LogViewerService, LogViewerMenuService,
SettingsMenuService, LogViewerService, LogViewerMenuService, TopNavService,
} from '@dbeaver/core/app';
import { PluginManifest } from '@dbeaver/core/di';
import { CommonDialogService, ContextMenuService, SessionExpireService } from '@dbeaver/core/dialogs';
@@ -55,6 +55,7 @@ export const coreManifest: PluginManifest = {
LogViewerMenuService,
LogViewerService,
MainMenuService,
TopNavService,
NavigationService,
NavigationTabsService,
NavigationTreeContextMenuService,
@@ -0,0 +1,12 @@
query authLogin(
$provider: ID!
$credentials: Object!
) {
user: authLogin(provider: $provider, credentials: $credentials) {
userId
displayName
authProvider
loginTime
message
}
}
@@ -0,0 +1,3 @@
query authLogout {
authLogout
}
@@ -0,0 +1,31 @@
query getAuthProviders {
providers: authProviders {
id
label
icon
description
defaultProvider
#configurationParameters {
# id
# displayName
# description
# category
# dataType
# value
# validValues
# defaultValue
# features
#}
credentialParameters {
id
displayName
description
admin
user
possibleValues
encryption
}
}
}
@@ -0,0 +1,9 @@
query getSessionUser {
user: sessionUser {
userId
displayName
authProvider
loginTime
message
}
}
@@ -6,31 +6,54 @@
* you may not use this file except in compliance with the License.
*/
import { Subject, Observable } from 'rxjs';
import { injectable } from '@dbeaver/core/di';
import { GraphQLService, CachedResource } from '@dbeaver/core/sdk';
import { SessionService } from './SessionService';
type PermissionsMetadata = {
loaded: boolean;
}
export enum EPermission {
public = 'public'
}
@injectable()
export class PermissionsService {
readonly onUpdate: Observable<unknown>;
private updateSubject: Subject<unknown>;
private permissions = new CachedResource(
new Map(),
this.refreshAsync.bind(this),
(_, { loaded }) => loaded
);
constructor(
private graphQLService: GraphQLService,
) { }
private sessionService: SessionService,
) {
this.updateSubject = new Subject();
this.onUpdate = this.updateSubject.asObservable();
this.sessionService.onUpdate.subscribe(this.update.bind(this));
}
has(id: string): boolean {
return this.permissions.data.has(id);
}
async hasAsync(id: string): Promise<boolean> {
const permissions = await this.permissions.load();
return permissions.has(id);
}
async update() {
await this.permissions.refresh();
this.updateSubject.next();
}
private async refreshAsync(
@@ -33,8 +33,7 @@ export class ServerService {
this.settings.clear();
parseJSONFlat(
serverConfig.productConfiguration,
this.settings.setSelfValue.bind(this.settings),
undefined
this.settings.setSelfValue.bind(this.settings)
);
}
return serverConfig;
@@ -6,6 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { Subject, Observable } from 'rxjs';
import { injectable } from '@dbeaver/core/di';
import {
ConnectionInfo, GraphQLService, SessionInfo, CachedResource,
@@ -21,12 +23,23 @@ export type SessionState = Pick<SessionInfo, 'createTime' | 'cacheExpired' | 'la
export class SessionService {
readonly session = new CachedResource(undefined, this.refreshSessionStateAsync.bind(this), data => !!data);
readonly settings = new SessionSettingsService('session_settings');
readonly onUpdate: Observable<unknown>;
constructor(private graphQLService: GraphQLService) {}
private updateSubject: Subject<unknown>;
constructor(private graphQLService: GraphQLService) {
this.updateSubject = new Subject();
this.onUpdate = this.updateSubject.asObservable();
}
async update() {
await this.session.refresh();
}
private async refreshSessionStateAsync(data: SessionState | undefined): Promise<SessionState> {
const { session } = await this.graphQLService.gql.openSession();
this.updateSubject.next();
return session;
}
}
+1
View File
@@ -3,3 +3,4 @@ export * from './ServerService';
export * from './PermissionsService';
export * from '../settings/ServerSettingsService';
export * from '../settings/SessionSettingsService';
export * from './usePermission';
@@ -8,6 +8,8 @@ query serverConfig {
supportsCustomConnections
supportsConnectionBrowser
supportsWorkspaces
anonymousAccessEnabled
authenticationEnabled
supportedLanguages {
isoCode
displayName
@@ -0,0 +1,9 @@
import { useService } from '@dbeaver/core/di';
import { PermissionsService } from './PermissionsService';
export function usePermission(key: string) {
const permissionsService = useService(PermissionsService);
return permissionsService.has(key);
}
@@ -0,0 +1,34 @@
/*
* 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 { observable, action } from 'mobx';
import { GQLError } from './GQLError';
export class GQLErrorCatcher {
@observable hasDetails = false
@observable responseMessage: string | null = null
@observable exception: GQLError | null = null;
@action catch(exception: any): boolean {
if (exception instanceof GQLError) {
this.responseMessage = exception.errorText;
this.hasDetails = exception.hasDetails();
this.exception = exception;
return true;
}
this.clear();
return false;
}
@action clear() {
this.hasDetails = false;
this.responseMessage = null;
this.exception = null;
}
}
@@ -11,10 +11,12 @@ import { ServerError } from './sdk';
export class ServerInternalError extends Error implements ServerError {
readonly errorCode?: string;
readonly stackTrace?: string;
readonly causedBy?: ServerError;
constructor(error: ServerError) {
super(error.message);
this.errorCode = error.errorCode;
this.stackTrace = error.stackTrace;
this.causedBy = error.causedBy;
}
}
+1
View File
@@ -5,3 +5,4 @@ export * from './GQLError';
export * from './ServerInternalError';
export * from './CachedResource';
export * from './EServerErrorCode';
export * from './GQLErrorCatcher';
File diff suppressed because it is too large Load Diff
@@ -17,14 +17,6 @@
},
"peerDependencies": {
"@dbeaver/core": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
"@material/ripple": "^4.0.0",
"@material/theme": "^4.0.0",
"@material/typography": "^4.0.0",
"graphql": "^14.5.8",
"graphql-request": "^1.8.2",
"graphql-tag": "^2.10.1",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
@@ -32,7 +32,7 @@ export const ConnectionForm = observer(function ConnectionForm({
const translate = useTranslate();
return styled(useStyles(formStyles))(
<>
<connection-form as='div'>
<connection-type as="div">
<Radio
name="type"
@@ -74,6 +74,6 @@ export const ConnectionForm = observer(function ConnectionForm({
</group>
)}
{!driver?.anonymousAccess && <CredentialsForm controller={controller}/>}
</>
</connection-form>
);
});
@@ -9,6 +9,10 @@
import { css } from 'reshadow';
export const formStyles = css`
connection-form {
flex-direction: column;
padding: 18px 24px;
}
connection-type {
padding: 12px;
}
@@ -7,13 +7,13 @@
*/
import { observer } from 'mobx-react';
import {
useTabState, Tab as BaseTab, TabList, TabPanel as BaseTabPanel,
} from 'reakit/Tab';
import { useState } from 'react';
import styled, { css } from 'reshadow';
import { DBDriver } from '@dbeaver/core/app';
import { SubmittingForm, ErrorMessage } from '@dbeaver/core/blocks';
import {
SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle, TabPanel
} from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
@@ -24,10 +24,9 @@ import { ConnectionFormDialogController } from './ConnectionFormDialogController
import { ConnectionFormDialogFooter } from './ConnectionFormDialogFooter';
import { DriverProperties } from './DriverProperties/DriverProperties';
const styles = composes(
css`
BaseTab {
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
ErrorMessage {
@@ -41,10 +40,8 @@ const styles = composes(
box-sizing: border-box;
}
CommonDialogWrapper {
display: flex;
flex-direction: column;
max-height: 330px;
min-height: 330px;
max-height: 500px;
min-height: 500px;
}
SubmittingForm {
overflow: auto;
@@ -55,39 +52,20 @@ const styles = composes(
flex-direction: column;
}
BaseTab, BaseTabPanel {
outline: none;
}
TabList {
box-sizing: border-box;
display: inline-flex;
width: 100%;
padding-left: 24px;
}
BaseTab {
Tab {
composes: theme-typography--body2 from global;
text-transform: uppercase;
padding: 12px 16px;
border-top: solid 2px transparent;
height: 48px;
font-weight: normal;
&:global([aria-selected='true']) {
border-top-color: #fd1d48;
&:before {
display: none;
}
&:global([aria-selected=true]) {
font-weight: normal !important;
}
&:not(:global([aria-selected='true'])) {
cursor: pointer;
background-color: transparent !important;
}
}
BaseTabPanel::first-child {
flex-direction: column;
padding: 18px 24px;
}
ErrorMessage {
position: sticky;
@@ -113,50 +91,54 @@ export const ConnectionFormDialog = observer(
}: ConnectionFormDialogProps) {
const translate = useTranslate();
const controller = useController(ConnectionFormDialogController, driver, onClose);
const tab = useTabState({
selectedId: 'options',
});
const [loadProperties, setLoadProperties] = useState(false);
return styled(useStyles(styles))(
<CommonDialogWrapper
title={title}
noBodyPadding
header={(
<TabList {...tab} aria-label="My tabs">
<BaseTab {...tab} type='button' stopId='options'>{translate('customConnection_options')}</BaseTab>
<BaseTab {...tab} type='button' stopId='driver_properties'>{translate('customConnection_properties')}</BaseTab>
</TabList>
)}
footer={(
<ConnectionFormDialogFooter
isConnecting={controller.isConnecting}
onConnectionTest={controller.onTestConnection}
onCreateConnection={controller.onCreateConnection}
onBack={onBack}
/>
)}
onReject={onClose}
>
<SubmittingForm onSubmit={controller.onCreateConnection}>
<BaseTabPanel {...tab} stopId='options'>
<ConnectionForm driver={driver} controller={controller} />
</BaseTabPanel>
<BaseTabPanel {...tab} stopId='driver_properties'>
<DriverProperties
driver={driver}
state={controller.config.properties}
isSelected={tab.selectedId === 'driver_properties'}
<TabsState selectedId='options'>
<CommonDialogWrapper
title={title}
noBodyPadding
header={(
<TabList>
<Tab tabId='options' >
<TabTitle title={translate('customConnection_options')} />
</Tab>
<Tab tabId='driver_properties' onOpen={() => setLoadProperties(true)} >
<TabTitle title={translate('customConnection_properties')} />
</Tab>
</TabList>
)}
footer={(
<ConnectionFormDialogFooter
isConnecting={controller.isConnecting}
onConnectionTest={controller.onTestConnection}
onCreateConnection={controller.onCreateConnection}
onBack={onBack}
/>
</BaseTabPanel>
</SubmittingForm>
{controller.responseMessage && (
<ErrorMessage
text={controller.responseMessage}
hasDetails={controller.hasDetails}
onShowDetails={controller.onShowDetails}
/>
)}
</CommonDialogWrapper>
)}
onReject={onClose}
>
<SubmittingForm onSubmit={controller.onCreateConnection}>
<TabPanel tabId='options'>
<ConnectionForm driver={driver} controller={controller} />
</TabPanel>
<TabPanel tabId='driver_properties'>
<DriverProperties
driver={driver}
state={controller.config.properties!}
loadProperties={loadProperties}
/>
</TabPanel>
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.onShowDetails}
/>
)}
</CommonDialogWrapper>
</TabsState>
);
}
);
@@ -12,7 +12,7 @@ import { DBDriver, ErrorDetailsDialog } from '@dbeaver/core/app';
import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { ConnectionConfig, GQLError } from '@dbeaver/core/sdk';
import { ConnectionConfig, GQLErrorCatcher } from '@dbeaver/core/sdk';
import { CustomConnectionService } from '../../CustomConnectionService';
@@ -39,10 +39,7 @@ implements IInitializableController, IDestructibleController {
url: '',
properties: {},
}
@observable hasDetails = false
@observable responseMessage: string | null = null
private exception: GQLError | null = null;
readonly error = new GQLErrorCatcher();
private onClose!: () => void
private isDistructed = false;
@@ -70,7 +67,7 @@ implements IInitializableController, IDestructibleController {
onTestConnection = async () => {
this.isConnecting = true;
this.clearError();
this.error.clear();
try {
await this.customConnectionService.testConnectionAsync(this.getConnectionConfig());
@@ -84,7 +81,7 @@ implements IInitializableController, IDestructibleController {
onCreateConnection = async () => {
this.isConnecting = true;
this.clearError();
this.error.clear();
try {
const connection = await this.customConnectionService.createConnectionAsync(this.getConnectionConfig());
@@ -98,8 +95,8 @@ implements IInitializableController, IDestructibleController {
}
onShowDetails = () => {
if (this.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.exception);
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
@@ -144,12 +141,6 @@ implements IInitializableController, IDestructibleController {
this.config.properties = {};
}
private clearError() {
this.responseMessage = null;
this.hasDetails = false;
this.exception = null;
}
/**
* Creates connection name based on connection url
* @param defaultName default connection name if url parsing failed
@@ -175,11 +166,7 @@ implements IInitializableController, IDestructibleController {
}
private showError(exception: Error, message: string) {
if (exception instanceof GQLError && !this.isDistructed) {
this.responseMessage = exception.errorText;
this.hasDetails = exception.hasDetails();
this.exception = exception;
} else {
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, message);
}
}

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