mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Cb 4112 open scripts datasets from virtual file systems (#2089)
* CB-4125 use base path * CB-4112 feat: read objectId * CB-4145 add required auth to fs system api * CB-4145 move feature * CB-4112 feat: open scripts from file systems * CB-4125 improve file reading * CB-4112 refactor: naming * CB-4112 update schema * CB-4112 fs by id gql * CB-4112 unique fs id * CB-4112 fix gql schema * CB-4112 fix gql schema * CB-4112 update manifest * CB-4112 feat: require authentication for nav-tree --------- Co-authored-by: Aleksey Potsetsuev <wrouds@gmail.com> Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
co-authored by
Aleksey Potsetsuev
Daria Marutkina
parent
0f9e7b650c
commit
cb60358ae6
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+4
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,4 @@
|
||||
class="io.cloudbeaver.service.fs.WebServiceBindingFS">
|
||||
</service>
|
||||
</extension>
|
||||
<extension point="io.cloudbeaver.feature">
|
||||
<feature id="fileSystems" label="File Systems" description="File Systems"/>
|
||||
</extension>
|
||||
</plugin>
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
+11
-2
@@ -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,
|
||||
|
||||
+9
-2
@@ -45,6 +45,13 @@ public class WebServiceBindingFS extends WebServiceBindingBase<DBWServiceFS> 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<DBWServiceFS> 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"),
|
||||
|
||||
+43
-9
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement, IContainerProps & React.HTMLAttributes<HTMLDivElement>>(function ColoredContainer(
|
||||
{ className, ...rest },
|
||||
ref,
|
||||
) {
|
||||
interface Props extends IContainerProps, React.HTMLAttributes<HTMLDivElement> {
|
||||
surface?: boolean;
|
||||
}
|
||||
|
||||
export const ColoredContainer = forwardRef<HTMLDivElement, Props>(function ColoredContainer({ className, surface, ...rest }, ref) {
|
||||
const styles = useS(coloredContainerStyles, containerStyles, elementsSizeStyles);
|
||||
const divProps = filterContainerFakeProps(rest);
|
||||
const containerProps = getContainerProps(rest);
|
||||
|
||||
return <div ref={ref} {...divProps} className={s(styles, { coloredContainer: true, container: true, ...containerProps }, className)} />;
|
||||
return <div ref={ref} {...divProps} className={s(styles, { surface, secondary: !surface, container: true, ...containerProps }, className)} />;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}`));
|
||||
}
|
||||
|
||||
@@ -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<ISessionAction | null>) {
|
||||
const action = contexts.getContext(sessionActionContext);
|
||||
|
||||
|
||||
+23
@@ -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<IElementsTreeLoadData>;
|
||||
|
||||
constructor() {
|
||||
this.onLoad = new Executor();
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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<boolean> {
|
||||
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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+20
-10
@@ -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<string>;
|
||||
read(dataSource: ResourceSqlDataSource, key: string): Promise<string>;
|
||||
@@ -103,7 +100,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
}
|
||||
|
||||
private actions?: IResourceActions;
|
||||
private info?: IResourceInfo;
|
||||
private lastAction: (() => Promise<void>) | 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();
|
||||
}
|
||||
|
||||
|
||||
+6
-39
@@ -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, 'dataSourceStateState' | 'createState'>(this, {
|
||||
createState: action,
|
||||
@@ -87,7 +84,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
}
|
||||
|
||||
register(): void | Promise<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {}
|
||||
private async focusChangeHandler(focused: boolean) {
|
||||
if (focused) {
|
||||
const dataSources = this.sqlDataSourceService.dataSources.values();
|
||||
|
||||
for (const [_, dataSource] of dataSources) {
|
||||
dataSource.markOutdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user