Merge branch 'devel' into feat/execution-plan

This commit is contained in:
Naumov Alexey
2021-06-21 16:34:43 +03:00
58 changed files with 354 additions and 171 deletions
@@ -90,6 +90,8 @@ type ServerConfig {
version: String!
workspaceId: ID!
serverURL: String!
anonymousAccessEnabled: Boolean
authenticationEnabled: Boolean @deprecated
supportsCustomConnections: Boolean
@@ -28,7 +28,7 @@ type SQLCompletionProposal {
replacementString: String!
replacementOffset: Int!
replacementLength: Int!
cursorPosition: Int!
cursorPosition: Int
icon: String
nodePath: String
@@ -45,6 +45,17 @@ public interface DBWAuthProviderExternal<AUTH_SESSION extends DBASession> extend
@NotNull Map<String, Object> authParameters // Passed auth parameters (e.g. user name or password)
) throws DBException;
/**
* Returns new identifying credentials which can be used to find/create user in database
*/
@NotNull
Map<String, Object> authFederatedUser(
@NotNull DBRProgressMonitor monitor,
@NotNull Map<String, Object> providerConfig, // Auth provider configuration (e.g. 3rd party auth server address)
@NotNull String faProvider,
@NotNull Map<String, Object> faParameters // Passed federated auth parameters (e.g. assertion ID, session token, etc)
) throws DBException;
/**
* Validates that external user may be associated with local user
* @param userCredentials credentials from authExternalUser
@@ -18,9 +18,14 @@ package io.cloudbeaver.auth.provider;
import io.cloudbeaver.DBWAuthProviderExternal;
import io.cloudbeaver.model.user.WebUser;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.access.DBASession;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.utils.CommonUtils;
import java.util.Map;
/**
* Abstract external auth provider
*/
@@ -34,4 +39,9 @@ public abstract class AbstractExternalAuthProvider<SESSION extends DBASession> i
}
}
@NotNull
@Override
public Map<String, Object> authFederatedUser(@NotNull DBRProgressMonitor monitor, @NotNull Map<String, Object> providerConfig, @NotNull String faProvider, @NotNull Map<String, Object> faParameters) throws DBException {
throw new DBException("Federated authorization not supported");
}
}
@@ -59,6 +59,11 @@ public class WebServerConfig {
return DBWorkbench.getPlatform().getWorkspace().getWorkspaceId();
}
@Property
public String getServerURL() {
return CommonUtils.notEmpty(application.getServerURL());
}
@Property
public boolean isAnonymousAccessEnabled() {
return application.getAppConfiguration().isAnonymousAccessEnabled();
@@ -19,6 +19,7 @@ package io.cloudbeaver.server;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebServiceRegistry;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
@@ -46,6 +47,7 @@ public class CBAppConfig {
private String[] enabledAuthProviders = null;
private DataSourceNavigatorSettings defaultNavigatorSettings = DEFAULT_VIEW_SETTINGS;
private Map<String, Object> plugins = new LinkedHashMap<>();
private Map<String, Object> authConfiguration = new LinkedHashMap<>();
public boolean isAnonymousAccessEnabled() {
return anonymousAccessEnabled;
@@ -132,15 +134,16 @@ public class CBAppConfig {
this.defaultNavigatorSettings = new DataSourceNavigatorSettings(defaultNavigatorSettings);
}
@NotNull
public Map<String, Object> getPlugins() {
return plugins;
}
public Map<String, Object> getPluginConfig(String pluginId) {
public Map<String, Object> getPluginConfig(@NotNull String pluginId) {
return getPluginConfig(pluginId, false);
}
public Map<String, Object> getPluginConfig(String pluginId, boolean create) {
public Map<String, Object> getPluginConfig(@NotNull String pluginId, boolean create) {
Object config = plugins.get(pluginId);
if (config instanceof Map) {
return (Map<String, Object>) config;
@@ -155,8 +158,21 @@ public class CBAppConfig {
}
}
public <T> T getPluginOption(String pluginId, String option) {
public <T> T getPluginOption(@NotNull String pluginId, @NotNull String option) {
return (T)getPluginConfig(pluginId).get(option);
}
public Map<String, Object> getAuthConfiguration() {
return authConfiguration;
}
@NotNull
public Map<String, Object> getAuthConfiguration(@NotNull String providerId) {
Object apConfig = authConfiguration.get(providerId);
if (apConfig instanceof Map) {
return (Map<String, Object>) apConfig;
}
return Collections.emptyMap();
}
}
@@ -74,6 +74,7 @@ public class CBApplication extends BaseApplicationImpl {
return (CBApplication) BaseApplicationImpl.getInstance();
}
private String serverURL;
private int serverPort = CBConstants.DEFAULT_SERVER_PORT;
private String serverName = null;
private String contentRoot = CBConstants.DEFAULT_CONTENT_ROOT;
@@ -103,6 +104,10 @@ public class CBApplication extends BaseApplicationImpl {
public CBApplication() {
}
public String getServerURL() {
return serverURL;
}
public int getServerPort() {
return serverPort;
}
@@ -403,6 +408,12 @@ public class CBApplication extends BaseApplicationImpl {
Map<String, Object> serverConfig = JSONUtils.getObject(configProps, "server");
serverPort = JSONUtils.getInteger(serverConfig, CBConstants.PARAM_SERVER_PORT, serverPort);
if (serverConfig.containsKey(CBConstants.PARAM_SERVER_URL)) {
serverURL = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_URL, serverURL);
} else if (serverURL == null) {
serverURL = "http://" + InetAddress.getLocalHost().getHostName() + ":" + serverPort;
}
serverName = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_NAME, serverName);
contentRoot = getRelativePath(
JSONUtils.getString(serverConfig, CBConstants.PARAM_CONTENT_ROOT, contentRoot), homeFolder);
@@ -522,6 +533,7 @@ public class CBApplication extends BaseApplicationImpl {
public synchronized void finishConfiguration(
@NotNull String newServerName,
@NotNull String newServerURL,
@NotNull String adminName,
@Nullable String adminPassword,
@NotNull List<WebAuthInfo> authInfoList,
@@ -538,7 +550,7 @@ public class CBApplication extends BaseApplicationImpl {
// Save runtime configuration
log.debug("Saving runtime configuration");
saveRuntimeConfig(newServerName, sessionExpireTime, appConfig);
saveRuntimeConfig(newServerName, newServerURL, sessionExpireTime, appConfig);
// Grant permissions to predefined connections
if (isConfigurationMode() && appConfig.isAnonymousAccessEnabled()) {
@@ -561,7 +573,7 @@ public class CBApplication extends BaseApplicationImpl {
}
public synchronized void flushConfiguration() throws DBException {
saveRuntimeConfig(serverName, maxSessionIdleTime, appConfiguration);
saveRuntimeConfig(serverName, serverURL, maxSessionIdleTime, appConfiguration);
}
@@ -583,7 +595,7 @@ public class CBApplication extends BaseApplicationImpl {
}
}
private void saveRuntimeConfig(String newServerName, long sessionExpireTime, CBAppConfig appConfig) throws DBException {
private void saveRuntimeConfig(String newServerName, String newServerURL, long sessionExpireTime, CBAppConfig appConfig) throws DBException {
File runtimeConfigFile = getRuntimeAppConfigFile();
try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigFile), StandardCharsets.UTF_8)) {
@@ -600,6 +612,9 @@ public class CBApplication extends BaseApplicationImpl {
if (!CommonUtils.isEmpty(newServerName)) {
JSONUtils.field(json, CBConstants.PARAM_SERVER_NAME, newServerName);
}
if (!CommonUtils.isEmpty(newServerURL)) {
JSONUtils.field(json, CBConstants.PARAM_SERVER_URL, newServerURL);
}
if (sessionExpireTime > 0) {
JSONUtils.field(json, CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime);
}
@@ -643,6 +658,9 @@ public class CBApplication extends BaseApplicationImpl {
if (!CommonUtils.isEmpty(appConfig.getPlugins())) {
JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins());
}
if (!CommonUtils.isEmpty(appConfig.getAuthConfiguration())) {
JSONUtils.serializeProperties(json, "authConfiguration", appConfig.getAuthConfiguration());
}
json.endObject();
}
@@ -38,6 +38,7 @@ public class CBConstants {
public static final String PARAM_SERVER_PORT = "serverPort";
public static final String PARAM_SERVER_NAME = "serverName";
public static final String PARAM_CONTENT_ROOT = "contentRoot";
public static final String PARAM_SERVER_URL = "serverURL";
public static final String PARAM_ROOT_URI = "rootURI";
public static final String PARAM_SERVICES_URI = "serviceURI";
public static final String PARAM_DRIVERS_LOCATION = "driversLocation";
@@ -69,6 +70,7 @@ public class CBConstants {
public static final String VAR_CB_LOCAL_HOST_ADDR = "CB_LOCAL_HOST_ADDR";
public static final String DEFAULT_SERVER_ADDR = "localhost";
public static final String VAR_HOST_DOCKER_INTERNAL = "host.docker.internal";
@@ -45,8 +45,9 @@ public class CBJettyServer {
// Handler configuration
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
servletContextHandler.setResourceBase(application.getContentRoot());
servletContextHandler.setContextPath(application.getRootURI());
servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), application.getRootURI());
String rootURI = application.getRootURI();
servletContextHandler.setContextPath(rootURI);
servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), "/");
servletContextHandler.addServlet(new ServletHolder("images", new CBImageServlet()), application.getServicesURI() + "images/*");
servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), application.getServicesURI() + "gql/*");
servletContextHandler.addEventListener(new CBServerContextListener());
@@ -50,6 +50,7 @@ type AdminPermissionInfo {
input ServerConfigInput {
serverName: String
serverURL: String
adminName: String
adminPassword: String
@@ -30,6 +30,7 @@ import java.util.Map;
public class AdminServerConfig {
private String serverName;
private String serverURL;
private String adminName;
private String adminPassword;
@@ -44,6 +45,7 @@ public class AdminServerConfig {
public AdminServerConfig(Map<String, Object> params) {
this.serverName = JSONUtils.getString(params, "serverName");
this.serverURL = JSONUtils.getString(params, "serverURL");
this.adminName = JSONUtils.getString(params, "adminName");
this.adminPassword = JSONUtils.getString(params, "adminPassword");
@@ -69,6 +71,14 @@ public class AdminServerConfig {
this.serverName = serverName;
}
public String getServerURL() {
return serverURL;
}
public void setServerURL(String serverURL) {
this.serverURL = serverURL;
}
public String getAdminName() {
return adminName;
}
@@ -419,6 +419,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
CBApplication.getInstance().finishConfiguration(
config.getServerName(),
config.getServerURL(),
adminName,
adminPassword,
authInfoList,
@@ -45,7 +45,7 @@ export const TreeNodeMenu = observer(function TreeNodeMenu({
}
return styled(treeNodeMenuStyles)(
<MenuTrigger panel={menuPanel} {...use({ selected })}>
<MenuTrigger panel={menuPanel} {...use({ selected })} modal>
<Icon name="snack" viewBox="0 0 16 10" />
</MenuTrigger>
);
@@ -8,7 +8,7 @@
import { action, makeObservable } from 'mobx';
import { ConnectionAuthService } from '@cloudbeaver/core-connections';
import { ConnectionAuthService, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
@@ -37,6 +37,7 @@ export class NavigationTreeService {
private navNodeManagerService: NavNodeManagerService,
private notificationService: NotificationService,
private connectionAuthService: ConnectionAuthService,
private connectionInfoResource: ConnectionInfoResource,
private navNodeExtensionsService: NavNodeExtensionsService,
private navTreeResource: NavTreeResource
) {
@@ -61,10 +62,22 @@ export class NavigationTreeService {
await this.navNodeManagerService.navToNode(id, parentId);
}
async loadNestedNodes(id = ROOT_NODE_PATH): Promise<boolean> {
async loadNestedNodes(id = ROOT_NODE_PATH, tryConnect?: boolean): Promise<boolean> {
try {
if (this.isConnectionNode(id) && !(await this.tryInitConnection(id))) {
return false;
if (this.isConnectionNode(id)) {
const connection = await this.connectionInfoResource.load(
NodeManagerUtils.connectionNodeIdToConnectionId(id)
);
if (!connection.connected && !tryConnect) {
return false;
}
const connected = await this.tryInitConnection(id);
if (!connected) {
return false;
}
}
await this.navNodeManagerService.loadTree(id);
return true;
@@ -88,7 +88,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
continue;
}
const loaded = await navTreeService.loadNestedNodes(child);
const loaded = await navTreeService.loadNestedNodes(child, false);
if (!loaded) {
const node = navNodeInfoResource.get(child);
@@ -205,7 +205,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
try {
if (state) {
state = await navTreeService.loadNestedNodes(node.id);
state = await navTreeService.loadNestedNodes(node.id, true);
}
await options.onExpand?.(node, state);
@@ -31,7 +31,7 @@ export const MainMenu = observer(function MainMenu() {
const mainMenuService = useService(MainMenuService);
return styled(styles)(
<menu-wrapper as="div">
<menu-wrapper>
{mainMenuService.getMainMenu().map((topItem, i) => (
<TopMenuItem key={i} menuItem={topItem} />
))}
@@ -22,7 +22,7 @@ export const SettingsMenu = observer(function SettingsMenu() {
const settingsMenuService = useService(SettingsMenuService);
return styled(useStyles(settingsMenuStyles))(
<MenuTrigger panel={settingsMenuService.getMenu()} style={[topMenuStyles, settingsMenuStyles]}>
<MenuTrigger panel={settingsMenuService.getMenu()} style={[topMenuStyles, settingsMenuStyles]} placement='bottom-end' modal rtl>
<Icon name="settings" viewBox="0 0 28 28" />
</MenuTrigger>
);
@@ -26,10 +26,8 @@ export const settingsMenuStyles = css`
height: 24px;
}
menu-panel-item {
flex-direction: row-reverse;
& menu-item-text {
text-align: right;
margin-left: auto;
}
& Icon {
transform: rotate(90deg);
@@ -25,7 +25,8 @@ type TopMenuItemProps = Omit<ButtonHTMLAttributes<any>, 'style'> & {
export const TopMenuItem = observer(function TopMenuItem({ menuItem, style = [], ...props }: TopMenuItemProps) {
const translate = useTranslate();
const styles = useStyles(!menuItem.panel ? [] : [...style, topMenuStyles]);
style = [...style, topMenuStyles];
const styles = useStyles(!menuItem.panel ? [] : style);
if (!menuItem.panel) {
return (
@@ -44,12 +45,13 @@ export const TopMenuItem = observer(function TopMenuItem({ menuItem, style = [],
{...props}
panel={menuItem.panel}
disabled={menuItem.isDisabled}
style={[...style, topMenuStyles]}
style={style}
placement="bottom-end"
modal
onClick={() => menuItem.onClick?.()}
>
{menuItem.icon && (
<menu-trigger-icon as="div">
<menu-trigger-icon>
<IconOrImage icon={menuItem.icon} />
</menu-trigger-icon>
)}
@@ -12,7 +12,7 @@ import { composes } from '@cloudbeaver/core-theming';
export const topMenuStyles = composes(
css`
Menu {
menu-box {
composes: theme-text-on-primary from global;
}
@@ -21,7 +21,7 @@ export const topMenuStyles = composes(
}
`,
css`
Menu {
menu-box {
background-color: #338ecc!important;
}
MenuTrigger {
@@ -55,7 +55,9 @@ export const topMenuStyles = composes(
background: #236ea0;
}
}
MenuItem {
MenuItem,
MenuItemCheckbox,
MenuItemRadio {
&:hover, &:global([aria-expanded="true"]) {
background: #236ea0;
}
@@ -6,8 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import { runInAction } from 'mobx';
import {
ConnectionAuthService, Connection, ConnectionInfoResource
} from '@cloudbeaver/core-connections';
@@ -17,9 +15,7 @@ import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/cor
import {
PermissionsService, EPermission, ServerService
} from '@cloudbeaver/core-root';
import {
resourceKeyList, ResourceKey, ResourceKeyUtils
} from '@cloudbeaver/core-sdk';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { NavigationService } from '@cloudbeaver/core-ui';
import { ENodeFeature } from './ENodeFeature';
@@ -116,11 +112,7 @@ export class NavNodeManagerService extends Bootstrap {
.addHandler(this.navigateHandler.bind(this));
}
register(): void {
this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this));
this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this));
this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this));
}
register(): void { }
load(): void {}
@@ -320,67 +312,6 @@ export class NavNodeManagerService extends Bootstrap {
};
};
private async connectionCreateHandler(connection: Connection) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
return;
}
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id);
this.navTree.markTreeOutdated(nodeId);
const tree = await this.navTree.load(ROOT_NODE_PATH);
if (!tree.includes(nodeId)) {
await this.navTree.refresh(ROOT_NODE_PATH);
}
}
private async connectionUpdateHandler(key: ResourceKey<string>) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
return;
}
await this.navTree.load(ROOT_NODE_PATH);
runInAction(() => {
ResourceKeyUtils.forEach(key, async key => {
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
if (this.navTree.has(nodeId)) {
const connectionInfo = this.connectionInfo.get(key);
if (!connectionInfo?.connected) {
this.removeTree(nodeId);
} else {
this.navTree.markTreeOutdated(nodeId);
}
}
const nodeInfo = this.navNodeInfoResource.get(nodeId);
if (nodeInfo) {
this.navTree.markOutdated(nodeInfo.parentId);
}
});
});
}
private connectionRemoveHandler(key: ResourceKey<string>) {
runInAction(() => {
ResourceKeyUtils.forEach(key, key => {
const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
const node = this.getNode(navNodeId);
if (!node) {
return;
}
this.navTree.deleteInNode(node.parentId, [navNodeId]);
});
});
}
private async navigateHandler(
data: INodeNavigationData,
contexts: IExecutionContextProvider<INodeNavigationData>
@@ -6,8 +6,9 @@
* you may not use this file except in compliance with the License.
*/
import { action, computed, makeObservable } from 'mobx';
import { action, computed, makeObservable, runInAction } from 'mobx';
import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
import { SessionDataResource } from '@cloudbeaver/core-root';
@@ -25,7 +26,8 @@ import {
import { MetadataMap } from '@cloudbeaver/core-utils';
import { CoreSettingsService } from '../../CoreSettingsService';
import { NavNodeInfoResource } from './NavNodeInfoResource';
import { NavNodeInfoResource, ROOT_NODE_PATH } from './NavNodeInfoResource';
import { NodeManagerUtils } from './NodeManagerUtils';
// TODO: so much dirty
export interface NodePath {
@@ -51,14 +53,16 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
private graphQLService: GraphQLService,
private navNodeInfoResource: NavNodeInfoResource,
private coreSettingsService: CoreSettingsService,
private sessionDataResource: SessionDataResource
private sessionDataResource: SessionDataResource,
private connectionInfo: ConnectionInfoResource
) {
super();
makeObservable<NavTreeResource, 'setNavObject'>(this, {
makeObservable<NavTreeResource, 'setNavObject' | 'connectionRemoveHandler'>(this, {
childrenLimit: computed,
setDetails: action,
setNavObject: action,
connectionRemoveHandler: action.bound,
});
this.metadata = new MetadataMap<string, INodeMetadata>(() => ({
@@ -72,19 +76,22 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
this.onNodeRefresh = new Executor<string>(null, (a, b) => a === b);
this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource));
this.sessionDataResource.onDataUpdate.addPostHandler(() => this.markOutdated());
this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this));
this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler);
this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this));
}
async refreshTree(navNodeId: string): Promise<void> {
await this.graphQLService.sdk.navRefreshNode({
nodePath: navNodeId,
});
this.markTreeOutdated(navNodeId);
await this.markTreeOutdated(navNodeId);
await this.refresh(navNodeId);
await this.onNodeRefresh.execute(navNodeId);
}
markTreeOutdated(navNodeId: ResourceKey<string>): void {
this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
async markTreeOutdated(navNodeId: ResourceKey<string>): Promise<void> {
await this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
}
setDetails(keyObject: ResourceKey<string>, state: boolean): void {
@@ -179,7 +186,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
const allKeys = resourceKeyList(items);
this.markUpdated(allKeys);
this.onItemDelete.execute(allKeys);
this.navNodeInfoResource.delete(resourceKeyList(items.filter(navNodeId => navNodeId !== key)));
this.navNodeInfoResource.delete(ResourceKeyUtils.exclude(allKeys, key));
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, string[]>> {
@@ -216,6 +223,50 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
return nestedChildren;
}
private async connectionUpdateHandler(key: ResourceKey<string>) {
await this.markOutdated(ROOT_NODE_PATH);
ResourceKeyUtils.forEach(key, async key => {
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
if (this.has(nodeId)) {
const connectionInfo = this.connectionInfo.get(key);
if (!connectionInfo?.connected) {
this.delete(nodeId);
} else {
this.markTreeOutdated(nodeId);
}
}
const node = this.navNodeInfoResource.get(nodeId);
if (node) {
this.markOutdated(node.parentId);
}
});
}
private connectionRemoveHandler(key: ResourceKey<string>) {
runInAction(() => {
ResourceKeyUtils.forEach(key, key => {
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
const node = this.navNodeInfoResource.get(nodeId);
if (node) {
this.deleteInNode(node.parentId, [nodeId]);
}
});
});
}
private async connectionCreateHandler(connection: Connection) {
const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id);
await this.markOutdated(ROOT_NODE_PATH);
await this.markTreeOutdated(nodeId);
}
private setNavObject(data: NavNodeChildrenQuery | NavNodeChildrenQuery[]) {
if (Array.isArray(data)) {
for (const node of data) {
+1 -1
View File
@@ -13,7 +13,7 @@ import { use } from 'reshadow';
import { useStyles, composes, ComponentStyle } from '@cloudbeaver/core-theming';
import { Icon } from './Icons';
import { Icon } from './Icon';
import { Loader } from './Loader/Loader';
import { useObjectRef } from './useObjectRef';
@@ -24,6 +24,7 @@ export const BASE_CONTAINERS_STYLES = composes(
display: flex;
flex-direction: row;
align-content: baseline;
position: relative;
&[vertical] {
flex-direction: column;
@@ -129,5 +130,15 @@ export const BASE_CONTAINERS_STYLES = composes(
margin: 0;
text-transform: uppercase;
opacity: 0.9;
}
}
GroupClose {
width: 18px;
height: 18px;
cursor: pointer;
display: flex;
position: absolute;
right: 24px;
margin-right: 0 !important;
}
`);
@@ -0,0 +1,20 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { Icon } from '../Icon';
interface IProps {
onClick?: () => void;
}
export const GroupClose: React.FC<IProps & React.HTMLAttributes<HTMLDivElement>> = function GroupClose({
onClick,
...rest
}) {
return <close {...rest}><Icon name="cross" viewBox="0 0 16 16" onClick={onClick} /></close>;
};
@@ -20,8 +20,8 @@ import styled, { css, use } from 'reshadow';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { Icon } from '../Icon';
import { IconButton } from '../IconButton';
import { Icon } from '../Icons/Icon';
import { baseFormControlStyles } from './baseFormControlStyles';
import { FormContext } from './FormContext';
@@ -21,8 +21,8 @@ import styled, { css, use } from 'reshadow';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import { Icon } from '../Icon';
import { IconButton } from '../IconButton';
import { Icon } from '../Icons/Icon';
import { baseFormControlStylesNew } from './baseFormControlStylesNew';
import { FormContext } from './FormContext';
@@ -9,7 +9,7 @@
import { Button, ButtonProps } from 'reakit/Button';
import styled, { css } from 'reshadow';
import { Icon } from './Icons/Icon';
import { Icon } from './Icon';
const styles = css`
Button {
@@ -8,7 +8,7 @@
import { useMemo } from 'react';
import { Icon } from './Icons';
import { Icon } from './Icon';
import { StaticImage } from './StaticImage';
export interface IconOrImageProps {
@@ -1 +0,0 @@
export * from './Icon';
@@ -15,7 +15,7 @@ import styled, { css, use } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ShadowInput } from '../FormControls/ShadowInput';
import { Icon } from '../Icons';
import { Icon } from '../Icon';
import type { IProperty } from './IProperty';
import { PropertyValueSelector } from './PropertyValueSelector';
@@ -10,7 +10,7 @@ import styled, { css } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { Icon } from '../Icons';
import { Icon } from '../Icon';
interface Props {
className?: string;
@@ -12,7 +12,7 @@ import styled, { use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { Icon } from '../Icons/Icon';
import { Icon } from '../Icon';
import { TableContext } from './TableContext';
import { TableItemContext } from './TableItemContext';
@@ -14,7 +14,7 @@ import styled from 'reshadow';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { Icon } from '../../Icons';
import { Icon } from '../../Icon';
import { TabContext } from '../TabContext';
import type { TabProps } from './TabProps';
import { useTab } from './useTab';
@@ -51,7 +51,7 @@ interface Props extends ButtonHTMLAttributes<any> {
export const ToolsAction: React.FC<Props> = function ToolsAction({ icon, viewBox, children, ...rest }) {
return styled(useStyles(styles))(
<button {...rest}>
<button type='button' {...rest}>
{icon && <IconOrImage icon={icon} viewBox={viewBox} />}
{children && <button-label>{children}</button-label>}
</button>
+2 -1
View File
@@ -1,5 +1,5 @@
export * from './ComplexLoader';
export * from './Icons';
export * from './Icon';
export * from './ItemList/ItemList';
export * from './ItemList/ItemListSearch';
@@ -99,6 +99,7 @@ export * from './FormControls/Filter';
export * from './Containers/BASE_CONTAINERS_STYLES';
export * from './Containers/Container';
export * from './Containers/Group';
export * from './Containers/GroupClose';
export * from './Containers/GroupItem';
export * from './Containers/GroupTitle';
export * from './Containers/ColoredContainer';
+6 -2
View File
@@ -41,7 +41,9 @@ export function useMouse<T extends HTMLElement>(options: IOptions): IMouseHook<T
handlersRef.onMouseEnter(event);
}
state.mouseEnter = true;
if (!state.mouseEnter) {
state.mouseEnter = true;
}
};
const mouseOutHandler = (event: MouseEvent) => {
@@ -49,7 +51,9 @@ export function useMouse<T extends HTMLElement>(options: IOptions): IMouseHook<T
handlersRef.onMouseLeave(event);
}
state.mouseEnter = false;
if (state.mouseEnter) {
state.mouseEnter = false;
}
};
const element = reference.current;
@@ -101,6 +101,7 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
isChecked?: () => boolean;
type?: MenuItemType;
separator?: boolean;
keepMenuOpen?: boolean;
iconGetter?: () => string | undefined;
constructor(private options: IContextMenuItem<T>,
@@ -112,6 +113,7 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
this.icon = options.icon;
this.type = options.type;
this.separator = options.separator;
this.keepMenuOpen = options.keepMenuOpen;
this.iconGetter = options.iconGetter;
this.id = `${options.id}-${context.contextId!}`;
@@ -28,6 +28,7 @@ export interface IMenuItem {
onClick?: () => void; // it is not mandatory if it is just opens submenu
isDisabled?: boolean;
isHidden?: boolean;
keepMenuOpen?: boolean;
icon?: string; // path to icon or svg icon name
tooltip?: string;
panel?: IMenuPanel; // if menu has sub-items
@@ -42,5 +43,6 @@ export type MenuTriggerProps = PropsWithChildren<{
placement?: MenuInitialState['placement'];
modal?: boolean;
visible?: boolean;
rtl?: boolean;
onVisibleSwitch?: (visible: boolean) => void;
}> & Omit<ButtonHTMLAttributes<any>, 'style'>;
@@ -44,16 +44,16 @@ export const MenuPanelItem: React.FC<MenuPanelItemProps> = function MenuPanelIte
}
return styled(useStyles(menuPanelStyles, style))(
<menu-panel-item as="div" {...use({ separator })}>
<menu-item-content as='div'>
<menu-panel-item {...use({ separator })}>
<menu-item-content>
{icon ? (
<IconOrImage icon={icon} />
) : control}
</menu-item-content>
<menu-item-text as="div">
<menu-item-text>
<Translate token={title} />
</menu-item-text>
<menu-item-content as='div'>
<menu-item-content>
{panel && <Icon name="arrow" viewBox="0 0 16 16" />}
</menu-item-content>
</menu-panel-item>
@@ -36,10 +36,15 @@ export const MenuTrigger: React.FC<MenuTriggerProps> = function MenuTrigger({
visible,
onVisibleSwitch,
modal,
rtl,
...props
}) {
const propsRef = useObjectRef({ onVisibleSwitch, visible }, { onVisibleSwitch });
const menu = useMenuState({ modal, placement, visible });
const menu = useMenuState({ modal, placement, visible, rtl });
const handleItemClose = useCallback(() => {
menu.hide();
}, [menu.hide]);
useEffect(() => {
propsRef.onVisibleSwitch?.(menu.visible);
@@ -48,11 +53,9 @@ export const MenuTrigger: React.FC<MenuTriggerProps> = function MenuTrigger({
return styled(useStyles(menuPanelStyles, ...style))(
<>
<MenuButton {...menu} {...props}>
<box as='div'>
{children}
</box>
<box>{children}</box>
</MenuButton>
<MenuPanel panel={panel} menu={menu} style={style} />
<MenuPanel panel={panel} menu={menu} style={style} rtl={rtl} onItemClose={handleItemClose} />
</>
);
};
@@ -64,12 +67,16 @@ export const MenuTrigger: React.FC<MenuTriggerProps> = function MenuTrigger({
interface MenuPanelProps {
panel: IMenuPanel;
menu: MenuStateReturn; // from reakit useMenuState
onItemClose?: () => void;
rtl?: boolean;
style?: Style[];
}
const MenuPanel = observer(function MenuPanel({
panel,
menu,
rtl,
onItemClose,
style = [],
}: MenuPanelProps) {
const styles = useStyles(menuPanelStyles, ...style);
@@ -79,10 +86,12 @@ const MenuPanel = observer(function MenuPanel({
}
return styled(styles)(
<Menu {...menu} aria-label={panel.id} modal>
{panel.menuItems.map(item => (
<MenuPanelElement key={item.id} item={item} menu={menu} style={style} />
))}
<Menu {...menu} aria-label={panel.id}>
<menu-box dir={rtl ? 'rtl' : undefined}>
{panel.menuItems.map(item => (
<MenuPanelElement key={item.id} item={item} menu={menu} style={style} onItemClose={onItemClose} />
))}
</menu-box>
</Menu>
);
});
@@ -94,21 +103,22 @@ const MenuPanel = observer(function MenuPanel({
type MenuPanelElementProps = Omit<React.ButtonHTMLAttributes<any>, 'style'> & {
item: IMenuItem;
menu: MenuStateReturn; // from reakit useMenuState
onItemClose?: () => void;
style?: Style[];
};
const MenuPanelElement = observer(function MenuPanelElement({
item, menu, style = [],
item, menu, onItemClose, style = [],
}: MenuPanelElementProps) {
const styles = useStyles(menuPanelStyles, ...style);
const onClick = useCallback(() => {
if (item.onClick) {
item.onClick();
}
if (!item.panel) {
menu.hide();
if (!item.keepMenuOpen && !item.panel) {
onItemClose?.();
}
}, [item, menu]);
}, [item, menu, onItemClose]);
if (item.panel) {
return styled(styles)(
@@ -119,6 +129,7 @@ const MenuPanelElement = observer(function MenuPanelElement({
disabled={item.isDisabled}
menuItem={item}
style={style}
onItemClose={onItemClose}
onClick={onClick}
{...{ as: MenuInnerTrigger }}
/>
@@ -180,6 +191,7 @@ const MenuPanelElement = observer(function MenuPanelElement({
type MenuInnerTriggerProps = Omit<React.ButtonHTMLAttributes<any>, 'style'> & {
menuItem: IMenuItem;
onItemClose?: () => void;
style?: Style[];
};
@@ -193,14 +205,19 @@ export const MenuInnerTrigger = forwardRef(function MenuInnerTrigger(
const menu = useMenuState();
const panel = useObserver(() => menuItem.panel);
const handleItemClose = useCallback(() => {
menu.hide();
props.onItemClose?.();
}, [menu.hide, props.onItemClose]);
return styled(useStyles(menuPanelStyles, ...style))(
<>
<MenuButton ref={ref} {...menu} {...rest}>
<box as='div'>
<box>
<MenuPanelItem menuItem={menuItem} style={style} />
</box>
</MenuButton>
<MenuPanel panel={panel!} menu={menu} style={style} />
<MenuPanel panel={panel!} menu={menu} style={style} onItemClose={handleItemClose} />
</>
);
});
@@ -12,7 +12,7 @@ import { composes } from '@cloudbeaver/core-theming';
export const menuPanelStyles = composes(
css`
Menu {
menu-box {
composes: theme-background-surface theme-text-on-surface from global;
}
MenuItem,
@@ -43,16 +43,17 @@ export const menuPanelStyles = composes(
}
}
Menu {
outline: none;
z-index: 999;
}
menu-box {
composes: theme-typography--body2 theme-elevation-z5 from global;
position: absolute;
display: flex;
min-width: 140px;
flex-direction: column;
padding: 12px 0;
outline: none;
z-index: 999;
& Menu {
& menu-box {
margin-top: -12px;
}
}
@@ -26,6 +26,7 @@ export class ComputedMenuItemModel implements IMenuItem {
panel?: IMenuPanel;
type?: MenuItemType;
separator?: boolean;
keepMenuOpen?: boolean;
rtl?: boolean;
get title(): TLocalizationToken {
@@ -73,6 +74,7 @@ export class ComputedMenuItemModel implements IMenuItem {
this.id = options.id;
this.type = options.type;
this.keepMenuOpen = options.keepMenuOpen;
this.separator = options.separator;
this.rtl = options.rtl;
this.panel = options.panel;
@@ -26,6 +26,7 @@ export interface IMenuItemOptions {
tooltipGetter?: () => TLocalizationToken | undefined;
order?: number;
isPanel?: boolean;
keepMenuOpen?: boolean;
panel?: IMenuPanel;
type?: MenuItemType;
separator?: boolean;
@@ -10,8 +10,8 @@ import { action, observable, makeObservable } from 'mobx';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { SessionService, ServerService } from '@cloudbeaver/core-root';
import { GraphQLService, ServerLanguage } from '@cloudbeaver/core-sdk';
import { ServerService, SessionResource, SessionDataResource } from '@cloudbeaver/core-root';
import type { ServerLanguage } from '@cloudbeaver/core-sdk';
import { SettingsService } from '@cloudbeaver/core-settings';
import type { ILocaleProvider } from './ILocaleProvider';
@@ -37,9 +37,9 @@ export class LocalizationService extends Bootstrap {
constructor(
private notificationService: NotificationService,
private sessionService: SessionService,
private sessionResource: SessionResource,
sessionDataResource: SessionDataResource,
private serverService: ServerService,
private graphQLService: GraphQLService,
private settingsService: SettingsService
) {
super();
@@ -49,6 +49,8 @@ export class LocalizationService extends Bootstrap {
localeMap: observable.shallow,
setCurrentLocale: action,
});
sessionDataResource.onDataUpdate.addHandler(this.syncLanguage.bind(this));
}
addProvider(provider: ILocaleProvider): void {
@@ -78,20 +80,12 @@ export class LocalizationService extends Bootstrap {
register(): void | Promise<void> {
this.addProvider(this.coreProvider.bind(this));
this.settingsService.registerSettings(this.settings, LANG_SETTINGS_KEY); // overwrite default value with settings
}
async load(): Promise<void> {
const session = await this.sessionService.session.load();
await this.loadLocaleAsync(DEFAULT_LOCALE_NAME);
if (!session) {
return;
}
this.settingsService.registerSettings(this.settings, LANG_SETTINGS_KEY); // overwrite default value with settings
this.setCurrentLocale(session.locale); // session language wins
await this.setLocale(this.getCurrentLanguage());
await this.loadLocaleAsync(this.settings.language);
}
getCurrentLanguage(): string {
@@ -102,10 +96,16 @@ export class LocalizationService extends Bootstrap {
if (key === this.settings.language) {
return;
}
const response = await this.graphQLService.sdk.changeSessionLanguage({ locale: key });
this.setLocale(key);
if (response.changeSessionLanguage) {
window.location.reload();
await this.sessionResource.changeLanguage(key);
// window.location.reload();
}
private async syncLanguage() {
await this.sessionResource.refreshSilent(); // TODO: remove
const session = this.sessionResource.data;
if (session) {
await this.setLocale(session.locale);
}
}
@@ -126,13 +126,15 @@ export class LocalizationService extends Bootstrap {
const config = await this.serverService.config.load();
if (!config) {
throw new Error('Cant\'t get server settings');
throw new Error('Can\'t get server settings');
}
if (!config.supportedLanguages.some(lang => lang.isoCode === key)) {
this.setCurrentLocale(config!.supportedLanguages[0]!.isoCode);
throw new Error(`Language '${key}' is not supported`);
}
this.setCurrentLocale(key);
await this.loadLocaleAsync(key);
}
@@ -28,6 +28,22 @@ export class SessionResource extends CachedDataResource<SessionState | null, voi
this.sync(serverConfiguration);
}
async refreshSilent(): Promise<void> {
const { session } = await this.graphQLService.sdk.openSession();
this.data = session;
}
async changeLanguage(locale: string): Promise<void> {
await this.performUpdate(undefined, undefined, async () => {
await this.graphQLService.sdk.changeSessionLanguage({ locale });
if (this.data) {
this.data.locale = locale;
}
});
}
protected async loader(): Promise<SessionState> {
const { session } = await this.graphQLService.sdk.openSession();
@@ -46,6 +46,7 @@ export interface ResourceKeyUtils {
every: <TKey>(key: ResourceKey<TKey>, predicate: (key: TKey, index: number) => boolean) => boolean;
map: MapFnc;
includes: <TKey>(first: ResourceKey<TKey>, second: ResourceKey<TKey>) => boolean;
exclude: <TKey>(first: ResourceKeyList<TKey>, second: ResourceKey<TKey>) => ResourceKey<TKey>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
@@ -142,6 +143,14 @@ export const ResourceKeyUtils: ResourceKeyUtils = {
return param === key;
},
exclude<TKey>(param: ResourceKeyList<TKey>, key: ResourceKey<TKey>): ResourceKey<TKey> {
if (isResourceKeyList(key)) {
return resourceKeyList(param.list.filter(param => !key.list.includes(param)), param.mark);
}
return resourceKeyList(param.list.filter(param => param !== key), param.mark);
},
};
export function isResourceKeyList<T>(data: any): data is ResourceKeyList<T> {
@@ -3,6 +3,7 @@ query serverConfig {
name
version
workspaceId
serverURL
productConfiguration
supportsCustomConnections
supportsConnectionBrowser
+4 -1
View File
@@ -516,6 +516,7 @@ export interface ServerConfig {
name: Scalars['String'];
version: Scalars['String'];
workspaceId: Scalars['ID'];
serverURL: Scalars['String'];
anonymousAccessEnabled?: Maybe<Scalars['Boolean']>;
/** @deprecated Field no longer supported */
authenticationEnabled?: Maybe<Scalars['Boolean']>;
@@ -952,6 +953,7 @@ export interface AdminPermissionInfo {
export interface ServerConfigInput {
serverName?: Maybe<Scalars['String']>;
serverURL?: Maybe<Scalars['String']>;
adminName?: Maybe<Scalars['String']>;
adminPassword?: Maybe<Scalars['String']>;
anonymousAccessEnabled?: Maybe<Scalars['Boolean']>;
@@ -1678,7 +1680,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
export interface ServerConfigQuery {
serverConfig: (
Pick<ServerConfig, 'name' | 'version' | 'workspaceId' | 'productConfiguration' | 'supportsCustomConnections' | 'supportsConnectionBrowser' | 'supportsWorkspaces' | 'sessionExpireTime' | 'anonymousAccessEnabled' | 'adminCredentialsSaveEnabled' | 'publicCredentialsSaveEnabled' | 'licenseRequired' | 'licenseValid' | 'configurationMode' | 'developmentMode' | 'enabledAuthProviders'>
Pick<ServerConfig, 'name' | 'version' | 'workspaceId' | 'serverURL' | 'productConfiguration' | 'supportsCustomConnections' | 'supportsConnectionBrowser' | 'supportsWorkspaces' | 'sessionExpireTime' | 'anonymousAccessEnabled' | 'adminCredentialsSaveEnabled' | 'publicCredentialsSaveEnabled' | 'licenseRequired' | 'licenseValid' | 'configurationMode' | 'developmentMode' | 'enabledAuthProviders'>
& { supportedLanguages: Array<Pick<ServerLanguage, 'isoCode' | 'displayName' | 'nativeName'>>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick<ProductInfo, 'id' | 'version' | 'name' | 'description' | 'buildTime' | 'releaseTime' | 'licenseInfo'> }
);
}
@@ -2691,6 +2693,7 @@ export const ServerConfigDocument = `
name
version
workspaceId
serverURL
productConfiguration
supportsCustomConnections
supportsConnectionBrowser
@@ -37,6 +37,17 @@ export const ServerConfigurationInfoForm: React.FC<Props> = observer(function Se
>
{translate('administration_configuration_wizard_configuration_server_name')}
</InputFieldNew>
<InputFieldNew
title={translate('administration_configuration_wizard_configuration_server_url_description')}
type="url"
name="serverURL"
state={state.serverConfig}
mod='surface'
required
tiny
>
{translate('administration_configuration_wizard_configuration_server_url')}
</InputFieldNew>
<InputFieldNew
title={translate('administration_configuration_wizard_configuration_server_session_lifetime_description')}
type="number"
@@ -161,6 +161,12 @@ export class ServerConfigurationService {
}
data.state.serverConfig.serverName = config.name || config.productInfo.name;
data.state.serverConfig.serverURL = config.serverURL;
if (this.administrationScreenService.isConfigurationMode) {
data.state.serverConfig.serverURL = window.location.origin;
}
data.state.serverConfig.sessionExpireTime = config.sessionExpireTime;
data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled;
@@ -18,6 +18,8 @@ export default [
['administration_configuration_wizard_configuration_server_info', 'Server Information'],
['administration_configuration_wizard_configuration_server_name', 'Server Name'],
['administration_configuration_wizard_configuration_server_url', 'Server URL'],
['administration_configuration_wizard_configuration_server_url_description', 'Global access server url'],
['administration_configuration_wizard_configuration_server_info_unsaved_title', 'Unsaved settings'],
['administration_configuration_wizard_configuration_server_info_unsaved_message', 'Settings can be saved on Server configuration page'],
['administration_configuration_wizard_configuration_server_info_unsaved_navigate', 'Open'],
@@ -53,7 +53,7 @@ export const UserInfo = observer(function UserInfo() {
}
return styled(style)(
<MenuTrigger panel={panel} style={[topMenuStyles]} disabled={hidden.get()}>
<MenuTrigger panel={panel} style={[topMenuStyles]} disabled={hidden.get()} modal>
<user>
<user-icon>
<IconOrImage icon='user' viewBox='0 0 28 28' />
@@ -6,8 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { ComplexLoader, createComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
@@ -22,7 +20,7 @@ const loader = createComplexLoader(async function loader() {
return { DataGridTable };
});
export const DataGridLoader: React.FC<Props> = observer(function DataGridLoader({
export const DataGridLoader: React.FC<Props> = function DataGridLoader({
tableModel, resultIndex, className,
}) {
return (
@@ -35,4 +33,4 @@ export const DataGridLoader: React.FC<Props> = observer(function DataGridLoader(
)}
</ComplexLoader>
);
});
};
@@ -58,6 +58,7 @@ export const CellMenu: React.FC<Props> = observer(function TreeNodeMenu({
<MenuTrigger
panel={panel}
style={[cellMenuStyles]}
modal
onClick={() => dataGridContextMenuService.openMenu(model, resultIndex, row, column)}
onVisibleSwitch={onStateSwitch}
>
@@ -86,6 +86,7 @@ export const TableFooterMenuItem = observer(function TableFooterMenuItem({
panel={menuItem.panel}
disabled={menuItem.isDisabled}
style={[tableFooterMenuStyles]}
modal
>
{menuItem.icon && (
<menu-trigger-icon>
@@ -18,7 +18,6 @@ import { VirtualFolderTabMixin } from './VirtualFolderTabMixin';
const style = css`
tab-wrapper {
position: relative;
display: flex;
width: 100%;
flex: 1 0 auto;
}
@@ -32,7 +31,7 @@ export const VirtualFolderTab = observer(function VirtualFolderTab() {
}
return styled(style)(
<tab-wrapper as="div">
<tab-wrapper>
<ObjectChildrenPropertyTable nodeIds={folderMixin.getChildrenId()} />
</tab-wrapper>
);