diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java index 389d994751..9ea63c2d8b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java @@ -56,7 +56,7 @@ public class VirtualProjectImpl extends BaseProjectImpl { @NotNull @Override public String getName() { - return project.getName(); + return project.getDisplayName(); } @NotNull diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 80d89ceeb2..f461124595 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -438,7 +438,8 @@ public class LocalResourceController implements RMController { public String setResourceContents( @NotNull String projectId, @NotNull String resourcePath, - @NotNull byte[] data) throws DBException + @NotNull byte[] data, + boolean forceOverwrite) throws DBException { validateResourcePath(resourcePath); Number fileSizeLimit = WebAppUtils.getWebApplication() @@ -452,6 +453,9 @@ public class LocalResourceController implements RMController { data.length); } Path targetPath = getTargetPath(projectId, resourcePath); + if (!forceOverwrite && Files.exists(targetPath)) { + throw new DBException("Resource '" + resourcePath + "' exists"); + } if (!Files.exists(targetPath.getParent())) { throw new DBException("Parent folder '" + targetPath.getParent().getFileName() + "' doesn't exist"); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index d2ce091a70..1d5fc1a711 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -493,8 +493,8 @@ public class WebSession extends AbstractSessionPersistent implements SMSession, return locale; } - public void setLocale(String locale) { - this.locale = locale; + public void setLocale(@Nullable String locale) { + this.locale = locale != null ? locale : Locale.getDefault().getLanguage(); } public DBNModel getNavigatorModel() { diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 3c1d6752a4..93a4bb342e 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -22,7 +22,7 @@ - + diff --git a/server/bundles/io.cloudbeaver.service.admin/plugin.xml b/server/bundles/io.cloudbeaver.service.admin/plugin.xml index 670cd647b6..c148923dc3 100644 --- a/server/bundles/io.cloudbeaver.service.admin/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.admin/plugin.xml @@ -5,7 +5,7 @@ - + diff --git a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls index 64cd93c62d..1746a11e6e 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -57,7 +57,8 @@ extend type Mutation { rmWriteResourceStringContent( projectId: String!, resourcePath: String!, - data: String!): String! + data: String!, + forceOverwrite: Boolean!): String! rmCreateProject( projectName: String!, diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java index 5cd13bf3c4..9c0f7805c5 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java @@ -80,7 +80,8 @@ public interface DBWServiceRM extends DBWService { @NotNull WebSession webSession, @NotNull @WebObjectId String projectId, @NotNull String resourcePath, - @NotNull String data) throws DBException; + @NotNull String data, + boolean forceOverwrite) throws DBException; @WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN}) WebProjectInfo createProject( diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java index 37554a7734..f1175d08ad 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java @@ -64,7 +64,8 @@ public class WebServiceBindingRM extends WebServiceBindingBase { env -> getService(env).writeResourceStringContent(getWebSession(env), env.getArgument("projectId"), env.getArgument("resourcePath"), - env.getArgument("data"))) + env.getArgument("data"), + env.getArgument("forceOverwrite"))) .dataFetcher("rmCreateProject", env -> getService(env).createProject( getWebSession(env), diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 414fcac1ae..92ef72c058 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -89,11 +89,17 @@ public class WebServiceRM implements DBWServiceRM { @NotNull @Override - public String writeResourceStringContent(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath, @NotNull String data) throws DBException { + public String writeResourceStringContent( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String resourcePath, + @NotNull String data, + boolean forceOverwrite + ) throws DBException { checkIsRmEnabled(webSession); try { byte[] bytes = data.getBytes(StandardCharsets.UTF_8); - return getResourceController(webSession).setResourceContents(projectId, resourcePath, bytes); + return getResourceController(webSession).setResourceContents(projectId, resourcePath, bytes, forceOverwrite); } catch (Exception e) { throw new DBWebException("Error writing resource '" + resourcePath + "' data", e); } diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index 22573dad98..60b470f15f 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -22,5 +22,6 @@ Require-Bundle: org.eclipse.core.runtime, io.cloudbeaver.server, io.cloudbeaver.resources.drivers.base, io.cloudbeaver.product.ce, - io.cloudbeaver.service.auth + io.cloudbeaver.service.auth, + io.cloudbeaver.service.rm Export-Package: io.cloudbeaver.test.platform diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index c4a36f14b3..a0b6874783 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -29,7 +29,7 @@ import java.net.http.HttpClient; import java.nio.file.Path; @RunWith(Suite.class) -@Suite.SuiteClasses({PlatformTest.class, AuthenticationTest.class }) +@Suite.SuiteClasses({PlatformTest.class, AuthenticationTest.class, ResourceManagerTest.class }) public class CEServerTestSuite { public static final String GQL_API_URL = "http://localhost:18978/api/gql"; diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java new file mode 100644 index 0000000000..28e12ff90c --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java @@ -0,0 +1,77 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.test.platform; + +import io.cloudbeaver.model.rm.local.LocalResourceController; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.WebTestUtils; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.CommonUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.net.CookieManager; +import java.net.http.HttpClient; +import java.util.Map; + +public class ResourceManagerTest { + + public static final String GQL_TEMPLATE_RM_WRITE_RESOURCE = "rmWriteResource.json"; + public static final String GQL_TEMPLATE_RM_DELETE_RESOURCE = "navDeleteNode.json"; + + @Test + public void createDeleteResourceTest() throws Exception { + Assert.assertTrue(CBApplication.getInstance().getAppConfiguration().isResourceManagerEnabled()); + HttpClient client = HttpClient.newBuilder() + .cookieHandler(new CookieManager()) + .version(HttpClient.Version.HTTP_2) + .build(); + Map authInfo = WebTestUtils.authenticateUser( + client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + Assert.assertTrue(createResource(client, false)); + Assert.assertFalse(createResource(client, false)); + Assert.assertTrue(createResource(client, true)); + Assert.assertEquals(1, deleteResource(client)); + } + + + private boolean createResource(HttpClient client, boolean forceOverwrite) throws Exception { + String input = WebTestUtils.readScriptTemplate( + GQL_TEMPLATE_RM_WRITE_RESOURCE, CEServerTestSuite.getScriptsPath() + ).replaceAll("\\{forceOverwrite}", CommonUtils.toString(forceOverwrite)); + Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); + Map data = JSONUtils.getObjectOrNull(map, "data"); + if (data != null) { + return LocalResourceController.DEFAULT_CHANGE_ID.equals(JSONUtils.getString(data, "rmWriteResourceStringContent")); + } + return false; + } + + private int deleteResource(HttpClient client) throws Exception { + String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_RM_DELETE_RESOURCE, CEServerTestSuite.getScriptsPath()); + Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); + Map data = JSONUtils.getObjectOrNull(map, "data"); + if (data != null) { + return JSONUtils.getInteger(data, "navDeleteNodes"); + } + return -1; + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json new file mode 100644 index 0000000000..7302133c3d --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json @@ -0,0 +1,9 @@ +{ + "query": "\n mutation navDeleteNodes($nodePaths: [ID!]!) {\n navDeleteNodes(nodePaths: $nodePaths)\n}\n ", + "variables": { + "nodePaths": [ + "ext://resources/u_test/testScript.sql" + ] + }, + "operationName": "navDeleteNodes" +} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json new file mode 100644 index 0000000000..1652331d3b --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json @@ -0,0 +1,10 @@ +{ + "query": "\n mutation writeContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) {\n rmWriteResourceStringContent(\n projectId: $projectId\n resourcePath: $resourcePath\n data: $data\n forceOverwrite: $forceOverwrite\n )\n}\n ", + "variables": { + "projectId": "u_test", + "resourcePath": "testScript.sql", + "data": "TEST SCRIPT;", + "forceOverwrite": {forceOverwrite} + }, + "operationName": "writeContent" +} \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts b/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts index 83cecc2e83..a85a940a6a 100644 --- a/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts +++ b/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts @@ -11,7 +11,7 @@ export function isControlPresented( ): boolean { if (state !== undefined && name !== undefined) { if (name in state) { - return state[name] !== null; + return state[name] !== null && state[name] !== undefined; } return defaultValue !== undefined; } diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index d1a82fb03c..49834b769d 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -231,6 +231,7 @@ export const RenderField = observer(function RenderField({ ); } + return ( ; @@ -3907,11 +3909,12 @@ export const ReadResourceDocument = ` } `; export const WriteResourceContentDocument = ` - mutation writeResourceContent($projectId: String!, $resourcePath: String!, $data: String!) { + mutation writeResourceContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) { rmWriteResourceStringContent( projectId: $projectId resourcePath: $resourcePath data: $data + forceOverwrite: $forceOverwrite ) } `; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Roles/Options/RoleOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Roles/Options/RoleOptions.tsx index 4609f18446..9fd4b20a74 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Roles/Options/RoleOptions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Roles/Options/RoleOptions.tsx @@ -78,15 +78,22 @@ export const RoleOptions: TabContainerPanelComponent = observer( {translate('administration_roles_role_permissions')} {permissionsResource.resource.values.map(permission => { - let label = permission.id; - let caption: string | undefined; + const label = permission.label ?? permission.id; + let caption = ''; + + if (permission.description) { + caption = permission.description; + } else if (permission.label) { + caption = `(${permission.id})`; + } + + let tooltip = `${permission.id}`; + if (permission.label) { - label = `${permission.label}`; - caption = permission.id; + tooltip = permission.label + ` (${permission.id})`; } - const tooltip = `${label}${permission.description ? '\n' + permission.description : ''}`; return ( = observer( readOnly={state.readonly} disabled={state.disabled} > - {label} {caption && ({caption})} + {label} + {caption ? {caption} : null} ); })} diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index f41dcce11c..ece75f25a5 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -23,11 +23,12 @@ import { AuthDialogService } from './Dialog/AuthDialogService'; import type { IAuthOptions } from './IAuthOptions'; import { isAutoLoginSessionAction } from './isAutoLoginSessionAction'; -export type LogoutEventType = 'before' | 'after'; +type AuthEventType = 'before' | 'after'; @injectable() export class AuthenticationService extends Bootstrap { - readonly onLogout: Executor; + readonly onLogout: Executor; + readonly onLogin: Executor; configureAuthProvider: (() => void) | null; configureIdentityProvider: (() => void) | null; @@ -52,6 +53,7 @@ export class AuthenticationService extends Bootstrap { super(); this.onLogout = new Executor(); + this.onLogin = new Executor(); this.authPromise = null; this.configureAuthProvider = null; @@ -131,13 +133,24 @@ export class AuthenticationService extends Bootstrap { } private async auth(persistent: boolean, options: IAuthOptions) { + const contexts = await this.onLogin.execute('before'); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + if (this.authPromise) { - return this.authPromise; + await this.authPromise; + return; } options = observable(options); - this.authPromise = this.authDialogService.showLoginForm(persistent, options); + this.authPromise = this.authDialogService.showLoginForm(persistent, options) + .then(async state => { + await this.onLogin.execute('after'); + return state; + }); if (this.serverConfigResource.redirectOnFederatedAuth) { await this.authProvidersResource.loadAll(); @@ -158,7 +171,7 @@ export class AuthenticationService extends Bootstrap { } try { - return await this.authPromise; + await this.authPromise; } finally { this.authPromise = null; } diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index 85ab02e640..1798fe6101 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -13,13 +13,11 @@ import { DATA_CONTEXT_MENU, MenuBaseItem, MenuService } from '@cloudbeaver/core- import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; import { AuthenticationService } from './AuthenticationService'; -import { AuthDialogService } from './Dialog/AuthDialogService'; @injectable() export class PluginBootstrap extends Bootstrap { constructor( private readonly serverConfigResource: ServerConfigResource, - private readonly authDialogService: AuthDialogService, private readonly authenticationService: AuthenticationService, private readonly authInfoService: AuthInfoService, private readonly menuService: MenuService, @@ -40,7 +38,7 @@ export class PluginBootstrap extends Bootstrap { label: 'authentication_login', tooltip: 'authentication_login', }, - { onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, linkUser: false }) } + { onSelect: () => this.authenticationService.authUser(null, false) } ), ]; } @@ -54,7 +52,7 @@ export class PluginBootstrap extends Bootstrap { label: 'authentication_logout', tooltip: 'authentication_logout', }, - { onSelect: this.authenticationService.logout.bind(this.authenticationService) } + { onSelect: () => this.authenticationService.logout() } ), ]; } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts index 6be41db8f9..e3abcbcc2a 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts @@ -152,6 +152,7 @@ export class ConnectionFormState implements IConnectionFormState { .next(this.formStateTask); makeObservable(this, { + projectId: observable, mode: observable, type: observable, config: observable, @@ -208,12 +209,17 @@ export class ConnectionFormState implements IConnectionFormState { } setConfig(projectId: string, config: ConnectionConfig): this { - this.projectId = projectId; + this.setProject(projectId); this.config = config; this.reset(); return this; } + setProject(projectId: string): this { + this.projectId = projectId; + return this; + } + setAvailableDrivers(drivers: string[]): this { this._availableDrivers = drivers; this.reset(); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts index 7d2d79df06..f96456c19c 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts @@ -46,6 +46,7 @@ export interface IConnectionFormState { type: ConnectionFormType ) => this; readonly setConfig: (projectId: string, config: ConnectionConfig) => this; + readonly setProject: (projectId: string) => this; readonly setAvailableDrivers: (drivers: string[]) => this; readonly save: () => Promise; readonly test: () => Promise; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index 9147c6633e..54528cbd67 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -11,6 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx'; import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils'; @@ -25,6 +26,7 @@ import { Options } from './Options'; @injectable() export class ConnectionOptionsTabService extends Bootstrap { constructor( + private readonly projectsResource: ProjectsResource, private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, @@ -120,14 +122,22 @@ export class ConnectionOptionsTabService extends Bootstrap { const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); if (!state.config.name?.length) { - validation.error("Field 'name' can't be empty"); + validation.error('plugin_connections_connection_form_name_invalid'); } if (state.config.driverId && state.config.configurationType) { const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); if (!driver.configurationTypes.includes(state.config.configurationType)) { - validation.error('Configuration is not supported'); + validation.error('plugin_connections_connection_form_host_configuration_invalid'); + } + } + + if (state.projectId !== null && state.mode === 'create') { + const project = this.projectsResource.get(state.projectId); + + if (!project?.canCreateConnections) { + validation.error('plugin_connections_connection_form_project_invalid'); } } @@ -226,7 +236,11 @@ export class ConnectionOptionsTabService extends Bootstrap { } tempConfig.description = state.config.description; - tempConfig.template = state.config.template; + + if (state.projectId === PROJECT_GLOBAL_ID) { + tempConfig.template = state.config.template; + } + tempConfig.driverId = state.config.driverId; if (!state.config.template && state.config.folder) { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 3f0ae0b60e..ba10f04288 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -34,9 +34,9 @@ import { import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import { Project, ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { usePermission } from '@cloudbeaver/core-root'; -import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { CachedMapAllKey, CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { useAuthenticationAction } from '@cloudbeaver/core-ui'; @@ -116,6 +116,17 @@ export const Options: TabContainerPanelComponent = observe optionsHook.setAuthModel(model); }, []); + const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey); + const projects = projectsLoader.data as Project[]; + + function handleProjectSelect(projectId: string) { + const project = projectsLoader.resource.get(projectId); + + if (project?.canCreateConnections) { + state.setProject(projectId); + } + } + const driverMap = useMapResource( Options, DBDriverResource, @@ -293,6 +304,24 @@ export const Options: TabContainerPanelComponent = observe > {translate('connections_connection_name')} + project.id} + valueSelector={project => project.name} + titleSelector={project => project.description} + isDisabled={project => !project.canCreateConnections} + searchable={projects.length > 10} + readOnly={readonly || edit || projects.length < 2} + disabled={disabled} + loading={projectsLoader.isLoading()} + tiny + fill + onSelect={handleProjectSelect} + > + {translate('connections_connection_project')} + {!config.template && ( = observe disabled={disabled} autoComplete={`section-${config.driverId || 'driver'} section-folder`} mod='surface' + autoHide readOnly tiny fill diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index 6c15a4cf04..c53dc58370 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -13,9 +13,9 @@ import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { SessionDataResource } from '@cloudbeaver/core-root'; import type { ConnectionConfig, ResourceKey } from '@cloudbeaver/core-sdk'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; import { ConnectionAuthService } from '../ConnectionAuthService'; import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService'; @@ -36,14 +36,29 @@ export class PublicConnectionFormService { private readonly connectionFormService: ConnectionFormService, private readonly connectionInfoResource: ConnectionInfoResource, private readonly connectionAuthService: ConnectionAuthService, - private readonly sessionDataResource: SessionDataResource + private readonly authenticationService: AuthenticationService, ) { this.formState = null; this.optionsPanelService.closeTask.addHandler(this.closeHandler); this.connectionInfoResource.onDataUpdate.addPostHandler(this.closeRemoved); this.connectionInfoResource.onItemDelete.addPostHandler(this.closeDeleted); - this.sessionDataResource.onDataOutdated.addHandler(() => { - this.close(true); + + this.authenticationService.onLogin.addHandler(async (event, context) => { + if (event === 'before') { + const confirmed = await this.showUnsavedChangesDialog(); + if (!confirmed) { + ExecutorInterrupter.interrupt(context); + } + } + }); + + this.authenticationService.onLogout.addHandler(async (event, context) => { + if (event === 'before') { + const confirmed = await this.close(false); + if (!confirmed) { + ExecutorInterrupter.interrupt(context); + } + } }); makeObservable(this, { @@ -87,9 +102,9 @@ export class PublicConnectionFormService { return state; } - async close(saved?: boolean): Promise { + async close(saved?: boolean): Promise { if (!this.formState) { - return; + return true; } if (saved) { @@ -101,6 +116,8 @@ export class PublicConnectionFormService { if (state) { this.clearFormState(); } + + return state; } async save(): Promise { @@ -147,6 +164,14 @@ export class PublicConnectionFormService { }; private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + const confirmed = await this.showUnsavedChangesDialog(); + + if (!confirmed) { + ExecutorInterrupter.interrupt(contexts); + } + }; + + private async showUnsavedChangesDialog(): Promise { if ( !this.formState || !this.optionsPanelService.isOpen(formGetter) @@ -159,13 +184,13 @@ export class PublicConnectionFormService { )) ) ) { - return; + return true; } const state = await this.formState.checkFormState(); if (!state?.edited) { - return; + return true; } const result = await this.commonDialogService.open(ConfirmationDialog, { @@ -174,10 +199,8 @@ export class PublicConnectionFormService { confirmActionText: 'ui_processing_ok', }); - if (result === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } - }; + return result !== DialogueStateResult.Rejected; + } private async tryReconnect(connectionKey: IConnectionInfoParams) { const result = await this.commonDialogService.open(ConfirmationDialog, { diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 35e4d191d4..8f5395b2dd 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -7,5 +7,8 @@ export default [ ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], + ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], + ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], + ['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index a250f28a29..677ddf1984 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -7,5 +7,8 @@ export default [ ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], + ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], + ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], + ['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 7595609214..cee3925c3d 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -7,5 +7,8 @@ export default [ ['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'], ['connections_public_connection_folder_move_failed', 'Ошибка перемещения в папку'], ['connections_public_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({args.name})'], + ['plugin_connections_connection_form_project_invalid', 'У вас нет разрешения создавать коннекшены в выбранном проекте'], + ['plugin_connections_connection_form_host_configuration_invalid', 'Конфигурация хоста не поддерживается'], + ['plugin_connections_connection_form_name_invalid', 'Поле \'Название\' не может быть пустым'], ['connections_public_connection_folder_delete_confirmation', 'Вы удаляете "{args.name}". Подключения не будут удалены. Вы уверены?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index 50053ac465..51abab77bc 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -7,5 +7,8 @@ export default [ ['connections_public_connection_edit_reconnect_failed', '重新连接失败'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], + ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], + ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], + ['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ]; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx index 19a613b5f7..6108efb502 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx @@ -102,6 +102,7 @@ export const ElementsTree = observer(function ElementsTree({ navNodeFilterCompare, filters = [], renderers = [], + expandStateGetters, style, className, getChildren, @@ -161,7 +162,7 @@ export const ElementsTree = observer(function ElementsTree({ }, [folderExplorer]); - const children = useMapResource(ElementsTree, navTreeResource, root, { + useMapResource(ElementsTree, navTreeResource, root, { onLoad: async resource => { let fullPath = folderExplorer.state.fullPath; const preload = await resource.preloadNodeParents(fullPath); @@ -199,6 +200,7 @@ export const ElementsTree = observer(function ElementsTree({ localState, filters: [nameFilter, ...filters, limitFilter], renderers: [...renderers, elementsTreeLimitRenderer], + expandStateGetters, getChildren, loadChildren, isGroup, @@ -279,7 +281,8 @@ export const ElementsTree = observer(function ElementsTree({ } }); - const hasChildren = (children.data?.length || 0) > 0; + const children = tree.getNodeChildren(root); + const hasChildren = children.length > 0; const loaderAvailable = !foldersTree || context.folderExplorer.root === root; return styled(useStyles(TREE_NODE_STYLES, styles, style))( @@ -290,7 +293,7 @@ export const ElementsTree = observer(function ElementsTree({ root={root} context={context} emptyPlaceholder={emptyPlaceholder} - childrenState={children} + childrenState={tree} hasChildren={hasChildren} > diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index 2df739a52b..4dbd7248ea 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -104,9 +104,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer treeContext?.tree.isNodeExpandable(node.id) ?? true); + return styled(TREE_NODE_STYLES, nodeIconStyle, styles)( - + {expandable && } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index a60a5c551c..112808a1a8 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -9,7 +9,7 @@ import { action, computed, observable, runInAction } from 'mobx'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { IFolderExplorerContext, useExecutor, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks'; +import { IFolderExplorerContext, ILoadableState, useExecutor, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -23,6 +23,16 @@ import type { NavigationNodeRendererComponent } from './NavigationNodeComponent' export type IElementsTreeCustomRenderer = (nodeId: string) => NavigationNodeRendererComponent | undefined; +export interface IElementsTreeNodeExpandedInfo { + expanded: boolean; + expandable?: boolean; +} + +export type IElementsTreeNodeExpandInfoGetter = ( + nodeId: string, + state: ITreeNodeState +) => IElementsTreeNodeExpandedInfo | null; + export type IElementsTreeFilter = ( filter: string, node: NavNode, @@ -56,6 +66,7 @@ export interface IElementsTreeOptions { disabled?: boolean; filters?: IElementsTreeFilter[]; renderers?: IElementsTreeCustomRenderer[]; + expandStateGetters?: IElementsTreeNodeExpandInfoGetter[]; localState?: MetadataMap; getChildren: (id: string) => string[] | undefined; loadChildren: (id: string, manual: boolean) => Promise; @@ -74,7 +85,7 @@ interface IOptions extends IElementsTreeOptions { folderExplorer: IFolderExplorerContext; } -export interface IElementsTree { +export interface IElementsTree extends ILoadableState { actions: ISyncExecutor; settings?: IElementsTreeSettings; baseRoot: string; @@ -89,6 +100,7 @@ export interface IElementsTree { getNodeState: (nodeId: string) => ITreeNodeState; isNodeExpanded: (nodeId: string, ignoreFilter?: boolean) => boolean; + isNodeExpandable: (nodeId: string) => boolean; getExpanded: () => string[]; getSelected: () => string[]; isNodeSelected: (nodeId: string) => boolean; @@ -294,6 +306,12 @@ export function useElementsTree(options: IOptions): IElementsTree { get filtering() { return this.filter !== ''; }, + isLoading() { + return this.loading; + }, + isLoaded() { + return true; + }, getNodeState(nodeId: string) { return this.state.get(nodeId); }, @@ -307,8 +325,34 @@ export function useElementsTree(options: IOptions): IElementsTree { } const nodeState = this.getNodeState(nodeId); + const expanded = nodeState.expanded || nodeState.showInFilter; - return nodeState.expanded || nodeState.showInFilter; + if (!expanded && options.expandStateGetters?.length) { + return options + .expandStateGetters + .map(getExpandState => getExpandState(nodeId, nodeState)) + .filter(stateInfo => stateInfo !== null) + .some(stateInfo => stateInfo?.expanded); + } + + return expanded; + }, + isNodeExpandable(nodeId: string): boolean { + if (nodeId === this.root) { + return true; + } + + if (options.expandStateGetters?.length) { + const nodeState = this.getNodeState(nodeId); + + return options + .expandStateGetters + .map(getExpandState => getExpandState(nodeId, nodeState)) + .filter(stateInfo => stateInfo !== null) + .every(stateInfo => stateInfo?.expandable !== false); + } + + return true; }, getExpanded(): string[] { return Array.from(this.state).filter(([key, state]) => state.expanded).map(([key]) => key); @@ -416,6 +460,10 @@ export function useElementsTree(options: IOptions): IElementsTree { } }, async expand(node: NavNode, state: boolean) { + if (!this.isNodeExpandable(node.id)) { + return; + } + const treeNodeState = this.state.get(node.id); try { @@ -499,7 +547,7 @@ export function useElementsTree(options: IOptions): IElementsTree { baseRoot: options.baseRoot, renderers, userData, - }); + }, ['isLoading', 'isLoaded']); function exitNodeFolder(nodeId: string) { runInAction(() => { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx index d8337f235d..6b268ee0af 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx @@ -13,7 +13,7 @@ import styled, { css } from 'reshadow'; import { useUserData } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { Translate } from '@cloudbeaver/core-localization'; -import { NavNodeInfoResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; +import { NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; import { usePermission, EPermission } from '@cloudbeaver/core-root'; import { CaptureView } from '@cloudbeaver/core-view'; @@ -26,6 +26,8 @@ import type { IElementsTreeSettings } from './ElementsTree/useElementsTree'; import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettingsId'; import { navigationTreeDuplicateFilter } from './navigationTreeDuplicateIdFilter'; import { NavigationTreeService } from './NavigationTreeService'; +import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter'; +import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter'; import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer'; import { useNavigationTree } from './useNavigationTree'; @@ -74,6 +76,7 @@ const elementsTreeStyles = css` export const NavigationTree = observer(function NavigationTree() { const navTreeService = useService(NavigationTreeService); const navNodeInfoResource = useService(NavNodeInfoResource); + const navTreeResource = useService(NavTreeResource); const navNodeViewService = useService(NavNodeViewService); const root = ROOT_NODE_PATH; @@ -94,7 +97,15 @@ export const NavigationTree = observer(function NavigationTree() { const duplicateFilter = useMemo(() => navigationTreeDuplicateFilter(navNodeViewService), [navNodeViewService]); const projectsRendererRenderer = useMemo( () => navigationTreeProjectsRendererRenderer(navNodeInfoResource), - [navNodeViewService] + [navNodeInfoResource] + ); + const projectsExpandStateGetter = useMemo( + () => navigationTreeProjectsExpandStateGetter(navNodeInfoResource), + [navNodeInfoResource] + ); + const projectFilter = useMemo( + () => navigationTreeProjectFilter(navNodeInfoResource, navTreeResource), + [navNodeInfoResource, navTreeResource] ); if (!isEnabled) { @@ -106,8 +117,9 @@ export const NavigationTree = observer(function NavigationTree() { styled(navigationTreeStyles)(
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx index 6c2d95e1f9..f9a2977de1 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -10,12 +10,11 @@ import { observer } from 'mobx-react-lite'; import React, { forwardRef, useContext, useState } from 'react'; import styled, { css, use } from 'reshadow'; -import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks'; +import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree'; -import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext'; import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent'; import { NavigationNodeEditor } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor'; import { TreeNodeMenu } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu'; @@ -62,7 +61,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading); const selected = treeNodeContext.selected; @@ -86,7 +84,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer - {editing ? ( setEditing(false)} /> diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts new file mode 100644 index 0000000000..e0fa9c5edf --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts @@ -0,0 +1,33 @@ +/* + * 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 { NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; +import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; + +import type { IElementsTreeFilter } from '../ElementsTree/useElementsTree'; + +export function navigationTreeProjectFilter( + navNodeInfoResource: NavNodeInfoResource, + navTreeResource: NavTreeResource, +): IElementsTreeFilter { + return (filter: string, node: NavNode, children: string[]) => { + if (node.id !== ROOT_NODE_PATH) { + return children; + } + + const nodes = navNodeInfoResource + .get(resourceKeyList(children)) + .filter((node => node !== undefined) as (node: NavNode | undefined) => node is NavNode) + .filter(node => node.nodeType !== NAV_NODE_TYPE_PROJECT || navTreeResource.get(node.id)?.length) + .map(node => node.id); + + return nodes; + }; +} diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts new file mode 100644 index 0000000000..419da94971 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts @@ -0,0 +1,31 @@ +/* + * 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 type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; + +import type { IElementsTreeNodeExpandInfoGetter } from '../ElementsTree/useElementsTree'; + + +export function navigationTreeProjectsExpandStateGetter( + navNodeInfoResource: NavNodeInfoResource +): IElementsTreeNodeExpandInfoGetter { + + return nodeId => { + const node = navNodeInfoResource.get(nodeId); + + if (node?.nodeType !== NAV_NODE_TYPE_PROJECT) { + return null; + } + + return { + expanded: true, + expandable: false, + }; + }; +} \ No newline at end of file diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx index 4b3bf515b1..f6839cb2e5 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -7,19 +7,29 @@ */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; +import styled, { css } from 'reshadow'; import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; import { Translate } from '@cloudbeaver/core-localization'; import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; +import { useChildren } from '../../NodesManager/useChildren'; import { useNode } from '../../NodesManager/useNode'; import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent'; import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer'; import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree'; import { NavigationNodeProjectControl } from './NavigationNodeProjectControl'; +const nestedStyles = css` + TreeNode { + margin-top: 8px; + } + NavigationNodeNested { + padding-left: 8px !important; + } +`; + export function navigationTreeProjectsRendererRenderer( navNodeInfoResource: NavNodeInfoResource ): IElementsTreeCustomRenderer { @@ -61,6 +71,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag dragging={dragging} className={className} control={NavigationNodeProjectControl} + style={nestedStyles} component={component} /> ); diff --git a/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts b/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts index abbda5a013..772c8d5026 100644 --- a/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts +++ b/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts @@ -40,7 +40,7 @@ export class NavResourceNodeService { async saveScript(resourceData: IResourceData, name: string, script: string): Promise { const resourcePath = createPath(resourceData.resourcePath, name); // await this.resourceManagerResource.createResource(resourceData.key.projectId, resourcePath, false); - await this.resourceManagerResource.writeResource(resourceData.key.projectId, resourcePath, script); + await this.resourceManagerResource.writeResource(resourceData.key.projectId, resourcePath, script, false); await this.navTreeResource.refreshTree(resourceData.nodeId); return createPath(RESOURCES_NODE_PATH, resourceData.key.projectId, resourcePath); @@ -57,7 +57,12 @@ export class NavResourceNodeService { } async write(resourceData: IResourceData, value: string) { - await this.resourceManagerResource.writeResource(resourceData.key.projectId, resourceData.resourcePath, value); + await this.resourceManagerResource.writeResource( + resourceData.key.projectId, + resourceData.resourcePath, + value, + true + ); } getResourceData(nodeId: string): IResourceData | undefined { diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts index f018e21fa7..7459f92ec5 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts @@ -59,11 +59,12 @@ export class ResourceManagerResource extends CachedMapResource