diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 810c538edd..9f16c555c9 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -27,6 +27,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.websocket, io.cloudbeaver.model, io.cloudbeaver.model.app, + io.cloudbeaver.model.fs, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java new file mode 100644 index 0000000000..9a8a4f240d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java @@ -0,0 +1,27 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.model.fs; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; + +public class FSUtils { + @NotNull + public static String makeUniqueFsId(@NotNull DBFVirtualFileSystem fileSystem) { + return fileSystem.getType() + ":" + fileSystem.getId(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index d38686e06e..4bc24544d4 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -78,7 +78,7 @@ type NavigatorNodeInfo { # Associated object. Return value depends on the node type - connectionId for connection node, resource path for resource node, etc. # null - if node currently not support this property - objectId: String + objectId: String @since(version: "23.2.4") # Supported features: item, container, leaf # canDelete, canRename diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index 3aa7f4e777..b53a5c39c8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.rm.DBNResourceManagerProject; import io.cloudbeaver.model.rm.DBNResourceManagerResource; import io.cloudbeaver.model.session.WebSession; @@ -32,6 +33,7 @@ import org.jkiss.dbeaver.model.edit.DBEObjectRenamer; import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystem; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; @@ -283,6 +285,8 @@ public class WebNavigatorNodeInfo { public String getObjectId() { if (node instanceof DBNPathBase dbnPath) { return dbnPath.getPath().toUri().toString(); + } else if (node instanceof DBNFileSystem dbnFs) { + return FSUtils.makeUniqueFsId(dbnFs.getFileSystem()); } return null; } diff --git a/server/bundles/io.cloudbeaver.service.fs/plugin.xml b/server/bundles/io.cloudbeaver.service.fs/plugin.xml index 09ad0fdeca..e3cbaf3f82 100644 --- a/server/bundles/io.cloudbeaver.service.fs/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.fs/plugin.xml @@ -8,7 +8,4 @@ class="io.cloudbeaver.service.fs.WebServiceBindingFS"> - - - diff --git a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls index 558d088c43..50ce1ef548 100644 --- a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls +++ b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls @@ -5,8 +5,15 @@ type FSFile @since(version: "23.2.2") { metaData: Object! } +type FSFileSystem @since(version: "23.2.4") { + id: ID! + requiredAuth: String +} + extend type Query @since(version: "23.2.2") { - fsListFileSystems(projectId: ID!): [String!]! + fsListFileSystems(projectId: ID!): [FSFileSystem!]! + + fsFileSystem(projectId: ID!, fileSystemId: ID!): FSFileSystem! @since(version: "23.2.4") fsFile(projectId: ID!, fileURI: String!): FSFile! @@ -21,14 +28,14 @@ extend type Mutation @since(version: "23.2.2") { fsCreateFolder(projectId: ID!, folderURI:String!): FSFile! - fsDeleteFile(projectId: ID!, fileURI:String!): Boolean! + fsDelete(projectId: ID!, fileURI:String!): Boolean! - fsMoveFile(projectId: ID!, fromURI: String!, toURI: String!): FSFile! + fsMove(projectId: ID!, fromURI: String!, toURI: String!): FSFile! fsWriteFileStringContent( projectId: ID!, fileURI:String!, data: String!, forceOverwrite: Boolean! - ): Boolean! + ): FSFile! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java index 461c088b0e..e4d37b4072 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; import java.net.URI; @@ -29,9 +30,17 @@ import java.net.URI; */ public interface DBWServiceFS extends DBWService { @NotNull - String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException; + + @NotNull + FSFileSystem getFileSystem( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String fileSystemId + ) throws DBWebException; + @NotNull FSFile getFile( @NotNull WebSession webSession, @@ -53,7 +62,7 @@ public interface DBWServiceFS extends DBWService { @NotNull URI fileURI ) throws DBWebException; - boolean writeFileContent( + FSFile writeFileContent( @NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileURI, diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java index 854352edcb..4f7e5e6552 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java @@ -45,6 +45,13 @@ public class WebServiceBindingFS extends WebServiceBindingBase imp model.getQueryType() .dataFetcher("fsListFileSystems", env -> getService(env).getAvailableFileSystems(getWebSession(env), env.getArgument("projectId"))) + .dataFetcher("fsFileSystem", + env -> getService(env).getFileSystem( + getWebSession(env), + env.getArgument("projectId"), + env.getArgument("fileSystemId") + ) + ) .dataFetcher("fsFile", env -> getService(env).getFile(getWebSession(env), env.getArgument("projectId"), @@ -77,13 +84,13 @@ public class WebServiceBindingFS extends WebServiceBindingBase imp URI.create(env.getArgument("folderURI")) ) ) - .dataFetcher("fsDeleteFile", + .dataFetcher("fsDelete", env -> getService(env).deleteFile(getWebSession(env), env.getArgument("projectId"), URI.create(env.getArgument("fileURI")) ) ) - .dataFetcher("fsMoveFile", + .dataFetcher("fsMove", env -> getService(env).moveFile( getWebSession(env), env.getArgument("projectId"), diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java index 2c0a7596d5..e527149b2f 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java @@ -17,14 +17,17 @@ package io.cloudbeaver.service.fs.impl; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.fs.DBWServiceFS; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -35,19 +38,47 @@ public class WebServiceFS implements DBWServiceFS { @NotNull @Override - public String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + public FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { try { + var fsRegistry = FileSystemProviderRegistry.getInstance(); return webSession.getFileSystemManager(projectId) .getVirtualFileSystems() .stream() - .map(DBFVirtualFileSystem::getType) - .toArray(String[]::new); + .map(fs -> new FSFileSystem( + FSUtils.makeUniqueFsId(fs), + fsRegistry.getProvider(fs.getProviderId()).getRequiredAuth() + ) + ) + .toArray(FSFileSystem[]::new); } catch (Exception e) { throw new DBWebException("Failed to load file systems: " + e.getMessage(), e); } } + @NotNull + @Override + public FSFileSystem getFileSystem( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String fileSystemId + ) throws DBWebException { + try { + var fsRegistry = FileSystemProviderRegistry.getInstance(); + return webSession.getFileSystemManager(projectId) + .getVirtualFileSystems() + .stream() + .filter(fs -> FSUtils.makeUniqueFsId(fs).equals(fileSystemId)) + .findFirst() + .map(fs -> new FSFileSystem( + FSUtils.makeUniqueFsId(fs), + fsRegistry.getProvider(fs.getProviderId()).getRequiredAuth() + )).orElseThrow(() -> new DBWebException("File system not found")); + } catch (Exception e) { + throw new DBWebException("Failed to get file system: " + e.getMessage(), e); + } + } + @NotNull @Override public FSFile getFile(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) @@ -80,15 +111,17 @@ public class WebServiceFS implements DBWServiceFS { public String readFileContent(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).getPathFromURI(webSession.getProgressMonitor(), fileUri); - return Files.readString(filePath); + Path filePath = webSession.getFileSystemManager(projectId) + .getPathFromURI(webSession.getProgressMonitor(), fileUri); + var data = Files.readAllBytes(filePath); + return new String(data, StandardCharsets.UTF_8); } catch (Exception e) { throw new DBWebException("Failed to read file content: " + e.getMessage(), e); } } @Override - public boolean writeFileContent( + public FSFile writeFileContent( @NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileURI, @@ -97,12 +130,13 @@ public class WebServiceFS implements DBWServiceFS { ) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).getPathFromURI(webSession.getProgressMonitor(), fileURI); + Path filePath = webSession.getFileSystemManager(projectId) + .getPathFromURI(webSession.getProgressMonitor(), fileURI); if (!forceOverwrite && Files.exists(filePath)) { throw new DBException("Cannot overwrite exist file"); } Files.writeString(filePath, data); - return true; + return new FSFile(filePath); } catch (Exception e) { throw new DBWebException("Failed to write file content: " + e.getMessage(), e); } diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css b/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css index 2d38b9b425..4c30c0cb88 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css @@ -1,3 +1,6 @@ -.coloredContainer { +.secondary { composes: theme-background-secondary theme-text-on-secondary from global; } +.surface { + composes: theme-background-surface theme-text-on-surface from global; +} diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx index fe7cce0814..ec572b0904 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx @@ -15,13 +15,14 @@ import { filterContainerFakeProps, getContainerProps } from './filterContainerFa import type { IContainerProps } from './IContainerProps'; import elementsSizeStyles from './shared/ElementsSize.m.css'; -export const ColoredContainer = forwardRef>(function ColoredContainer( - { className, ...rest }, - ref, -) { +interface Props extends IContainerProps, React.HTMLAttributes { + surface?: boolean; +} + +export const ColoredContainer = forwardRef(function ColoredContainer({ className, surface, ...rest }, ref) { const styles = useS(coloredContainerStyles, containerStyles, elementsSizeStyles); const divProps = filterContainerFakeProps(rest); const containerProps = getContainerProps(rest); - return
; + return
; }); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts index daf26f1667..fa14691ce9 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts @@ -322,13 +322,13 @@ export class NavNodeManagerService extends Bootstrap { let icon: string | undefined; let canOpen = false; - if (NodeManagerUtils.isDatabaseObject(nodeId)) { - const node = this.getNode(nodeId); - - if (node) { - name = node.name; - icon = node.icon; + const node = this.getNode(nodeId); + if (node) { + name = node.name; + icon = node.icon; + projectId ||= node.projectId; + if (NodeManagerUtils.isDatabaseObject(nodeId)) { if (node.folder) { const parent = this.getParent(node); folderId = nodeId; diff --git a/webapp/packages/core-projects/src/isResourceOfType.ts b/webapp/packages/core-projects/src/isResourceOfType.ts index 2e9f076aba..3f8a9bbfc2 100644 --- a/webapp/packages/core-projects/src/isResourceOfType.ts +++ b/webapp/packages/core-projects/src/isResourceOfType.ts @@ -8,5 +8,5 @@ import type { ProjectInfoResourceType } from './ProjectInfoResource'; export function isResourceOfType(resourceType: ProjectInfoResourceType, name: string): boolean { - return resourceType.fileExtensions.some(type => name.endsWith(`.${type}`)); + return resourceType.fileExtensions.some(type => name.toLowerCase().endsWith(`.${type.toLowerCase()}`)); } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 20ab3ebac6..c89974080a 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -214,8 +214,6 @@ export class AuthenticationService extends Bootstrap { this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler); } - load(): void {} - private async authSessionAction(data: ISessionAction | null, contexts: IExecutionContextProvider) { const action = contexts.getContext(sessionActionContext); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeService.ts new file mode 100644 index 0000000000..891de97644 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeService.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { injectable } from '@cloudbeaver/core-di'; +import { Executor, type IExecutor } from '@cloudbeaver/core-executor'; + +export interface IElementsTreeLoadData { + nodeId: string; + manual: boolean; +} + +@injectable() +export class ElementsTreeService { + readonly onLoad: IExecutor; + + constructor() { + this.onLoad = new Executor(); + } +} 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 7ef4442738..1858edc4fe 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -12,13 +12,14 @@ import { getComputed, IFolderExplorerContext, useExecutor, useObjectRef, useObse import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, 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, getNextPageOffset, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import type { IDNDData } from '@cloudbeaver/core-ui'; import { ILoadableState, MetadataMap, throttle } from '@cloudbeaver/core-utils'; +import { ElementsTreeService } from './ElementsTreeService'; import type { IElementsTreeAction } from './IElementsTreeAction'; import type { INavTreeNodeInfo } from './INavTreeNodeInfo'; import type { NavigationNodeRendererComponent } from './NavigationNodeComponent'; @@ -137,6 +138,7 @@ export function useElementsTree(options: IOptions): IElementsTree { const navNodeInfoResource = useService(NavNodeInfoResource); const navTreeResource = useService(NavTreeResource); const connectionInfoResource = useService(ConnectionInfoResource); + const elementsTreeService = useService(ElementsTreeService); const [localTreeNodesState] = useState( () => @@ -156,6 +158,12 @@ export function useElementsTree(options: IOptions): IElementsTree { async function handleLoadChildren(id: string, manual: boolean): Promise { try { + const context = await elementsTreeService.onLoad.execute({ nodeId: id, manual }); + + if (ExecutorInterrupter.isInterrupted(context)) { + return false; + } + return await options.loadChildren(id, manual); } catch (exception: any) { notificationService.logException(exception); diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index 9f2df7770f..1a763288d1 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -26,6 +26,7 @@ export * from './NavigationTree/ElementsTree/useElementsTree'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService'; export * from './NavigationTree/ElementsTree/elementsTreeNameFilter'; +export * from './NavigationTree/ElementsTree/ElementsTreeService'; export * from './NavigationTree/NavigationTreeBootstrap'; export * from './NavigationTree/NavigationTreeService'; export { default as ElementsTreeToolsStyles } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.m.css'; diff --git a/webapp/packages/plugin-navigation-tree/src/manifest.ts b/webapp/packages/plugin-navigation-tree/src/manifest.ts index a6c87d5c80..6cc907c4ec 100644 --- a/webapp/packages/plugin-navigation-tree/src/manifest.ts +++ b/webapp/packages/plugin-navigation-tree/src/manifest.ts @@ -8,6 +8,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; +import { ElementsTreeService } from './NavigationTree/ElementsTree/ElementsTreeService'; import { ElementsTreeToolsMenuService } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService'; import { ElementsTreeSettingsService } from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService'; import { NavigationTreeBootstrap } from './NavigationTree/NavigationTreeBootstrap'; @@ -27,5 +28,6 @@ export const navigationTreePlugin: PluginManifest = { NavNodeViewService, ElementsTreeSettingsService, NavigationTreeSettingsService, + ElementsTreeService, ], }; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts index f778fd15ab..6c514ee951 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts @@ -14,7 +14,7 @@ import { isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbe import { CachedMapAllKey, CachedTreeChildrenKey } from '@cloudbeaver/core-resource'; import { getRmResourcePath, NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager'; import { createPath, getPathName } from '@cloudbeaver/core-utils'; -import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, KEY_BINDING_SAVE, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; +import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { getResourceKeyFromNodeId } from '@cloudbeaver/plugin-navigation-tree-rm'; import { RESOURCE_NAME_REGEX, ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -55,7 +55,6 @@ export class PluginBootstrap extends Bootstrap { private readonly sqlEditorSettingsService: SqlEditorSettingsService, private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, - private readonly keyBindingService: KeyBindingService, ) { super(); } @@ -83,10 +82,6 @@ export class PluginBootstrap extends Bootstrap { return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; } - if (action === ACTION_SAVE) { - return dataSource?.isAutoSaveEnabled === false; - } - return false; }, handler: async (context, action) => { @@ -187,34 +182,9 @@ export class PluginBootstrap extends Bootstrap { } } } - - if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return; - } - - await source.save(); - } - }, - isDisabled: (context, action) => { - if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return true; - } - - return source.isLoading() || source.isSaved; - } - - return false; }, getActionInfo: (context, action) => { - if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) { + if (action === ACTION_SAVE_AS_SCRIPT) { return { ...action.info, label: '', @@ -241,23 +211,7 @@ export class PluginBootstrap extends Bootstrap { !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) ); }, - getItems: (context, items) => [...items, ACTION_SAVE_AS_SCRIPT, ACTION_SAVE], - }); - - this.keyBindingService.addKeyBindingHandler({ - id: 'script-save', - binding: KEY_BINDING_SAVE, - isBindingApplicable: (context, action) => action === ACTION_SAVE, - handler: async context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return; - } - - await source.save(); - }, + getItems: (context, items) => [...items, ACTION_SAVE_AS_SCRIPT], }); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts index a724107bb6..1d7f71fd93 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts @@ -17,16 +17,13 @@ import { TaskScheduler } from '@cloudbeaver/core-executor'; import type { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; +import type { NetworkStateService } from '@cloudbeaver/core-root'; import { debounce, getPathName, isArraysEqual, isNotNullDefined, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts'; import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; -interface IResourceInfo { - isReadonly?: (dataSource: ResourceSqlDataSource) => boolean; -} - interface IResourceActions { rename(dataSource: ResourceSqlDataSource, key: string, name: string): Promise; read(dataSource: ResourceSqlDataSource, key: string): Promise; @@ -103,7 +100,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { } private actions?: IResourceActions; - private info?: IResourceInfo; private lastAction: (() => Promise) | undefined; private state!: IResourceSqlDataSourceState; @@ -112,6 +108,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { private resourceUseKeyId: string | null; constructor( + private readonly networkStateService: NetworkStateService, private readonly projectInfoResource: ProjectInfoResource, private readonly connectionInfoResource: ConnectionInfoResource, private readonly resourceManagerResource: ResourceManagerResource, @@ -144,10 +141,22 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { } isReadonly(): boolean { - return !this.isLoaded() || this.info?.isReadonly?.(this) === true; + if (!this.projectId || !this.networkStateService.state) { + return true; + } + + const project = this.projectInfoResource.get(this.projectId); + + return !this.isLoaded() || !project?.canEditResources; } isOutdated(): boolean { + if (this.projectId) { + if (this.projectInfoResource.isOutdated(this.projectId)) { + return true; + } + } + return this.resourceKey !== undefined && super.isOutdated(); } @@ -189,10 +198,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.actions = actions; } - setInfo(info?: IResourceInfo): void { - this.info = info; - } - setName(name: string | null): void { name = name?.trim() ?? null; if (!name || name === this.name) { @@ -267,6 +272,11 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { if (this.state.resourceKey && !this.resourceUseKeyId) { this.resourceUseKeyId = this.resourceManagerResource.useTracker.use(this.state.resourceKey); } + + if (this.projectId) { + await this.projectInfoResource.load(this.projectId); + } + await this.read(); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts index fb006be0b6..8f807ee0fa 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, observable, untracked } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; @@ -13,11 +13,11 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; -import { getRmResourceKey, IResourceManagerMoveData, ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; -import { NetworkStateService, WindowEventsService } from '@cloudbeaver/core-root'; +import { resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { IResourceManagerMoveData, ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; +import { NetworkStateService } from '@cloudbeaver/core-root'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; -import { createPath, getPathParent, throttle } from '@cloudbeaver/core-utils'; +import { createPath, getPathParent } from '@cloudbeaver/core-utils'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -30,7 +30,6 @@ import { ResourceSqlDataSource } from './ResourceSqlDataSource'; import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; const RESOURCE_TAB_STATE = 'sql_editor_resource_tab_state'; -const SYNC_DELAY = 5 * 60 * 1000; @injectable() export class ResourceSqlDataSourceBootstrap extends Bootstrap { @@ -49,14 +48,12 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, private readonly projectInfoResource: ProjectInfoResource, - private readonly windowEventsService: WindowEventsService, private readonly sqlEditorTabResourceService: SqlEditorTabResourceService, private readonly sqlEditorService: SqlEditorService, localStorageSaveService: LocalStorageSaveService, ) { super(); this.dataSourceStateState = new Map(); - this.focusChangeHandler = throttle(this.focusChangeHandler.bind(this), SYNC_DELAY, false); makeObservable(this, { createState: action, @@ -87,7 +84,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { } register(): void | Promise { - this.windowEventsService.onFocusChange.addHandler(this.focusChangeHandler.bind(this)); this.resourceManagerResource.onItemDelete.addHandler(this.resourceDeleteHandler.bind(this)); this.resourceManagerResource.onMove.addHandler(this.resourceMoveHandler.bind(this)); @@ -95,6 +91,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { key: ResourceSqlDataSource.key, getDataSource: (editorId, options) => { const dataSource = new ResourceSqlDataSource( + this.networkStateService, this.projectInfoResource, this.connectionInfoResource, this.resourceManagerResource, @@ -118,20 +115,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { setProperties: this.setProperties.bind(this), }); - dataSource.setInfo({ - isReadonly: (dataSource: ResourceSqlDataSource) => { - if (!dataSource.resourceKey) { - return true; - } - const resourceKey = getRmResourceKey(dataSource.resourceKey); - - untracked(() => this.projectInfoResource.load(CachedMapAllKey)); - const project = this.projectInfoResource.get(resourceKey.projectId); - - return !this.networkStateService.state || !project?.canEditResources; - }, - }); - return dataSource; }, onDestroy: (_, editorId) => this.deleteState(editorId), @@ -188,22 +171,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { this.dataSourceStateState.delete(editorId); } - private async focusChangeHandler(focused: boolean) { - if (!this.resourceManagerService.enabled) { - return; - } - - if (focused) { - const dataSources = this.sqlDataSourceService.dataSources - .filter(([, dataSource]) => dataSource instanceof ResourceSqlDataSource) - .map(([, dataSource]) => dataSource as ResourceSqlDataSource); - - for (const dataSource of dataSources) { - dataSource.markOutdated(); - } - } - } - private resourceMoveHandler(data: IResourceManagerMoveData) { if (!this.resourceManagerService.enabled) { return; diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 7b8540f542..e993177ea2 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -7,7 +7,21 @@ */ import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ACTION_REDO, ACTION_UNDO, ActionService, IAction, KEY_BINDING_REDO, KEY_BINDING_UNDO, KeyBindingService } from '@cloudbeaver/core-view'; +import { WindowEventsService } from '@cloudbeaver/core-root'; +import { throttle } from '@cloudbeaver/core-utils'; +import { + ACTION_REDO, + ACTION_SAVE, + ACTION_UNDO, + ActionService, + DATA_CONTEXT_MENU, + IAction, + KEY_BINDING_REDO, + KEY_BINDING_SAVE, + KEY_BINDING_UNDO, + KeyBindingService, + MenuService, +} from '@cloudbeaver/core-view'; import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE'; import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW'; @@ -20,16 +34,114 @@ import { KEY_BINDING_SQL_EDITOR_EXECUTE_NEW } from './actions/bindings/KEY_BINDI import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT'; import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT'; import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from './DATA_CONTEXT_SQL_EDITOR_STATE'; import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; +import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService'; import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA'; +import { SQL_EDITOR_TOOLS_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MENU'; + +const SYNC_DELAY = 5 * 60 * 1000; @injectable() export class MenuBootstrap extends Bootstrap { - constructor(private readonly actionService: ActionService, private readonly keyBindingService: KeyBindingService) { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly keyBindingService: KeyBindingService, + private readonly sqlDataSourceService: SqlDataSourceService, + private readonly windowEventsService: WindowEventsService, + ) { super(); } register(): void { + this.windowEventsService.onFocusChange.addHandler(throttle(this.focusChangeHandler.bind(this), SYNC_DELAY, false)); + this.actionService.addHandler({ + id: 'sql-editor-base-handler', + isActionApplicable: (context, action): boolean => { + const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (!state) { + return false; + } + + const dataSource = this.sqlDataSourceService.get(state.editorId); + + if (action === ACTION_SAVE) { + return dataSource?.isAutoSaveEnabled === false; + } + + return false; + }, + handler: async (context, action) => { + if (action === ACTION_SAVE) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return; + } + + await source.save(); + } + }, + isDisabled: (context, action) => { + if (action === ACTION_SAVE) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return true; + } + + return source.isLoading() || source.isSaved || source.isReadonly(); + } + + return false; + }, + getActionInfo: (context, action) => { + if (action === ACTION_SAVE) { + return { + ...action.info, + label: '', + }; + } + + return action.info; + }, + }); + + this.menuService.addCreator({ + isApplicable: context => { + const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + + if (!state) { + return false; + } + + const dataSource = this.sqlDataSourceService.get(state.editorId); + + return context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_TOOLS_MENU && !!dataSource?.hasFeature(ESqlDataSourceFeatures.script); + }, + getItems: (context, items) => [...items, ACTION_SAVE], + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'sql-editor-save', + binding: KEY_BINDING_SAVE, + isBindingApplicable: (context, action) => action === ACTION_SAVE, + handler: async context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return; + } + + await source.save(); + }, + }); + this.actionService.addHandler({ id: 'sql-editor-actions', isActionApplicable: (contexts, action): boolean => { @@ -179,5 +291,13 @@ export class MenuBootstrap extends Bootstrap { } } - load(): void | Promise {} + private async focusChangeHandler(focused: boolean) { + if (focused) { + const dataSources = this.sqlDataSourceService.dataSources.values(); + + for (const [_, dataSource] of dataSources) { + dataSource.markOutdated(); + } + } + } }