Merge branches 'cb_release_1_0_3' and 'devel' of https://github.com/dbeaver/cloudbeaver into cb_release_1_0_3

This commit is contained in:
Serge Rider
2020-06-03 11:08:15 +03:00
271 changed files with 6981 additions and 2611 deletions
+2 -2
View File
@@ -44,8 +44,8 @@ cd ..\deploy
echo Copy static content
xcopy /E /Q ..\webapp\packages\dbeaver\dist cloudbeaver\web >NUL
xcopy /E /Q ..\webapp\packages\cloudbeaver\dist cloudbeaver\web >NUL
echo Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server.
pause
pause
+1 -1
View File
@@ -43,6 +43,6 @@ cd ../deploy
echo "Copy static content"
cp -rp ../webapp/packages/dbeaver/dist/* cloudbeaver/web
cp -rp ../webapp/packages/cloudbeaver/dist/* cloudbeaver/web
echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server."
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.connect.provider.sample;singleton:=true
Bundle-Version: 1.0.5.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.6.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.connect.provider.sample</artifactId>
<version>1.0.5-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.1.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.0.1-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Base JDBC drivers
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
Bundle-Version: 1.0.2.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.3.qualifier
Bundle-Release-Date: 20200601
Bundle-Vendor: DBeaver Corp
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
<version>1.0.2-SNAPSHOT</version>
<version>1.0.3-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -4,7 +4,7 @@ Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
Bundle-Version: 1.0.2.qualifier
Bundle-Release-Date: 20200518
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-Activator: io.cloudbeaver.server.WebPlatformActivator
@@ -20,7 +20,7 @@
<extension point="io.cloudbeaver.service">
<service id="core" label="Core service" description="Core services" class="io.cloudbeaver.service.core.WebServiceBindingCore">
<permission id="public" label="Public access" category="general"/>
</service>
<service id="navigator" label="Database navigator" description="Database navigator services" class="io.cloudbeaver.service.navigator.WebServiceBindingNavigator">
<premission id="navigate" label="Navigate over database metadata"/>
@@ -62,6 +62,7 @@ input SQLDataFilter {
constraints: [ SQLDataFilterConstraint ]
where: String
orderBy: String
}
type SQLResultColumn {
@@ -42,10 +42,14 @@ public interface DBWSecurityController {
void setUserRoles(String userId, String[] roleIds, String grantorId) throws DBCException;
@NotNull
WebRole[] getUserRoles(String userId) throws DBCException;
WebUser getUserById(String userId) throws DBCException;
@NotNull
WebUser[] findUsers(String userNameMask) throws DBCException;
///////////////////////////////////////////
// Credentials
@@ -69,8 +73,11 @@ public interface DBWSecurityController {
///////////////////////////////////////////
// Roles
@NotNull
WebRole[] readAllRoles() throws DBCException;
WebRole[] findRoles(String roleName) throws DBCException;
void createRole(WebRole role) throws DBCException;
void deleteRole(String roleId) throws DBCException;
@@ -80,8 +87,10 @@ public interface DBWSecurityController {
void setSubjectPermissions(String subjectId, String[] permissionIds, String grantorId) throws DBCException;
@NotNull
Set<String> getSubjectPermissions(String subjectId) throws DBCException;
@NotNull
Set<String> getUserPermissions(String userId) throws DBCException;
///////////////////////////////////////////
@@ -94,7 +103,8 @@ public interface DBWSecurityController {
void updateSession(WebSession session) throws DBCException;
///////////////////////////////////////////
// Meta info
// Permissions
///////////////////////////////////////////
// Utils
@@ -39,6 +39,9 @@ import java.util.Map;
public class DBWebException extends DBException implements GraphQLError {
public static final String ERROR_CODE_SESSION_EXPIRED = "sessionExpired";
public static final String ERROR_CODE_ACCESS_DENIED = "accessDenied";
public static final String ERROR_CODE_IDENT_REQUIRED = "identRequired";
public static final String ERROR_CODE_AUTH_REQUIRED = "authRequired";
private List<Object> path;
private List<SourceLocation> locations;
@@ -21,8 +21,6 @@ package io.cloudbeaver;
*/
public class DBWebExceptionAccessDenied extends DBWebException {
public static final String ERROR_CODE_ACCESS_DENIED = "accessDenied";
public DBWebExceptionAccessDenied(String message) {
super(message, ERROR_CODE_ACCESS_DENIED);
}
@@ -30,6 +30,7 @@ import org.eclipse.core.runtime.Status;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.access.DBASession;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.meta.Association;
@@ -53,7 +54,7 @@ import java.util.function.Function;
* Web session.
* Is the main source of data in web application
*/
public class WebSession {
public class WebSession implements DBASession {
private static final Log log = Log.getLog(WebSession.class);
@@ -104,10 +105,15 @@ public class WebSession {
}
@Property
public String getId() {
public String getSessionId() {
return id;
}
@Override
public boolean isApplicationSession() {
return false;
}
@Property
public String getCreateTime() {
return CBConstants.ISO_DATE_FORMAT.format(createTime);
@@ -95,7 +95,7 @@ public class WebSessionManager {
}
}
log.debug("> New web session '" + webSession.getId() + "'");
log.debug("> New web session '" + webSession.getSessionId() + "'");
} else {
if (updateInfo) {
// Update only once per request
@@ -132,7 +132,7 @@ public class WebSessionManager {
}
for (WebSession session : expiredList) {
log.debug("> Expire session '" + session.getId() + "'");
log.debug("> Expire session '" + session.getSessionId() + "'");
session.close();
}
}
@@ -30,7 +30,6 @@ public class WebRole implements WebAuthSubject {
private String roleId;
private String name;
private String icon;
private String description;
private Set<String> permissions = new LinkedHashSet<>();
@@ -43,10 +42,6 @@ public class WebRole implements WebAuthSubject {
return roleId;
}
public void setRoleId(String id) {
this.roleId = id;
}
public String getName() {
return name;
}
@@ -55,14 +50,6 @@ public class WebRole implements WebAuthSubject {
this.name = name;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getDescription() {
return description;
}
@@ -35,7 +35,7 @@ public class WebUser implements WebAuthSubject {
private Map<String, Object> metaParameters = new LinkedHashMap<>();
private Map<String, Object> configurationParameters = new LinkedHashMap<>();
private Set<WebRole> roles = new LinkedHashSet<>();
private WebRole[] roles = null;
private String activeAuthModel;
private Map<String, Map<String, Object>> authCredentials = new HashMap<>();
@@ -61,7 +61,7 @@ public class WebUser implements WebAuthSubject {
}
public String[] getGrantedRoles() {
return roles.stream().map(WebRole::getRoleId).toArray(String[]::new);
return Arrays.stream(roles).map(WebRole::getRoleId).toArray(String[]::new);
}
public Map<String, Object> getMetaParameters() {
@@ -72,8 +72,12 @@ public class WebUser implements WebAuthSubject {
return Collections.unmodifiableMap(configurationParameters);
}
public Set<WebRole> getRoles() {
return Collections.unmodifiableSet(roles);
public WebRole[] getRoles() {
return roles;
}
public void setRoles(WebRole[] roles) {
this.roles = roles;
}
@Override
@@ -110,4 +110,8 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor {
return instance;
}
@Override
public String toString() {
return getId();
}
}
@@ -18,6 +18,10 @@ package io.cloudbeaver.server;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Application configuration
*/
@@ -33,6 +37,7 @@ public class CBAppConfig {
private String[] enabledDrivers = new String[0];
private String[] disabledDrivers = new String[0];
private String defaultAuthProvider = LocalAuthProvider.PROVIDER_ID;
private Map<String, Object> plugins = new LinkedHashMap<>();
public boolean isAuthenticationEnabled() {
return authenticationEnabled;
@@ -77,4 +82,18 @@ public class CBAppConfig {
public String getDefaultAuthProvider() {
return defaultAuthProvider;
}
public Map<String, Object> getPluginConfig(String pluginId) {
Object config = plugins.get(pluginId);
if (config instanceof Map) {
return (Map<String, Object>) config;
} else {
return Collections.emptyMap();
}
}
public <T> T getPluginOption(String pluginId, String option) {
return (T)getPluginConfig(pluginId).get(option);
}
}
@@ -76,6 +76,7 @@ class CBSecurityController implements DBWSecurityController {
@Override
public void deleteUser(String userId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
deleteAuthSubject(dbCon, userId);
JDBCUtils.executeStatement(dbCon, "DELETE FROM CB_USER WHERE USER_ID=?", userId);
} catch (SQLException e) {
throw new DBCException("Error deleting user from database", e);
@@ -102,10 +103,13 @@ class CBSecurityController implements DBWSecurityController {
}
}
@NotNull
@Override
public WebRole[] getUserRoles(String userId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement("SELECT * FROM CB_USER WHERE USER_ID=?")) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"SELECT R.* FROM CB_USER_ROLE UR,CB_ROLE R " +
"WHERE UR.USER_ID=? AND UR.ROLE_ID=R.ROLE_ID")) {
dbStat.setString(1, userId);
List<WebRole> roles = new ArrayList<>();
try (ResultSet dbResult = dbStat.executeQuery()) {
@@ -137,6 +141,28 @@ class CBSecurityController implements DBWSecurityController {
}
}
@NotNull
@Override
public WebUser[] findUsers(String userNameMask) throws DBCException {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement("SELECT * FROM CB_USER" +
(CommonUtils.isEmpty(userNameMask) ? "" : " WHERE USER_ID=?"))) {
if (!CommonUtils.isEmpty(userNameMask)) {
dbStat.setString(1, userNameMask);
}
try (ResultSet dbResult = dbStat.executeQuery()) {
List<WebUser> result = new ArrayList<>();
while (dbResult.next()) {
result.add(new WebUser(dbResult.getString(1)));
}
return result.toArray(new WebUser[0]);
}
}
} catch (SQLException e) {
throw new DBCException("Error while searching credentials", e);
}
}
///////////////////////////////////////////
// Credentials
@@ -273,6 +299,7 @@ class CBSecurityController implements DBWSecurityController {
///////////////////////////////////////////
// Roles
@NotNull
@Override
public WebRole[] readAllRoles() throws DBCException {
try (Connection dbCon = database.openConnection()) {
@@ -301,6 +328,11 @@ class CBSecurityController implements DBWSecurityController {
}
}
@Override
public WebRole[] findRoles(String roleName) throws DBCException {
return readAllRoles();
}
@NotNull
private WebRole fetchRole(ResultSet dbResult) throws SQLException {
WebRole role = new WebRole(dbResult.getString("ROLE_ID"));
@@ -328,6 +360,7 @@ class CBSecurityController implements DBWSecurityController {
@Override
public void deleteRole(String roleId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
deleteAuthSubject(dbCon, roleId);
JDBCUtils.executeStatement(dbCon, "DELETE FROM CB_ROLE WHERE ROLE_ID=?", roleId);
} catch (SQLException e) {
throw new DBCException("Error deleting role from database", e);
@@ -357,6 +390,7 @@ class CBSecurityController implements DBWSecurityController {
}
}
@NotNull
@Override
public Set<String> getSubjectPermissions(String subjectId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
@@ -375,6 +409,7 @@ class CBSecurityController implements DBWSecurityController {
}
}
@NotNull
@Override
public Set<String> getUserPermissions(String userId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
@@ -430,7 +465,7 @@ class CBSecurityController implements DBWSecurityController {
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO CB_SESSION(SESSION_ID,USER_ID,CREATE_TIME,LAST_ACCESS_TIME,LAST_ACCESS_REMOTE_ADDRESS,LAST_ACCESS_USER_AGENT) VALUES(?,?,?,?,?,?)")) {
dbStat.setString(1, session.getId());
dbStat.setString(1, session.getSessionId());
WebUser user = session.getUser();
if (user == null) {
dbStat.setNull(2, Types.VARCHAR);
@@ -480,7 +515,7 @@ class CBSecurityController implements DBWSecurityController {
dbStat.setNull(4, Types.VARCHAR);
}
dbStat.setString(5, session.getId());
dbStat.setString(5, session.getSessionId());
if (dbStat.executeUpdate() == 0) {
throw new DBCException("Session not exists in database");
}
@@ -527,5 +562,11 @@ class CBSecurityController implements DBWSecurityController {
}
}
private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLException {
try (PreparedStatement dbStat = dbCon.prepareStatement("DELETE FROM CB_AUTH_SUBJECT WHERE SUBJECT_ID=?")) {
dbStat.setString(1, subjectId);
dbStat.execute();
}
}
}
@@ -99,6 +99,11 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
getServletRequest(env));
}
protected static WebSession getWebSession(DataFetchingEnvironment env, boolean errorOnNotFound) throws DBWebException {
return CBPlatform.getInstance().getSessionManager().getWebSession(
getServletRequest(env), errorOnNotFound);
}
protected static WebSession findWebSession(DataFetchingEnvironment env) {
return CBPlatform.getInstance().getSessionManager().findWebSession(
getServletRequest(env));
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
Bundle-Version: 1.0.1.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.2.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -5,7 +5,7 @@
<extension point="io.cloudbeaver.service">
<service id="admin" label="User administration" description="User administration services" class="io.cloudbeaver.service.admin.WebServiceBindingAdmin">
<permission id="admin" label="Admin Full Access" category="admin"/>
</service>
</extension>
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.admin</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.0.2-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -1,6 +1,6 @@
type AdminUserInfo {
userName: ID!
userId: ID!
metaParameters: Object!
configurationParameters: Object!
@@ -8,15 +8,16 @@ type AdminUserInfo {
}
type AdminRoleInfo {
roleName: ID!
roleId: ID!
roleName: String
rolePermissions: [ID]!
}
type AdminPermissionInfo {
id: ID!
label: [String]
description: [String]
label: String
description: String
provider: String!
category: String
@@ -24,20 +25,21 @@ type AdminPermissionInfo {
extend type Query {
listUsers(userName: ID): [AdminUserInfo]!
listRoles(roleName: ID): [AdminRoleInfo]!
listPermissions(permissionId: ID): [AdminPermissionInfo]
listUsers(userId: ID): [AdminUserInfo]!
listRoles(roleId: ID): [AdminRoleInfo]!
listPermissions: [AdminPermissionInfo]!
createUser(userName: ID!): AdminUserInfo!
deleteUser(userName: ID!): Boolean
createUser(userId: ID!): AdminUserInfo!
deleteUser(userId: ID!): Boolean
getUserAdminCredentials(userName: ID!): Object
createRole(roleId: ID!): AdminRoleInfo!
deleteRole(roleId: ID!): Boolean
setUserAdminCredentials(userName: ID!, credentials: Object): Boolean
grantUserRole(userId: ID!, roleId: ID!): Boolean
revokeUserRole(userId: ID!, roleId: ID!): Boolean
grantUserRole(user: ID!, role: ID!): Boolean
revokeUserRole(user: ID!, role: ID!): Boolean
setRolePermissions(roleId: ID!, permissions: [ID]!): Boolean
setRolePermissions(roleID: ID!, permissions: [ID]!): Boolean
setUserCredentials(userId: ID!, providerId: ID!, credentials: Object!): Boolean
}
@@ -0,0 +1,48 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.admin;
import io.cloudbeaver.registry.WebPermissionDescriptor;
/**
* Web permission ID
*/
public class AdminPermissionInfo {
private final WebPermissionDescriptor permission;
public AdminPermissionInfo(WebPermissionDescriptor permission) {
this.permission = permission;
}
public String getId() {
return permission.getId();
}
public String getLabel() {
return permission.getLabel();
}
public String getDescription() {
return permission.getDescription();
}
public String getCategory() {
return permission.getCategory();
}
}
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.admin.impl;
package io.cloudbeaver.service.admin;
/**
* Web service implementation
@@ -0,0 +1,50 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.admin;
import io.cloudbeaver.model.user.WebRole;
import java.util.List;
/**
* Web role info
*/
public class AdminRoleInfo {
private final WebRole role;
private List<String> rolePermissions;
public AdminRoleInfo(WebRole role) {
this.role = role;
}
public String getRoleId() {
return role.getRoleId();
}
public String getRoleName() {
return role.getName();
}
public List<String> getRolePermissions() {
return rolePermissions;
}
public void setRolePermissions(List<String> rolePermissions) {
this.rolePermissions = rolePermissions;
}
}
@@ -0,0 +1,60 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.admin;
import io.cloudbeaver.model.user.WebRole;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.dbeaver.model.exec.DBCException;
import java.util.Map;
/**
* Web user info
*/
public class AdminUserInfo {
private final WebUser user;
private Map<String, Object> metaParameters;
private Map<String, Object> configurationParameters;
public AdminUserInfo(WebUser user) {
this.user = user;
}
public String getUserId() {
return user.getUserId();
}
public Map<String, Object> getMetaParameters() {
return metaParameters;
}
public Map<String, Object> getConfigurationParameters() {
return configurationParameters;
}
public String[] getGrantedRoles() throws DBCException {
if (user.getRoles() == null) {
WebRole[] userRoles = CBPlatform.getInstance().getApplication().getSecurityController().getUserRoles(getUserId());
user.setRoles(userRoles);
}
return user.getGrantedRoles();
}
}
@@ -17,11 +17,56 @@
package io.cloudbeaver.service.admin;
import io.cloudbeaver.DBWService;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebAction;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import java.util.List;
import java.util.Map;
/**
* Web service API
*/
public interface DBWServiceAdmin extends DBWService {
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
@NotNull
List<AdminUserInfo> listUsers(@NotNull WebSession webSession, @Nullable String userName) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
@NotNull
List<AdminRoleInfo> listRoles(@NotNull WebSession webSession, @Nullable String roleName) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
@NotNull
List<AdminPermissionInfo> listPermissions(@NotNull WebSession webSession) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
@NotNull
AdminUserInfo createUser(@NotNull WebSession webSession, String userName) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
@NotNull
AdminRoleInfo createRole(@NotNull WebSession webSession, String roleId) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean deleteRole(@NotNull WebSession webSession, String roleId) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean grantUserRole(@NotNull WebSession webSession, String user, String role) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean revokeUserRole(@NotNull WebSession webSession, String user, String role) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean setRolePermissions(@NotNull WebSession webSession, String roleID, String[] permissions) throws DBWebException;
@WebAction(requirePermissions = AdminPermissions.PERMISSION_ADMIN)
boolean setUserCredentials(@NotNull WebSession webSession, @NotNull String userID, @NotNull String providerId, @NotNull Map<String, Object> credentials) throws DBWebException;
}
@@ -16,7 +16,6 @@
*/
package io.cloudbeaver.service.admin;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.WebServiceBindingBase;
@@ -35,10 +34,29 @@ public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmi
@Override
public void bindWiring(DBWBindingContext model) throws DBWebException {
model.getQueryType().dataFetcher("listUsers", env -> {
throw new DBWebException("Not implemented");
});
model.getQueryType().dataFetcher("listUsers",
env -> getService(env).listUsers(getWebSession(env), env.getArgument("userId")));
model.getQueryType().dataFetcher("listRoles",
env -> getService(env).listRoles(getWebSession(env), env.getArgument("roleId")));
model.getQueryType().dataFetcher("listPermissions",
env -> getService(env).listPermissions(getWebSession(env)));
model.getQueryType().dataFetcher("createUser",
env -> getService(env).createUser(getWebSession(env), env.getArgument("userId")));
model.getQueryType().dataFetcher("deleteUser",
env -> getService(env).deleteUser(getWebSession(env), env.getArgument("userId")));
model.getQueryType().dataFetcher("createRole",
env -> getService(env).createRole(getWebSession(env), env.getArgument("roleId")));
model.getQueryType().dataFetcher("deleteRole",
env -> getService(env).deleteRole(getWebSession(env), env.getArgument("roleId")));
model.getQueryType().dataFetcher("grantUserRole",
env -> getService(env).grantUserRole(getWebSession(env), env.getArgument("userId"), env.getArgument("roleId")));
model.getQueryType().dataFetcher("revokeUserRole",
env -> getService(env).revokeUserRole(getWebSession(env), env.getArgument("userId"), env.getArgument("roleId")));
model.getQueryType().dataFetcher("setRolePermissions",
env -> getService(env).setRolePermissions(getWebSession(env), env.getArgument("roleId"), env.getArgument("permissions")));
model.getQueryType().dataFetcher("setUserCredentials",
env -> getService(env).setUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"), env.getArgument("credentials")));
}
}
@@ -16,7 +16,28 @@
*/
package io.cloudbeaver.service.admin.impl;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebRole;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebPermissionDescriptor;
import io.cloudbeaver.registry.WebServiceDescriptor;
import io.cloudbeaver.registry.WebServiceRegistry;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.service.admin.AdminPermissionInfo;
import io.cloudbeaver.service.admin.AdminRoleInfo;
import io.cloudbeaver.service.admin.AdminUserInfo;
import io.cloudbeaver.service.admin.DBWServiceAdmin;
import org.jkiss.code.NotNull;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Web service implementation
@@ -24,4 +45,179 @@ import io.cloudbeaver.service.admin.DBWServiceAdmin;
public class WebServiceAdmin implements DBWServiceAdmin {
@NotNull
@Override
public List<AdminUserInfo> listUsers(@NotNull WebSession webSession, String userName) throws DBWebException {
try {
List<AdminUserInfo> webUsers = new ArrayList<>();
for (WebUser user : CBPlatform.getInstance().getApplication().getSecurityController().findUsers(userName)) {
webUsers.add(new AdminUserInfo(user));
}
return webUsers;
} catch (Exception e) {
throw new DBWebException("Error reading users", e);
}
}
@NotNull
@Override
public List<AdminRoleInfo> listRoles(@NotNull WebSession webSession, String roleName) throws DBWebException {
try {
List<AdminRoleInfo> webUsers = new ArrayList<>();
for (WebRole role : CBPlatform.getInstance().getApplication().getSecurityController().findRoles(roleName)) {
webUsers.add(new AdminRoleInfo(role));
}
return webUsers;
} catch (Exception e) {
throw new DBWebException("Error reading users", e);
}
}
@NotNull
@Override
public List<AdminPermissionInfo> listPermissions(@NotNull WebSession webSession) throws DBWebException {
try {
List<AdminPermissionInfo> permissionInfos = new ArrayList<>();
for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) {
for (WebPermissionDescriptor pd : wsd.getPermissions()) {
permissionInfos.add(new AdminPermissionInfo(pd));
}
}
return permissionInfos;
} catch (Exception e) {
throw new DBWebException("Error reading users", e);
}
}
@NotNull
@Override
public AdminUserInfo createUser(@NotNull WebSession webSession, String userName) throws DBWebException {
if (userName.isEmpty()) {
throw new DBWebException("Empty user name");
}
try {
WebUser newUser = new WebUser(userName);
CBPlatform.getInstance().getApplication().getSecurityController().createUser(newUser);
return new AdminUserInfo(newUser);
} catch (Exception e) {
throw new DBWebException("Error creating new user", e);
}
}
@Override
public boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException {
if (CommonUtils.equalObjects(userName, webSession.getUser().getUserId())) {
throw new DBWebException("You cannot delete yourself");
}
try {
CBPlatform.getInstance().getApplication().getSecurityController().deleteUser(userName);
return true;
} catch (Exception e) {
throw new DBWebException("Error deleting user", e);
}
}
@NotNull
@Override
public AdminRoleInfo createRole(@NotNull WebSession webSession, String roleId) throws DBWebException {
if (roleId.isEmpty()) {
throw new DBWebException("Empty role ID");
}
try {
WebRole newRole = new WebRole(roleId);
CBPlatform.getInstance().getApplication().getSecurityController().createRole(newRole);
return new AdminRoleInfo(newRole);
} catch (Exception e) {
throw new DBWebException("Error creating new role", e);
}
}
@Override
public boolean deleteRole(@NotNull WebSession webSession, String roleId) throws DBWebException {
try {
WebRole[] userRoles = CBPlatform.getInstance().getApplication().getSecurityController().getUserRoles(webSession.getUser().getUserId());
if (Arrays.stream(userRoles).anyMatch(webRole -> webRole.getRoleId().equals(roleId))) {
throw new DBWebException("You can not delete your own role");
}
CBPlatform.getInstance().getApplication().getSecurityController().deleteRole(roleId);
return true;
} catch (Exception e) {
throw new DBWebException("Error deleting role", e);
}
}
@Override
public boolean grantUserRole(@NotNull WebSession webSession, String user, String role) throws DBWebException {
WebUser grantor = webSession.getUser();
if (grantor == null) {
throw new DBWebException("Cannot grant role in anonymous mode");
}
try {
WebRole[] userRoles = CBPlatform.getInstance().getApplication().getSecurityController().getUserRoles(user);
String[] roleIds = Arrays.stream(userRoles).map(WebRole::getRoleId).toArray(String[]::new);
if (!ArrayUtils.contains(roleIds, role)) {
roleIds = ArrayUtils.add(String.class, roleIds, role);
CBPlatform.getInstance().getApplication().getSecurityController().setUserRoles(user, roleIds, grantor.getUserId());
} else {
throw new DBWebException("User '" + user + "' already has role '" + role + "'");
}
return true;
} catch (Exception e) {
throw new DBWebException("Error granting role", e);
}
}
@Override
public boolean revokeUserRole(@NotNull WebSession webSession, String user, String role) throws DBWebException {
WebUser grantor = webSession.getUser();
if (grantor == null) {
throw new DBWebException("Cannot grant role in anonymous mode");
}
try {
WebRole[] userRoles = CBPlatform.getInstance().getApplication().getSecurityController().getUserRoles(user);
String[] roleIds = Arrays.stream(userRoles).map(WebRole::getRoleId).toArray(String[]::new);
if (ArrayUtils.contains(roleIds, role)) {
roleIds = ArrayUtils.remove(String.class, roleIds, role);
CBPlatform.getInstance().getApplication().getSecurityController().setUserRoles(user, roleIds, grantor.getUserId());
} else {
throw new DBWebException("User '" + user + "' doesn't have role '" + role + "'");
}
return true;
} catch (Exception e) {
throw new DBWebException("Error revoking role", e);
}
}
@Override
public boolean setRolePermissions(@NotNull WebSession webSession, String roleID, String[] permissions) throws DBWebException {
WebUser grantor = webSession.getUser();
if (grantor == null) {
throw new DBWebException("Cannot change permissions in anonymous mode");
}
try {
CBPlatform.getInstance().getApplication().getSecurityController().setSubjectPermissions(roleID, permissions, grantor.getUserId());
return true;
} catch (Exception e) {
throw new DBWebException("Error setting role permissions", e);
}
}
@Override
public boolean setUserCredentials(@NotNull WebSession webSession, @NotNull String userID, @NotNull String providerId, @NotNull Map<String, Object> credentials) throws DBWebException {
WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(providerId);
if (authProvider == null) {
throw new DBWebException("Invalid auth provider '" + providerId + "'");
}
// Check userId credential.
// FIXME: It is actually a hack. All crdentials must be passed from client
if (LocalAuthProvider.PROVIDER_ID.equals(providerId)) {
credentials.put(LocalAuthProvider.CRED_USER, userID);
}
try {
CBPlatform.getInstance().getApplication().getSecurityController().setUserCredentials(userID, authProvider, credentials);
return true;
} catch (Exception e) {
throw new DBWebException("Error setting user credentials", e);
}
}
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true
Bundle-Version: 1.0.1.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.2.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.auth</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.0.2-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -36,14 +36,14 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
public void bindWiring(DBWBindingContext model) throws DBWebException {
model.getQueryType()
.dataFetcher("authLogin", env -> getService(env).authLogin(
getWebSession(env),
getWebSession(env, false),
env.getArgument("provider"),
env.getArgument("credentials")))
.dataFetcher("authLogout", env -> {
getService(env).authLogout(getWebSession(env));
return true;
})
.dataFetcher("sessionUser", env -> getService(env).sessionUser(getWebSession(env)))
.dataFetcher("sessionUser", env -> getService(env).sessionUser(getWebSession(env, false)))
.dataFetcher("authProviders", env -> getService(env).getAuthProviders(getWebSession(env)))
;
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true
Bundle-Version: 1.0.2.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.3.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.data.transfer</artifactId>
<version>1.0.2-SNAPSHOT</version>
<version>1.0.3-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -17,13 +17,12 @@
package io.cloudbeaver.service.data.transfer.impl;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer;
import io.cloudbeaver.service.sql.WebSQLContextInfo;
import io.cloudbeaver.service.sql.WebSQLProcessor;
import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer;
import io.cloudbeaver.service.sql.WebSQLQueryDataContainer;
import io.cloudbeaver.service.sql.WebSQLResultsInfo;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
@@ -102,7 +101,7 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
@NotNull
private String makeUniqueFileName(WebSQLProcessor sqlProcessor, DataTransferProcessorDescriptor processor) {
return sqlProcessor.getWebSession().getId() + "_" + UUID.randomUUID() + "." + WebDataTransferUtils.getProcessorFileExtension(processor);
return sqlProcessor.getWebSession().getSessionId() + "_" + UUID.randomUUID() + "." + WebDataTransferUtils.getProcessorFileExtension(processor);
}
@Override
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true
Bundle-Version: 1.0.5.qualifier
Bundle-Release-Date: 20200518
Bundle-Version: 1.0.6.qualifier
Bundle-Release-Date: 20200601
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.metadata</artifactId>
<version>1.0.5-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.ce.drivers.feature"
label="Cloudbeaver CE 3rd Party Drivers"
version="1.0.25.qualifier"
version="1.0.26.qualifier"
provider-name="DBeaver Corp">
<plugin id="io.cloudbeaver.resources.drivers.base" download-size="0" install-size="0" version="0.0.0"/>
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.ce.drivers.feature</artifactId>
<version>1.0.25-SNAPSHOT</version>
<version>1.0.26-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
</project>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.product.ce.feature"
label="%featureName"
version="1.0.0.qualifier"
version="1.0.1.qualifier"
provider-name="%providerName"
plugin="io.cloudbeaver.product.ce">
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce.feature</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.0.1-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
<build>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.sample.feature"
label="%featureName"
version="1.0.5.qualifier"
version="1.0.6.qualifier"
provider-name="%providerName">
<description>
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.sample.feature</artifactId>
<version>1.0.5-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
</project>
+3 -1
View File
@@ -106,7 +106,9 @@ module.exports = {
'import/export': 'off',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'warn',
// import/no-extraneous-dependencies off because peer dependencies are moved to builder
// todo probably there is a way to tune this (see packageDir option in documentation)
'import/no-extraneous-dependencies': 'off',
'import/prefer-default-export': 'off',
'import/no-cycle': 'off',
'import-helpers/order-imports': [
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
presets: [
'@babel/preset-react',
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'react-require',
'reshadow/babel',
],
}
-17
View File
@@ -1,17 +0,0 @@
module.exports = {
presets: [
"@babel/preset-env",
"@babel/preset-react",
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-syntax-dynamic-import',
'babel-plugin-transform-typescript-metadata',
'@babel/plugin-proposal-optional-chaining',
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
'react-require',
'reshadow/babel',
],
}
-81
View File
@@ -1,81 +0,0 @@
import babelPlugin from 'rollup-plugin-babel';
const path = require('path');
import postcssPlugin from 'rollup-plugin-postcss';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import includePaths from 'rollup-plugin-includepaths';
import typescriptPlugin from 'rollup-plugin-typescript2';
const includePathOptions = {
include: {},
paths: ['src'],
external: [
],
extensions: ['.ts', '.tsx', '.js']
};
export const defaultBuilderOptions = {
babelConfig: path.join(__dirname, './babel-rollup.config.js'),
typescriptConfig: '', // tbd
}
export function configBuilder(options = defaultBuilderOptions) {
return {
input: 'src/index.ts',
output: [
{
dir: 'dist',
format: 'esm',
},
],
external: [
// externals beyond peer dependencies
],
plugins: [
includePaths(includePathOptions),
nodeResolve(),
commonjs({
sourceMap: false,
}),
typescriptPlugin({
tsconfig: 'tsconfig.build.json',
useTsconfigDeclarationDir: true,
}),
babelPlugin({
exclude: 'node_modules/**',
extensions: ['.js', '.ts', '.tsx'],
configFile: options.babelConfig,
}),
postcssPlugin({
extract: false,
// enable CSS modules for .module.css .module.scss files
autoModules: true,
// modules: {
// generateScopedName: "[local]___[hash:base64:5]"
// },
inject: true,
use: {
sass: {
includePaths: ['node_modules', '../../node_modules'],
}
},
// this option is added in dkrupenya/rollup-plugin-postcss#develop
// todo to be converted to custom loader
firstPlugin: require('reshadow/postcss'),
plugins: [
require('postcss-preset-env')({ stage: 0 }),
require('postcss-discard-comments'),
// todo move 'reshadow/postcss' to custom loader to run it before postcss-loader
// require('reshadow/postcss'),
]
}),
peerDepsExternal(),
],
};
}
export default configBuilder(defaultBuilderOptions);
-23
View File
@@ -1,23 +0,0 @@
const merge = require('webpack-merge');
const {resolve} = require('path');
var PeerDepsExternalsPlugin = require('peer-deps-externals-webpack-plugin');
const commonConfig = require('./common');
module.exports = (env, argv) => merge(commonConfig(env, argv), {
mode: argv.mode || 'development',
entry: resolve(__dirname, '../../src/index.ts'), // todo
output: {
filename: 'index.js',
path: resolve(__dirname, '../../dist'),
library: 'basicConnectionPlugin', // todo pass from package.js
libraryTarget: 'umd',
},
externals: [
/^@dbeaver\/.+$/
],
devtool: 'source-map',
plugins: [
new PeerDepsExternalsPlugin(), // allow to treat all peerDependencies as externals
],
});
-187
View File
@@ -1,187 +0,0 @@
// shared config (dev and prod)
const webpack = require('webpack');
const ModuleDependencyWarning = require("webpack/lib/ModuleDependencyWarning");
const path = require('path');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// Temporary solution to remove wrong warning messages (it's was fixed https://github.com/microsoft/TypeScript/pull/35200 in typescript 3.8)
class IgnoreNotFoundExportPlugin {
apply(compiler) {
const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/
function doneHook(stats) {
stats.compilation.warnings = stats.compilation.warnings.filter(function (warn) {
if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) {
return false
}
return true;
})
}
if (compiler.hooks) {
compiler.hooks.done.tap("IgnoreNotFoundExportPlugin", doneHook)
} else {
compiler.plugin("done", doneHook)
}
}
}
module.exports = (env, argv) => {
function generateStyleLoaders(options = { hasModule: false }) {
const moduleScope = options.hasModule ? 'local' : 'global';
const modules = {
mode: moduleScope,
localIdentName: '[local]___[hash:base64:5]',
};
const postCssPlugins = [
require('postcss-preset-env')({ stage: 0 }),
require('@csstools/postcss-sass')({
includePaths: ['node_modules', path.resolve('../../node_modules')],
}),
require('postcss-discard-comments'),
require('reshadow/postcss')({ scopeBehaviour: moduleScope })
];
// 'use' clause in webpack rules
return [
{
loader: MiniCssExtractPlugin.loader,
options: {
hot: argv.mode !== 'production',
},
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: modules,
}
},
{
loader: 'postcss-loader',
options: {
syntax: 'postcss-scss',
plugins: postCssPlugins,
},
},
];
}
return {
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
configFile: path.join(__dirname, '../babel.config.js')
},
},
'source-map-loader'
],
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
configFile: path.join(__dirname, '../babel.config.js')
},
},
{
loader: 'eslint-loader',
// uncomment to fix after rule changes
// options: {
// fix: true,
// },
}
],
},
{
test: /\.(css|scss|sass)$/,
oneOf: [
// css-module files ( should have *.module mask )
{
test: /\.module\.(css|scss|sass)$/,
use: generateStyleLoaders({ hasModule: true }),
},
{
include: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hot: argv.mode !== 'production',
},
},
'css-loader',
]
},
{
use: generateStyleLoaders({ hasModule: false }),
}
]
},
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
optimization: {
minimize: false,
namedModules: true,
concatenateModules: false,
},
plugins: [
new IgnoreNotFoundExportPlugin(),
new webpack.ProgressPlugin({
entries: true,
modules: true,
modulesCount: 100,
profile: true,
handler: (percentage, message, ...args) => {
// custom logic
}
}),
new CircularDependencyPlugin({
// exclude detection of files based on a RegExp
exclude: /a\.js|node_modules/,
// include specific files based on a RegExp
// include: /dir/,
// add errors to webpack instead of warnings
failOnError: false,
// allow import cycles that include an asyncronous import,
// e.g. via import(/* webpackMode: "weak" */ './file.js')
allowAsyncCycles: false,
// set the current working directory for displaying module paths
cwd: process.cwd(),
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// all options are optional
filename: argv.mode !== 'production' ? '[name].css' : '[name].[hash].css',
chunkFilename: argv.mode !== 'production' ? '[name].css' : '[name].[hash].css',
ignoreOrder: false, // Enable to remove warnings about conflicting order
}),
// new webpack.optimize.LimitChunkCountPlugin({
// maxChunks: 1,
// }),
],
performance: {
hints: false,
},
};
};
+8 -4
View File
@@ -16,6 +16,7 @@
"build": "lerna run build --stream",
"serve": "lerna run serve --stream",
"serve-plugin": "lerna run serve-plugin --stream --parallel",
"builder-help": "builder help",
"clean-dist": "lerna run clean-dist --stream",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -23,10 +24,13 @@
"@babel/runtime": "7.8.4"
},
"devDependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^12.7.8",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.6",
"@graphql-codegen/add": "^1.8.1",
"@graphql-codegen/cli": "^1.8.1",
"@graphql-codegen/near-operation-file-preset": "^1.8.0",
"@graphql-codegen/typescript": "^1.8.1",
"@graphql-codegen/typescript-graphql-files-modules": "^1.8.0",
"@graphql-codegen/typescript-graphql-request": "^1.8.1",
"@graphql-codegen/typescript-operations": "^1.8.1",
"@typescript-eslint/eslint-plugin": "^2.5.0",
"@typescript-eslint/parser": "^2.8.0",
"eslint": "^6.6.0",
+4 -10
View File
@@ -6,22 +6,16 @@
"main": "dist/index.js",
"module": "dist/index.js",
"scripts": {
"build": "npm run clean-dist && builder build-plugin",
"build": "yarn run clean-dist && builder build-plugin",
"serve-plugin": "builder serve-plugin",
"clean-dist": "rimraf dist/*",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@dbeaver/core": "^0.1.0"
"@dbeaver/core": "^0.1.0",
"@dbeaver/builder": "^0.1.0"
},
"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.77",
"rxjs": "^6.5.5"
"@dbeaver/core": "^0.1.0"
}
}
@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "dist/types",
"declaration": true,
"noEmit": false,
"outDir": "dist/js"
},
"exclude": [
"node_modules",
"@dbeaver/**"
],
"include": ["src/**/*"]
}
@@ -1,5 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declarationDir": "dist",
"outDir": "dist"
},
"include": [
"src"
],
@@ -0,0 +1,4 @@
{
"administration_menu_enter": "Administration",
"administration_menu_back": "Back to app"
}
@@ -0,0 +1,4 @@
{
"administration_menu_enter": "Администрирование",
"administration_menu_back": "Вернуться в приложение"
}
+5 -12
View File
@@ -3,25 +3,18 @@
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/types",
"main": "./dist/index.js",
"scripts": {
"build": "npm run clean-dist && builder build-plugin",
"build": "yarn run clean-dist && builder build-plugin",
"serve-plugin": "builder serve-plugin",
"clean-dist": "rimraf dist/*",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@dbeaver/core": "^0.1.0"
"@dbeaver/core": "^0.1.0",
"@dbeaver/builder": "^0.1.0"
},
"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.77",
"rxjs": "^6.5.5"
"@dbeaver/core": "^0.1.0"
}
}
@@ -0,0 +1,97 @@
/*
* 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 { TabsState, TabList, verticalTabStyles } from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { composes, useStyles } from '@dbeaver/core/theming';
import { AdministrationController } from './AdministrationController';
import { DrawerItem } from './DrawerItem';
import { ItemContent } from './ItemContent';
const tabsStyles = composes(
css`
TabList {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
}
Tab {
composes: theme-ripple theme-background-background theme-ripple-selectable from global;
}
`,
css`
Tab {
color: inherit;
}
`
);
const administrationStyles = composes(
css`
container {
composes: theme-background-surface theme-text-on-surface from global;
}
drawer {
composes: theme-border-color-background from global;
}
`,
css`
container {
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
}
drawer {
width: 250px;
padding-top: 16px;
border-right: 2px solid;
}
content {
flex: 1;
display: flex;
flex-direction: column;
}
`
);
type Props = {
activeItem: string | null;
activeItemSub: string | null;
activeItemSubParam: string | null;
onItemSelect(name: string): void;
}
export const Administration = observer(function Administration({
activeItem, activeItemSub, activeItemSubParam, onItemSelect,
}: Props) {
const controller = useController(AdministrationController);
return styled(useStyles(verticalTabStyles, administrationStyles, tabsStyles))(
<container as='div'>
<TabsState currentTabId={activeItem} orientation='vertical'>
<drawer as='div'>
<TabList aria-label="Administration items">
{controller.items.map(item => (
<DrawerItem key={item.name} item={item} onSelect={onItemSelect} style={[verticalTabStyles, tabsStyles]} />
))}
</TabList>
</drawer>
<content as='div'>
<ItemContent
activeItemName={activeItem}
activeItemSub={activeItemSub}
activeItemSubParam={activeItemSubParam}
/>
</content>
</TabsState>
</container>
);
});
@@ -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 { computed } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService';
@injectable()
export class AdministrationController {
@computed get items() {
return this.administrationItemService
.items
.concat()
.sort((a, b) => a.order - b.order);
}
constructor(
private administrationItemService: AdministrationItemService
) {}
}
@@ -0,0 +1,28 @@
/*
* 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`
administration-tools {
height: 48px;
}
`;
type Props = React.PropsWithChildren<{
className?: string;
}>
export function AdministrationTools({ children, className }: Props) {
return styled(styles)(
<administration-tools as='div' className={className}>
{children}
</administration-tools>
);
}
@@ -0,0 +1,17 @@
/*
* 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 { AdministrationItemDrawerProps } from '../AdministrationItem/IAdministrationItem';
export const DrawerItem = observer(function DrawerItem({ item, onSelect, style }: AdministrationItemDrawerProps) {
const Component = item.getDrawerComponent();
return <Component item={item} onSelect={onSelect} style={style}/>;
});
@@ -0,0 +1,46 @@
/*
* 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 { useService } from '@dbeaver/core/di';
import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService';
type Props = {
activeItemName: string | null;
activeItemSub: string | null;
activeItemSubParam: string | null;
}
export const ItemContent = observer(function ItemContent({ activeItemName, activeItemSub, activeItemSubParam }: Props) {
const administrationItemService = useService(AdministrationItemService);
if (!activeItemName) {
return null;
}
const item = administrationItemService.getItem(activeItemName);
if (!item) {
return null;
}
if (activeItemSub) {
const sub = administrationItemService.getItemSub(item, activeItemSub);
if (sub) {
const Component = sub.getComponent();
return <Component item={item} sub={sub} param={activeItemSubParam} />;
}
}
const Component = item.getContentComponent();
return <Component item={item} />;
});
@@ -0,0 +1,75 @@
/*
* 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 { IAdministrationItem, IAdministrationItemOptions, IAdministrationItemSubItem } from './IAdministrationItem';
@injectable()
export class AdministrationItemService {
@observable items: IAdministrationItem[] = []
getDefaultItem() {
if (this.items.length === 0) {
return null;
}
return this.items[0].name;
}
getItem(name: string): IAdministrationItem | null {
const item = this.items.find(item => item.name === name);
if (!item) {
return null;
}
return item;
}
getItemSub(item: IAdministrationItem, subItem: string): IAdministrationItemSubItem | null {
const sub = item.sub.find(sub => sub.name === subItem);
if (!sub) {
return null;
}
return sub;
}
create(options: IAdministrationItemOptions) {
if (this.items.some(item => item.name === options.name)) {
throw new Error(`Administration item "${options.name}" already exists`);
}
const item: IAdministrationItem = {
...options,
sub: options.sub || [],
order: options.order || Number.MAX_SAFE_INTEGER,
};
this.items.push(item);
}
async activate(name: string, itemSub: string | null, param: string | null) {
const item = this.getItem(name);
if (!item) {
return;
}
if (item.onActivate) {
await item.onActivate();
}
if (itemSub) {
const sub = this.getItemSub(item, itemSub);
if (sub && sub.onActivate) {
await sub.onActivate(param);
}
}
}
}
@@ -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 { Style } from '@dbeaver/core/theming';
export type AdministrationItemDrawerProps = {
item: IAdministrationItem;
onSelect(id: string): void;
style: Style[];
}
export type AdministrationItemDrawerComponent = React.FunctionComponent<AdministrationItemDrawerProps>
export type AdministrationItemContentProps = {
item: IAdministrationItem;
}
export type AdministrationItemContentComponent = React.FunctionComponent<AdministrationItemContentProps>
export type AdministrationItemSubContentProps = {
item: IAdministrationItem;
sub: IAdministrationItemSubItem;
param: string | null;
}
export type AdministrationItemSubContentComponent = React.FunctionComponent<AdministrationItemSubContentProps>
export type AdministrationItemEvent = () => Promise<void> | void
export type AdministrationItemSubEvent = (param: string | null) => Promise<void> | void
export interface IAdministrationItemSubItem {
name: string;
getComponent(): AdministrationItemSubContentComponent;
onActivate?: AdministrationItemSubEvent;
}
export interface IAdministrationItemOptions {
name: string;
order?: number;
sub?: IAdministrationItemSubItem[];
getDrawerComponent(): AdministrationItemDrawerComponent;
getContentComponent(): AdministrationItemContentComponent;
onActivate?: AdministrationItemEvent;
}
export interface IAdministrationItem extends IAdministrationItemOptions {
order: number;
sub: IAdministrationItemSubItem[];
}
@@ -6,28 +6,45 @@
* you may not use this file except in compliance with the License.
*/
import { SettingsMenuService } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { SettingsMenuService, ScreenService, AppScreenService } from '@dbeaver/core/app';
import { injectable, Bootstrap } from '@dbeaver/core/di';
import { PermissionsService } from '@dbeaver/core/root';
export const ADMINISTRATION_PERMISSION = 'admin';
import { AdministrationScreenService } from './AdministrationScreen/AdministrationScreenService';
import { EAdminPermission } from './EAdminPermission';
@injectable()
export class AdministrationMenuService {
static administrationMenuToken = 'administrationMenu';
export class AdministrationMenuService extends Bootstrap {
constructor(
private settingsMenuService: SettingsMenuService,
private permissionsService: PermissionsService,
) { }
private screenService: ScreenService,
private administrationScreenService: AdministrationScreenService,
private appScreenService: AppScreenService
) {
super();
}
register() {
bootstrap() {
this.settingsMenuService.addMenuItem(
SettingsMenuService.settingsMenuToken,
{
id: AdministrationMenuService.administrationMenuToken,
id: 'administrationMenuEnter',
order: 0,
isHidden: () => !this.permissionsService.has(ADMINISTRATION_PERMISSION),
isHidden: () => !this.permissionsService.has(EAdminPermission.admin)
|| this.screenService.isActive(AdministrationScreenService.screenName),
title: 'administration_menu_enter',
onClick: () => this.administrationScreenService.navigateToRoot(),
}
);
this.settingsMenuService.addMenuItem(
SettingsMenuService.settingsMenuToken,
{
id: 'administrationMenuBack',
order: 0,
isHidden: () => !this.screenService.isActive(AdministrationScreenService.screenName),
title: 'administration_menu_back',
onClick: () => this.appScreenService.navigateToRoot(),
}
);
}
@@ -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 { useCallback } from 'react';
import { useService } from '@dbeaver/core/di';
import { Translate } from '@dbeaver/core/localization';
import { usePermission } from '@dbeaver/core/root';
import { Administration } from '../Administration/Administration';
import { EAdminPermission } from '../EAdminPermission';
import { AdministrationScreenService } from './AdministrationScreenService';
import { AdministrationTopAppBar } from './AdministrationTopAppBar/AdministrationTopAppBar';
export const AdministrationScreen = observer(function AdministrationScreen() {
const administrationScreenService = useService(AdministrationScreenService);
if (!usePermission(EAdminPermission.admin)) {
return <Translate token='root_permission_denied'/>;
}
const handleSelect = useCallback(
(item: string) => administrationScreenService.navigateToItem(item),
[administrationScreenService]
);
return (
<>
<AdministrationTopAppBar />
<Administration
activeItem={administrationScreenService.activeItem}
activeItemSub={administrationScreenService.activeItemSub}
activeItemSubParam={administrationScreenService.activeItemSubParam}
onItemSelect={handleSelect}
/>
</>
);
});
@@ -0,0 +1,111 @@
/*
* 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 { computed } from 'mobx';
import { ScreenService, RouterService, AppScreenService } from '@dbeaver/core/app';
import { injectable, Bootstrap } from '@dbeaver/core/di';
import { PermissionsService } from '@dbeaver/core/root';
import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService';
import { EAdminPermission } from '../EAdminPermission';
import { AdministrationScreen } from './AdministrationScreen';
@injectable()
export class AdministrationScreenService extends Bootstrap {
static screenName = 'administration'
static itemRouteName = 'administration.item'
static itemSubRouteName = 'administration.item.sub'
static itemSubParamRouteName = 'administration.item.sub.param'
@computed get activeItem(): string | null {
if (!this.screenService.isActive(AdministrationScreenService.screenName)) {
return null;
}
return this.routerService.params.item || this.administrationItemService.getDefaultItem();
}
@computed get activeItemSub(): string | null {
if (!this.screenService.isActive(AdministrationScreenService.screenName)) {
return null;
}
return this.routerService.params.sub || null;
}
@computed get activeItemSubParam(): string | null {
if (!this.screenService.isActive(AdministrationScreenService.screenName)) {
return null;
}
return this.routerService.params.param || null;
}
constructor(
private screenService: ScreenService,
private routerService: RouterService,
private permissionsService: PermissionsService,
private appScreenService: AppScreenService,
private administrationItemService: AdministrationItemService
) {
super();
this.permissionsService.onUpdate.subscribe(this.handleActivate.bind(this));
}
navigateToRoot() {
this.routerService.router.navigate(AdministrationScreenService.screenName);
}
navigateToItem(item: string) {
this.routerService.router.navigate(AdministrationScreenService.itemRouteName, { item });
}
navigateToItemSub(item: string, sub: string, param?: string) {
if (!param) {
this.routerService.router.navigate(AdministrationScreenService.itemSubRouteName, { item, sub });
return;
}
this.routerService.router.navigate(AdministrationScreenService.itemSubParamRouteName, { item, sub, param });
}
bootstrap() {
this.screenService.create({
name: AdministrationScreenService.screenName,
routes: [
{
name: AdministrationScreenService.screenName,
path: '/admin',
},
{
name: AdministrationScreenService.itemRouteName,
path: '/:item',
},
{
name: AdministrationScreenService.itemSubRouteName,
path: '/:sub',
},
{
name: AdministrationScreenService.itemSubParamRouteName,
path: '/:param',
},
],
component: AdministrationScreen,
onActivate: this.handleActivate.bind(this),
});
}
private async handleActivate() {
if (!this.permissionsService.has(EAdminPermission.admin)) {
this.appScreenService.navigateToRoot();
return;
}
if (this.activeItem) {
await this.administrationItemService.activate(this.activeItem, this.activeItemSub, this.activeItemSubParam);
}
}
}
@@ -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 { Placeholder, TopAppBar } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { AdministrationTopAppBarService } from './AdministrationTopAppBarService';
export function AdministrationTopAppBar() {
const administrationTopAppBarService = useService(AdministrationTopAppBarService);
return (
<TopAppBar>
<Placeholder container={administrationTopAppBarService.placeholder} />
</TopAppBar>
);
}
@@ -0,0 +1,24 @@
/*
* 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 { SettingsMenu, Logo } from '@dbeaver/core/app';
import { PlaceholderContainer } from '@dbeaver/core/blocks';
import { injectable } from '@dbeaver/core/di';
import { Fill } from './Fill';
@injectable()
export class AdministrationTopAppBarService {
readonly placeholder = new PlaceholderContainer();
constructor() {
this.placeholder.add(Logo, 0);
this.placeholder.add(Fill, 3);
this.placeholder.add(SettingsMenu, 4);
}
}
@@ -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' />);
}
@@ -1,20 +0,0 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@dbeaver/core/di';
import { AdministrationMenuService } from './AdministrationMenuService';
@injectable()
export class Bootstrap {
constructor(private administrationMenuService: AdministrationMenuService) { }
bootstrap() {
this.administrationMenuService.register();
}
}
@@ -0,0 +1,11 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export enum EAdminPermission {
admin = 'admin'
}
@@ -2,4 +2,9 @@ import { manifest } from './manifest';
export default manifest;
export * from './Administration/AdministrationTools/AdministrationTools';
export * from './AdministrationItem/AdministrationItemService';
export * from './AdministrationItem/IAdministrationItem';
export * from './AdministrationScreen/AdministrationScreenService';
export * from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService';
export * from './AdministrationMenuService';
@@ -8,8 +8,10 @@
import { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { AdministrationItemService } from './AdministrationItem/AdministrationItemService';
import { AdministrationMenuService } from './AdministrationMenuService';
import { Bootstrap } from './Bootstrap';
import { AdministrationScreenService } from './AdministrationScreen/AdministrationScreenService';
import { AdministrationTopAppBarService } from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService';
export const manifest: PluginManifest = {
info: {
@@ -18,11 +20,11 @@ export const manifest: PluginManifest = {
providers: [
AdministrationMenuService,
AdministrationScreenService,
AdministrationTopAppBarService,
AdministrationItemService,
],
async initialize(services: IServiceInjector) {
services
.resolveServiceByClass(Bootstrap)
.bootstrap();
},
};
@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "dist/types",
"declaration": true,
"noEmit": false,
"outDir": "dist/js"
},
"exclude": [
"node_modules",
"@dbeaver/**"
],
"include": ["src/**/*"]
}
@@ -1,5 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declarationDir": "dist",
"outDir": "dist"
},
"include": [
"src/**/*"
],
+10 -15
View File
@@ -3,31 +3,26 @@
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/types",
"main": "./dist/index.js",
"scripts": {
"build": "npm run clean-dist && builder build-plugin",
"build": "yarn run clean-dist && builder build-plugin",
"serve-plugin": "builder serve-plugin",
"clean-dist": "rimraf dist/*",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@ag-grid-community/all-modules": "^23.0.2",
"@ag-grid-community/core": "^23.0.2",
"ag-grid-community": "^23.0.2",
"ag-grid-react": "^23.0.3"
"@ag-grid-community/all-modules": "^23.1.1",
"@ag-grid-community/core": "^23.1.1",
"ag-grid-community": "^23.1.1",
"ag-grid-react": "^23.1.1"
},
"devDependencies": {
"@dbeaver/core": "^0.1.0"
"@dbeaver/core": "^0.1.0",
"@dbeaver/builder": "^0.1.0"
},
"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.77",
"rxjs": "^6.5.5"
"react": "^16.3.0",
"react-dom": "^16.3.0"
}
}
@@ -17,6 +17,7 @@ import {
GridOptions,
CellEditingStoppedEvent,
} from 'ag-grid-community';
import { SortChangedEvent } from 'ag-grid-community/dist/lib/events';
import { RowDataTransaction } from 'ag-grid-community/dist/lib/interfaces/rowDataTransaction';
import { computed, observable } from 'mobx';
@@ -24,11 +25,12 @@ import { injectable, IInitializableController, IDestructibleController } from '@
import { AgGridContext } from './AgGridContext';
import {
AgGridRow, IAgGridActions, IAgGridCol, IAgGridModel,
AgGridRow, IAgGridActions, IAgGridCol, IAgGridModel, SortMode, SortModel,
} from './IAgGridModel';
import { RowSelection } from './TableSelection/RowSelection';
import { TableSelection } from './TableSelection/TableSelection';
@injectable()
export class AgGridTableController implements IInitializableController, IDestructibleController {
@observable refreshId = 0;
@@ -68,6 +70,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
onBodyScroll: this.handleBodyScroll.bind(this),
onCellEditingStopped: this.handleCellEditingStopped.bind(this),
onSortChanged: this.handleSortChanged.bind(this),
};
@observable columns: ColDef[] = [];
@@ -75,7 +78,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
/**
* use this object to dynamically change ag-grid properties
*/
@computed get dynamicOptions() {
@computed get dynamicOptions(): GridOptions {
return {
enableRangeSelection: !!this.selection,
};
@@ -128,15 +131,22 @@ export class AgGridTableController implements IInitializableController, IDestruc
startRow,
endRow,
successCallback,
sortModel,
failCallback,
} = params;
try {
const length = endRow - startRow;
const requestedData = await this.gridModel.onRequestData(startRow, length);
const requestedData = await this.gridModel.onRequestData(
startRow,
length,
{
sorting: sortModel as SortModel,
}
);
// update columns only once after first data fetching
if (!this.columns.length) {
this.columns = mapDataToColumns(requestedData.columns || []);
if (isColumnsChanged(this.columns, requestedData.columns)) {
this.columns = mapDataToColumns(requestedData.columns);
}
successCallback(
this.cloneRows(requestedData.rows),
@@ -190,16 +200,22 @@ export class AgGridTableController implements IInitializableController, IDestruc
this.setInitialRow(this.gridModel.initialRows);
}
private handleSortChanged(event: SortChangedEvent) {
if (this.gridModel.onSortChanged) {
const sortModel = event.api.getSortModel() as SortModel;
this.gridModel.onSortChanged(sortModel);
}
}
/* Actions */
private resetData(columns?: IAgGridCol[], rows?: AgGridRow[]): void {
this.selection.clear();
if (this.api) {
// only purgeInfiniteCache() doesn't work when cache is empty.
// probably it thinks that nothing to delete - nothing to refresh
this.api.refreshInfiniteCache(); // it will mark internal state for reload
this.api.purgeInfiniteCache(); // it will reset internal state
this.columns = columns ? mapDataToColumns(columns) : [];
if (columns) {
this.columns = mapDataToColumns(columns);
}
this.setInitialRow(rows);
}
}
@@ -212,7 +228,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
addIndex: 0,
add: initialRows || [],
};
this.api!.updateRowData(transaction);
this.api!.applyTransaction(transaction);
}
private updateCellValue(rowNumber: number, colNumber: number, value: any): void {
@@ -257,16 +273,18 @@ export const INDEX_COLUMN_DEF: ColDef = {
suppressNavigable: true,
suppressMenu: true,
editable: false,
sortable: false,
cellRenderer: row => row.rowIndex + 1,
};
function mapDataToColumns(columns: IAgGridCol[]): ColDef[] {
if (!columns.length) {
function mapDataToColumns(columns?: IAgGridCol[]): ColDef[] {
if (!columns || !columns.length) {
return [];
}
return [
INDEX_COLUMN_DEF,
...columns.map(v => ({
colId: v.name,
headerName: v.label,
field: `${v.position}`,
valueGetter: v.dataKind === 'OBJECT' ? getObjectValue : undefined,
@@ -280,3 +298,11 @@ function mapDataToColumns(columns: IAgGridCol[]): ColDef[] {
function getObjectValue({ data, colDef }: ValueGetterParams) {
return data[colDef.field || 'node.id'].value;
}
function isColumnsChanged(oldColumns: ColDef[], newColumns: IAgGridCol[] = []): boolean {
const [indexColumn, ...withoutIndexColumn] = oldColumns;
if (withoutIndexColumn.length !== newColumns.length) {
return true;
}
return withoutIndexColumn.some((col, ind) => col.colId !== newColumns[ind].name);
}
@@ -10,6 +10,18 @@ import { RowSelection } from './TableSelection/RowSelection';
export type AgGridRow = any[];
export type SortMode = 'asc' | 'desc' | null;
export type SortModel = {
colId: string;
sort: SortMode;
}[];
export interface IRequestDataOptions {
sorting?: SortModel;
}
export interface IAgGridModel {
initialRows: AgGridRow[];
@@ -19,8 +31,9 @@ export interface IAgGridModel {
actions: IAgGridActions | null;
// hooks
onRequestData(rowOffset: number, count: number): Promise<IRequestedData>;
onRequestData(rowOffset: number, count: number, options?: IRequestDataOptions): Promise<IRequestedData>;
onCellEditingStopped?(rowNumber: number, colNumber: number, value: any): void;
onSortChanged?(sorting: SortModel): void;
onEditSave(): void;
onEditCancel(): void;
}
@@ -36,6 +49,7 @@ export interface IAgGridActions {
export interface IAgGridCol {
icon?: string;
label?: string;
name?: string;
position?: number;
dataKind?: string;
}
@@ -6,11 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { useCallback, useEffect, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { AgEvent, GridApi, Column } from '@ag-grid-community/core';
import { StaticImage } from '@dbeaver/core/blocks';
import { StaticImage, Icon } from '@dbeaver/core/blocks';
import { SortMode } from '../IAgGridModel';
type HeaderProps = {
api: GridApi;
@@ -21,7 +23,7 @@ type HeaderProps = {
menuIcon: string;
displayName: string;
showColumnMenu: (button?: HTMLDivElement) => void;
setSort: (order: string, shiftKey: boolean) => void;
setSort: (order: SortMode, shiftKey: boolean) => void;
icon?: string;
}
@@ -42,6 +44,28 @@ const headerStyles = css`
margin-left: 8px;
text-transform: uppercase;
font-weight: 400;
flex-grow: 1;
}
sort-icon {
margin-left: 4px;
display: flex;
padding: 2px 4px;
flex-direction: column;
align-content: center;
}
sort-icon > Icon {
width: 8px;
fill: #cbcbcb;
}
sort-icon > Icon:last-child {
transform: scaleY(-1);
}
sort-icon > Icon[|active] {
fill: #338ECC;
}
sort-icon:hover > Icon {
width: 9px;
}
`;
@@ -63,12 +87,47 @@ export function TableColumnHeader(props: HeaderProps) {
props.api.dispatchEvent(event);
}, []);
const [sortMode, setSortMode] = useState<SortMode>(() => props.column.getSort() as SortMode);
useEffect(() => {
function onSortChanged() {
setSortMode(props.column.getSort() as SortMode);
}
props.column.addEventListener('sortChanged', onSortChanged);
return function cleanup() {
props.column.removeEventListener('sortChanged', onSortChanged);
};
});
const handleSort = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const sort = props.column.getSort() as SortMode;
let nextSort: SortMode;
switch (sort) {
case 'asc':
nextSort = 'desc';
break;
case 'desc':
nextSort = null;
break;
default:
nextSort = 'asc';
}
props.setSort(nextSort, e.ctrlKey);
},
[]
);
return styled(headerStyles)(
<table-header as="div" onClick={handleClick}>
<icon as="div">
<StaticImage icon={props.icon} />
</icon>
<name as="div">{props.displayName}</name>
{props.enableSorting && <sort-icon as="div" onClick={handleSort}>
<Icon name="sort-arrow" viewBox="0 0 6 6" {...use({ active: sortMode === 'asc' })} />
<Icon name="sort-arrow" viewBox="0 0 6 6" {...use({ active: sortMode === 'desc' })} />
</sort-icon>}
</table-header>
);
}
@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "dist/types",
"declaration": true,
"noEmit": false,
"outDir": "dist/js"
},
"exclude": [
"node_modules",
"@dbeaver/core/**"
],
"include": ["src/**/*"]
}
@@ -1,5 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declarationDir": "dist",
"outDir": "dist"
},
"include": [
"src"
],
@@ -0,0 +1,12 @@
{
"authentication_administration_item": "Users",
"authentication_administration_confirm_user_deletion": "Confirm user deletion",
"authentication_create_user_dialog_title": "Create new user",
"authentication_create_user_dialog_create": "Create",
"authentication_user_name": "User name",
"authentication_user_password": "User password",
"authentication_user_password_repeat": "Repeat password",
"authentication_user_role": "User role",
"authentication_user_passwords_not_match": "Passwords don't match",
"authentication_user_user_created": "User was created successfully"
}
@@ -0,0 +1,11 @@
{
"authentication_administration_item": "Пользователи",
"authentication_create_user_dialog_title": "Создание нового пользователя",
"authentication_create_user_dialog_create": "Создать",
"authentication_user_name": "Имя",
"authentication_user_password": "Пароль",
"authentication_user_password_repeat": "Повторите пароль",
"authentication_user_role": "Роль",
"authentication_user_passwords_not_match": "Пароли не совпадают",
"authentication_user_user_created": "Пользователь создан успешно"
}
+6 -11
View File
@@ -3,25 +3,20 @@
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/types",
"main": "./dist/index.js",
"scripts": {
"build": "npm run clean-dist && builder build-plugin",
"build": "yarn run clean-dist && builder build-plugin",
"serve-plugin": "builder serve-plugin",
"clean-dist": "rimraf dist/*",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@dbeaver/administration": "^0.1.0",
"@dbeaver/builder": "^0.1.0",
"@dbeaver/core": "^0.1.0"
},
"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.76",
"rxjs": "^6.5.5"
"@dbeaver/administration": "^0.1.0",
"@dbeaver/core": "^0.1.0"
}
}
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path fill="#F0840E" d="M9 0c2.485 0 4.5 2.015 4.5 4.5S11.485 9 9 9 4.5 6.985 4.5 4.5 6.515 0 9 0zm0 2.25c-1.243 0-2.25 1.007-2.25 2.25S7.757 6.75 9 6.75s2.25-1.007 2.25-2.25S10.243 2.25 9 2.25zm0 7.875c3.004 0 9 1.496 9 4.5V18H0v-3.375c0-3.004 5.996-4.5 9-4.5zm0 2.137c-3.341 0-6.862 1.643-6.862 2.363v1.238h13.725v-1.238c0-.72-3.522-2.363-6.863-2.363z"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

@@ -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 { AdministrationItemService } from '@dbeaver/administration';
import { injectable, Bootstrap } from '@dbeaver/core/di';
@injectable()
export class AuthAdministrationService extends Bootstrap {
constructor(
private administrationItemService: AdministrationItemService
) {
super();
}
bootstrap() {
}
}
@@ -0,0 +1,47 @@
/*
* 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 {
CachedResource, GraphQLService, AdminRoleInfo
} from '@dbeaver/core/sdk';
@injectable()
export class RolesManagerService {
readonly roles = new CachedResource(
[],
this.refreshAsync.bind(this),
data => !!data.length
)
constructor(
private graphQLService: GraphQLService,
) {
}
private async refreshAsync(
data: AdminRoleInfo[],
_: any,
update: boolean,
roleId?: string
): Promise<AdminRoleInfo[]> {
const { roles } = await this.graphQLService.gql.getRolesList({ roleId });
if (!roleId) {
return roles as AdminRoleInfo[];
}
const index = data.findIndex(role => role.roleId === roleId);
if (index !== -1) {
data.splice(index, 1, ...roles as AdminRoleInfo[]);
} else {
data.push(...roles as AdminRoleInfo[]);
}
return data;
}
}
@@ -0,0 +1,103 @@
/*
* 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, { css } from 'reshadow';
import {
InputField, SubmittingForm, ErrorMessage, Button
} from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles, composes } from '@dbeaver/core/theming';
import { CreateUserController } from './CreateUserController';
const styles = composes(
css`
`,
css`
SubmittingForm {
display: flex;
flex: 1;
}
create-form {
flex: 1;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
group {
box-sizing: border-box;
display: flex;
margin: 0 12px;
}
controls {
display: flex;
align-items: center;
justify-content: flex-end;
}
item-title {
composes: theme-typography--headline5 from global;
padding: 16px;
}
`
);
export const CreateUser = observer(function CreateUser() {
const controller = useController(CreateUserController);
const translate = useTranslate();
const handleChange = useCallback(
(value: string) => controller.login = value,
[]
);
return styled(useStyles(styles))(
<>
<item-title as='div'>
User creation
</item-title>
<SubmittingForm>
<create-form as='div'>
<group as="div">
<InputField
type='text'
name='login'
value={controller.login}
onChange={handleChange}
disabled={controller.isCreating}
mod='surface'
>
{translate('User name')}
</InputField>
</group>
<controls as="div">
<Button
type="button"
mod={['unelevated']}
onClick={controller.create}
disabled={controller.isCreating}
>
{translate('Create')}
</Button>
</controls>
</create-form>
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.showDetails}
/>
)}
</>
);
});
@@ -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 { observable } from 'mobx';
import { AdministrationScreenService } from '@dbeaver/administration';
import { ErrorDetailsDialog } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GQLErrorCatcher } from '@dbeaver/core/sdk';
import { UsersManagerService } from '../../UsersManagerService';
@injectable()
export class CreateUserController {
@observable isCreating = false;
@observable login = '';
readonly error = new GQLErrorCatcher();
constructor(
private notificationService: NotificationService,
private administrationScreenService: AdministrationScreenService,
private usersManagerService: UsersManagerService,
private commonDialogService: CommonDialogService,
) { }
create = async () => {
if (this.isCreating) {
return;
}
this.isCreating = true;
try {
await this.usersManagerService.create(this.login);
this.administrationScreenService.navigateToItem('users');
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'User create failed');
}
} finally {
this.isCreating = false;
}
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
}
@@ -0,0 +1,159 @@
/*
* 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, { css } from 'reshadow';
import {
SubmittingForm, ErrorMessage, InputField, Checkbox
} 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 { CreateUserDialogController } from './CreateUserDialogController';
import { CreateUserDialogFooter } from './CreateUserDialogFooter';
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`
CommonDialogWrapper {
min-height: 400px;
min-width: 600px;
}
SubmittingForm {
overflow: auto;
margin-top: 20px;
margin-bottom: 20px;
}
SubmittingForm, create-form {
flex: 1;
display: flex;
flex-direction: column;
}
create-form {
flex-direction: column;
padding: 18px 24px;
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
`
);
export const CreateUserDialog: DialogComponent<null, null> = observer(
function CreateUserDialog(props) {
const controller = useController(CreateUserDialogController, props.rejectDialog);
const translate = useTranslate();
const handleLoginChange = useCallback(
(value: string) => controller.credentials.login = value,
[]
);
const handlePasswordChange = useCallback(
(value: string) => controller.credentials.password = value,
[]
);
const handlePasswordRepeatChange = useCallback(
(value: string) => controller.credentials.passwordRepeat = value,
[]
);
const handleRoleChange = useCallback(
(roleId: string, value: boolean) => controller.credentials.roles.set(roleId, value),
[]
);
return styled(useStyles(styles))(
<CommonDialogWrapper
title={translate('authentication_create_user_dialog_title')}
noBodyPadding
footer={(
<CreateUserDialogFooter
isCreating={controller.isCreating}
onCancel={props.rejectDialog}
onCreate={controller.create}
/>
)}
onReject={props.rejectDialog}
>
<SubmittingForm onSubmit={controller.create} autoComplete="disabled">
<create-form as='div'>
<group as="div">
<InputField
type='text'
name='login'
value={controller.credentials.login}
onChange={handleLoginChange}
disabled={controller.isCreating}
mod='surface'
>
{translate('authentication_user_name')}
</InputField>
</group>
<group as="div">
<InputField
type='password'
name='password'
value={controller.credentials.password}
onChange={handlePasswordChange}
disabled={controller.isCreating}
mod='surface'
>
{translate('authentication_user_password')}
</InputField>
</group>
<group as="div">
<InputField
type='password'
name='password_repeat'
value={controller.credentials.passwordRepeat}
onChange={handlePasswordRepeatChange}
disabled={controller.isCreating}
mod='surface'
>
{translate('authentication_user_password_repeat')}
</InputField>
</group>
{controller.roles.map((role, i) => (
<group as="div" key={role.roleId}>
<Checkbox
type='checkbox'
name='role'
checkboxLabel={role.roleName || role.roleId}
onChange={checked => handleRoleChange(role.roleId, checked)}
checked={controller.credentials.roles.get(role.roleId)}
disabled={controller.isCreating}
mod='surface'
>
{i === 0 && translate('authentication_user_role')}
</Checkbox>
</group>
))}
</create-form>
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.showDetails}
/>
)}
</CommonDialogWrapper>
);
}
);
@@ -0,0 +1,112 @@
/*
* 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 { 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 { GQLErrorCatcher } from '@dbeaver/core/sdk';
import { RolesManagerService } from '../../RolesManagerService';
import { UsersManagerService } from '../../UsersManagerService';
@injectable()
export class CreateUserDialogController implements IInitializableController, IDestructibleController {
@observable isCreating = false;
@observable credentials = {
login: '',
password: '',
passwordRepeat: '',
roles: new Map<string, boolean>(),
};
@computed get roles() {
return this.rolesManagerService.roles.data;
}
readonly error = new GQLErrorCatcher();
private isDistructed = false;
private close!: () => void;
constructor(
private notificationService: NotificationService,
private usersManagerService: UsersManagerService,
private commonDialogService: CommonDialogService,
private rolesManagerService: RolesManagerService,
) { }
init(onClose: () => void) {
this.close = onClose;
this.loadRoles();
}
destruct(): void {
this.isDistructed = true;
}
create = async () => {
if (this.isCreating) {
return;
}
if (this.credentials.password !== this.credentials.passwordRepeat) {
this.notificationService.logError({ title: 'authentication_user_passwords_not_match' });
return;
}
this.isCreating = true;
let isUserCreated = false;
try {
const user = await this.usersManagerService.create(this.credentials.login, false);
isUserCreated = !!user;
await this.usersManagerService.updateCredentials(user.userId, { password: this.credentials.password });
for (const [roleId, checked] of this.credentials.roles) {
if (checked) {
await this.usersManagerService.grantRole(user.userId, roleId);
}
}
await this.usersManagerService.users.refresh(user.userId);
this.notificationService.logInfo({ title: 'authentication_user_user_created' });
this.close();
} catch (exception) {
if (isUserCreated) {
await this.deleteUser(this.credentials.login);
}
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, 'Error creating new user');
}
} finally {
this.isCreating = false;
}
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
private async deleteUser(userId: string) {
try {
await this.usersManagerService.delete(userId);
} catch (exception) {
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, 'Error deleting partially created user');
}
}
}
private async loadRoles() {
try {
await this.rolesManagerService.roles.load();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load roles');
}
}
}
@@ -0,0 +1,66 @@
/*
* 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;
}
fill {
flex: 1;
}
`;
export type Props = {
isCreating: boolean;
onCancel(): void;
onCreate(): void;
}
export const CreateUserDialogFooter = observer(
function CreateUserDialogFooter({
isCreating,
onCancel,
onCreate,
}: Props) {
const translate = useTranslate();
return styled(styles)(
<controls as="div">
<Button
type="button"
mod={['outlined']}
onClick={onCancel}
disabled={isCreating}
>
{translate('ui_processing_cancel')}
</Button>
<fill as="div"/>
<Button
type="button"
mod={['unelevated']}
onClick={onCreate}
disabled={isCreating}
>
{translate('authentication_create_user_dialog_create')}
</Button>
</controls>
);
}
);
@@ -0,0 +1,65 @@
/*
* 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 { AdministrationTools } from '@dbeaver/administration';
import { Loader, IconButton } from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { useStyles, composes } from '@dbeaver/core/theming';
import { UsersAdministrationController } from './UsersAdministrationController';
import { UsersTable } from './UsersTable/UsersTable';
const styles = composes(
css`
AdministrationTools {
composes: theme-background-secondary theme-text-on-secondary from global;
}
`,
css`
content {
position: relative;
padding-top: 16px;
flex: 1;
overflow: auto;
}
TableColumnHeader {
border-top: solid 1px;
}
AdministrationTools {
display: flex;
padding: 0 16px;
align-items: center;
}
IconButton {
height: 32px;
width: 32px;
margin-right: 16px;
}
`
);
export const UsersAdministration = observer(function UsersAdministration() {
const controller = useController(UsersAdministrationController);
return styled(useStyles(styles))(
<>
<AdministrationTools>
<IconButton name="add" viewBox="0 0 28 28" onClick={controller.create} />
<IconButton name="trash" viewBox="0 0 28 28" onClick={controller.delete} />
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
</AdministrationTools>
<content as='div'>
<UsersTable users={controller.users} selectedItems={controller.selectedItems} />
{controller.isLoading && <Loader overlay/>}
</content>
</>
);
});
@@ -0,0 +1,97 @@
/*
* 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 { ErrorDetailsDialog } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService, ConfirmationDialog } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GQLErrorCatcher } from '@dbeaver/core/sdk';
import { UsersManagerService } from '../UsersManagerService';
import { CreateUserDialog } from './CreateUserDialog/CreateUserDialog';
@injectable()
export class UsersAdministrationController {
@observable isDeleting = false;
readonly selectedItems = observable<string, boolean>(new Map())
readonly error = new GQLErrorCatcher();
get users() {
return this.usersManagerService.users.data;
}
get isLoading() {
return this.usersManagerService.users.isLoading();
}
constructor(
private notificationService: NotificationService,
private usersManagerService: UsersManagerService,
private commonDialogService: CommonDialogService,
) { }
create = () => {
this.commonDialogService.open(CreateUserDialog, null);
}
update = async () => {
try {
await this.usersManagerService.users.refresh(undefined);
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Users update failed');
}
}
}
delete = async () => {
if (this.isDeleting) {
return;
}
this.isDeleting = true;
try {
const deletionList = Array
.from(this.selectedItems)
.filter(([_, value]) => value)
.map(([userId]) => userId);
if (deletionList.length === 0) {
return;
}
const confirmed = await this.commonDialogService.open(ConfirmationDialog, {
title: 'authentication_administration_confirm_user_deletion',
message: `Would you like to delete users: ${deletionList.join(', ')}`,
});
if (!confirmed) {
return;
}
for (const userId of deletionList) {
await this.usersManagerService.delete(userId);
}
this.selectedItems.clear();
await this.usersManagerService.users.refresh(undefined);
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'User delete failed');
}
} finally {
this.isDeleting = false;
}
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
}
@@ -0,0 +1,67 @@
/*
* 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 { AdministrationItemService, AdministrationScreenService } from '@dbeaver/administration';
import { injectable, Bootstrap } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { UsersManagerService } from '../UsersManagerService';
import { CreateUser } from './CreateUser/CreateUser';
import { UsersAdministration } from './UsersAdministration';
import { UsersDrawerItem } from './UsersDrawerItem';
@injectable()
export class UsersAdministrationService extends Bootstrap {
constructor(
private administrationItemService: AdministrationItemService,
private administrationScreenService: AdministrationScreenService,
private notificationService: NotificationService,
private usersManagerService: UsersManagerService,
) {
super();
}
bootstrap() {
this.administrationItemService.create({
name: 'users',
sub: [
{
name: 'create',
getComponent: () => CreateUser,
},
{
name: 'edit',
getComponent: () => UsersAdministration,
},
],
getContentComponent: () => UsersAdministration,
getDrawerComponent: () => UsersDrawerItem,
onActivate: this.loadUsers.bind(this),
});
}
navToRoot() {
this.administrationScreenService.navigateToItem('users');
}
navToCreate() {
this.administrationScreenService.navigateToItemSub('users', 'create');
}
navToEdit(userId: string) {
this.administrationScreenService.navigateToItemSub('users', 'edit', userId);
}
private async loadUsers() {
try {
await this.usersManagerService.users.load(undefined);
} catch (exception) {
this.notificationService.logException(exception, 'Error occurred while loading users');
}
}
}
@@ -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 styled from 'reshadow';
import { AdministrationItemDrawerProps } from '@dbeaver/administration';
import { Tab, TabTitle, TabIcon } from '@dbeaver/core/blocks';
import { Translate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
export function UsersDrawerItem({ item, onSelect, style }: AdministrationItemDrawerProps) {
return styled(useStyles(...style))(
<Tab tabId={item.name} onOpen={() => onSelect(item.name)}>
<TabIcon icon='/icons/account.svg' />
<TabTitle><Translate token='authentication_administration_item'/></TabTitle>
</Tab>
);
}

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