Merge branch 'devel' into CB-2878-versions-load-error-message

This commit is contained in:
naumov
2022-11-21 16:27:57 +04:00
33 changed files with 611 additions and 307 deletions
@@ -38,6 +38,7 @@
anonymousAccessEnabled: true,
anonymousUserRole: "user",
supportsCustomConnections: false,
showReadOnlyConnectionInfo: false,
forwardProxy: false,
@@ -38,6 +38,7 @@
anonymousAccessEnabled: true,
anonymousUserRole: "user",
supportsCustomConnections: false,
showReadOnlyConnectionInfo: false,
forwardProxy: false,
@@ -17,10 +17,12 @@
package io.cloudbeaver.model;
import io.cloudbeaver.WebProjectImpl;
import io.cloudbeaver.model.app.BaseWebAppConfiguration;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.security.SMUtils;
import io.cloudbeaver.service.sql.WebDataFormat;
import io.cloudbeaver.utils.CBModelConstants;
import io.cloudbeaver.utils.WebAppUtils;
import io.cloudbeaver.utils.WebCommonUtils;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.DBPDataSource;
@@ -39,10 +41,7 @@ import org.jkiss.dbeaver.model.rm.RMProjectPermission;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.CommonUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -50,6 +49,7 @@ import java.util.stream.Collectors;
*/
public class WebConnectionInfo {
private static final String SECURED_VALUE = "********";
private final WebSession session;
private final DBPDataSourceContainer dataSourceContainer;
private WebServerError connectError;
@@ -100,12 +100,18 @@ public class WebConnectionInfo {
@Property
public String getHost() {
return dataSourceContainer.getConnectionConfiguration().getHostName();
if (canViewReadOnlyConnections()) {
return dataSourceContainer.getConnectionConfiguration().getHostName();
}
return SECURED_VALUE;
}
@Property
public String getPort() {
return dataSourceContainer.getConnectionConfiguration().getHostPort();
if (canViewReadOnlyConnections()) {
return dataSourceContainer.getConnectionConfiguration().getHostPort();
}
return SECURED_VALUE;
}
@Property
@@ -115,17 +121,26 @@ public class WebConnectionInfo {
@Property
public String getDatabaseName() {
return dataSourceContainer.getConnectionConfiguration().getDatabaseName();
if (canViewReadOnlyConnections()) {
return dataSourceContainer.getConnectionConfiguration().getDatabaseName();
}
return SECURED_VALUE;
}
@Property
public String getUrl() {
return dataSourceContainer.getConnectionConfiguration().getUrl();
if (canViewReadOnlyConnections()) {
return dataSourceContainer.getConnectionConfiguration().getUrl();
}
return SECURED_VALUE;
}
@Property
public Map<String, String> getProperties() {
return dataSourceContainer.getConnectionConfiguration().getProperties();
if (canViewReadOnlyConnections()) {
return dataSourceContainer.getConnectionConfiguration().getProperties();
}
return Collections.emptyMap();
}
@Property
@@ -352,7 +367,7 @@ public class WebConnectionInfo {
@Property
public boolean isCanViewSettings() {
return true;
return canViewReadOnlyConnections();
}
@Property
@@ -382,4 +397,13 @@ public class WebConnectionInfo {
}
return SMUtils.hasProjectPermission(session, ((WebProjectImpl) project).getRmProject(), projectPermission);
}
private boolean canViewReadOnlyConnections() {
if (isCanEdit()) {
return true;
}
BaseWebAppConfiguration appConfig = (BaseWebAppConfiguration) WebAppUtils.getWebApplication().getAppConfiguration();
return appConfig.isShowReadOnlyConnectionInfo();
}
}
@@ -31,6 +31,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration {
protected final Map<String, Object> plugins;
protected String defaultUserTeam;
protected boolean resourceManagerEnabled;
protected boolean showReadOnlyConnectionInfo;
protected String[] enabledFeatures;
public BaseWebAppConfiguration() {
@@ -38,6 +39,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration {
this.defaultUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME;
this.resourceManagerEnabled = true;
this.enabledFeatures = null;
this.showReadOnlyConnectionInfo = false;
}
public BaseWebAppConfiguration(BaseWebAppConfiguration src) {
@@ -45,6 +47,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration {
this.defaultUserTeam = src.defaultUserTeam;
this.resourceManagerEnabled = src.resourceManagerEnabled;
this.enabledFeatures = src.enabledFeatures;
this.showReadOnlyConnectionInfo = src.showReadOnlyConnectionInfo;
}
@Override
@@ -98,4 +101,8 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration {
public void setEnabledFeatures(String[] enabledFeatures) {
this.enabledFeatures = enabledFeatures;
}
public boolean isShowReadOnlyConnectionInfo() {
return showReadOnlyConnectionInfo;
}
}
@@ -155,6 +155,7 @@ public class LocalResourceController implements RMController {
if (userProject != null) {
projects.add(0, userProject);
}
projects.sort(Comparator.comparing(RMProject::getDisplayName));
return projects.toArray(new RMProject[0]);
}
@@ -172,7 +173,6 @@ public class LocalResourceController implements RMController {
RMProjectType.SHARED, true)
)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(RMProject::getDisplayName))
.collect(Collectors.toList());
}
@@ -64,6 +64,7 @@ import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor;
import org.jkiss.dbeaver.model.security.*;
import org.jkiss.dbeaver.model.security.user.SMObjectPermissions;
import org.jkiss.dbeaver.model.sql.DBQuotaException;
import org.jkiss.dbeaver.registry.BaseProjectImpl;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.jobs.DisconnectJob;
import org.jkiss.utils.CommonUtils;
@@ -373,11 +374,7 @@ public class WebSession extends AbstractSessionPersistent
RMController controller = getRmController();
RMProject[] rmProjects = controller.listAccessibleProjects();
for (RMProject project : rmProjects) {
WebProjectImpl virtualProject = createWebProject(project);
if (!virtualProject.getRmProject().getProjectPermissions().contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId())) {
// Projects for which user don't have edit permission can't be saved. So mark the as in memory
virtualProject.setInMemory(true);
}
createWebProject(project);
}
if (user == null) {
WebProjectImpl anonymousProject = createWebProject(RMUtils.createAnonymousProject());
@@ -59,7 +59,7 @@ public class WebUserContext implements SMCredentialsProvider {
public WebUserContext(WebApplication application) throws DBException {
this.application = application;
this.securityController = application.createSecurityController(this);
this.userPermissions = getDefaultPermissions();
setUserPermissions(getDefaultPermissions());
}
/**
@@ -87,7 +87,7 @@ public class WebUserContext implements SMCredentialsProvider {
authPermissions.getPermissions()
);
this.refreshToken = smAuthInfo.getSmRefreshToken();
this.userPermissions = authPermissions.getPermissions();
setUserPermissions(authPermissions.getPermissions());
this.securityController = application.createSecurityController(this);
this.adminSecurityController = application.getAdminSecurityController(this);
this.secretController = application.getSecretController(this);
@@ -122,7 +122,7 @@ public class WebUserContext implements SMCredentialsProvider {
} catch (Exception e) {
log.error("Error logging out user", e);
}
this.userPermissions = getDefaultPermissions();
setUserPermissions(getDefaultPermissions());
this.smCredentials = null;
this.user = null;
this.securityController = application.createSecurityController(this);
@@ -171,11 +171,16 @@ public class WebUserContext implements SMCredentialsProvider {
*/
public synchronized void refreshPermissions() throws DBException {
if (isAuthorizedInSecurityManager()) {
this.userPermissions = securityController.getTokenPermissions().getPermissions();
setUserPermissions(securityController.getTokenPermissions().getPermissions());
} else {
this.userPermissions = getDefaultPermissions();
setUserPermissions(getDefaultPermissions());
}
if (userPermissions != null && !userPermissions.isEmpty()) {
}
private void setUserPermissions(Set<String> permissions) {
this.userPermissions = permissions;
// FIXME: automatically assign public permission in sm controller˚
if (!CommonUtils.isEmpty(userPermissions)) {
userPermissions.add(DBWConstants.PERMISSION_PUBLIC);
}
}
@@ -16,8 +16,8 @@
*/
package io.cloudbeaver.service;
import io.cloudbeaver.model.app.WebAppConfiguration;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBAppConfig;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -28,8 +28,12 @@ import org.jkiss.dbeaver.model.app.DBPApplication;
*/
public interface DBWServiceServerConfigurator extends DBWServiceBinding {
void configureServer(@NotNull DBPApplication application, @Nullable WebSession session, @NotNull CBAppConfig appConfig) throws DBException;
void configureServer(
@NotNull DBPApplication application,
@Nullable WebSession session,
@NotNull WebAppConfiguration appConfig
) throws DBException;
void reloadConfiguration(@NotNull CBAppConfig appConfig) throws DBException;
void reloadConfiguration(@NotNull WebAppConfiguration appConfig) throws DBException;
}
@@ -930,23 +930,25 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
rootConfig.put("app", appConfigProperties);
copyConfigValue(
oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled());
oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled());
copyConfigValue(
oldAppConfig, appConfigProperties, "supportsCustomConnections", appConfig.isSupportsCustomConnections());
oldAppConfig, appConfigProperties, "supportsCustomConnections", appConfig.isSupportsCustomConnections());
copyConfigValue(
oldAppConfig, appConfigProperties, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled());
oldAppConfig, appConfigProperties, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled());
copyConfigValue(
oldAppConfig, appConfigProperties, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled());
oldAppConfig, appConfigProperties, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled());
copyConfigValue(
oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth());
oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth());
copyConfigValue(
oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy());
oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy());
copyConfigValue(
oldAppConfig, appConfigProperties, "linkExternalCredentialsWithUser", appConfig.isLinkExternalCredentialsWithUser());
oldAppConfig, appConfigProperties, "linkExternalCredentialsWithUser", appConfig.isLinkExternalCredentialsWithUser());
copyConfigValue(
oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth());
oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth());
copyConfigValue(
oldAppConfig, appConfigProperties, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled());
oldAppConfig, appConfigProperties, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled());
copyConfigValue(
oldAppConfig, appConfigProperties, CBConstants.PARAM_SHOW_READ_ONLY_CONN_INFO, appConfig.isShowReadOnlyConnectionInfo());
Map<String, Object> resourceQuotas = new LinkedHashMap<>();
Map<String, Object> originResourceQuotas = JSONUtils.getObject(oldAppConfig, CBConstants.PARAM_RESOURCE_QUOTAS);
@@ -39,6 +39,7 @@ public class CBConstants {
public static final String PARAM_STATIC_CONTENT = "staticContent";
public static final String PARAM_RESOURCE_QUOTAS = "resourceQuotas";
public static final String PARAM_RESOURCE_MANAGER_ENABLED = "resourceManagerEnabled";
public static final String PARAM_SHOW_READ_ONLY_CONN_INFO = "showReadOnlyConnectionInfo";
public static final String PARAM_AUTH_PROVIDERS = "authConfiguration";
public static final String PARAM_DB_CONFIGURATION = "database";
@@ -42,7 +42,7 @@ public class CBDataSourceUpdatedEventHandlerImpl extends CBProjectUpdatedEventHa
CBEventConstants.EventType eventType =
CBEventConstants.EventType.valueOf(JSONUtils.getString(event.getEventData(), "eventType"));
WebProjectImpl project = activeUserSession.getProjectById(projectId);
if (project == null || !project.getRmProject().isShared()) {
if (project == null) {
return;
}
activeUserSession.updateProjectConnection(project, dataSourceIds, eventType);
@@ -161,6 +161,9 @@ public class WebServiceNavigator implements DBWServiceNavigator {
List<WebNavigatorNodeInfo> nodeParents = new ArrayList<>();
for (DBNNode parent = node.getParentNode(); parent != null && !(parent instanceof DBNRoot); parent = parent.getParentNode()) {
if (parent instanceof DBNProjectDatabases) {
continue;
}
nodeParents.add(new WebNavigatorNodeInfo(session, parent));
}
@@ -44,11 +44,11 @@ import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils;
import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.LoggingProgressMonitor;
import org.jkiss.dbeaver.model.security.*;
import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException;
import org.jkiss.dbeaver.model.security.exception.SMException;
import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException;
import org.jkiss.dbeaver.model.security.user.*;
import org.jkiss.dbeaver.model.security.*;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
@@ -757,8 +757,11 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
dbStat.execute();
}
insertPermissions(dbCon, teamId,
new String[] {DBWConstants.PERMISSION_PUBLIC} , grantor);
insertPermissions(dbCon,
teamId,
getDefaultTeamPermissions(),
grantor
);
txn.commit();
}
@@ -767,6 +770,10 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
}
protected String[] getDefaultTeamPermissions() {
return new String[]{DBWConstants.PERMISSION_PUBLIC};
}
@Override
public void updateTeam(String teamId, String name, String description) throws DBCException {
if (!isSubjectExists(teamId)) {
@@ -1803,7 +1810,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
@Override
public void updateSession(@NotNull String sessionId, @NotNull Map<String, Object> parameters) throws DBCException {
String userId = getUserIdOrThrow();
String userId = getUserIdOrNull();
try (Connection dbCon = database.openConnection()) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"UPDATE CB_SESSION SET USER_ID=?,LAST_ACCESS_TIME=?,LAST_ACCESS_REMOTE_ADDRESS=?,LAST_ACCESS_USER_AGENT=?,LAST_ACCESS_INSTANCE_ID=? WHERE SESSION_ID=?")) {
@@ -2156,10 +2163,21 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
return originalLink + "?authId=" + authId;
}
@NotNull
private String getUserIdOrThrow() throws SMException {
String userId = getUserIdOrNull();
if (userId == null) {
throw new SMException("User not authenticated");
}
return userId;
}
@Nullable
private String getUserIdOrNull() {
SMCredentials activeUserCredentials = credentialsProvider.getActiveUserCredentials();
if (activeUserCredentials == null || activeUserCredentials.getUserId() == null) {
throw new SMException("User not authenticated");
return null;
}
return activeUserCredentials.getUserId();
}
@@ -174,7 +174,7 @@ export class NavNodeManagerService extends Bootstrap {
}
async canOpen(nodeId: string, parentId: string, folderId?: string): Promise<boolean> {
if (!this.navTree.has(nodeId)) {
if (!this.navNodeInfoResource.has(nodeId)) {
return false;
}
@@ -30,6 +30,30 @@ describe('Is array equals', () => {
});
test('should use isEqual argument if passed', () => {
expect(isArraysEqual([{ a:1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true);
expect(isArraysEqual([{ a: 1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true);
});
test('should return "true" when arrays are the same and "order" argument is "true"', () => {
expect(isArraysEqual([1, '1', true], [1, '1', true], undefined, true)).toBe(true);
});
test('should return "false" when arrays are not the same and "order" argument is "true"', () => {
expect(isArraysEqual([1, '2', true], [2, '1', true], undefined, true)).toBe(false);
});
test('should return "false" when arrays have the same elements but elements order is different and "order" argument is "true"', () => {
expect(isArraysEqual(['1', 1, true], [1, '1', true], undefined, true)).toBe(false);
});
test('should return "false" when arrays length is different and the first array is larger and "order" argument is "true"', () => {
expect(isArraysEqual([1, true, '1'], [1, true], undefined, true)).toBe(false);
});
test('should return "false" when arrays length is different and the second array is larger and "order" argument is "true"', () => {
expect(isArraysEqual([1, true], [1, true, '1'], undefined, true)).toBe(false);
});
test('should use isEqual argument if passed and "order" argument is "true"', () => {
expect(isArraysEqual([{ a: 1 }], [{ a: 1 }], (a, b) => a.a === b.a, true)).toBe(true);
});
});
@@ -11,11 +11,19 @@ const isObjectEqual = <T>(a: T, b: T) => a === b;
export function isArraysEqual<T>(
first: T[],
second: T[],
isEqual: (a: T, b: T) => boolean = isObjectEqual
isEqual: (a: T, b: T) => boolean = isObjectEqual,
order?: boolean
): boolean {
if (first.length !== second.length) {
return false;
}
return !first.some(b => !second.some(a => isEqual(a, b)));
return !first.some((b, index) => {
if (order) {
return !isEqual(second[index], b);
}
return !second.some(a => isEqual(a, b));
});
}
@@ -62,6 +62,7 @@ const topBarStyles = css`
position: relative;
display: flex;
padding-top: 16px;
margin-bottom: 24px;
&:before {
content: '';
@@ -37,7 +37,7 @@ export const UserInfo: TabContainerPanelComponent<IUserFormProps> = observer(fun
);
return styled(style)(
<ColoredContainer parent gap overflow>
<ColoredContainer gap overflow>
<Group small gap vertical overflow>
<Container gap vertical>
<GroupTitle keepSize>{translate('authentication_user_credentials')}</GroupTitle>
@@ -0,0 +1,115 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import styled, { css, use } from 'reshadow';
import { AuthRolesResource } from '@cloudbeaver/core-authentication';
import { BASE_CONTAINERS_STYLES, Combobox, Filter, Group, IconOrImage, useDataResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { IUserFilters, USER_ROLE_ALL, USER_STATUSES } from './useUsersTableFilters';
const styles = css`
filter-container {
display: flex;
gap: 12px;
}
Filter {
flex: 1;
}
actions {
composes: theme-form-element-radius theme-background-surface theme-text-on-surface theme theme-border-color-background from global;
box-sizing: border-box;
border: 2px solid;
height: 32px;
}
button {
position: relative;
box-sizing: border-box;
background: inherit;
cursor: pointer;
width: 28px;
height: 100%;
padding: 4px;
&:hover {
opacity: 0.8;
}
&[|active] {
background: var(--theme-secondary);
}
}
IconOrImage {
width: 100%;
height: 100%;
}
`;
interface Props {
filters: IUserFilters;
}
export const UsersTableFilters = observer<Props>(function UsersTableFilters({ filters }) {
const translate = useTranslate();
const style = useStyles(BASE_CONTAINERS_STYLES, styles);
const authRolesResource = useDataResource(UsersTableFilters, AuthRolesResource, undefined);
const [open, setOpen] = useState(false);
return styled(style)(
<Group parent compact gap>
<filter-container>
<Filter
placeholder={translate('authentication_administration_users_filters_search_placeholder')}
value={filters.search}
max
onFilter={filters.setSearch}
/>
<actions>
<button
{...use({ active: open })}
onClick={() => setOpen(!open)}
>
<IconOrImage icon='filter' />
</button>
</actions>
</filter-container>
{open && (
<Group box gap>
<Combobox
value={filters.status}
items={USER_STATUSES}
valueSelector={value => translate(value.label)}
keySelector={value => value.value}
keepSize
onSelect={filters.setStatus}
>
{translate('authentication_user_status')}
</Combobox>
{!!authRolesResource.data.length && (
<Combobox
items={[...authRolesResource.data, USER_ROLE_ALL]}
value={filters.role}
keepSize
onSelect={filters.setRole}
>
{translate('authentication_user_role')}
</Combobox>
)}
</Group>
)}
</Group>
);
});
@@ -0,0 +1,90 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { action, computed, observable } from 'mobx';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
import type { AdminUser } from '@cloudbeaver/core-authentication';
import { isArraysEqual } from '@cloudbeaver/core-utils';
export enum EUserStatus {
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
ALL = 'ALL'
}
interface IStatus {
label: TLocalizationToken;
value: EUserStatus;
}
export const USER_ROLE_ALL = 'ALL';
export const USER_STATUSES: IStatus[] = [
{
label: 'authentication_administration_users_filters_status_enabled',
value: EUserStatus.ENABLED,
},
{
label: 'authentication_administration_users_filters_status_disabled',
value: EUserStatus.DISABLED,
},
{
label: 'authentication_administration_users_filters_status_all',
value: EUserStatus.ALL,
},
];
export interface IUserFilters {
search: string;
role: string;
status: EUserStatus;
filteredUsers: AdminUser[];
setSearch: (value: string) => void;
setRole: (role: string) => void;
setStatus: (status: EUserStatus) => void;
}
export function useUsersTableFilters(users: AdminUser[]) {
const filters: IUserFilters = useObservableRef(() => ({
search: '',
role: USER_ROLE_ALL,
status: EUserStatus.ENABLED,
get filteredUsers() {
return this.users.filter(user => {
const matchSearch = user.userId.toLowerCase().includes(this.search.trim().toLowerCase());
const matchStatus = this.status === EUserStatus.ALL
|| (this.status === EUserStatus.ENABLED ? user.enabled : !user.enabled);
const matchRole = this.role === USER_ROLE_ALL || this.role === user.authRole;
return matchSearch && matchStatus && matchRole;
})
},
setSearch(value: string) {
this.search = value;
},
setRole(role: string) {
this.role = role;
},
setStatus(status: EUserStatus) {
this.status = status;
},
}), {
search: observable.ref,
role: observable.ref,
status: observable.ref,
users: observable.ref,
filteredUsers: computed<AdminUser[]>({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }),
setSearch: action.bound,
setRole: action.bound,
setStatus: action.bound,
}, { users });
return filters;
}
@@ -7,36 +7,24 @@
*/
import { observer } from 'mobx-react-lite';
import styled, { css, use } from 'reshadow';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
import { AdminUser, AuthRolesResource, UsersResource } from '@cloudbeaver/core-authentication';
import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, useMapResource, ToolsAction, ToolsPanel, Loader, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles, useDataResource } from '@cloudbeaver/core-blocks';
import { useController, useService } from '@cloudbeaver/core-di';
import { AuthProvidersResource, AuthRolesResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication';
import {
Table, TableHeader, TableColumnHeader, TableBody, useMapResource, ToolsAction,
ToolsPanel, Loader, useTranslate, useStyles, useDataResource,
BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { filterUndefined } from '@cloudbeaver/core-utils';
import { CreateUser } from './CreateUser';
import { CreateUserService } from './CreateUserService';
import { UsersTableFilters } from './Filters/UsersTableFilters';
import { useUsersTableFilters } from './Filters/useUsersTableFilters';
import { User } from './User';
import { UsersTableController } from './UsersTableController';
const layoutStyles = css`
layout-grid {
width: 100%;
overflow: auto;
}
layout-grid-inner {
min-height: 100%;
}
layout-grid-cell {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
position: relative;
border: solid 1px;
}
`;
import { useUsersTable } from './useUsersTable';
const loaderStyle = css`
ExceptionMessage {
@@ -45,16 +33,36 @@ const loaderStyle = css`
`;
const styles = css`
ToolsPanel {
border-bottom: none;
border-radius: inherit;
}
content {
display: flex;
flex-direction: column;
overflow: auto;
gap: 24px;
}
Group {
padding: 0;
}
Table {
width: 100%;
}
[|table] {
min-height: 140px; /* loader overlay size */
TableHeader {
background: var(--theme-surface);
position: sticky;
top: 0;
z-index: 1;
}
ToolsPanel {
border-bottom: none;
table-container {
overflow: auto;
height: 100%;
}
`;
@@ -65,53 +73,48 @@ interface Props {
export const UsersTable = observer<Props>(function UsersTable({ sub, param }) {
const translate = useTranslate();
const style = useStyles(BASE_LAYOUT_GRID_STYLES, styles, ADMINISTRATION_TOOLS_PANEL_STYLES, layoutStyles);
const createUserService = useService(CreateUserService);
const controller = useController(UsersTableController);
const usersResource = useMapResource(UsersTable, UsersResource, CachedMapAllKey);
const authRoles = useDataResource(UsersTable, AuthRolesResource, undefined);
const isLocalProviderAvailable = controller.isLocalProviderAvailable;
const users = usersResource.resource.values
.filter<AdminUser>(filterUndefined)
.sort((a, b) => {
if (usersResource.resource.isNew(a.userId) === usersResource.resource.isNew(b.userId)) {
return a.userId.localeCompare(b.userId);
}
if (usersResource.resource.isNew(a.userId)) {
return -1;
}
return 1;
});
const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_CONTAINERS_STYLES);
const createUserService = useService(CreateUserService);
const usersResource = useMapResource(UsersTable, UsersResource, CachedMapAllKey);
const authProvidersResource = useMapResource(UsersTable, AuthProvidersResource, CachedMapAllKey);
const authRolesResource = useDataResource(UsersTable, AuthRolesResource, undefined);
const table = useUsersTable(usersResource.resource);
const filters = useUsersTableFilters(table.users);
const isLocalProviderAvailable = authProvidersResource.resource.has(AUTH_PROVIDER_LOCAL_ID);
const create = param === 'create';
const keys = users.map(user => user.userId);
const displayAuthRole = authRoles.data.length > 0;
const displayAuthRole = authRolesResource.data.length > 0;
const keys = filters.filteredUsers.map(user => user.userId);
return styled(style)(
<layout-grid>
<layout-grid-inner>
<layout-grid-cell data-span='12'>
<ToolsPanel>
{isLocalProviderAvailable && (
<ColoredContainer wrap gap parent overflow vertical>
<Container gap vertical>
<Container gap keepSize>
<Group>
<ToolsPanel>
{isLocalProviderAvailable && (
<ToolsAction
title={translate('authentication_administration_tools_add_tooltip')}
icon='add'
viewBox="0 0 24 24"
disabled={create && !!createUserService.user}
onClick={createUserService.create}
>
{translate('ui_add')}
</ToolsAction>
)}
<ToolsAction
title={translate('authentication_administration_tools_add_tooltip')}
icon="add"
title={translate('authentication_administration_tools_refresh_tooltip')}
icon='refresh'
viewBox="0 0 24 24"
disabled={create && !!createUserService.user}
onClick={createUserService.create}
onClick={table.update}
>
{translate('ui_add')}
{translate('ui_refresh')}
</ToolsAction>
)}
<ToolsAction
title={translate('authentication_administration_tools_refresh_tooltip')}
icon="refresh"
viewBox="0 0 24 24"
onClick={controller.update}
>
{translate('ui_refresh')}
</ToolsAction>
{/* {isLocalProviderAvailable && (
{/* {isLocalProviderAvailable && (
<ToolsAction
title={translate('authentication_administration_tools_delete_tooltip')}
icon="trash"
@@ -122,51 +125,56 @@ export const UsersTable = observer<Props>(function UsersTable({ sub, param }) {
{translate('ui_delete')}
</ToolsAction>
)} */}
</ToolsPanel>
</layout-grid-cell>
</ToolsPanel>
</Group>
<UsersTableFilters filters={filters} />
</Container>
{create && createUserService.user && (
<layout-grid-cell data-span='12'>
<CreateUser user={createUserService.user} onCancel={createUserService.cancelCreate} />
</layout-grid-cell>
)}
<layout-grid-cell data-span='12' {...use({ table: true })}>
<Loader style={loaderStyle} state={[usersResource, authRoles]} overlay>
<Table
keys={keys}
selectedItems={controller.selectedItems}
expandedItems={controller.expandedItems}
size='big'
>
<TableHeader>
{/* {isLocalProviderAvailable && (
<content>
{create && createUserService.user && (
<Group>
<CreateUser user={createUserService.user} onCancel={createUserService.cancelCreate} />
</Group>
)}
<Group>
<Loader style={loaderStyle} state={[usersResource, authRolesResource]} overlay>
<Table
keys={keys}
selectedItems={table.state.selected}
expandedItems={table.state.expanded}
size='big'
>
<TableHeader>
{/* {isLocalProviderAvailable && (
<TableColumnHeader min flex centerContent>
<TableSelect />
</TableColumnHeader>
)} */}
<TableColumnHeader min />
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
{displayAuthRole && (
<TableColumnHeader>{translate('authentication_user_role')}</TableColumnHeader>
)}
<TableColumnHeader>{translate('authentication_user_team')}</TableColumnHeader>
<TableColumnHeader>{translate('authentication_user_enabled')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{users.map(user => (
<User
key={user.userId}
user={user}
displayAuthRole={displayAuthRole}
// selectable={isLocalProviderAvailable}
/>
))}
</TableBody>
</Table>
</Loader>
</layout-grid-cell>
</layout-grid-inner>
</layout-grid>
<TableColumnHeader min />
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
{displayAuthRole && (
<TableColumnHeader>{translate('authentication_user_role')}</TableColumnHeader>
)}
<TableColumnHeader>{translate('authentication_user_team')}</TableColumnHeader>
<TableColumnHeader>{translate('authentication_user_enabled')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{filters.filteredUsers.map(user => (
<User
key={user.userId}
user={user}
displayAuthRole={displayAuthRole}
// selectable={isLocalProviderAvailable}
/>
))}
</TableBody>
</Table>
</Loader>
</Group>
</content>
</Container>
</ColoredContainer>
);
});
@@ -1,133 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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, makeObservable } from 'mobx';
import { AdminUser, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication';
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
@injectable()
export class UsersTableController implements IInitializableController {
isDeleting = false;
readonly selectedItems = observable<string, boolean>(new Map());
readonly expandedItems = observable<string, boolean>(new Map());
readonly error = new GQLErrorCatcher();
get users(): AdminUser[] {
return Array.from(this.usersResource.data.values())
.sort((a, b) => {
if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) {
return a.userId.localeCompare(b.userId);
}
if (this.usersResource.isNew(a.userId)) {
return -1;
}
return 1;
});
}
get isProvidersLoading(): boolean {
return this.authProvidersResource.isLoading();
}
get isLocalProviderAvailable(): boolean {
return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
}
get isLoading(): boolean {
return this.usersResource.isLoading() || this.isDeleting;
}
get itemsSelected(): boolean {
return Array.from(this.selectedItems.values()).some(v => v);
}
constructor(
private notificationService: NotificationService,
private authProvidersResource: AuthProvidersResource,
private usersResource: UsersResource,
private commonDialogService: CommonDialogService,
private localizationService: LocalizationService
) {
makeObservable(this, {
isDeleting: observable,
users: computed,
itemsSelected: computed,
});
}
init(): void {
this.authProvidersResource.loadAll();
}
update = async () => {
try {
await this.usersResource.refreshAll();
this.notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' });
} catch (exception: any) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Users update failed');
}
}
};
delete = async () => {
if (this.isDeleting) {
return;
}
const deletionList = Array
.from(this.selectedItems)
.filter(([_, value]) => value)
.map(([userId]) => userId);
if (deletionList.length === 0) {
return;
}
const userNames = deletionList.map(name => `"${name}"`).join(', ');
const message = `${this.localizationService.translate('authentication_administration_users_delete_confirmation')}${userNames}. ${this.localizationService.translate('ui_are_you_sure')}`;
const result = await this.commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
message,
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
this.isDeleting = true;
try {
await this.usersResource.delete(resourceKeyList(deletionList));
this.selectedItems.clear();
for (const id of deletionList) {
this.expandedItems.delete(id);
}
} catch (exception: any) {
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,103 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { action, computed, observable } from 'mobx';
import type { AdminUser, UsersResource } from '@cloudbeaver/core-authentication';
import { TableState, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
interface State {
loading: boolean;
state: TableState;
users: AdminUser[];
update: () => Promise<void>;
delete: () => Promise<void>;
}
export function useUsersTable(usersResource: UsersResource) {
const translate = useTranslate();
const notificationService = useService(NotificationService);
const commonDialogService = useService(CommonDialogService);
const state: State = useObservableRef(() => ({
loading: false,
state: new TableState(),
get users() {
return this.usersResource.values
.slice()
.sort((a, b) => {
if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) {
return a.userId.localeCompare(b.userId);
}
if (this.usersResource.isNew(a.userId)) {
return -1;
}
return 1;
});
},
async update() {
try {
await this.usersResource.refreshAll();
notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' });
} catch (exception: any) {
notificationService.logException(exception, 'authentication_administration_tools_refresh_fail');
}
},
async delete() {
if (this.loading) {
return;
}
const deletionList = this.state.selectedList.filter(([_, value]) => value).map(([userId]) => userId);
if (deletionList.length === 0) {
return;
}
const userNames = deletionList.map(name => `"${name}"`).join(', ');
const message = `${translate('authentication_administration_users_delete_confirmation')}${userNames}. ${translate('ui_are_you_sure')}`;
const result = await commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
message,
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
this.loading = true;
try {
await this.usersResource.delete(resourceKeyList(deletionList));
this.state.unselect();
for (const id of deletionList) {
this.state.unexpand(id);
}
} catch (exception: any) {
notificationService.logException(exception, 'authentication_administration_user_delete_fail');
} finally {
this.loading = false;
}
},
}), {
loading: observable.ref,
users: computed<AdminUser[]>({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }),
update: action.bound,
delete: action.bound,
}, { usersResource });
return state;
}
@@ -18,11 +18,17 @@ export default [
['authentication_administration_tools_refresh_tooltip', 'Refresh user list'],
['authentication_administration_tools_delete_tooltip', 'Delete selected users'],
['authentication_administration_tools_refresh_success', 'User list was refreshed'],
['authentication_administration_tools_refresh_fail', 'Users update failed'],
['authentication_administration_user_delete_fail', 'Error deleting user'],
['authentication_administration_user_update_failed', 'Error saving user'],
['authentication_administration_user_updated', 'User updated'],
['authentication_administration_user_created', 'User was created successfully'],
['authentication_administration_user_create_failed', 'Error creating new user'],
['authentication_administration_users_delete_confirmation', "You're going to delete these users: "],
['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'],
['authentication_administration_users_filters_status_enabled', 'ENABLED'],
['authentication_administration_users_filters_status_disabled', 'DISABLED'],
['authentication_administration_users_filters_status_all', 'ALL'],
['administration_configuration_wizard_configuration_admin', 'Administrator Credentials'],
['administration_configuration_wizard_configuration_admin_name', 'Login'],
@@ -15,10 +15,16 @@ export default [
['authentication_administration_tools_refresh_tooltip', 'Aggiorna la lista utenti'],
['authentication_administration_tools_delete_tooltip', 'Elimina gli utenti selezionati'],
['authentication_administration_tools_refresh_success', 'Lista degli utenti ricaricata'],
['authentication_administration_tools_refresh_fail', 'Users update failed'],
['authentication_administration_user_delete_fail', 'Error deleting user'],
['authentication_administration_user_update_failed', 'Errore al salvataggio utente'],
['authentication_administration_user_updated', 'Utente aggiornato'],
['authentication_administration_user_created', 'Utente creato con successo'],
['authentication_administration_user_create_failed', 'Errore di creazione nuovo utente'],
['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'],
['authentication_administration_users_filters_status_enabled', 'ENABLED'],
['authentication_administration_users_filters_status_disabled', 'DISABLED'],
['authentication_administration_users_filters_status_all', 'ALL'],
['administration_teams_team_info_created', 'Team created'],
['administration_teams_team_info_updated', 'Team updated'],
@@ -18,11 +18,17 @@ export default [
['authentication_administration_tools_refresh_tooltip', 'Обновить список пользователей'],
['authentication_administration_tools_delete_tooltip', 'Удалить выбранных пользователей'],
['authentication_administration_tools_refresh_success', 'Список пользователей обновлен'],
['authentication_administration_tools_refresh_fail', 'Не удалось обновить список пользователей'],
['authentication_administration_user_delete_fail', 'Не удалось удалить пользователя'],
['authentication_administration_user_update_failed', 'Не удалось обновить пользователя'],
['authentication_administration_user_updated', 'Пользователь обновлен'],
['authentication_administration_user_created', 'Пользователь создан успешно'],
['authentication_administration_user_create_failed', 'Не удалось создать пользователя'],
['authentication_administration_users_delete_confirmation', 'Вы собираетесь удалить следующих пользователей: '],
['authentication_administration_users_filters_search_placeholder', 'Поиск по имени пользователя...'],
['authentication_administration_users_filters_status_enabled', 'ВКЛЮЧЕН'],
['authentication_administration_users_filters_status_disabled', 'ВЫКЛЮЧЕН'],
['authentication_administration_users_filters_status_all', 'ВСЕ'],
['administration_configuration_wizard_configuration_admin', 'Учетные данные администратора'],
['administration_configuration_wizard_configuration_admin_name', 'Логин'],
@@ -17,11 +17,17 @@ export default [
['authentication_administration_tools_refresh_tooltip', '刷新用户列表'],
['authentication_administration_tools_delete_tooltip', '删除选中的用户'],
['authentication_administration_tools_refresh_success', '用户列表已刷新'],
['authentication_administration_tools_refresh_fail', 'Users update failed'],
['authentication_administration_user_delete_fail', 'Error deleting user'],
['authentication_administration_user_update_failed', '保存用户出错'],
['authentication_administration_user_updated', '用户信息已更新'],
['authentication_administration_user_created', '创建用户成功'],
['authentication_administration_user_create_failed', '创建新用户出错'],
['authentication_administration_users_delete_confirmation', '您将删除这些用户:'],
['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'],
['authentication_administration_users_filters_status_enabled', 'ENABLED'],
['authentication_administration_users_filters_status_disabled', 'DISABLED'],
['authentication_administration_users_filters_status_all', 'ALL'],
['administration_configuration_wizard_configuration_admin', '管理员凭据'],
['administration_configuration_wizard_configuration_admin_name', '登录'],
@@ -26,6 +26,7 @@ export const NetworkHandlerAuthForm = observer<Props>(function NetworkHandlerAut
const translate = useTranslate();
const handler = useMapResource(NetworkHandlerAuthForm, NetworkHandlerResource, id);
//@TODO Do not mutate state in component body
if (!networkHandlersConfig.some(state => state.id === id)) {
networkHandlersConfig.push({
id,
@@ -49,7 +49,6 @@ export const DBAuthDialogFooter = observer<React.PropsWithChildren<Props>>(funct
type="button"
mod={['unelevated']}
disabled={isAuthenticating}
loading={isAuthenticating}
onClick={onLogin}
>
{translate('authentication_login')}
@@ -15,6 +15,7 @@ import {
useFocus,
ErrorMessage,
useStyles,
Loader,
} from '@cloudbeaver/core-blocks';
import { IConnectionInfoParams, useConnectionInfo, useDBDriver } from '@cloudbeaver/core-connections';
import { useController } from '@cloudbeaver/core-di';
@@ -86,20 +87,20 @@ export const DatabaseAuthDialog: DialogComponent<Payload> = observer(function Da
)}
onReject={options?.persistent ? undefined : rejectDialog}
>
<SubmittingForm
ref={focusedRef}
disabled={!connection.isLoaded() || connection.isLoading() || !controller.configured}
onSubmit={controller.login}
>
<ConnectionAuthenticationForm
config={controller.config}
authModelId={authModelId}
authProperties={connection.connectionInfo?.authProperties}
networkHandlers={payload.networkHandlers}
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
disabled={controller.isAuthenticating}
/>
<SubmittingForm ref={focusedRef} onSubmit={controller.login}>
{!connection.isLoaded() || connection.isLoading() || !controller.configured
? <Loader />
: (
<ConnectionAuthenticationForm
config={controller.config}
authModelId={authModelId}
authProperties={connection.connectionInfo?.authProperties}
networkHandlers={payload.networkHandlers}
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
disabled={controller.isAuthenticating}
/>
)}
</SubmittingForm>
</CommonDialogWrapper>
);
@@ -191,9 +191,9 @@ export class PluginBootstrap extends Bootstrap {
try {
const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext);
const node = await this.navNodeInfoResource.load(data.nodeId);
const node = this.navNodeInfoResource.get(data.nodeId);
if (node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) {
if (!node || node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) {
return;
}
@@ -129,7 +129,7 @@ export const topMenuStyles = css`
display: none;
}
}
MenuButton, MenuBarElement {
MenuBarElement {
&:hover, &:global([aria-expanded="true"]) {
background: #338ecc;
}
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@cloudbeaver/product-default",
"sideEffects": false,
"version": "22.2.4",
"version": "22.3.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
@@ -22,8 +22,6 @@
"@cloudbeaver/core-bootstrap": "0.1.0",
"@cloudbeaver/core-di": "0.1.0",
"@cloudbeaver/core-product": "0.1.0",
"@cloudbeaver/plugin-user-profile": "0.1.0",
"@cloudbeaver/plugin-help": "0.1.0",
"@cloudbeaver/plugin-administration": "0.1.0",
"@cloudbeaver/plugin-authentication": "0.1.0",
"@cloudbeaver/plugin-authentication-administration": "0.1.0",
@@ -36,6 +34,7 @@
"@cloudbeaver/plugin-data-viewer": "0.1.0",
"@cloudbeaver/plugin-datasource-context-switch": "0.1.0",
"@cloudbeaver/plugin-ddl-viewer": "0.1.0",
"@cloudbeaver/plugin-help": "0.1.0",
"@cloudbeaver/plugin-localization": "0.1.0",
"@cloudbeaver/plugin-log-viewer": "0.1.0",
"@cloudbeaver/plugin-navigation-tree": "0.1.0",
@@ -48,6 +47,7 @@
"@cloudbeaver/plugin-sql-editor-screen": "0.1.0",
"@cloudbeaver/plugin-theme": "0.1.0",
"@cloudbeaver/plugin-top-app-bar": "0.1.0",
"@cloudbeaver/plugin-user-profile": "0.1.0",
"@cloudbeaver/plugin-version": "0.1.0",
"@cloudbeaver/plugin-version-update-administration": "0.1.0"
},