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:
Alexander Skoblikov
2023-10-31 17:18:32 +03:00
committed by GitHub
co-authored by Aleksey Potsetsuev Daria Marutkina
parent 0f9e7b650c
commit cb60358ae6
22 changed files with 311 additions and 138 deletions
@@ -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
@@ -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!
}
@@ -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,
@@ -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"),
@@ -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);
@@ -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();
}
}
@@ -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],
});
}
@@ -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();
}
@@ -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();
}
}
}
}