diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 0537b2569b..762ee418f1 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -183,9 +183,12 @@ export class UserInfoResource extends CachedDataResource { + this.resetIncludes(); + this.setData(data); + this.sessionResource.markOutdated(); + }); return result; } diff --git a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts index 6a52a3dc5c..386c31c381 100644 --- a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts +++ b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'; import { type ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; import { getComputed } from '../getComputed.js'; +import { useObjectRef } from '../useObjectRef.js'; export function useAutoLoad( component: { name: string }, @@ -18,6 +19,7 @@ export function useAutoLoad( lazy = false, throwExceptions = false, ) { + const unmountedRef = useObjectRef({ unmounted: false }); const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const); if (!Array.isArray(state)) { state = [state] as ReadonlyArray; @@ -32,7 +34,7 @@ export function useAutoLoad( const obj = { [loadFunctionName]: async () => { - if (!enabled) { + if (!enabled || unmountedRef.unmounted) { return; } @@ -70,4 +72,11 @@ export function useAutoLoad( useEffect(() => { obj[loadFunctionName]!(); }); + + useEffect( + () => () => { + unmountedRef.unmounted = true; + }, + [], + ); } diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index 5b75716efc..1dccb3280a 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -132,6 +132,7 @@ export function useResource< ): IMapResourceResult | IMapResourceListResult | IDataResourceResult { // eslint-disable-next-line react-hooks/rules-of-hooks const resource = ctor instanceof Resource ? ctor : useService(ctor); + const unmountedRef = useObjectRef({ unmounted: false }); const errorContext = useContext(ErrorContext); let key: ResourceKey | null = keyObj as ResourceKey; let includes: TIncludes = [] as unknown as TIncludes; @@ -234,7 +235,7 @@ export function useResource< } }, async load(refresh?: boolean): Promise { - if (propertiesRef.key === null) { + if (propertiesRef.key === null || unmountedRef.unmounted) { return; } @@ -474,6 +475,13 @@ export function useResource< } }, [canLoad, loadKey]); + useEffect( + () => () => { + unmountedRef.unmounted = true; + }, + [], + ); + if (actions?.forceSuspense) { result.data; } diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 73e87cdb23..d1c75ea399 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -10,6 +10,7 @@ import { action, makeObservable, observable, runInAction, toJS } from 'mobx'; import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, @@ -291,9 +292,18 @@ export class ConnectionInfoResource extends CachedMapResource connection?.connected ?? false); } + // TODO: we need here node path ie ['', 'project://', 'database://...', '...'] + getConnectionIdForNodeId(projectId: string, nodeId: string): IConnectionInfoParams | undefined { + if (!NodeManagerUtils.isDatabaseObject(nodeId)) { + return; + } + + return createConnectionParam(projectId, NodeManagerUtils.getConnectionId(nodeId)); + } + // TODO: we need here node path ie ['', 'project://', 'database://...', '...'] getConnectionForNode(nodeId: string): Connection | undefined { - if (!nodeId.startsWith('database://')) { + if (!NodeManagerUtils.isDatabaseObject(nodeId)) { return; } diff --git a/webapp/packages/core-connections/src/ConnectionsManagerService.ts b/webapp/packages/core-connections/src/ConnectionsManagerService.ts index 5e0a946a66..400995d7f8 100644 --- a/webapp/packages/core-connections/src/ConnectionsManagerService.ts +++ b/webapp/packages/core-connections/src/ConnectionsManagerService.ts @@ -32,7 +32,9 @@ export interface IConnectionExecutorData { @injectable() export class ConnectionsManagerService { get projectConnections(): Connection[] { - return this.connectionInfo.values.filter(connection => this.projectsService.activeProjects.some(project => project.id === connection.projectId)); + return this.connectionInfoResource.values.filter(connection => + this.projectsService.activeProjects.some(project => project.id === connection.projectId), + ); } get createConnectionProjects(): ProjectInfo[] { return this.projectsService.activeProjects.filter(project => project.canEditDataSources).sort(projectInfoSortByName); @@ -44,7 +46,7 @@ export class ConnectionsManagerService { private disconnecting: boolean; constructor( - readonly connectionInfo: ConnectionInfoResource, + readonly connectionInfoResource: ConnectionInfoResource, readonly containerContainers: ContainerResource, private readonly notificationService: NotificationService, private readonly commonDialogService: CommonDialogService, @@ -56,9 +58,9 @@ export class ConnectionsManagerService { this.onDisconnect = new Executor(); this.onDelete = new Executor(); - this.connectionExecutor.addHandler(data => connectionInfo.load(data.key)); + this.connectionExecutor.addHandler(data => connectionInfoResource.load(data.key)); this.onDelete.before(this.onDisconnect); - this.connectionInfo.onConnectionClose.next(this.onDisconnect, key => ({ + this.connectionInfoResource.onConnectionClose.next(this.onDisconnect, key => ({ connections: [key], state: 'after' as const, })); @@ -80,10 +82,6 @@ export class ConnectionsManagerService { return connection.connection; } - addOpenedConnection(connection: Connection): void { - this.connectionInfo.add(connection); - } - getObjectContainerById(connectionKey: IConnectionInfoParams, objectCatalogId?: string, objectSchemaId?: string): ObjectContainer | undefined { if (objectCatalogId) { const objectContainers = this.containerContainers.getCatalogData(connectionKey, objectCatalogId); @@ -107,7 +105,7 @@ export class ConnectionsManagerService { } async deleteConnection(key: IConnectionInfoParams): Promise { - const connection = await this.connectionInfo.load(key); + const connection = await this.connectionInfoResource.load(key); if (!connection.canDelete) { return; @@ -131,7 +129,7 @@ export class ConnectionsManagerService { return; } - await this.connectionInfo.deleteConnection(key); + await this.connectionInfoResource.deleteConnection(key); this.onDelete.execute({ connections: [key], @@ -157,7 +155,7 @@ export class ConnectionsManagerService { return; } - await this.connectionInfo.close(createConnectionParam(connection)); + await this.connectionInfoResource.close(createConnectionParam(connection)); } async closeAllConnections(): Promise { @@ -192,7 +190,7 @@ export class ConnectionsManagerService { } async closeConnectionAsync(key: IConnectionInfoParams): Promise { - const connection = this.connectionInfo.get(key); + const connection = this.connectionInfoResource.get(key); if (!connection || !connection.connected) { return; } diff --git a/webapp/packages/core-connections/src/DataContexts/DATA_CONTEXT_CONNECTION.ts b/webapp/packages/core-connections/src/DataContexts/DATA_CONTEXT_CONNECTION.ts index 8f13e2a4a9..3e81aba0e1 100644 --- a/webapp/packages/core-connections/src/DataContexts/DATA_CONTEXT_CONNECTION.ts +++ b/webapp/packages/core-connections/src/DataContexts/DATA_CONTEXT_CONNECTION.ts @@ -7,6 +7,6 @@ */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { Connection } from '../ConnectionInfoResource.js'; +import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA.js'; -export const DATA_CONTEXT_CONNECTION = createDataContext('connection'); +export const DATA_CONTEXT_CONNECTION = createDataContext('connection'); diff --git a/webapp/packages/core-connections/src/NavTree/NavNodeExtensionsService.ts b/webapp/packages/core-connections/src/NavTree/NavNodeExtensionsService.ts index ccd6c1eafa..9763c28975 100644 --- a/webapp/packages/core-connections/src/NavTree/NavNodeExtensionsService.ts +++ b/webapp/packages/core-connections/src/NavTree/NavNodeExtensionsService.ts @@ -11,7 +11,7 @@ import { NavNodeInfoResource, NavNodeManagerService } from '@cloudbeaver/core-na import { projectProvider } from '@cloudbeaver/core-projects'; import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA.js'; -import { ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource.js'; +import { ConnectionInfoResource } from '../ConnectionInfoResource.js'; import { connectionProvider } from '../extensions/IConnectionProvider.js'; import { objectCatalogProvider } from '../extensions/IObjectCatalogProvider.js'; import { objectSchemaProvider } from '../extensions/IObjectSchemaProvider.js'; @@ -40,13 +40,17 @@ export class NavNodeExtensionsService { } getConnection(navNodeId: string): IConnectionInfoParams | undefined { - const connection = this.connectionInfoResource.getConnectionForNode(navNodeId); + const node = this.navNodeInfoResource.get(navNodeId); + if (!node?.projectId) { + return; + } + const connectionKey = this.connectionInfoResource.getConnectionIdForNodeId(node.projectId, navNodeId); - if (!connection) { + if (!connectionKey) { return; } - return createConnectionParam(connection); + return connectionKey; } getDBObjectCatalog(navNodeId: string) { diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index d6ab151553..9684cd153c 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -18,7 +18,6 @@ export * from './extensions/IObjectSchemaProvider.js'; export * from './extensions/IObjectSchemaSetter.js'; export * from './extensions/IObjectLoaderProvider.js'; export * from './extensions/IExecutionContextProvider.js'; -export * from './NavTree/ConnectionNavNodeService.js'; export * from './NavTree/NavNodeExtensionsService.js'; export * from './NavTree/getConnectionFolderIdFromNodeId.js'; export * from './NavTree/getConnectionFolderId.js'; @@ -31,6 +30,7 @@ export * from './NavTree/isConnectionNode.js'; export * from './extensions/IConnectionProvider.js'; export * from './extensions/IConnectionSetter.js'; +export * from './ConnectionFolderEventHandler.js'; export * from './ConnectionsManagerService.js'; export * from './ConnectionFolderResource.js'; export * from './ConnectionDialectResource.js'; diff --git a/webapp/packages/core-connections/src/manifest.ts b/webapp/packages/core-connections/src/manifest.ts index 3bb1690a91..1ee195cb3f 100644 --- a/webapp/packages/core-connections/src/manifest.ts +++ b/webapp/packages/core-connections/src/manifest.ts @@ -27,7 +27,6 @@ export const manifest: PluginManifest = { () => import('./DBDriverResource.js').then(m => m.DBDriverResource), () => import('./NetworkHandlerResource.js').then(m => m.NetworkHandlerResource), () => import('./ConnectionDialectResource.js').then(m => m.ConnectionDialectResource), - () => import('./NavTree/ConnectionNavNodeService.js').then(m => m.ConnectionNavNodeService), () => import('./NavTree/NavNodeExtensionsService.js').then(m => m.NavNodeExtensionsService), () => import('./ConnectionInfoEventHandler.js').then(m => m.ConnectionInfoEventHandler), () => import('./ConnectionFolderEventHandler.js').then(m => m.ConnectionFolderEventHandler), diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index c2f4d0e24b..32cc406ed9 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -84,6 +84,7 @@ export default [ ['ui_create_processing', 'Creating...'], ['ui_folder', 'Folder'], ['ui_folder_new', 'New folder'], + ['ui_folder_new_default_name', 'New folder'], ['ui_rename_processing', 'Renaming...'], ['ui_interval', 'Interval'], ['ui_name', 'Name'], diff --git a/webapp/packages/core-localization/src/locales/fr.ts b/webapp/packages/core-localization/src/locales/fr.ts index 4f03c54f30..b255f081f6 100644 --- a/webapp/packages/core-localization/src/locales/fr.ts +++ b/webapp/packages/core-localization/src/locales/fr.ts @@ -78,6 +78,7 @@ export default [ ['ui_create_processing', 'Création...'], ['ui_folder', 'Dossier'], ['ui_folder_new', 'Nouveau dossier'], + ['ui_folder_new_default_name', 'Nouveau dossier'], ['ui_rename_processing', 'Renommage...'], ['ui_interval', 'Intervalle'], ['ui_name', 'Nom'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index ea2c2abb6a..8d2159e380 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -81,6 +81,7 @@ export default [ ['ui_create_processing', 'Creating...'], ['ui_folder', 'Folder'], ['ui_folder_new', 'New folder'], + ['ui_folder_new_default_name', 'New folder'], ['ui_rename_processing', 'Renaming...'], ['ui_interval', 'Interval'], ['ui_name', 'Nome'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index dc790070a3..dd027ae4d3 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -80,6 +80,7 @@ export default [ ['ui_create_processing', 'Создание...'], ['ui_folder', 'Папка'], ['ui_folder_new', 'Новая папка'], + ['ui_folder_new_default_name', 'Новая папка'], ['ui_rename_processing', 'Переименование...'], ['ui_name', 'Название'], ['ui_value', 'Значение'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index 55bf0b5d74..6cbdb763c7 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -80,6 +80,7 @@ export default [ ['ui_create_processing', '创建中...'], ['ui_folder', '文件夹'], ['ui_folder_new', '新建文件夹'], + ['ui_folder_new_default_name', '新建文件夹'], ['ui_rename', '重命名'], ['ui_rename_processing', '重命名中...'], ['ui_name', '名称'], diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index e6fe3be622..8e04a8e5ef 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -60,7 +60,6 @@ export class DBObjectResource extends CachedMapResource { }); this.beforeLoad.addHandler(async (originalKey, context) => { - await this.navTreeResource.waitLoad(); const parentKey = this.aliases.isAlias(originalKey, DBObjectParentKey); const pageKey = this.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || this.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); @@ -80,6 +79,15 @@ export class DBObjectResource extends CachedMapResource { } const key = ResourceKeyUtils.toList(this.aliases.transformToKey(originalKey)); + + for (const nodeId of key) { + const preloaded = await this.navTreeResource.preloadParents(nodeId); + + if (!preloaded) { + throw new DetailsError('Not found: ' + nodeId); + } + } + const parents = [ ...new Set( key.map(nodeId => this.navNodeInfoResource.get(nodeId)?.parentId).filter((nodeId): nodeId is string => nodeId !== undefined), diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index b6a8441dcb..325496a7a5 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -455,17 +455,22 @@ export class NavTreeResource extends CachedMapResource { + if (!this.navNodeInfoResource.has(nodeId) && nodeId !== ROOT_NODE_PATH) { + await this.navNodeInfoResource.loadNodeParents(nodeId); + } + const parents = this.navNodeInfoResource.getParents(nodeId); + return await this.preloadNodeParents(parents, nodeId); + } + protected override async preLoadData(key: ResourceKey, contexts: IExecutionContext>): Promise { await ResourceKeyUtils.forEachAsync(key, async nodeId => { if (isResourceAlias(nodeId)) { return; } - if (!this.navNodeInfoResource.has(nodeId) && nodeId !== ROOT_NODE_PATH) { - await this.navNodeInfoResource.loadNodeParents(nodeId); - } + const preloaded = await this.preloadParents(nodeId); const parents = this.navNodeInfoResource.getParents(nodeId); - const preloaded = await this.preloadNodeParents(parents, nodeId); if (!preloaded) { const cause = new DetailsError(`Entity not found:\n"${nodeId}"\nPath:\n${parents.map(parent => `"${parent}"`).join('\n')}`); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NodeManagerUtils.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NodeManagerUtils.ts index 9b6debbfb0..d6b895fb5e 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NodeManagerUtils.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NodeManagerUtils.ts @@ -6,13 +6,22 @@ * you may not use this file except in compliance with the License. */ +const CONNECTION_NODE_ID_PREFIX = 'database://'; + export const NodeManagerUtils = { + getConnectionId(nodeId: string) { + const indexOfConnectionPart = nodeId.indexOf('/', CONNECTION_NODE_ID_PREFIX.length); + const connectionId = nodeId.slice(CONNECTION_NODE_ID_PREFIX.length, indexOfConnectionPart > -1 ? indexOfConnectionPart : nodeId.length); + + return connectionId; + }, + connectionIdToConnectionNodeId(connectionId: string): string { - return `database://${connectionId}`; + return `${CONNECTION_NODE_ID_PREFIX}${connectionId}`; }, - isDatabaseObject(objectId: string): boolean { - return objectId.startsWith('database://'); + isDatabaseObject(nodeId: string): boolean { + return nodeId.startsWith(CONNECTION_NODE_ID_PREFIX); }, concatSchemaAndCatalog(catalogId?: string, schemaId?: string): string { diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts index d5fa48dd17..ab8b45356d 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts @@ -6,16 +6,21 @@ * you may not use this file except in compliance with the License. */ import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { ConnectionsManagerService, getFolderPath, isConnectionNode } from '@cloudbeaver/core-connections'; +import { ConnectionsManagerService, getFolderPath } from '@cloudbeaver/core-connections'; import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { DATA_CONTEXT_NAV_NODE, isConnectionFolder, isProjectNode } from '@cloudbeaver/core-navigation-tree'; import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { ActionService, type IAction, MenuService } from '@cloudbeaver/core-view'; +import { ActionService, DATA_CONTEXT_MENU, type IAction, MenuService } from '@cloudbeaver/core-view'; import { ACTION_TREE_CREATE_CONNECTION, MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; -import { DATA_CONTEXT_ELEMENTS_TREE, MENU_NAVIGATION_TREE_CREATE, TreeSelectionService } from '@cloudbeaver/plugin-navigation-tree'; +import { + DATA_CONTEXT_ELEMENTS_TREE, + MENU_ELEMENTS_TREE_TOOLS, + MENU_NAVIGATION_TREE_CREATE, + TreeSelectionService, +} from '@cloudbeaver/plugin-navigation-tree'; import { ACTION_CONNECTION_CUSTOM } from './Actions/ACTION_CONNECTION_CUSTOM.js'; import { CustomConnectionSettingsService } from './CustomConnectionSettingsService.js'; @@ -42,12 +47,40 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { getItems: (context, items) => [...items, ACTION_CONNECTION_CUSTOM], }); + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // this.menuService.addCreator({ + // menus: [MENU_ELEMENTS_TREE_TOOLS], + // isApplicable: context => { + // const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + + // return tree.baseRoot === ROOT_NODE_PATH; + // }, + // getItems: (context, items) => { + // if (!items.includes(ACTION_TREE_CREATE_CONNECTION)) { + // return [...items, ACTION_TREE_CREATE_CONNECTION]; + // } + + // return items; + // }, + // }); + this.menuService.addCreator({ + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 menus: [MENU_NAVIGATION_TREE_CREATE], + // root: true, isApplicable: context => { const node = context.get(DATA_CONTEXT_NAV_NODE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const targetNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); - if (![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true)) { + if (![isConnectionFolder, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true) || targetNode === undefined) { return false; } @@ -58,9 +91,14 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'nav-tree-create-create-connection-handler', - menus: [MENU_NAVIGATION_TREE_CREATE], actions: [ACTION_TREE_CREATE_CONNECTION], - contexts: [DATA_CONTEXT_ELEMENTS_TREE], + getActionInfo: (context, action) => { + const menu = context.get(DATA_CONTEXT_MENU); + if (menu === MENU_ELEMENTS_TREE_TOOLS) { + return { ...action.info, icon: '/icons/add_sm.svg' }; + } + return action.info; + }, getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.createConnectionHandler.bind(this), }); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts index c7cbb6f177..ab6ffb2680 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts @@ -83,10 +83,6 @@ export class ConnectionsAdministrationService extends Bootstrap { this.connectionDetailsPlaceholder.add(SSH, 2); } - override async load(): Promise { - await this.connectionInfoResource.load(); - } - private async refreshUserConnections(configuration: boolean, outside: boolean, outsideAdminPage: boolean): Promise { // TODO: we have to track users' leaving the page if (outside) { diff --git a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts index 8aaa7e1829..27edb90bc1 100644 --- a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts +++ b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts @@ -9,4 +9,5 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_TREE_CREATE_CONNECTION = createAction('create-tree-connection', { label: 'plugin_connections_connection_create_menu_title', + tooltip: 'plugin_connections_connection_create_menu_title', }); diff --git a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts index 6330328cd4..96adf93dd2 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts @@ -63,24 +63,15 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.menuService.addCreator({ root: true, + contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], isApplicable: context => { if (this.pluginConnectionsSettingsService.hideConnectionViewForUsers && !this.permissionsService.has(EAdminPermission.admin)) { return false; } - const connection = context.get(DATA_CONTEXT_CONNECTION); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (!connection?.connected) { - return false; - } - - const node = context.get(DATA_CONTEXT_NAV_NODE); - - if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { - return false; - } - - return context.has(DATA_CONTEXT_CONNECTION); + return node.objectFeatures.includes(EObjectFeature.dataSource) && node.objectFeatures.includes(EObjectFeature.dataSourceConnected); }, getItems: (context, items) => [...items, MENU_CONNECTION_VIEW], }); @@ -101,7 +92,12 @@ export class ConnectionMenuBootstrap extends Bootstrap { actions: [ACTION_CONNECTION_VIEW_SIMPLE, ACTION_CONNECTION_VIEW_ADVANCED, ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS], contexts: [DATA_CONTEXT_CONNECTION], isChecked: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = this.connectionInfoResource.get(connectionKey); + + if (!connection) { + return false; + } switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -118,7 +114,8 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = await this.connectionInfoResource.load(connectionKey); switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -140,6 +137,10 @@ export class ConnectionMenuBootstrap extends Bootstrap { } } }, + getLoader: context => { + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + return getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKey, undefined, true); + }, }); this.menuService.addCreator({ @@ -156,57 +157,65 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'connection-management', - contexts: [DATA_CONTEXT_CONNECTION], - isActionApplicable: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + actions: [ + ACTION_DELETE, + ACTION_CONNECTION_CHANGE_CREDENTIALS, + ACTION_CONNECTION_EDIT, + ACTION_CONNECTION_DISCONNECT, + ACTION_CONNECTION_DISCONNECT_ALL, + ], + contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], + isActionApplicable: context => { + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (!connection) { - return false; - } - const node = context.get(DATA_CONTEXT_NAV_NODE); + return node.objectFeatures.includes(EObjectFeature.dataSource); + }, + isHidden: (context, action) => { + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = this.connectionInfoResource.get(connectionKey); - if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { - return false; + if (!connection) { + return true; } if (action === ACTION_CONNECTION_DISCONNECT) { - return connection.connected; + return !connection.connected; } if (action === ACTION_CONNECTION_DISCONNECT_ALL) { - return this.connectionsManagerService.hasAnyConnection(true); + return !this.connectionsManagerService.hasAnyConnection(true); } if (action === ACTION_DELETE) { - return connection.canDelete; + return !connection.canDelete; } if (action === ACTION_CONNECTION_EDIT) { - return connection.canEdit || connection.canViewSettings; + return !(connection.canEdit || connection.canViewSettings); } if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { - return this.serverConfigResource.distributed && !connection.sharedCredentials; + return !this.serverConfigResource.distributed || connection.sharedCredentials; } - return false; + return true; }, getLoader: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION)!; - + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { return getCachedMapResourceLoaderState( this.connectionInfoResource, - () => createConnectionParam(connection), + () => connectionKey, () => ['includeCredentialsSaved' as const], true, ); } - return []; + return getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKey, undefined, true); }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = await this.connectionInfoResource.load(connectionKey); switch (action) { case ACTION_CONNECTION_DISCONNECT: { diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index ebfef01efd..a70171e7a4 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -78,6 +78,8 @@ export class ConnectionFoldersBootstrap extends Bootstrap { private readonly navNodeInfoResource: NavNodeInfoResource, private readonly projectInfoResource: ProjectInfoResource, private readonly projectsNavNodeService: ProjectsNavNodeService, + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // private readonly navigationTreeService: NavigationTreeService, private readonly treeSelectionService: TreeSelectionService, ) { super(); @@ -159,17 +161,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { }); this.menuService.addCreator({ + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // root: true, menus: [MENU_NAVIGATION_TREE_CREATE], contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], - getItems: (context, items) => [...items, ACTION_TREE_CREATE_FOLDER], - }); - - this.actionService.addHandler({ - id: 'nav-tree-create-create-folders-handler', - menus: [MENU_NAVIGATION_TREE_CREATE], - contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], - actions: [ACTION_TREE_CREATE_FOLDER], - isActionApplicable: (context, action) => { + isApplicable: context => { const node = context.get(DATA_CONTEXT_NAV_NODE)!; const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; const targetNode = this.treeSelectionService.getFirstSelectedNode( @@ -181,8 +177,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { ); if ( - action !== ACTION_TREE_CREATE_FOLDER || - ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || + ![isConnectionFolder, isProjectNode].some(check => check(node)) || !this.userInfoResource.isAuthenticated() || tree.baseRoot !== ROOT_NODE_PATH || targetNode === undefined @@ -192,6 +187,14 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return true; }, + getItems: (context, items) => [...items, ACTION_TREE_CREATE_FOLDER], + }); + + this.actionService.addHandler({ + id: 'nav-tree-create-create-folders-handler', + // menus: [MENU_NAVIGATION_TREE_CREATE], + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + actions: [ACTION_TREE_CREATE_FOLDER], getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.elementsTreeActionHandler.bind(this), }); @@ -301,7 +304,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { } const result = await this.commonDialogService.open(FolderDialog, { - value: this.localizationService.translate('ui_folder_new'), + value: this.localizationService.translate('ui_folder_new_default_name'), projectId: targetNode.projectId, folder: parentFolderParam?.folderId, title: 'core_view_action_new_folder', @@ -335,6 +338,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { ? getConnectionFolderId(createConnectionFolderParam(result.projectId, result.folder)) : getProjectNodeId(result.projectId), ); + + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // const newFolderId = getConnectionFolderId(createConnectionFolderParam(result.projectId, createPath(result.folder, result.name))); + // await this.navNodeInfoResource.loadNodeParents(newFolderId); + // await this.navigationTreeService.showNode(newFolderId, this.navNodeInfoResource.getParents(newFolderId)); } catch (exception: any) { this.notificationService.logException(exception, "Can't create folder"); } diff --git a/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts similarity index 79% rename from webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts rename to webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts index c78e4a9c54..6942c4c466 100644 --- a/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts @@ -19,14 +19,19 @@ import { import { getProjectNodeId } from '@cloudbeaver/core-projects'; import { isResourceAlias, type ResourceKey, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { ServerEventId } from '@cloudbeaver/core-root'; - -import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA.js'; -import { ConnectionFolderEventHandler, type IConnectionFolderEvent } from '../ConnectionFolderEventHandler.js'; -import { type Connection, ConnectionInfoActiveProjectKey, ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource.js'; -import { ConnectionsManagerService } from '../ConnectionsManagerService.js'; -import { ContainerResource } from '../ContainerResource.js'; -import { getConnectionParentId } from './getConnectionParentId.js'; -import { getFolderNodeParents } from './getFolderNodeParents.js'; +import { + ConnectionInfoResource, + ContainerResource, + ConnectionsManagerService, + getFolderNodeParents, + type Connection, + ConnectionInfoActiveProjectKey, + type IConnectionInfoParams, + getConnectionParentId, + createConnectionParam, + ConnectionFolderEventHandler, + type IConnectionFolderEvent, +} from '@cloudbeaver/core-connections'; @injectable() export class ConnectionNavNodeService extends Dependency { @@ -36,6 +41,8 @@ export class ConnectionNavNodeService extends Dependency { private readonly containerResource: ContainerResource, private readonly navNodeInfoResource: NavNodeInfoResource, private readonly navNodeManagerService: NavNodeManagerService, + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // private readonly navigationTreeService: NavigationTreeService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly connectionFolderEventHandler: ConnectionFolderEventHandler, ) { @@ -196,51 +203,57 @@ export class ConnectionNavNodeService extends Dependency { return; } - const parentId = getConnectionParentId(connection.projectId, connection.folder); + try { + const parentId = getConnectionParentId(connection.projectId, connection.folder); - await this.navTreeResource.waitLoad(); + await this.navTreeResource.waitLoad(); - if (!this.navTreeResource.has(parentId)) { - await this.navNodeInfoResource.loadNodeParents(parentId); - const parents = this.navNodeInfoResource.getParents(parentId); + if (!this.navTreeResource.has(parentId)) { + await this.navNodeInfoResource.loadNodeParents(parentId); + const parents = this.navNodeInfoResource.getParents(parentId); - this.navTreeResource.markOutdated(parents[parents.length - 1]); - const preloaded = await this.navTreeResource.preloadNodeParents(parents, parentId); + this.navTreeResource.markOutdated(parents[parents.length - 1]); + const preloaded = await this.navTreeResource.preloadNodeParents(parents, parentId); - if (!preloaded) { - return; + if (!preloaded) { + return; + } } - } - let children = this.navTreeResource.get(parentId); + let children = this.navTreeResource.get(parentId); - if (!children || children.includes(connection.nodePath)) { - return; - } + if (!children || children.includes(connection.nodePath)) { + return; + } - const connectionNode = await this.navNodeInfoResource.load(connection.nodePath); - await this.navTreeResource.waitLoad(); + const connectionNode = await this.navNodeInfoResource.load(connection.nodePath); + await this.navTreeResource.waitLoad(); - this.navNodeInfoResource.setParent(connection.nodePath, parentId); + this.navNodeInfoResource.setParent(connection.nodePath, parentId); - children = this.navTreeResource.get(parentId); + children = this.navTreeResource.get(parentId); - if (!children || children.includes(connection.nodePath)) { - return; // double check - } + if (!children || children.includes(connection.nodePath)) { + return; // double check + } - let insertIndex = 0; + let insertIndex = 0; - const nodes = this.navNodeInfoResource.get(resourceKeyList(children)); + const nodes = this.navNodeInfoResource.get(resourceKeyList(children)); - for (const node of nodes) { - if (!node?.folder && node?.name?.localeCompare(connectionNode.name!) === 1) { - break; + for (const node of nodes) { + if (!node?.folder && node?.name?.localeCompare(connectionNode.name!) === 1) { + break; + } + insertIndex++; } - insertIndex++; - } - this.navTreeResource.insertToNode(parentId, insertIndex, connection.nodePath); + this.navTreeResource.insertToNode(parentId, insertIndex, connection.nodePath); + } finally { + // TODO: https://dbeaver.atlassian.net/browse/CB-6272 + // await this.navNodeInfoResource.loadNodeParents(connection.nodePath); + // await this.navigationTreeService.showNode(connection.nodePath, this.navNodeInfoResource.getParents(connection.nodePath)); + } } private async navigateHandler({ nodeId }: INodeNavigationData, contexts: IExecutionContextProvider): Promise { diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index d10ffc9f40..626d15be3d 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -27,6 +27,7 @@ export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthMod export * from './ContextMenu/MENU_CONNECTION_VIEW.js'; export * from './ContextMenu/MENU_CONNECTIONS.js'; export * from './PublicConnectionForm/PublicConnectionFormService.js'; +export * from './NavNodes/ConnectionNavNodeService.js'; export * from './ConnectionAuthService.js'; export * from './PluginConnectionsSettingsService.js'; export * from './ConnectionShieldLazy.js'; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index c14a187a37..3c4c0da1c1 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -44,6 +44,6 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], - ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/fr.ts b/webapp/packages/plugin-connections/src/locales/fr.ts index 15080c6c5c..61a6f58029 100644 --- a/webapp/packages/plugin-connections/src/locales/fr.ts +++ b/webapp/packages/plugin-connections/src/locales/fr.ts @@ -53,6 +53,6 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info', "Vous pouvez gérer les identifiants dans l'onglet "], ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Onglet Identifiants'], ['plugin_connections_connection_auth_secret_description', 'Veuillez sélectionner les identifiants fournis par une de vos équipes'], - ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index d73f119492..069732c5d5 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -46,6 +46,6 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], - ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index ccaafc4475..77b3d7556a 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -44,6 +44,6 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'У вас есть несколько учетных записей для авторизации.\nВыберите учетную запись из списка.', ], - ['plugin_connections_connection_create_menu_title', 'Подключение'], + ['plugin_connections_connection_create_menu_title', 'Новое Подключение'], ['plugin_connections_connection_driver_not_installed_message', 'Драйвер не установлен. Вы можете установить его в "Администрированой" части.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index 79d3d9f7aa..79a4182ed6 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -40,6 +40,6 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info', '您可在此管理凭证 '], ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', '凭证页签'], ['plugin_connections_connection_auth_secret_description', '有多个凭证可用于身份验证.\n请选择您要使用的凭证。'], - ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/manifest.ts b/webapp/packages/plugin-connections/src/manifest.ts index 642fc7c779..0bf221c773 100644 --- a/webapp/packages/plugin-connections/src/manifest.ts +++ b/webapp/packages/plugin-connections/src/manifest.ts @@ -26,5 +26,6 @@ export const connectionPlugin: PluginManifest = { () => import('./NavNodes/ConnectionFoldersBootstrap.js').then(m => m.ConnectionFoldersBootstrap), () => import('./ConnectionForm/SSL/ConnectionSSLTabService.js').then(m => m.ConnectionSSLTabService), () => import('./PluginConnectionsSettingsService.js').then(m => m.PluginConnectionsSettingsService), + () => import('./NavNodes/ConnectionNavNodeService.js').then(m => m.ConnectionNavNodeService), ], }; diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index fe514e48ba..b423be9fad 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { createConnectionParam, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { LocalizationService } from '@cloudbeaver/core-localization'; @@ -35,6 +35,7 @@ export class DataExportMenuService { private readonly menuService: MenuService, private readonly localizationService: LocalizationService, private readonly dataViewerService: DataViewerService, + private readonly connectionInfoResource: ConnectionInfoResource, ) {} register(): void { @@ -130,11 +131,12 @@ export class DataExportMenuService { contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], handler: async context => { const node = context.get(DATA_CONTEXT_NAV_NODE)!; - const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = await this.connectionInfoResource.load(connectionKey); const fileName = withTimestamp(`${connection.name}${node.name ? ` - ${node.name}` : ''}`); this.commonDialogService.open(DataExportDialog, { - connectionKey: createConnectionParam(connection), + connectionKey, name: node.name, fileName, containerNodePath: node.id, diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index c696ef956b..a73379b0f9 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -9,6 +9,7 @@ import { AppAuthService } from '@cloudbeaver/core-authentication'; import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { compareConnectionsInfo, + ConnectionInfoActiveProjectKey, ConnectionInfoResource, ConnectionsManagerService, ConnectionsSettingsService, @@ -85,7 +86,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { iconComponent: () => ConnectionIcon, hideIfEmpty: () => false, getExtraProps: () => ({ connectionKey: this.connectionSchemaManagerService.currentConnectionKey, small: true }), - getLoader: (context, menu) => { + getLoader: () => { if (this.isHidden()) { return []; } @@ -93,12 +94,13 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { const activeConnectionKey = this.connectionSchemaManagerService.activeConnectionKey; if (!activeConnectionKey) { - return this.appAuthService.loaders; + return [...this.appAuthService.loaders, getCachedMapResourceLoaderState(this.connectionInfoResource, () => ConnectionInfoActiveProjectKey)]; } return [ ...this.appAuthService.loaders, ...this.connectionSchemaManagerService.currentObjectLoaders, + getCachedMapResourceLoaderState(this.connectionInfoResource, () => ConnectionInfoActiveProjectKey), getCachedMapResourceLoaderState(this.containerResource, () => ({ ...activeConnectionKey, catalogId: this.connectionSchemaManagerService.activeObjectCatalogId, diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 49078b088d..3bdf20043b 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -161,7 +161,7 @@ export class ConnectionSchemaManagerService { return; } - return this.connectionInfo.get(this.currentConnectionKey); + return this.connectionInfoResource.get(this.currentConnectionKey); } get currentObjectCatalog(): ObjectContainer | undefined { @@ -250,7 +250,7 @@ export class ConnectionSchemaManagerService { constructor( private readonly navigationTabsService: NavigationTabsService, - private readonly connectionInfo: ConnectionInfoResource, + private readonly connectionInfoResource: ConnectionInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, private readonly dbDriverResource: DBDriverResource, private readonly notificationService: NotificationService, @@ -422,7 +422,7 @@ export class ConnectionSchemaManagerService { return; } - const connection = this.connectionInfo.get(key); + const connection = this.connectionInfoResource.get(key); if (!connection) { console.warn(`Connection Schema Manager: connection (${serializeConnectionParam(key)}) not exists`); diff --git a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTree.tsx b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTree.tsx index 5a16484e31..9fa89fb0d9 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTree.tsx +++ b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTree.tsx @@ -25,6 +25,7 @@ import { validateElementsTreeSettings, } from '@cloudbeaver/plugin-navigation-tree'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter.js'; import { navigationTreeProjectSearchCompare } from './ProjectsRenderer/navigationTreeProjectSearchCompare.js'; @@ -52,6 +53,7 @@ export const ResourceManagerTree: React.FC = observer(function ResourceMa const navTreeService = useService(NavigationTreeService); const resourceManagerService = useService(ResourceManagerService); const navTreeResource = useService(NavTreeResource); + const userInfoResource = useService(UserInfoResource); const key = getComputed(() => projectsService.activeProjects.map(project => getRmResourcePath(project.id)), isArraysEqual); useResource(ResourceManagerTree, ResourceManagerResource, resourceKeyList(key)); @@ -100,13 +102,22 @@ export const ResourceManagerTree: React.FC = observer(function ResourceMa const settingsElements = useMemo(() => [ProjectsSettingsPlaceholderElement], []); + async function loadChildren(nodeId: string, manual: boolean) { + await userInfoResource.load(); + if (!resourceManagerService.enabled) { + return false; + } + + return navTreeService.loadNestedNodes(nodeId, manual); + } + return ( (function TreeNodeMenu({ node, actions, selected, contextMenuPosition, onClose }) { const styles = useS(style); - const connectionsInfoResource = useService(ConnectionInfoResource); + const connectionInfoResource = useService(ConnectionInfoResource); const menu = useMenu({ menu: MENU_NAV_TREE }); - const connection = getComputed(() => connectionsInfoResource.getConnectionForNode(node.id)); + const connectionKey = getComputed(() => connectionInfoResource.getConnectionIdForNodeId(node.projectId!, node.id)); useDataContextLink(menu.context, (context, id) => { context.set(DATA_CONTEXT_NAV_NODE, node, id); context.set(DATA_CONTEXT_NAV_NODE_ACTIONS, actions, id); - if (connection) { - context.set(DATA_CONTEXT_CONNECTION, connection, id); + if (connectionKey) { + context.set(DATA_CONTEXT_CONNECTION, connectionKey, id); } }); 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 f3f2aadbfc..183ecd117e 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -17,19 +17,12 @@ import { useResource, useUserData, } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import { - CachedMapAllKey, - CachedResourceOffsetPageKey, - CachedResourceOffsetPageTargetKey, - getNextPageOffset, - ResourceKeyUtils, -} from '@cloudbeaver/core-resource'; +import { type NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; +import { ProjectsService } from '@cloudbeaver/core-projects'; +import { CachedResourceOffsetPageKey, CachedResourceOffsetPageTargetKey, getNextPageOffset, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import type { IDNDData } from '@cloudbeaver/core-ui'; import { type ILoadableState, MetadataMap, throttle } from '@cloudbeaver/core-utils'; @@ -147,11 +140,9 @@ export interface IElementsTree extends ILoadableState { export function useElementsTree(options: IOptions): IElementsTree { const projectsService = useService(ProjectsService); - const projectInfoResource = useService(ProjectInfoResource); const notificationService = useService(NotificationService); const navNodeInfoResource = useService(NavNodeInfoResource); const navTreeResource = useService(NavTreeResource); - const connectionInfoResource = useService(ConnectionInfoResource); const elementsTreeService = useService(ElementsTreeService); const [localTreeNodesState] = useState( @@ -189,8 +180,6 @@ export function useElementsTree(options: IOptions): IElementsTree { const functionsRef = useObjectRef({ async loadTree(...nodes: string[]) { await Promise.all(loadingNodes.values()); - await projectInfoResource.load(); - await connectionInfoResource.load(ConnectionInfoActiveProjectKey); const preloadedRoot = await elementsTree.loadPath(options.folderExplorer.state.fullPath); if (preloadedRoot !== options.folderExplorer.state.folder) { @@ -218,11 +207,6 @@ export function useElementsTree(options: IOptions): IElementsTree { }, async loadNode(nodeId: string) { - await projectInfoResource.waitLoad(); - await connectionInfoResource.waitLoad(); - await navTreeResource.waitLoad(); - await navNodeInfoResource.waitLoad(); - const expanded = elementsTree.isNodeExpanded(nodeId, true); if (!expanded && nodeId !== options.root) { if (navNodeInfoResource.isOutdated(nodeId)) { @@ -757,12 +741,6 @@ export function useElementsTree(options: IOptions): IElementsTree { onData: () => loadTreeThreshold(), }); - useResource(useElementsTree, ProjectInfoResource, CachedMapAllKey, { - onData: () => { - loadTreeThreshold(); - }, - }); - useExecutor({ executor: navNodeInfoResource.onDataOutdated, postHandlers: [loadTreeThreshold], @@ -796,11 +774,6 @@ export function useElementsTree(options: IOptions): IElementsTree { ], }); - useExecutor({ - executor: projectInfoResource.onDataOutdated, - handlers: [() => navTreeResource.markOutdated(ROOT_NODE_PATH)], - }); - useExecutor({ executor: navTreeResource.onItemUpdate, handlers: [ diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts index f60035e4f6..197ac3004c 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts @@ -8,9 +8,9 @@ import { action, makeObservable } from 'mobx'; import { + type Connection, ConnectionInfoResource, ConnectionsManagerService, - createConnectionParam, type IConnectionInfoParams, NavNodeExtensionsService, } from '@cloudbeaver/core-connections'; @@ -88,10 +88,16 @@ export class NavigationTreeService extends View { async loadNestedNodes(id = ROOT_NODE_PATH, tryConnect?: boolean): Promise { if (this.isConnectionNode(id)) { - let connection = this.connectionInfoResource.getConnectionForNode(id); + const node = this.navNodeInfoResource.get(id); - if (connection) { - connection = await this.connectionInfoResource.load(createConnectionParam(connection)); + if (!node?.projectId) { + return false; + } + + const connectionParam = this.connectionInfoResource.getConnectionIdForNodeId(node.projectId, id); + let connection: Connection | undefined; + if (connectionParam) { + connection = await this.connectionInfoResource.load(connectionParam); } else { return false; } @@ -101,22 +107,20 @@ export class NavigationTreeService extends View { return false; } - const connected = await this.tryInitConnection(createConnectionParam(connection)); + const connected = await this.tryInitConnection(connectionParam); if (!connected) { return false; } } } - await this.navTreeResource.waitLoad(); - if (tryConnect && this.navTreeResource.getException(id)) { this.navTreeResource.markOutdated(id); } - const parents = this.navNodeInfoResource.getParents(id); + const preloaded = await this.navTreeResource.preloadParents(id); - if (parents.length > 0 && !this.navNodeInfoResource.has(id)) { + if (!preloaded) { return false; } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx index 8706c35bb3..ded2ba7122 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx @@ -30,18 +30,18 @@ interface Props { export const Menu = observer(function Menu({ value, node }) { const styles = useS(classes); const navNodeManagerService = useService(NavNodeManagerService); - const connectionsInfoResource = useService(ConnectionInfoResource); + const connectionInfoResource = useService(ConnectionInfoResource); const menu = useMenu({ menu: MENU_NAV_TREE }); const mouse = useMouse(); const [menuOpened, switchState] = useState(false); - const connection = connectionsInfoResource.getConnectionForNode(node.id); + const connectionKey = connectionInfoResource.getConnectionIdForNodeId(node.projectId!, node.id); const contextMenuPosition = useContextMenuPosition(); useDataContextLink(menu.context, (context, id) => { context.set(DATA_CONTEXT_NAV_NODE, node, id); - if (connection) { - context.set(DATA_CONTEXT_CONNECTION, connection, id); + if (connectionKey) { + context.set(DATA_CONTEXT_CONNECTION, connectionKey, id); } }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index 99308cb14c..a2c57085d3 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -12,7 +12,6 @@ import { ConnectionExecutionContextResource, ConnectionInfoActiveProjectKey, ConnectionInfoResource, - ConnectionNavNodeService, connectionProvider, createConnectionParam, executionContextProvider, @@ -35,6 +34,7 @@ import { import { projectProvider } from '@cloudbeaver/core-projects'; import { type ResourceKey, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { type ITab, NavigationTabsService, TabHandler } from '@cloudbeaver/plugin-navigation-tabs'; +import { ConnectionNavNodeService } from '@cloudbeaver/plugin-connections'; import type { IObjectViewerTabContext } from './IObjectViewerTabContext.js'; import type { IObjectViewerTabState } from './IObjectViewerTabState.js'; diff --git a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts index 3f27ceb767..11e2b79982 100644 --- a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts @@ -8,7 +8,7 @@ import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { SideBarPanelService } from '@cloudbeaver/core-ui'; import { ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { MENU_TOOLS } from '@cloudbeaver/plugin-tools-panel'; @@ -21,11 +21,11 @@ const ResourceManagerScripts = importLazyComponent(() => import('./ResourceManag @injectable() export class PluginBootstrap extends Bootstrap { constructor( + private readonly userInfoResource: UserInfoResource, private readonly sideBarPanelService: SideBarPanelService, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, private readonly menuService: MenuService, private readonly actionService: ActionService, - private readonly serverConfigResource: ServerConfigResource, ) { super(); } @@ -37,6 +37,7 @@ export class PluginBootstrap extends Bootstrap { order: 0, name: 'plugin_resource_manager_scripts_title', isHidden: () => !this.resourceManagerScriptsService.active, + getLoader: () => [getCachedDataResourceLoaderState(this.userInfoResource, () => undefined)], onClose: this.resourceManagerScriptsService.togglePanel, panel: () => ResourceManagerScripts, }); @@ -57,12 +58,7 @@ export class PluginBootstrap extends Bootstrap { actions: [ACTION_RESOURCE_MANAGER_SCRIPTS], isHidden: () => !this.resourceManagerScriptsService.enabled, isChecked: () => this.resourceManagerScriptsService.active, - getLoader: () => - getCachedDataResourceLoaderState( - this.serverConfigResource, - () => undefined, - () => undefined, - ), + getLoader: () => [getCachedDataResourceLoaderState(this.userInfoResource, () => undefined)], handler: (context, action) => { switch (action) { case ACTION_RESOURCE_MANAGER_SCRIPTS: { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index 67e1758a95..45bb2fed94 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -159,11 +159,11 @@ export class SqlEditorBootstrap extends Bootstrap { break; } case ACTION_SQL_EDITOR_OPEN: { - const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; this.sqlEditorNavigatorService.openNewEditor({ dataSourceKey: LocalStorageSqlDataSource.key, - connectionKey: createConnectionParam(connection), + connectionKey, }); break; }