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

This commit is contained in:
serge-rider
2020-09-09 21:49:33 +03:00
61 changed files with 532 additions and 328 deletions
+1
View File
@@ -7,3 +7,4 @@ services:
- "8978:8978"
volumes:
- "./logs:/opt/cloudbeaver/logs"
- "./workspace:/opt/cloudbeaver/workspace"
@@ -303,6 +303,9 @@ extend type Mutation {
# Refreshes session on server and returns its state
touchSession: Boolean
# Refresh session connection list
refreshSessionConnections: Boolean
# Refreshes session on server and returns its state
changeSessionLanguage(locale: String): Boolean
@@ -16,6 +16,8 @@
*/
package io.cloudbeaver.model.session;
import io.cloudbeaver.DBWConnectionGrant;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWSecurityController;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.WebAsyncTaskInfo;
@@ -31,14 +33,12 @@ 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.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.meta.Association;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.navigator.DBNModel;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.navigator.DBNProject;
import org.jkiss.dbeaver.model.navigator.DBNProjectDatabases;
import org.jkiss.dbeaver.model.navigator.*;
import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.dbeaver.model.runtime.BaseProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
@@ -52,6 +52,7 @@ import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Web session.
@@ -74,6 +75,8 @@ public class WebSession implements DBASession {
private WebUser user;
private Set<String> sessionPermissions = null;
private Set<String> accessibleConnectionIds = Collections.emptySet();
private String locale;
private boolean cacheExpired;
@@ -153,7 +156,11 @@ public class WebSession implements DBASession {
return user;
}
public synchronized Set<String> getSessionPermissions() throws DBCException {
public synchronized boolean hasPermission(String perm) {
return getSessionPermissions().contains(perm);
}
public synchronized Set<String> getSessionPermissions() {
if (sessionPermissions == null) {
refreshSessionAuth();
}
@@ -166,22 +173,22 @@ public class WebSession implements DBASession {
}
this.user = user;
refreshSessionAuth();
try {
resetSessionCache();
initNavigatorModel();
} catch (DBCException e) {
log.error(e);
}
try {
refreshSessionAuth();
} catch (DBCException e) {
log.error(e);
}
}
private void initNavigatorModel() {
CBPlatform platform = CBPlatform.getInstance();
this.navigatorModel = new DBNModel(platform, this);
// Add datasource filter (based on permissions)
navigatorModel.addFilter(node ->
!(node instanceof DBNDataSource) || isDataSourceAccessible(((DBNDataSource)node).getDataSourceContainer()));
this.navigatorModel.initialize();
DBPProject project = platform.getWorkspace().getActiveProject();
@@ -189,17 +196,66 @@ public class WebSession implements DBASession {
this.databases = projectNode.getDatabases();
this.locale = Locale.getDefault().getLanguage();
try {
this.refreshConnections();
} catch (Exception e) {
log.error("Error getting connection list", e);
}
}
public void refreshConnections() {
// Add all provided datasources to the session
List<WebConnectionInfo> connList = new ArrayList<>();
DBPDataSourceRegistry registry = databases.getDataSourceRegistry();
registry.refreshConfig();
for (DBPDataSourceContainer ds : registry.getDataSources()) {
if (ds.isProvided()) {
WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds);
connList.add(connectionInfo);
}
}
filterAccessibleConnections(connList);
// Add all provided datasources to the session
synchronized (connections) {
for (DBPDataSourceContainer ds : databases.getDataSourceRegistry().getDataSources()) {
if (ds.isProvided()) {
WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds);
connections.put(connectionInfo.getId(), connectionInfo);
}
connections.clear();
for (WebConnectionInfo connectionInfo : connList) {
connections.put(connectionInfo.getId(), connectionInfo);
}
}
}
public void filterAccessibleConnections(List<WebConnectionInfo> connections) {
if (this.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
// All connections are accessible
return;
}
connections.removeIf(c -> !accessibleConnectionIds.contains(c.getId()));
}
private boolean isDataSourceAccessible(DBPDataSourceContainer dataSource) {
return this.hasPermission(DBWConstants.PERMISSION_ADMIN) ||
accessibleConnectionIds.contains(dataSource.getId());
}
@NotNull
private Set<String> readAccessibleConnectionIds() {
CBApplication application = CBApplication.getInstance();
String subjectId = user == null ?
application.getAppConfiguration().getAnonymousUserRole() : user.getUserId();
try {
return Arrays.stream(application.getSecurityController()
.getSubjectConnectionAccess(new String[]{subjectId}))
.map(DBWConnectionGrant::getConnectionId).collect(Collectors.toSet());
} catch (DBCException e) {
log.error("Error reading connection grants", e);
return Collections.emptySet();
}
}
private void resetSessionCache() throws DBCException {
// Clear attributes
synchronized (attributes) {
@@ -230,17 +286,25 @@ public class WebSession implements DBASession {
this.databases = null;
}
private void refreshSessionAuth() throws DBCException {
CBApplication application = CBPlatform.getInstance().getApplication();
if (this.user == null) {
if (application.getAppConfiguration().isAnonymousAccessEnabled()) {
sessionPermissions = application.getSecurityController().getSubjectPermissions(
application.getAppConfiguration().getAnonymousUserRole());
private void refreshSessionAuth() {
try {
CBApplication application = CBPlatform.getInstance().getApplication();
if (this.user == null) {
if (application.getAppConfiguration().isAnonymousAccessEnabled()) {
sessionPermissions = application.getSecurityController().getSubjectPermissions(
application.getAppConfiguration().getAnonymousUserRole());
} else {
sessionPermissions = Collections.emptySet();
}
} else {
sessionPermissions = Collections.emptySet();
sessionPermissions = application.getSecurityController().getUserPermissions(this.user.getUserId());
}
} else {
sessionPermissions = application.getSecurityController().getUserPermissions(this.user.getUserId());
accessibleConnectionIds = readAccessibleConnectionIds();
} catch (Exception e) {
log.error("Error reading session permissions", e);
}
}
@@ -20,7 +20,9 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.stream.JsonWriter;
import io.cloudbeaver.DBWConnectionGrant;
import io.cloudbeaver.DBWSecurityController;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.server.jetty.CBJettyServer;
import org.eclipse.core.runtime.Platform;
import org.eclipse.equinox.app.IApplicationContext;
@@ -29,6 +31,7 @@ import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ModelPreferences;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.app.DBPApplication;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
@@ -38,6 +41,7 @@ import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.dbeaver.utils.PrefUtils;
import org.jkiss.dbeaver.utils.SystemVariablesResolver;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import org.jkiss.utils.StandardConstants;
@@ -407,6 +411,11 @@ public class CBApplication extends BaseApplicationImpl {
log.debug("Saving runtime configuration");
saveRuntimeConfig(newServerName, appConfig);
// Grant permissions to predefined connections
if (appConfig.isAnonymousAccessEnabled()) {
grantAnonymousAccessToConnections(appConfig, adminName);
}
// Re-load runtime configuration
try {
log.debug("Reloading application configuration");
@@ -422,6 +431,24 @@ public class CBApplication extends BaseApplicationImpl {
configurationMode = CommonUtils.isEmpty(serverName);
}
private void grantAnonymousAccessToConnections(CBAppConfig appConfig, String adminName) {
try {
String anonymousRoleId = appConfig.getAnonymousUserRole();
DBWSecurityController securityController = getSecurityController();
for (DBPDataSourceContainer ds : WebServiceUtils.getDataSourceRegistry().getDataSources()) {
DBWConnectionGrant[] grants = securityController.getConnectionSubjectAccess(ds.getId());
if (ArrayUtils.isEmpty(grants)) {
securityController.setConnectionSubjectAccess(
ds.getId(),
new String[] { anonymousRoleId },
adminName);
}
}
} catch (Exception e) {
log.error("Error granting anonymous access to connections", e);
}
}
private void saveRuntimeConfig(String newServerName, CBAppConfig appConfig) throws DBException {
File runtimeConfigFile = getRuntimeConfigFile();
@@ -28,7 +28,6 @@ import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.server.graphql.GraphQLEndpoint;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.utils.CommonUtils;
import javax.servlet.http.HttpServletRequest;
@@ -147,12 +146,7 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
}
if (!CBApplication.getInstance().isConfigurationMode()) {
// Check permissions
Set<String> sessionPermissions;
try {
sessionPermissions = session.getSessionPermissions();
} catch (DBCException e) {
throw new DBWebExceptionAccessDenied("Can't retrieve session permissions", e);
}
Set<String> sessionPermissions = session.getSessionPermissions();
if (CommonUtils.isEmpty(sessionPermissions)) {
throw new DBWebExceptionAccessDenied("Anonymous access restricted");
}
@@ -69,6 +69,9 @@ public interface DBWServiceCore extends DBWService {
@WebAction(requirePermissions = {})
boolean touchSession(HttpServletRequest request) throws DBWebException;
@WebAction(requirePermissions = {})
boolean refreshSessionConnections(HttpServletRequest request) throws DBWebException;
@WebAction
boolean changeSessionLanguage(WebSession webSession, String locale) throws DBWebException;
@@ -66,6 +66,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
.dataFetcher("openSession", env -> getService(env).openSession(sessionManager.getWebSession(GraphQLEndpoint.getServletRequest(env), false)))
.dataFetcher("closeSession", env -> getService(env).closeSession(GraphQLEndpoint.getServletRequest(env)))
.dataFetcher("touchSession", env -> getService(env).touchSession(GraphQLEndpoint.getServletRequest(env)))
.dataFetcher("refreshSessionConnections", env -> getService(env).refreshSessionConnections(GraphQLEndpoint.getServletRequest(env)))
.dataFetcher("changeSessionLanguage", env -> getService(env).changeSessionLanguage(getWebSession(env), env.getArgument("locale")))
.dataFetcher("openConnection", env -> getService(env).openConnection(getWebSession(env), getConnectionConfig(env)))
@@ -33,7 +33,6 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
@@ -101,10 +100,13 @@ public class WebServiceCore implements DBWServiceCore {
public List<WebConnectionInfo> getTemplateConnections(WebSession webSession) throws DBWebException {
List<WebConnectionInfo> result = new ArrayList<>();
for (DBPDataSourceContainer ds : WebServiceUtils.getDataSourceRegistry().getDataSources()) {
if (ds.isTemplate() && CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) {
if (ds.isTemplate() &&
CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver()))
{
result.add(new WebConnectionInfo(webSession, ds));
}
}
webSession.filterAccessibleConnections(result);
return result;
}
@@ -117,11 +119,7 @@ public class WebServiceCore implements DBWServiceCore {
DBWConstants.PERMISSION_ADMIN
};
}
try {
return webSession.getSessionPermissions().toArray(new String[0]);
} catch (DBCException e) {
throw new DBWebException("Error reading session permissions", e);
}
return webSession.getSessionPermissions().toArray(new String[0]);
}
@Override
@@ -149,6 +147,17 @@ public class WebServiceCore implements DBWServiceCore {
return CBPlatform.getInstance().getSessionManager().touchSession(request);
}
@Override
public boolean refreshSessionConnections(HttpServletRequest request) throws DBWebException {
WebSession session = CBPlatform.getInstance().getSessionManager().getWebSession(request);
if (session == null) {
return false;
} else {
session.refreshConnections();
return true;
}
}
@Override
public boolean changeSessionLanguage(WebSession webSession, String locale) {
webSession.setLocale(locale);
@@ -40,7 +40,7 @@ export const ItemContent = observer(function ItemContent({
if (activeItemSub) {
const sub = administrationItemService.getItemSub(item, activeItemSub);
if (sub) {
const Component = sub.getComponent();
const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent();
return <Component item={item} sub={sub} param={activeItemSubParam} configurationWizard={configurationWizard} />;
}
@@ -49,7 +49,7 @@ export type AdministrationItemSubCanActivateEvent = (
export interface IAdministrationItemSubItem {
name: string;
getComponent(): AdministrationItemSubContentComponent;
getComponent?: () => AdministrationItemSubContentComponent;
onActivate?: AdministrationItemSubEvent;
canActivate?: AdministrationItemSubCanActivateEvent;
}
@@ -9,6 +9,7 @@
import { computed, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { IExecutor, Executor } from '@cloudbeaver/core-executor';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { ScreenService, RouterService } from '@cloudbeaver/core-routing';
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
@@ -57,6 +58,7 @@ export class AdministrationScreenService {
get isConfigurationMode() {
return !!this.serverConfigResource.data?.configurationMode;
}
readonly activationEvent: IExecutor<boolean>;
constructor(
private screenService: ScreenService,
@@ -66,6 +68,7 @@ export class AdministrationScreenService {
private serverConfigResource: ServerConfigResource
) {
this.itemState = new Map();
this.activationEvent = new Executor();
this.autoSaveService.withAutoSave(this.itemState, ADMINISTRATION_ITEMS_STATE);
}
@@ -124,7 +127,7 @@ export class AdministrationScreenService {
this.itemState.clear();
}
private isAdministrationRouteActive() {
isAdministrationRouteActive() {
return this.screenService.isActive(AdministrationScreenService.screenName)
|| this.screenService.isActive(AdministrationScreenService.setupName);
}
@@ -57,6 +57,7 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap {
],
component: AdministrationScreen,
onActivate: this.handleActivate.bind(this),
onDeactivate: this.handleDeactivate.bind(this),
});
this.screenService.create({
@@ -99,6 +100,10 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap {
}
private async handleDeactivate() {
if (!this.administrationScreenService.isAdministrationRouteActive()) {
this.administrationScreenService.activationEvent.execute(false);
}
if (this.administrationScreenService.isConfigurationMode
&& !this.screenService.isActive(AdministrationScreenService.setupName)) {
this.administrationScreenService.navigateToRoot();
@@ -122,6 +127,7 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap {
if (!await this.checkPermissions()) {
return;
}
this.administrationScreenService.activationEvent.execute(true);
if (this.administrationScreenService.activeItem) {
await this.administrationItemService.activate(
@@ -41,6 +41,11 @@ const styles = composes(
border: solid 1px;
padding: 16px 24px
}
p {
line-height: 2;
white-space: pre;
}
`
);
@@ -52,8 +57,8 @@ export const FinishPage = observer(function FinishPage() {
<layout-grid as="div">
<layout-grid-inner as="div">
<layout-grid-cell as='div' {...use({ span: 12 })}>
<h3>All done</h3>
<p>Go to fun</p>
<h3>{translate('administration_configuration_wizard_finish_title')}</h3>
<p>{translate('administration_configuration_wizard_finish_message')}</p>
<Button
type="button"
@@ -10,6 +10,7 @@ import { observer } from 'mobx-react';
import styled, { use, css } from 'reshadow';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { ServerConfigurationForm } from './ServerConfigurationForm';
@@ -40,18 +41,23 @@ const styles = composes(
border: solid 1px;
padding: 16px 24px
}
p {
line-height: 2;
}
`
);
export const ServerConfigurationPage = observer(function ServerConfigurationPage() {
const translate = useTranslate();
const controller = useController(ServerConfigurationPageController);
return styled(useStyles(styles))(
<layout-grid as="div">
<layout-grid-inner as="div">
<layout-grid-cell as='div' {...use({ span: 12 })}>
<h3>Welcome to initial server configuration</h3>
<p>Here you can configure something...</p>
<h3>{translate('administration_configuration_wizard_configuration_title')}</h3>
<p>{translate('administration_configuration_wizard_configuration_message')}</p>
<ServerConfigurationForm
serverConfig={controller.state.serverConfig}
onChange={controller.onChange}
@@ -8,6 +8,7 @@
import styled, { use, css } from 'reshadow';
import { Translate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
const styles = composes(
@@ -35,6 +36,14 @@ const styles = composes(
border: solid 1px;
padding: 16px 24px
}
p {
line-height: 2;
}
note {
composes: theme-typography--body2 from global;
}
`
);
@@ -43,8 +52,9 @@ export function WelcomePage() {
<layout-grid as="div">
<layout-grid-inner as="div">
<layout-grid-cell as='div' {...use({ span: 12 })}>
<h3>Welcome to initial server configuration</h3>
<p>Here you can configure something...</p>
<h3><Translate token='administration_configuration_wizard_welcome_title'/></h3>
<p><Translate token='administration_configuration_wizard_welcome_message'/></p>
<note as='div'><Translate token='administration_configuration_wizard_welcome_note'/></note>
</layout-grid-cell>
</layout-grid-inner>
</layout-grid>
@@ -3,9 +3,14 @@ export default [
['administration_menu_back', 'Back to app'],
['administration_configuration_wizard_title', 'Initial Server Configuration'],
['administration_configuration_wizard_welcome', 'Welcome'],
['administration_configuration_wizard_welcome_step_description', 'Welcome'],
['administration_configuration_wizard_welcome_step_description', 'Welcome to CloudBeaver'],
['administration_configuration_wizard_welcome_title', 'Welcome to CloudBeaver, cloud database management system!'],
['administration_configuration_wizard_welcome_message', 'The easy configuration wizard will guide you through the several simple steps to set up the CloudBeaver server. You will need to set server information and administrator credentials. You can also add first database connections here.'],
['administration_configuration_wizard_welcome_note', 'Note: you will be able to change these configuration parameters later on the administration panel.'],
['administration_configuration_wizard_configuration', 'Server configuration'],
['administration_configuration_wizard_configuration_step_description', 'Server configuration'],
['administration_configuration_wizard_configuration_step_description', 'Main server configuration'],
['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'],
['administration_configuration_wizard_configuration_message', 'Administrator is super user which can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'],
['administration_configuration_wizard_configuration_plugins', 'Configuration'],
['administration_configuration_wizard_configuration_server_info', 'Server Info'],
['administration_configuration_wizard_configuration_server_name', 'Server Name'],
@@ -28,4 +33,6 @@ export default [
['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Utility Objects'],
['administration_configuration_wizard_finish', 'Confirmation'],
['administration_configuration_wizard_finish_step_description', 'Confirmation'],
['administration_configuration_wizard_finish_title', 'That is almost it.'],
['administration_configuration_wizard_finish_message', 'Press the Finish button to complete the server configuration. You can return to the previous pages if you want to change or add something.\nWhen the configuration is completed all entered settings will be applied for your CloudBeaver server. You will be redirected to the main page to start working.\nYou can always login to the system as administrator to change the server settings.'],
];
@@ -12,7 +12,9 @@ import {
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { IExecutor, Executor, IContextProvider } from '@cloudbeaver/core-executor';
import { PermissionsService, EPermission, SessionResource } from '@cloudbeaver/core-root';
import {
PermissionsService, EPermission, SessionResource, ServerService
} from '@cloudbeaver/core-root';
import {
GraphQLService, resourceKeyList, isResourceKeyList, ResourceKey
} from '@cloudbeaver/core-sdk';
@@ -98,6 +100,7 @@ export class NavNodeManagerService extends Bootstrap {
private connectionAuthService: ConnectionAuthService,
private notificationService: NotificationService,
private sessionResource: SessionResource,
private serverService: ServerService,
) {
super();
this.activeNavigationNodes = [];
@@ -144,10 +147,11 @@ export class NavNodeManagerService extends Bootstrap {
}
async updateRootChildren() {
if (!await this.permissionsService.hasAsync(EPermission.public)) {
if (!await this.isNavTreeEnabled()) {
this.navTree.delete(ROOT_NODE_PATH);
return;
}
await this.navTree.refresh(ROOT_NODE_PATH);
}
@@ -326,6 +330,10 @@ export class NavNodeManagerService extends Bootstrap {
}
private async connectionUpdateHandler(key: ResourceKey<string>) {
if (!await this.isNavTreeEnabled()) {
return;
}
const keys = isResourceKeyList(key) ? key.list : [key];
for (const id of keys) {
@@ -341,13 +349,10 @@ export class NavNodeManagerService extends Bootstrap {
await this.refreshNode(nodeId);
if (connectionInfo?.features.includes(EConnectionFeature.temporary)) {
const tree = this.navTree.get(ROOT_NODE_PATH);
const tree = this.navTree.get(ROOT_NODE_PATH);
if (!tree?.includes(nodeId)) {
this.navTree.unshiftToNode(ROOT_NODE_PATH, [nodeId]);
}
if (!tree?.includes(nodeId)) {
this.navTree.unshiftToNode(ROOT_NODE_PATH, [nodeId]);
}
}
}
@@ -395,4 +400,18 @@ export class NavNodeManagerService extends Bootstrap {
}
}
}
private async isNavTreeEnabled() {
if (!await this.permissionsService.hasAsync(EPermission.public)) {
return false;
}
// TODO: IT'S IS REALLY BAD PLACE FOR THAT
const config = await this.serverService.config.load(null);
if (config?.configurationMode) {
return false;
}
return true;
}
}
@@ -26,7 +26,6 @@ type AdminUserNew = AdminUserInfo & { [NEW_USER_SYMBOL]: boolean }
type UserCreateOptions = {
userId: string;
newId?: string;
roles: string[];
credentials: Record<string, any>;
grantedConnections: string[];
@@ -59,22 +58,6 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
return this.data.has(id);
}
addNew() {
const user = {
userId: `new-${uuid()}`,
grantedRoles: [],
grantedConnections: [],
configurationParameters: {},
metaParameters: {},
[NEW_USER_SYMBOL]: true,
} as AdminUserNew;
this.data.set(user.userId, user);
this.markUpdated(user.userId);
return user;
}
async loadConnections(userId: string): Promise<AdminConnectionGrantInfo[]> {
if (this.isNew(userId)) {
return [];
@@ -90,15 +73,10 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
}
async create({
userId, newId, roles, credentials, grantedConnections,
userId, roles, credentials, grantedConnections,
}: UserCreateOptions): Promise<AdminUserInfo> {
const { user } = await this.graphQLService.gql.createUser({ userId });
if (newId) {
this.data.delete(newId);
}
this.set(userId, user as AdminUserInfo);
try {
await this.updateCredentials(userId, credentials);
for (const roleId of roles) {
@@ -106,6 +84,8 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
}
await this.setConnections(userId, grantedConnections);
const user = await this.refresh(userId)as AdminUserNew;
user[NEW_USER_SYMBOL] = true;
} catch (exception) {
this.delete(userId);
throw exception;
@@ -13,4 +13,5 @@ export interface IProperty {
defaultValue?: string;
description?: string;
validValues?: string[];
new?: boolean;
}
@@ -105,7 +105,7 @@ export const PropertiesTable = observer(function PropertiesTable({
{translate('block_properties_table_value')}
</properties-header-value>
<properties-header-right as="div">
{onAdd && <Button type='button' mod={['outlined']} onClick={onAdd}>{translate('block_properties_table_add')}</Button> }
{onAdd && <Button type='button' mod={['outlined']} onClick={() => onAdd()}>{translate('block_properties_table_add')}</Button> }
</properties-header-right>
</properties-header>
<properties-list as="div">
@@ -134,10 +134,10 @@ export const PropertyItem = observer(function PropertyItem({
const handleRemove = useCallback(() => onRemove(property.id), [property]);
useLayoutEffect(() => {
if (keyInputRef.current && isKeyEditable) {
if (keyInputRef.current && isKeyEditable && property.new) {
keyInputRef.current.focus();
}
}, []);
}, [property]);
return styled(useStyles(styles))(
<property-item as="div">
@@ -12,12 +12,10 @@ import {
NavigationTreeContextMenuService,
LogViewerMenuService,
ConnectionSchemaManagerService,
AppScreenService,
} from '@cloudbeaver/core-app';
import { injectable } from '@cloudbeaver/core-di';
import { SessionExpireService } from '@cloudbeaver/core-dialogs';
import { ExceptionsCatcherService } from '@cloudbeaver/core-events';
import { RouterService } from '@cloudbeaver/core-routing';
import { ThemeService } from '@cloudbeaver/core-theming';
/**
@@ -39,7 +39,7 @@ export const DriverProperties = observer(function DriverProperties({
state,
loadProperties,
}: DriverPropertiesProps) {
const controller = useController(DriverPropertiesController, driverId);
const controller = useController(DriverPropertiesController, driverId, state);
useMemo(() => {
if (loadProperties) {
@@ -54,7 +54,7 @@ export const DriverProperties = observer(function DriverProperties({
<PropertiesTable
properties={controller.driverProperties}
propertiesState={state}
onAdd={controller.onAddProperty}
onAdd={controller.addProperty}
/>
)}
</properties>
@@ -32,20 +32,24 @@ export class DriverPropertiesController implements IInitializableController {
loaded = false;
private state!: Record<string, string>;
constructor(
private driverPropertiesService: DriverPropertiesService,
private notificationService: NotificationService
) { }
init(driverId: string) {
init(driverId: string, state: Record<string, string>) {
this.driverId = driverId;
this.state = state;
}
onAddProperty = () => {
addProperty = (key?: string, value?: string) => {
this.driverProperties.unshift({
id: uuid(),
key: 'property',
defaultValue: '',
key: key ?? 'property',
defaultValue: value ?? '',
new: !key,
});
}
@@ -64,6 +68,14 @@ export class DriverPropertiesController implements IInitializableController {
description: property.description,
validValues: property.validValues,
})));
for (const key of Object.keys(this.state)) {
if (this.driverProperties.some(property => property.key === key)) {
continue;
}
this.addProperty(key, this.state[key]);
}
this.loaded = true;
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver properties');
@@ -100,7 +100,7 @@ implements IInitializableController {
const prevDriver = this.dbDriverResource.get(prevDriverId || '');
if (this.model.connection.host === prevDriver?.defaultServer) {
this.model.connection.host = this.driver?.defaultServer;
this.model.connection.host = this.driver?.defaultServer || 'localhost';
}
if (this.model.connection.port === prevDriver?.defaultPort) {
@@ -12,6 +12,7 @@ import styled, { css, use } from 'reshadow';
import { AdministrationTools, AdministrationItemContentProps } from '@cloudbeaver/core-administration';
import { Loader, IconButton } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { Translate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { ConnectionsAdministrationController } from './ConnectionsAdministrationController';
@@ -24,11 +25,16 @@ const styles = composes(
composes: theme-background-surface theme-text-on-surface from global;
}
layout-grid-cell {
layout-grid-cell, message-box {
composes: theme-border-color-background from global;
}
`,
css`
message-box {
padding: 16px 24px;
border-bottom: solid 1px;
}
layout-grid {
width: 100%;
flex: 1;
@@ -59,6 +65,11 @@ const styles = composes(
padding: 0 12px;
padding-right: 24px;
}
p {
line-height: 2;
white-space: pre;
}
`
);
@@ -73,6 +84,11 @@ export const ConnectionsAdministration = observer(function ConnectionsAdministra
<layout-grid as="div">
<layout-grid-inner as="div">
<layout-grid-cell as='div' {...use({ span: 12 })}>
{configurationWizard && (
<message-box as='div'>
<p><Translate token='connections_administration_configuration_wizard_message'/></p>
</message-box>
)}
<AdministrationTools>
<IconButton name="add" viewBox="0 0 28 28" onClick={controller.create} />
<IconButton name="trash" viewBox="0 0 28 28" onClick={controller.delete} />
@@ -11,8 +11,7 @@ import { observable, computed } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { ConnectionsResource, isSearchedConnection } from '../ConnectionsResource';
import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService';
@@ -22,8 +21,6 @@ export class ConnectionsAdministrationController {
@observable isProcessing = false;
readonly selectedItems = observable<string, boolean>(new Map())
readonly expandedItems = observable<string, boolean>(new Map())
readonly error = new GQLErrorCatcher();
@computed
get connections() {
return Array.from(this.connectionsResource.data.values())
@@ -65,9 +62,7 @@ export class ConnectionsAdministrationController {
try {
await this.connectionsResource.refresh('all');
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Connections update failed');
}
this.notificationService.logException(exception, 'Connections update failed');
}
}
@@ -108,17 +103,9 @@ export class ConnectionsAdministrationController {
this.expandedItems.delete(id);
}
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Connections delete failed');
}
this.notificationService.logException(exception, 'Connections delete failed');
} finally {
this.isProcessing = false;
}
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
}
@@ -27,6 +27,7 @@ export class ConnectionsAdministrationService extends Bootstrap {
}
register() {
this.administrationItemService.create({
name: 'connections',
type: AdministrationItemType.Default,
@@ -36,10 +37,7 @@ export class ConnectionsAdministrationService extends Bootstrap {
description: 'connections_administration_configuration_wizard_step_description',
},
sub: [
{
name: 'create',
getComponent: () => ConnectionsAdministration,
},
{ name: 'create' },
],
getContentComponent: () => ConnectionsAdministration,
getDrawerComponent: () => ConnectionsDrawerItem,
@@ -26,18 +26,13 @@ const styles = composes(
`,
css`
box {
box-sizing: border-box;
padding: 24px;
min-height: 320px;
max-height: 500px;
display: flex;
flex-direction: column;
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
`
);
@@ -25,6 +25,9 @@ const styles = composes(
}
`,
css`
Table {
width: 100%;
}
TableColumnHeader {
border-top: solid 1px;
}
@@ -202,10 +202,10 @@ export const CreateConnection = observer(function CreateConnection({
</title-bar>
<TabList>
<Tab tabId='driver'>
<TabTitle>{translate('Driver')}</TabTitle>
<TabTitle>{translate('connections_connection_driver')}</TabTitle>
</Tab>
<Tab tabId='search-database'>
<TabTitle>{translate('Search Database')}</TabTitle>
<TabTitle>{translate('connections_connection_create_search_database')}</TabTitle>
</Tab>
</TabList>
</TabsState>
@@ -9,15 +9,12 @@
import { observable, computed } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, AdminConnectionSearchInfo, ConnectionInfo } from '@cloudbeaver/core-sdk';
import { AdminConnectionSearchInfo, ConnectionInfo } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import { DBDriverResource } from '../../../DBDriverResource';
import { ConnectionsResource } from '../../ConnectionsResource';
import { ConnectionsAdministrationNavService } from '../ConnectionsAdministrationNavService';
@injectable()
export class CreateConnectionController {
@@ -37,14 +34,10 @@ export class CreateConnectionController {
return this.dbDriverResource.get(this.connection.driverId);
}
readonly error = new GQLErrorCatcher();
constructor(
private notificationService: NotificationService,
private connectionsResource: ConnectionsResource,
private commonDialogService: CommonDialogService,
private dbDriverResource: DBDriverResource,
private connectionsAdministrationNavService: ConnectionsAdministrationNavService
) {
this.credentials = {};
this.databases = [];
@@ -67,9 +60,7 @@ export class CreateConnectionController {
this.databases = await this.connectionsResource.searchDatabases(hosts);
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Databases search failed');
}
this.notificationService.logException(exception, 'Databases search failed');
} finally {
this.isProcessing = false;
}
@@ -109,10 +100,4 @@ export class CreateConnectionController {
this.connection = null;
this.availableDrivers = [];
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
}
@@ -10,6 +10,7 @@ import { observer } from 'mobx-react';
import { useState, useMemo } from 'react';
import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { DBDriver } from '../../../DBDriverResource';
import { Driver } from './Driver';
@@ -21,6 +22,7 @@ type Props = {
}
export const DriverList = observer(function DriverList({ drivers, className, onSelect }: Props) {
const translate = useTranslate();
const [search, setSearch] = useState('');
const filteredDrivers = useMemo(() => {
if (!search) {
@@ -31,7 +33,7 @@ export const DriverList = observer(function DriverList({ drivers, className, onS
return (
<ItemList className={className}>
<ItemListSearch value={search} onSearch={setSearch} />
<ItemListSearch value={search} onSearch={setSearch} placeholder={translate('connections_driver_search_placeholder')} />
{filteredDrivers.map(driver => <Driver key={driver.id} driver={driver} onSelect={onSelect}/>)}
</ItemList>
);
@@ -1,89 +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 { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { Button, InputField, SubmittingForm } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
const styles = composes(
css`
SubmittingForm {
composes: theme-border-color-background from global;
}
`,
css`
SubmittingForm {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
border-top: solid 1px;
}
group {
box-sizing: border-box;
display: flex;
}
action {
padding-left: 24px;
}
InputField {
width: 450px;
}
`
);
type Props = {
hosts: string;
className?: string;
onChange(hosts: string): void;
onSearch(): void;
disabled?: boolean;
}
export const DatabasesSearch = observer(function DatabasesSearch({
hosts,
className,
onChange,
onSearch,
disabled,
}: Props) {
const translate = useTranslate();
return styled(useStyles(styles))(
<SubmittingForm onSubmit={onSearch} className={className}>
<group as="div">
<InputField
name="hosts"
value={hosts}
placeholder='localhost 127.0.0.1'
onChange={onChange}
disabled={disabled}
mod='surface'
>
{translate('connections_connection_edit_search_hosts')}
</InputField>
</group>
<action as='div'>
<Button
type="submit"
disabled={disabled}
mod={['outlined']}
onClick={onSearch}
>
{translate('connections_connection_edit_search')}
</Button>
</action>
</SubmittingForm>
);
});
@@ -87,6 +87,7 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
async create(config: ConnectionConfig) {
const { connection } = await this.graphQLService.gql.createConnectionConfiguration({ config });
await this.graphQLService.gql.refreshSessionConnections();
const newConnection: ConnectionNew = {
...connection as ConnectionInfo,
@@ -105,11 +106,13 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
async update(id: string, config: ConnectionConfig) {
await this.performUpdate(id, () => this.updateConnection(id, config));
await this.graphQLService.gql.refreshSessionConnections();
return this.get(id)!;
}
async delete(key: ResourceKey<string>) {
await this.performUpdate(key, () => this.deleteConnectionTask(key));
await this.graphQLService.gql.refreshSessionConnections();
}
async loadAccessSubjects(connectionId: string): Promise<AdminConnectionGrantInfo[]> {
@@ -11,15 +11,26 @@ import {
ConnectionInfo,
GraphQLService,
CachedMapResource,
ObjectPropertyInfo
ObjectPropertyInfo,
ResourceKey,
isResourceKeyList,
resourceKeyList
} from '@cloudbeaver/core-sdk';
import { ConnectionsResource } from './Administration/ConnectionsResource';
export type Connection = Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'connected' | 'readOnly' | 'driverId' | 'features' | 'authModel' | 'authNeeded'> & { authProperties?: ObjectPropertyInfo[] }
@injectable()
export class ConnectionInfoResource extends CachedMapResource<string, Connection> {
constructor(private graphQLService: GraphQLService) {
constructor(
private graphQLService: GraphQLService,
private connectionsResource: ConnectionsResource
) {
super(new Map());
connectionsResource.onItemAdd.subscribe(this.addHandler.bind(this));
connectionsResource.onItemDelete.subscribe(this.delete.bind(this));
connectionsResource.onDataOutdated.subscribe(this.markOutdated.bind(this));
}
async createFromTemplate(templateId: string): Promise<Connection> {
@@ -79,6 +90,19 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
return this.data;
}
private async addHandler(key: ResourceKey<string>) {
if (isResourceKeyList(key)) {
this.load(resourceKeyList(key.list.filter(id => !this.connectionsResource.get(id)?.template)));
return;
}
if (this.connectionsResource.get(key)?.template) {
return;
}
this.load(key);
}
private async getAuthProperties(id: string): Promise<ObjectPropertyInfo[]> {
const { connection: { authProperties } } = await this.graphQLService.gql.connectionAuthProperties({ id });
@@ -5,6 +5,8 @@ export default [
['connections_administration_new_connection', 'New connection'],
['connections_administration_configuration_wizard_step_title', 'Database connections'],
['connections_administration_configuration_wizard_step_description', 'Database connections'],
['connections_administration_configuration_wizard_message', 'Here you can create the database connections manually or using a database server search feature.\nYou can skip this step and configure connections later.'],
['connections_connection_create_search_database', 'Search Database'],
['connections_connection_edit_authentication', 'Authentication'],
['connections_connection_edit_access', 'Access'],
['connections_connection_edit_access_load_failed', 'Connection access loading failed'],
@@ -19,4 +21,5 @@ export default [
['connections_connection_port', 'Port'],
['connections_connection_template', 'Template'],
['connections_connection_test', 'Test Connection'],
['connections_driver_search_placeholder', 'Type driver name...'],
];
@@ -5,6 +5,7 @@ export default [
['connections_administration_new_connection', 'Создание подключения'],
['connections_administration_configuration_wizard_step_title', 'Подключения к базам'],
['connections_administration_configuration_wizard_step_description', 'Подключения к базам'],
['connections_connection_create_search_database', 'Поиск баз'],
['connections_connection_edit_authentication', 'Авторизация'],
['connections_connection_edit_access', 'Доступ'],
['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'],
@@ -14,9 +15,10 @@ export default [
['connections_connection_address', 'Адрес'],
['connections_connection_name', 'Название'],
['connections_connection_description', 'Описапние'],
['connections_connection_driver', 'Driver'],
['connections_connection_driver', 'Драйвер'],
['connections_connection_host', 'Хост'],
['connections_connection_port', 'Порт'],
['connections_connection_template', 'Шаблон'],
['connections_connection_test', 'Проверить подключение'],
['connections_driver_search_placeholder', 'Введите название драйвера...'],
];
@@ -51,14 +51,16 @@ export class LocalizationService extends Bootstrap {
}
readonly translate = (token: TLocalizationToken): string => {
let locale = this.localeMap.get(this.getCurrentLanguage());
let translation = this.localeMap
.get(this.getCurrentLanguage())
?.get(token);
if (!locale) {
locale = this.localeMap.get(DEFAULT_LOCALE_NAME);
if (!translation) {
translation = this.localeMap
.get(DEFAULT_LOCALE_NAME)
?.get(token);
}
const translation = locale && locale.get(token);
if (typeof translation === 'string') {
return translation;
}
@@ -13,16 +13,16 @@ export const defaultRULocale = [
['ui_errors_details', 'Информация'],
['ui_search', 'Поиск...'],
['ui_data_saving_error', 'Ошибка при сохранении изменений'],
['block_properties_name', 'Название'],
['block_properties_value', 'Значение'],
['block_properties_add', 'Добавить'],
['block_properties_table_name', 'Название'],
['block_properties_table_value', 'Значение'],
['block_properties_table_add', 'Добавить'],
['root_permission_denied', 'Отказано в доступе'],
['app_root_session_expired_title', 'Сесстия истекла'],
['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'],
['app_root_session_expired_reload', 'Перезагрузить'],
['app_shared_connectionMenu_newConnection', 'Новое подключение'],
['app_shared_connectionMenu_connection', 'Подключение'],
['app_shared_connectionMenu_custom', 'Custom'],
['app_shared_connectionMenu_custom', 'Настроить'],
['app_shared_connectionMenu_disconnect', 'Отключить'],
['app_shared_toolsMenu_tools', 'Инструменты'],
['app_shared_toolsMenu_logViewer', 'Просмотр лога'],
@@ -48,7 +48,13 @@ export abstract class CachedMapResource<TKey, TValue> extends CachedResource<
return this.outdated.has(key);
}
markOutdated(key: ResourceKey<TKey>): void {
markOutdated(): void
markOutdated(key: ResourceKey<TKey>): void
markOutdated(key?: ResourceKey<TKey>): void {
if (!key) {
key = resourceKeyList(Array.from(this.data.keys()));
}
if (isResourceKeyList(key)) {
for (const itemKey of key.list) {
this.outdated.add(itemKey);
@@ -59,7 +65,13 @@ export abstract class CachedMapResource<TKey, TValue> extends CachedResource<
this.outdatedSubject.next(key);
}
markUpdated(key: ResourceKey<TKey>): void {
markUpdated(): void
markUpdated(key: ResourceKey<TKey>): void
markUpdated(key?: ResourceKey<TKey>): void {
if (!key) {
key = resourceKeyList(Array.from(this.data.keys()));
}
if (isResourceKeyList(key)) {
for (const itemKey of key.list) {
this.outdated.delete(itemKey);
@@ -0,0 +1,3 @@
mutation refreshSessionConnections {
refreshSessionConnections
}
+13
View File
@@ -252,6 +252,7 @@ export type Mutation = {
openConnection: ConnectionInfo;
openSession: SessionInfo;
readDataFromContainer?: Maybe<SqlExecuteInfo>;
refreshSessionConnections?: Maybe<Scalars['Boolean']>;
setConnectionNavigatorSettings: Scalars['Boolean'];
sqlContextCreate: SqlContextInfo;
sqlContextDestroy: Scalars['Boolean'];
@@ -1023,6 +1024,10 @@ export type InitConnectionMutationVariables = Exact<{
export type InitConnectionMutation = { connection: Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'connected' | 'readOnly' | 'features' | 'authNeeded' | 'authModel'> };
export type RefreshSessionConnectionsMutationVariables = Exact<{ [key: string]: never }>;
export type RefreshSessionConnectionsMutation = Pick<Mutation, 'refreshSessionConnections'>;
export type GetTemplateConnectionsQueryVariables = Exact<{ [key: string]: never }>;
export type GetTemplateConnectionsQuery = { connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'connected' | 'readOnly' | 'features' | 'authNeeded' | 'authModel'>> };
@@ -1712,6 +1717,11 @@ export const InitConnectionDocument = `
}
}
`;
export const RefreshSessionConnectionsDocument = `
mutation refreshSessionConnections {
refreshSessionConnections
}
`;
export const GetTemplateConnectionsDocument = `
query getTemplateConnections {
connections: templateConnections {
@@ -2295,6 +2305,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
initConnection(variables: InitConnectionMutationVariables): Promise<InitConnectionMutation> {
return withWrapper(() => client.request<InitConnectionMutation>(InitConnectionDocument, variables));
},
refreshSessionConnections(variables?: RefreshSessionConnectionsMutationVariables): Promise<RefreshSessionConnectionsMutation> {
return withWrapper(() => client.request<RefreshSessionConnectionsMutation>(RefreshSessionConnectionsDocument, variables));
},
getTemplateConnections(variables?: GetTemplateConnectionsQueryVariables): Promise<GetTemplateConnectionsQuery> {
return withWrapper(() => client.request<GetTemplateConnectionsQuery>(GetTemplateConnectionsDocument, variables));
},
@@ -12,10 +12,15 @@
min-width: 100%;
text-align: left;
border-collapse: collapse;
table-layout: fixed;
&[use|size="big"] {
> thead > tr > th {
height: 54px;
&[use|min] {
width: 46px;
}
}
> tbody > tr > td {
@@ -53,6 +58,7 @@
}
th {
box-sizing: border-box;
white-space: nowrap;
padding: 16px;
height: 36px;
@@ -64,7 +70,7 @@
text-decoration: none !important;
&[use|min] {
width: 1%;
width: 28px;
}
&:last-child {
@@ -74,6 +80,7 @@
td {
position: relative;
box-sizing: border-box;
height: 28px;
padding: 0 16px;
transition: padding ease-in-out 0.24s;
@@ -84,6 +84,8 @@
}
tab-title {
overflow: hidden;
text-overflow: ellipsis;
padding: 0 12px;
}
@@ -28,13 +28,13 @@ const styles = composes(
css`
title-bar {
composes: theme-typography--headline6 from global;
padding: 0 16px;
box-sizing: border-box;
padding: 16px 24px;
border-top: solid 1px;
align-items: center;
height: 48px;
display: flex;
font-weight: 400;
flex: 1;
flex: auto 0 0;
}
`
);
@@ -47,7 +47,7 @@ export function CreateUser({
return styled(useStyles(styles))(
<user-create as='div'>
<title-bar as='div'><Translate token='authentication_administration_user_connections_user_add'/></title-bar>
<UserForm userId={user.userId} onCancel={onCancel} />
<UserForm user={user} onCancel={onCancel} />
</user-create>
);
}
@@ -21,6 +21,7 @@ import {
} from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { GrantedConnections } from './GrantedConnections';
@@ -53,10 +54,6 @@ const styles = composes(
}
`,
css`
box {
padding: 24px;
}
SubmittingForm {
flex: 1;
display: flex;
@@ -128,16 +125,18 @@ const styles = composes(
);
type Props = {
userId: string;
user: AdminUserInfo;
editing?: boolean;
onCancel: () => void;
}
export const UserForm = observer(function UserForm({
userId,
user,
editing = false,
onCancel,
}: Props) {
const translate = useTranslate();
const controller = useController(UserFormController, userId, onCancel);
const controller = useController(UserFormController, user, editing, onCancel);
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const handleLoginChange = useCallback(
@@ -182,7 +181,7 @@ export const UserForm = observer(function UserForm({
mod={['unelevated']}
onClick={controller.save}
>
{translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')}
{translate(!editing ? 'ui_processing_create' : 'ui_processing_save')}
</Button>
</TabList>
<content-box as='div'>
@@ -199,7 +198,7 @@ export const UserForm = observer(function UserForm({
name='login'
value={controller.credentials.login}
onChange={handleLoginChange}
disabled={!controller.isNew || controller.isSaving}
disabled={editing || controller.isSaving}
mod='surface'
required
>
@@ -31,10 +31,6 @@ export class UserFormController implements IInitializableController, IDestructib
roles: new Map<string, boolean>(),
};
@computed get isNew() {
return this.usersResource.isNew(this.userId);
}
@computed get connections() {
return Array.from(this.connectionsResource.data.values())
.filter(connection => !this.connectionsResource.isNew(connection.id));
@@ -44,16 +40,14 @@ export class UserFormController implements IInitializableController, IDestructib
return Array.from(this.rolesManagerService.roles.data.values());
}
@computed get user(): AdminUserInfo {
return this.usersResource.get(this.userId)!;
}
user!: AdminUserInfo;
readonly error = new GQLErrorCatcher();
private isDistructed = false;
private userId!: string;
private connectionAccessChanged = false;
private connectionAccessLoaded = false;
private collapse!: () => void;
private editing!: boolean;
constructor(
private notificationService: NotificationService,
@@ -64,8 +58,9 @@ export class UserFormController implements IInitializableController, IDestructib
private dbDriverResource: DBDriverResource
) { }
init(id: string, collapse: () => void) {
this.userId = id;
init(user: AdminUserInfo, editing: boolean, collapse: () => void) {
this.user = user;
this.editing = editing;
this.collapse = collapse;
this.loadRoles();
}
@@ -84,10 +79,9 @@ export class UserFormController implements IInitializableController, IDestructib
this.isSaving = true;
try {
if (this.isNew) {
if (!this.editing) {
await this.usersResource.create({
userId: this.credentials.login,
newId: this.userId,
credentials: { password: this.credentials.password },
roles: this.getGrantedRoles(),
grantedConnections: this.getGrantedConnections(),
@@ -103,12 +97,9 @@ export class UserFormController implements IInitializableController, IDestructib
await this.usersResource.refresh(this.user.userId);
this.notificationService.logInfo({ title: 'authentication_administration_user_updated' });
}
this.connectionAccessLoaded = false;
await this.loadConnectionsAccess();
} catch (exception) {
if (!this.error.catch(exception) || this.isDistructed) {
if (this.isNew) {
if (!this.editing) {
this.notificationService.logException(exception, 'Error creating new user');
} else {
this.notificationService.logException(exception, 'Error saving user');
@@ -134,12 +125,14 @@ export class UserFormController implements IInitializableController, IDestructib
this.isLoading = true;
try {
this.grantedConnections = await this.usersResource.loadConnections(this.userId);
if (this.editing) {
this.grantedConnections = await this.usersResource.loadConnections(this.user.userId);
this.selectedConnections.clear();
for (const connection of this.grantedConnections) {
if (connection.subjectType !== AdminSubjectType.Role) {
this.selectedConnections.set(connection.connectionId, true);
this.selectedConnections.clear();
for (const connection of this.grantedConnections) {
if (connection.subjectType !== AdminSubjectType.Role) {
this.selectedConnections.set(connection.connectionId, true);
}
}
}
this.connectionAccessLoaded = true;
@@ -161,12 +154,12 @@ export class UserFormController implements IInitializableController, IDestructib
return;
}
if (!this.credentials.password && this.isNew) {
if (!this.credentials.password && !this.editing) {
this.notificationService.logError({ title: 'authentication_user_password_not_set' });
return;
}
if (!this.credentials.password && this.isNew) {
if (!this.credentials.password && !this.editing) {
this.notificationService.logError({ title: 'authentication_user_password_not_set' });
return;
}
@@ -190,7 +183,7 @@ export class UserFormController implements IInitializableController, IDestructib
if (!this.user.grantedRoles.includes(roleId)) {
await this.usersResource.grantRole(this.user.userId, roleId, true);
}
} else if (!this.isNew) {
} else {
await this.usersResource.revokeRole(this.user.userId, roleId, true);
}
}
@@ -215,8 +208,10 @@ export class UserFormController implements IInitializableController, IDestructib
if (!this.connectionAccessChanged) {
return;
}
await this.usersResource.setConnections(this.userId, this.getGrantedConnections());
await this.usersResource.setConnections(this.user.userId, this.getGrantedConnections());
this.connectionAccessChanged = false;
this.connectionAccessLoaded = false;
await this.loadConnectionsAccess();
}
private async loadRoles() {
@@ -232,11 +227,7 @@ export class UserFormController implements IInitializableController, IDestructib
private async loadUser() {
try {
await this.usersResource.load(this.userId);
if (!this.isNew) {
this.credentials.login = this.user.userId;
}
this.credentials.login = this.user.userId;
this.credentials.roles = new Map(this.user.grantedRoles.map(roleId => ([roleId, true])));
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load user');
@@ -13,7 +13,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
import { GQLErrorCatcher, resourceKeyList, AdminUserInfo } from '@cloudbeaver/core-sdk';
import { UsersAdministrationNavigationService } from './UsersAdministrationNavigationService';
@@ -26,10 +26,9 @@ export class UsersAdministrationController {
@computed get users() {
return Array.from(this.usersResource.data.values())
.filter(user => !this.usersResource.isNew(user.userId))
.sort((a, b) => {
if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) {
return 0;
return a.userId.localeCompare(b.userId);
}
if (this.usersResource.isNew(a.userId)) {
return -1;
@@ -38,10 +37,7 @@ export class UsersAdministrationController {
});
}
@computed get creatingUser() {
return Array.from(this.usersResource.data.values())
.find(user => this.usersResource.isNew(user.userId));
}
@observable creatingUser: AdminUserInfo | null = null;
get isLoading() {
return this.usersResource.isLoading() || this.isDeleting;
@@ -55,7 +51,13 @@ export class UsersAdministrationController {
) { }
create = () => {
this.usersResource.addNew();
this.creatingUser = {
userId: '',
grantedRoles: [],
grantedConnections: [],
configurationParameters: {},
metaParameters: {},
} as AdminUserInfo;
this.usersAdministrationNavigationService.navToAdd();
}
@@ -64,7 +66,7 @@ export class UsersAdministrationController {
return;
}
this.usersResource.delete(this.creatingUser.userId);
this.creatingUser = null;
this.usersAdministrationNavigationService.navToRoot();
}
@@ -32,7 +32,6 @@ export class UsersAdministrationService extends Bootstrap {
sub: [
{
name: UsersAdministrationNavigationService.AddItemName,
getComponent: () => UsersAdministration,
onActivate: this.onCreateActivate.bind(this),
},
],
@@ -7,14 +7,11 @@
*/
import { observer } from 'mobx-react';
import styled, { css, use } from 'reshadow';
import styled, { css } from 'reshadow';
import { UsersResource } from '@cloudbeaver/core-authentication';
import {
TableItem, TableColumnValue, TableItemSelect, TableItemExpand
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -31,9 +28,6 @@ type Props = {
}
export const User = observer(function User({ user }: Props) {
const translate = useTranslate();
const usersResource = useService(UsersResource);
const isNew = usersResource.isNew(user.userId);
return styled(useStyles(styles))(
<TableItem item={user.userId} expandElement={UserEdit}>
@@ -43,9 +37,9 @@ export const User = observer(function User({ user }: Props) {
<TableColumnValue centerContent flex expand>
<TableItemExpand />
</TableColumnValue>
<TableColumnValue expand>{isNew ? translate('authentication_administration_user_connections_user_new') : user.userId}</TableColumnValue>
<TableColumnValue expand>{user.userId}</TableColumnValue>
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
<TableColumnValue align='right'>{isNew && <tag as='div' {...use({ mod: 'positive' })}>{translate('ui_tag_new')}</tag>}</TableColumnValue>
<TableColumnValue></TableColumnValue>
</TableItem>
);
});
@@ -8,12 +8,34 @@
import { observer } from 'mobx-react';
import { useContext, useCallback } from 'react';
import styled, { css } from 'reshadow';
import {
TableContext,
} from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { UserForm } from '../UserForm/UserForm';
import { UserEditController } from './UserEditController';
const styles = composes(
css`
box {
composes: theme-background-secondary theme-text-on-secondary from global;
}
`,
css`
box {
box-sizing: border-box;
padding: 24px;
min-height: 320px;
max-height: 500px;
display: flex;
flex-direction: column;
}
`
);
type Props = {
item: string;
@@ -22,8 +44,15 @@ type Props = {
export const UserEdit = observer(function UserEdit({
item,
}: Props) {
const controller = useController(UserEditController, item);
const tableContext = useContext(TableContext);
const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]);
return <UserForm userId={item} onCancel={collapse} />;
return styled(useStyles(styles))(
<box as='div'>
{controller.user && (
<UserForm user={controller.user} onCancel={collapse} editing/>
)}
</box>
);
});
@@ -0,0 +1,55 @@
/*
* 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 { UsersResource } from '@cloudbeaver/core-authentication';
import {
injectable, IInitializableController, IDestructibleController
} from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GQLErrorCatcher, AdminUserInfo } from '@cloudbeaver/core-sdk';
@injectable()
export class UserEditController
implements IInitializableController, IDestructibleController {
@observable isLoading = true;
@observable user: AdminUserInfo | null = null;
@computed get isDisabled() {
return this.isLoading;
}
userId!: string;
readonly error = new GQLErrorCatcher();
constructor(
private notificationService: NotificationService,
private usersResource: UsersResource,
) { }
init(id: string) {
this.userId = id;
this.loadUser();
}
destruct(): void { }
private async loadUser() {
this.isLoading = true;
try {
// we create a copy to protect the current value from mutation
this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId)));
} catch (exception) {
this.notificationService.logException(exception, `Can't load user ${this.userId}`);
} finally {
this.isLoading = false;
}
}
}
@@ -19,6 +19,9 @@ import { useStyles } from '@cloudbeaver/core-theming';
import { User } from './User';
const styles = css`
Table {
width: 100%;
}
TableColumnHeader {
border-top: solid 1px;
}
@@ -35,7 +35,7 @@ export class AuthenticationService extends Bootstrap {
throw new Error('Can\'t configure Authentication');
}
if (!config.authenticationEnabled) {
if (!config.authenticationEnabled || config.configurationMode) {
return;
}
@@ -136,12 +136,12 @@ implements IInitializableController, IDestructibleController {
@action
private setDriverDefaults() {
this.config.name = `${this.driver?.name} (custom)`;
this.config.driverId = this.driver?.id;
this.config.host = '';
this.config.port = this.driver?.defaultPort || '';
this.config.databaseName = '';
this.config.url = this.driver?.sampleURL || '';
this.config.name = `${this.driver.name} (custom)`;
this.config.driverId = this.driver.id;
this.config.host = this.driver.defaultServer || 'localhost';
this.config.port = this.driver.defaultPort || '';
this.config.databaseName = this.driver.defaultDatabase;
this.config.url = this.driver.sampleURL || '';
this.config.properties = {};
this.config.authModelId = this.driver.defaultAuthModel;
this.config.credentials = {};
@@ -41,7 +41,7 @@ export const DriverProperties = observer(function DriverProperties({
state,
loadProperties,
}: DriverPropertiesProps) {
const controller = useController(DriverPropertiesController, driver);
const controller = useController(DriverPropertiesController, driver, state);
useEffect(() => {
if (loadProperties) {
@@ -56,7 +56,7 @@ export const DriverProperties = observer(function DriverProperties({
<PropertiesTable
properties={controller.driverProperties}
propertiesState={state}
onAdd={controller.onAddProperty}
onAdd={controller.addProperty}
/>
)}
</properties>
@@ -34,21 +34,24 @@ export class DriverPropertiesController implements IInitializableController {
@observable driverProperties = observable<IProperty>([])
private loaded = false;
private state!: Record<string, string>;
constructor(
private driverPropertiesService: DriverPropertiesService,
private notificationService: NotificationService
) { }
init(driver: DBDriver) {
init(driver: DBDriver, state: Record<string, string>) {
this.driver = driver;
this.state = state;
}
onAddProperty = () => {
addProperty = (key?: string, value?: string) => {
this.driverProperties.unshift({
id: uuid(),
key: 'property',
defaultValue: '',
key: key ?? 'property',
defaultValue: value ?? '',
new: !key,
});
}
@@ -67,6 +70,14 @@ export class DriverPropertiesController implements IInitializableController {
description: property.description,
validValues: property.validValues,
})));
for (const key of Object.keys(this.state)) {
if (this.driverProperties.some(property => property.key === key)) {
continue;
}
this.addProperty(key, this.state[key]);
}
this.loaded = true;
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver properties');
@@ -10,6 +10,7 @@ import { observer } from 'mobx-react';
import { useState, useMemo } from 'react';
import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { Driver, IDriver } from './Driver';
@@ -20,6 +21,7 @@ type DriverSelectorProps = {
}
export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) {
const translate = useTranslate();
const [search, setSearch] = useState('');
const filteredDrivers = useMemo(() => {
if (!search) {
@@ -30,7 +32,7 @@ export const DriverSelector = observer(function DriverSelector({ drivers, classN
return (
<ItemList className={className}>
<ItemListSearch onSearch={setSearch} />
<ItemListSearch onSearch={setSearch} placeholder={translate('connections_driver_search_placeholder')}/>
{filteredDrivers.map(driver => <Driver key={driver.id} driver={driver} onSelect={onSelect}/>)}
</ItemList>
);
@@ -18,7 +18,6 @@ import {
ColDef,
ValueGetterParams,
GridOptions,
CellEditingStoppedEvent,
CellClassParams,
SortChangedEvent,
RowNode,
@@ -74,7 +73,6 @@ export class AgGridTableController implements IInitializableController, IDestruc
onGridReady: this.handleGridReady.bind(this),
onBodyScroll: this.handleBodyScroll.bind(this),
onCellEditingStopped: this.handleCellEditingStopped.bind(this),
onSortChanged: this.handleSortChanged.bind(this),
};
@@ -163,11 +161,6 @@ export class AgGridTableController implements IInitializableController, IDestruc
this.gridModel.onCellEditingStopped(rowIndex, colId, value, editing);
}
private handleCellEditingStopped(event: CellEditingStoppedEvent) {
// TODO: probably no more needed because editCellValue executes on every change in editor
// this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value, false);
}
private isCellEdited(rowIndex: number, column: string) {
return this.gridModel.isCellEdited(rowIndex, column);
}
@@ -75,7 +75,16 @@ export const TableFooter = observer(function TableFooter({
}: TableFooterProps) {
const ref = useRef<HTMLInputElement>(null);
const handleChange = useCallback(
() => ref.current && model.setChunkSize(parseInt(ref.current.value, 10)),
() => {
if (!ref.current) {
return;
}
const value = parseInt(ref.current.value, 10);
if (model.getChunkSize() !== value) {
model.setChunkSize(value);
}
},
[model]
);