mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into CB-2386-cb-aws-the-changed-admin-in-the-easy-config-lost-the-admin-role-after-another-user-login
This commit is contained in:
@@ -56,7 +56,7 @@ public class VirtualProjectImpl extends BaseProjectImpl {
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return project.getName();
|
||||
return project.getDisplayName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+5
-1
@@ -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");
|
||||
}
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<extension point="io.cloudbeaver.service">
|
||||
<service id="core" label="Core service" description="Core services" class="io.cloudbeaver.service.core.WebServiceBindingCore">
|
||||
<permission id="public" label="Public access" category="general" scope="subject"/>
|
||||
<permission id="public" label="Public access" category="general" scope="subject" description="Provides access to the data management interface"/>
|
||||
<permission id="access" label="Data source access" category="general" scope="datasource"/>
|
||||
</service>
|
||||
<service id="navigator" label="Database navigator" description="Database navigator services" class="io.cloudbeaver.service.navigator.WebServiceBindingNavigator">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<extension point="io.cloudbeaver.service">
|
||||
<service id="admin" label="User administration" description="User administration services" class="io.cloudbeaver.service.admin.WebServiceBindingAdmin">
|
||||
<permission id="admin" label="Admin Full Access" category="admin" scope="subject"/>
|
||||
<permission id="admin" label="Admin Full Access" category="admin" scope="subject" description="Provides access to the CloudBeaver configuration"/>
|
||||
</service>
|
||||
</extension>
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ extend type Mutation {
|
||||
rmWriteResourceStringContent(
|
||||
projectId: String!,
|
||||
resourcePath: String!,
|
||||
data: String!): String!
|
||||
data: String!,
|
||||
forceOverwrite: Boolean!): String!
|
||||
|
||||
rmCreateProject(
|
||||
projectName: String!,
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
|
||||
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),
|
||||
|
||||
+8
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+77
@@ -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<String, Object> 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<String, Object> map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client);
|
||||
Map<String, Object> 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<String, Object> map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client);
|
||||
Map<String, Object> data = JSONUtils.getObjectOrNull(map, "data");
|
||||
if (data != null) {
|
||||
return JSONUtils.getInteger(data, "navDeleteNodes");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1
@@ -231,6 +231,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
</InputField>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InputField
|
||||
type={password ? 'password' : 'text'}
|
||||
|
||||
@@ -39,6 +39,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', 'Search for user or role name'],
|
||||
['connections_connection_access_admin_info', 'Administrators see all connections.'],
|
||||
['connections_connection_description', 'Description'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_driver', 'Driver'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', 'Host'],
|
||||
|
||||
@@ -42,6 +42,7 @@ export default [
|
||||
['connections_connection_access_admin_info', 'Gli amministratori vedono tutte le connessioni.'],
|
||||
['connections_connection_description', 'Descrizione'],
|
||||
['connections_connection_driver', 'Driver'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', 'Host'],
|
||||
['connections_connection_port', 'Porta'],
|
||||
|
||||
@@ -36,6 +36,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', 'Поиск по имени пользователя или роли'],
|
||||
['connections_connection_access_admin_info', 'Администраторы видят все подключения.'],
|
||||
['connections_connection_description', 'Описание'],
|
||||
['connections_connection_project', 'Проект'],
|
||||
['connections_connection_driver', 'Драйвер'],
|
||||
['connections_connection_configuration', 'Конфигурация'],
|
||||
['connections_connection_host', 'Хост'],
|
||||
|
||||
@@ -39,6 +39,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', '搜索用户或角色名称'],
|
||||
['connections_connection_access_admin_info', '管理员查看所有连接。'],
|
||||
['connections_connection_description', '描述'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_driver', '驱动'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', '主机'],
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
mutation writeResourceContent($projectId: String!, $resourcePath: String!, $data: String!) {
|
||||
rmWriteResourceStringContent(projectId: $projectId, resourcePath: $resourcePath, data: $data)
|
||||
mutation writeResourceContent(
|
||||
$projectId: String!,
|
||||
$resourcePath: String!,
|
||||
$data: String!,
|
||||
$forceOverwrite: Boolean!
|
||||
) {
|
||||
rmWriteResourceStringContent(
|
||||
projectId: $projectId,
|
||||
resourcePath: $resourcePath,
|
||||
data: $data,
|
||||
forceOverwrite: $forceOverwrite
|
||||
)
|
||||
}
|
||||
@@ -563,6 +563,7 @@ export interface MutationRmMoveResourceArgs {
|
||||
|
||||
export interface MutationRmWriteResourceStringContentArgs {
|
||||
data: Scalars['String'];
|
||||
forceOverwrite: Scalars['Boolean'];
|
||||
projectId: Scalars['String'];
|
||||
resourcePath: Scalars['String'];
|
||||
}
|
||||
@@ -2446,6 +2447,7 @@ export type WriteResourceContentMutationVariables = Exact<{
|
||||
projectId: Scalars['String'];
|
||||
resourcePath: Scalars['String'];
|
||||
data: Scalars['String'];
|
||||
forceOverwrite: Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
+14
-6
@@ -78,15 +78,22 @@ export const RoleOptions: TabContainerPanelComponent<IRoleFormProps> = observer(
|
||||
<Group small gap>
|
||||
<GroupTitle>{translate('administration_roles_role_permissions')}</GroupTitle>
|
||||
{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 (
|
||||
<FieldCheckbox
|
||||
key={permission.id}
|
||||
@@ -98,7 +105,8 @@ export const RoleOptions: TabContainerPanelComponent<IRoleFormProps> = observer(
|
||||
readOnly={state.readonly}
|
||||
disabled={state.disabled}
|
||||
>
|
||||
{label} {caption && <caption>({caption})</caption>}
|
||||
{label}
|
||||
{caption ? <caption>{caption}</caption> : null}
|
||||
</FieldCheckbox>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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<LogoutEventType>;
|
||||
readonly onLogout: Executor<AuthEventType>;
|
||||
readonly onLogin: Executor<AuthEventType>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ export class ConnectionFormState implements IConnectionFormState {
|
||||
.next(this.formStateTask);
|
||||
|
||||
makeObservable<IConnectionFormState, '_availableDrivers' | 'stateInfo' | 'updateFormState'>(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();
|
||||
|
||||
@@ -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<void>;
|
||||
readonly test: () => Promise<void>;
|
||||
|
||||
+17
-3
@@ -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) {
|
||||
|
||||
@@ -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<IConnectionFormProps> = 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<IConnectionFormProps> = observe
|
||||
>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
<Combobox
|
||||
name='projectId'
|
||||
value={state.projectId ?? PROJECT_GLOBAL_ID}
|
||||
items={projects}
|
||||
keySelector={project => 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')}
|
||||
</Combobox>
|
||||
{!config.template && (
|
||||
<InputField
|
||||
type="text"
|
||||
@@ -301,6 +330,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
disabled={disabled}
|
||||
autoComplete={`section-${config.driverId || 'driver'} section-folder`}
|
||||
mod='surface'
|
||||
autoHide
|
||||
readOnly
|
||||
tiny
|
||||
fill
|
||||
|
||||
+35
-12
@@ -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<void> {
|
||||
async close(saved?: boolean): Promise<boolean> {
|
||||
if (!this.formState) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (saved) {
|
||||
@@ -101,6 +116,8 @@ export class PublicConnectionFormService {
|
||||
if (state) {
|
||||
this.clearFormState();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
@@ -147,6 +164,14 @@ export class PublicConnectionFormService {
|
||||
};
|
||||
|
||||
private readonly closeHandler: IExecutorHandler<any> = async (data, contexts) => {
|
||||
const confirmed = await this.showUnsavedChangesDialog();
|
||||
|
||||
if (!confirmed) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
};
|
||||
|
||||
private async showUnsavedChangesDialog(): Promise<boolean> {
|
||||
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, {
|
||||
|
||||
@@ -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?'],
|
||||
];
|
||||
|
||||
@@ -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?'],
|
||||
];
|
||||
|
||||
@@ -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}". Подключения не будут удалены. Вы уверены?'],
|
||||
];
|
||||
|
||||
@@ -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?'],
|
||||
];
|
||||
|
||||
+6
-3
@@ -102,6 +102,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
|
||||
navNodeFilterCompare,
|
||||
filters = [],
|
||||
renderers = [],
|
||||
expandStateGetters,
|
||||
style,
|
||||
className,
|
||||
getChildren,
|
||||
@@ -161,7 +162,7 @@ export const ElementsTree = observer<Props>(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<Props>(function ElementsTree({
|
||||
localState,
|
||||
filters: [nameFilter, ...filters, limitFilter],
|
||||
renderers: [...renderers, elementsTreeLimitRenderer],
|
||||
expandStateGetters,
|
||||
getChildren,
|
||||
loadChildren,
|
||||
isGroup,
|
||||
@@ -279,7 +281,8 @@ export const ElementsTree = observer<Props>(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<Props>(function ElementsTree({
|
||||
root={root}
|
||||
context={context}
|
||||
emptyPlaceholder={emptyPlaceholder}
|
||||
childrenState={children}
|
||||
childrenState={tree}
|
||||
hasChildren={hasChildren}
|
||||
>
|
||||
<ElementsTreeContext.Provider value={context}>
|
||||
|
||||
+3
-1
@@ -104,9 +104,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
|
||||
treeNodeContext.select(event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
const expandable = getComputed(() => treeContext?.tree.isNodeExpandable(node.id) ?? true);
|
||||
|
||||
return styled(TREE_NODE_STYLES, nodeIconStyle, styles)(
|
||||
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
|
||||
<TreeNodeExpand filterActive={treeContext?.tree.filtering} />
|
||||
{expandable && <TreeNodeExpand filterActive={treeContext?.tree.filtering} />}
|
||||
<TreeNodeIcon icon={icon} style={nodeIconStyle} {...use({ connected })}>
|
||||
<ConnectionMark connected={connected} />
|
||||
</TreeNodeIcon>
|
||||
|
||||
+52
-4
@@ -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<string, ITreeNodeState>;
|
||||
getChildren: (id: string) => string[] | undefined;
|
||||
loadChildren: (id: string, manual: boolean) => Promise<boolean>;
|
||||
@@ -74,7 +85,7 @@ interface IOptions extends IElementsTreeOptions {
|
||||
folderExplorer: IFolderExplorerContext;
|
||||
}
|
||||
|
||||
export interface IElementsTree {
|
||||
export interface IElementsTree extends ILoadableState {
|
||||
actions: ISyncExecutor<IElementsTreeAction>;
|
||||
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(() => {
|
||||
|
||||
@@ -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() {
|
||||
<ElementsTree
|
||||
root={root}
|
||||
localState={navTreeService.treeState}
|
||||
filters={[duplicateFilter, connectionGroupFilter]}
|
||||
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
|
||||
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer]}
|
||||
expandStateGetters={[projectsExpandStateGetter]}
|
||||
emptyPlaceholder={() => styled(navigationTreeStyles)(
|
||||
<center>
|
||||
<message>
|
||||
|
||||
+1
-4
@@ -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<Na
|
||||
dndPlaceholder,
|
||||
}, ref) {
|
||||
const treeNodeContext = useContext(TreeNodeContext);
|
||||
const treeContext = useContext(ElementsTreeContext);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
|
||||
const selected = treeNodeContext.selected;
|
||||
@@ -86,7 +84,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
|
||||
|
||||
return styled(TREE_NODE_STYLES, styles)(
|
||||
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
|
||||
<TreeNodeExpand filterActive={treeContext?.tree.filtering} />
|
||||
<TreeNodeName title={node.name}>
|
||||
{editing ? (
|
||||
<NavigationNodeEditor node={node} onClose={() => setEditing(false)} />
|
||||
|
||||
+33
@@ -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<NavNode>((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;
|
||||
};
|
||||
}
|
||||
+31
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
+12
-1
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ export class NavResourceNodeService {
|
||||
async saveScript(resourceData: IResourceData, name: string, script: string): Promise<string> {
|
||||
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 {
|
||||
|
||||
@@ -59,11 +59,12 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
|
||||
await this.load({ projectId, folder:folder ? resourcePath : this.getFolder(resourcePath) });
|
||||
}
|
||||
|
||||
async writeResource(projectId: string, resourcePath: string, data: string) {
|
||||
async writeResource(projectId: string, resourcePath: string, data: string, forceOverwrite: boolean) {
|
||||
await this.graphQLService.sdk.writeResourceContent({
|
||||
projectId,
|
||||
resourcePath,
|
||||
data,
|
||||
forceOverwrite,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user