CB-2660 add set resource property mutation to resource manager updated (#1214)

* CB-2660: Add set property mutation to resource manager

* CB-2660 fix: save resource property

* CB-2660 Modify graphql schema to allow properties in RMResource to be null

* CB-2660 fix: resource properties keys

* CB-2660 fix: codestyle

* CB-2660 Read resource properties in navigator model

* CB-2660 Respect nameMask parameter

* CB-2660 feat: bind execution context information

* CB-2660 fix: don't save properties for readonly scripts

* CB-2660 fix: don't link readonly scripts with datasource

* CB-2660 fix: script linking with datasource

* CB-2660 fix: sql editor context switch flickering

* CB-2660 fix(plugin-resource-manager): resource finding

Co-authored-by: Wroud <wroud@yandex.ru>
Co-authored-by: serge-rider <serge@jkiss.org>
Co-authored-by: Alexey <wrouds@gmail.com>
This commit is contained in:
Oksana Cherniavskaia
2022-10-17 23:06:28 +03:00
committed by GitHub
co-authored by Wroud serge-rider Alexey
parent 74b960ed57
commit 39987507a5
36 changed files with 783 additions and 146 deletions
@@ -91,7 +91,7 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode {
if (children == null) {
List<DBNResourceManagerResource> rfList = new ArrayList<>();
for (RMResource resource : getResourceController().listResources(
project.getId(), null, null, false, false, false)) {
project.getId(), null, null, true, false, false)) {
rfList.add(new DBNResourceManagerResource(this, resource));
}
@@ -76,7 +76,7 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
if (children == null) {
List<DBNResourceManagerResource> rfList = new ArrayList<>();
for (RMResource resource : getResourceController().listResources(
getResourceProject().getId(), getResourceFolder(), null, false, false, false)) {
getResourceProject().getId(), getResourceFolder(), null, true, false, false)) {
rfList.add(new DBNResourceManagerResource(this, resource));
}
@@ -347,7 +347,7 @@ public class LocalResourceController implements RMController {
if (!folderPath.startsWith(projectPath)) {
throw new DBException("Invalid folder path");
}
return readChildResources(projectId, folderPath, readProperties, readHistory, recursive);
return readChildResources(projectId, folderPath, nameMask, readProperties, readHistory, recursive);
} catch (NoSuchFileException e) {
throw new DBException("Invalid resource folder " + folder);
} catch (IOException e) {
@@ -359,14 +359,18 @@ public class LocalResourceController implements RMController {
private RMResource[] readChildResources(
@NotNull String projectId,
@NotNull Path folderPath,
@Nullable String nameMask,
boolean readProperties,
boolean readHistory,
boolean recursive
) throws IOException {
try (Stream<Path> files = Files.list(folderPath)) {
return files.filter(path -> !path.getFileName().toString().startsWith(".")) // skip hidden files
return files.filter(path -> {
String fileName = path.getFileName().toString();
return (nameMask == null || nameMask.equals(fileName)) && !fileName.startsWith(".");
}) // skip hidden files
.sorted(Comparator.comparing(path -> path.getFileName().toString(), String.CASE_INSENSITIVE_ORDER))
.map((Path path) -> makeResourceFromPath(projectId, path, readProperties, readHistory, recursive))
.map((Path path) -> makeResourceFromPath(projectId, path, nameMask, readProperties, readHistory, recursive))
.filter(Objects::nonNull)
.toArray(RMResource[]::new);
}
@@ -631,7 +635,7 @@ public class LocalResourceController implements RMController {
for (var resourceName : relativeResourcePath) {
resourcePath = resourcePath.resolve(resourceName);
result.add(makeResourceFromPath(projectId, resourcePath, false, false, recursive));
result.add(makeResourceFromPath(projectId, resourcePath, null, false, false, recursive));
}
return result;
@@ -640,6 +644,7 @@ public class LocalResourceController implements RMController {
private RMResource makeResourceFromPath(
@NotNull String projectId,
@NotNull Path path,
@Nullable String nameMask,
boolean readProperties,
boolean readHistory,
boolean recursive
@@ -687,7 +692,7 @@ public class LocalResourceController implements RMController {
if (recursive && resource.isFolder()) {
try {
resource.setChildren(readChildResources(projectId, path, readProperties, readHistory, true));
resource.setChildren(readChildResources(projectId, path, nameMask, readProperties, readHistory, true));
} catch (IOException e) {
log.error(e);
}
@@ -15,6 +15,8 @@ type RMResource {
name: String!
folder: Boolean!
length: Int!
# Properties map
properties: Object
}
input RMSubjectProjectPermissions {
@@ -90,4 +92,5 @@ extend type Mutation {
rmSetSubjectProjectPermissions(subjectId: String!, permissions: [RMProjectPermissions!]!): Boolean!
rmSetResourceProperty(projectId: String!, resourcePath: String!, name: ID!, value: String): Boolean!
}
@@ -58,6 +58,25 @@ public interface DBWServiceRM extends DBWService {
boolean readProperties,
boolean readHistory) throws DBException;
/**
* Sets resource property.
*
* @param webSession the web session
* @param projectId the project id
* @param resourcePath the resource path
* @param propertyName the property name
* @param propertyValue the property value
* @return the resource property
* @throws DBException the db exception
*/
@WebProjectAction(requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_EDIT)
String setResourceProperty(
@NotNull WebSession webSession,
@NotNull @WebObjectId String projectId,
@NotNull String resourcePath,
@NotNull String propertyName,
@Nullable Object propertyValue) throws DBException;
@WebProjectAction(
requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_VIEW
)
@@ -81,7 +81,6 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
env.getArgument("resourcePath"),
env.getArgument("data"),
env.getArgument("forceOverwrite")))
.dataFetcher("rmCreateProject", env -> getService(env).createProject(
getWebSession(env),
env.getArgument("projectName"),
@@ -96,12 +95,18 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
env.getArgument("projectId"),
new RMSubjectProjectPermissions(env.getArgument("permissions"))
))
.dataFetcher("rmSetResourceProperty", env -> getService(env).setResourceProperty(
getWebSession(env),
env.getArgument("projectId"),
env.getArgument("resourcePath"),
env.getArgument("name"),
env.getArgument("value")
))
.dataFetcher("rmSetSubjectProjectPermissions", env -> getService(env).setSubjectProjectPermissions(
getWebSession(env),
env.getArgument("subjectId"),
new RMProjectPermissions(env.getArgument("permissions"))
))
;
}
}
@@ -43,7 +43,7 @@ import java.util.Set;
public class WebServiceRM implements DBWServiceRM {
@Override
public RMProject[] listProjects(WebSession webSession) throws DBWebException {
public RMProject[] listProjects(@NotNull WebSession webSession) throws DBWebException {
try {
return getResourceController(webSession).listAccessibleProjects();
} catch (DBException e) {
@@ -71,7 +71,13 @@ public class WebServiceRM implements DBWServiceRM {
@NotNull
@Override
public RMResource[] listResources(WebSession webSession, @NotNull String projectId, @Nullable String folder, @Nullable String nameMask, boolean readProperties, boolean readHistory) throws DBException {
public RMResource[] listResources(@NotNull WebSession webSession,
@NotNull String projectId,
@Nullable String folder,
@Nullable String nameMask,
boolean readProperties,
boolean readHistory
) throws DBException {
checkIsRmEnabled(webSession);
try {
return getResourceController(webSession).listResources(projectId, folder, nameMask, readProperties, readHistory, false);
@@ -80,8 +86,40 @@ public class WebServiceRM implements DBWServiceRM {
}
}
/**
* Sets resource property.
*
* @param webSession the web session
* @param projectId the project id
* @param resourcePath the resource path
* @param propertyName the property name
* @param propertyValue the property value
* @return the resource property
* @throws DBException the db exception
*/
@NotNull
@Override
public String readResourceAsString(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath) throws DBException {
public String setResourceProperty(@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull String resourcePath,
@NotNull String propertyName,
@Nullable Object propertyValue
) throws DBException {
checkIsRmEnabled(webSession);
try {
getResourceController(webSession).setResourceProperty(projectId, resourcePath, propertyName, propertyValue);
return Boolean.TRUE.toString();
} catch (DBException e) {
String message = String.format("Error setting property [%s] for the the resource: [%s]", resourcePath, propertyName);
throw new DBWebException(message, e);
}
}
@Override
public String readResourceAsString(@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull String resourcePath
) throws DBException {
checkIsRmEnabled(webSession);
try {
byte[] data = getResourceController(webSession).getResourceContents(projectId, resourcePath);
@@ -92,7 +130,11 @@ public class WebServiceRM implements DBWServiceRM {
}
@Override
public String createResource(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath, boolean isFolder) throws DBException {
public String createResource(@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull String resourcePath,
boolean isFolder
) throws DBException {
checkIsRmEnabled(webSession);
try {
return getResourceController(webSession).createResource(projectId, resourcePath, isFolder);
@@ -102,7 +144,10 @@ public class WebServiceRM implements DBWServiceRM {
}
@Override
public boolean deleteResource(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath) throws DBException {
public boolean deleteResource(@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull String resourcePath
) throws DBException {
checkIsRmEnabled(webSession);
try {
getResourceController(webSession).deleteResource(projectId, resourcePath, false);
@@ -215,7 +260,9 @@ public class WebServiceRM implements DBWServiceRM {
}
@Override
public List<SMObjectPermissionsGrant> listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException {
public List<SMObjectPermissionsGrant> listProjectGrantedPermissions(@NotNull WebSession webSession,
@NotNull String projectId
) throws DBWebException {
SMController sm = webSession.getSecurityController();
try {
return sm.getObjectPermissionGrants(projectId, SMObjects.PROJECT);
@@ -225,7 +272,9 @@ public class WebServiceRM implements DBWServiceRM {
}
@Override
public List<SMObjectPermissionsGrant> listSubjectProjectsPermissionGrants(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException {
public List<SMObjectPermissionsGrant> listSubjectProjectsPermissionGrants(@NotNull WebSession webSession,
@NotNull String subjectId
) throws DBWebException {
try {
SMAdminController sm = webSession.getAdminSecurityController();
return sm.getSubjectObjectPermissionGrants(subjectId, SMObjects.PROJECT);
@@ -33,6 +33,8 @@ export const ConnectionExecutionContextProjectKey = (projectId: string) => resou
projectId
);
export const NOT_INITIALIZED_CONTEXT_ID = '-1';
@injectable()
export class ConnectionExecutionContextResource extends CachedMapResource<string, IConnectionExecutionContextInfo> {
constructor(
@@ -157,7 +159,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
originalKey: ResourceKey<string>
): Promise<Map<string, IConnectionExecutionContextInfo>> {
let projectId: string | undefined;
const all = this.includes(originalKey, CachedMapAllKey);
const all = this.isAliasEqual(originalKey, CachedMapAllKey);
const isProjectFolders = isConnectionExecutionContextProjectKey(originalKey);
originalKey = this.transformParam(originalKey);
@@ -260,3 +262,11 @@ function isConnectionExecutionContextProjectKey(
): param is ResourceKeyList<string> {
return isResourceKeyList(param) && param.list.includes(connectionExecutionContextProjectKeySymbol);
}
export function getRealExecutionContextId(id: string | undefined | null): string | null {
if (id === NOT_INITIALIZED_CONTEXT_ID) {
return null;
}
return id ?? null;
}
@@ -391,7 +391,7 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
includes: CachedResourceIncludeArgs<Connection, ConnectionInfoIncludes>
): Promise<Map<IConnectionInfoParams, Connection>> {
let projectId: string | undefined;
const all = this.includes(originalKey, CachedMapAllKey);
const all = this.isAliasEqual(originalKey, CachedMapAllKey);
const isProjectKey = isConnectionInfoProjectKey(originalKey);
const key = this.transformParam(originalKey);
@@ -418,6 +418,10 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
...this.getIncludesMap(key, includes),
});
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
throw new Error(`Connection is not found (${connectionId})`);
}
runInAction(() => {
if (all) {
this.resetIncludes();
@@ -27,7 +27,7 @@ export interface IScheduleOptions {
const queueLimit = 100;
export class TaskScheduler<TIdentifier> {
export class TaskScheduler<TIdentifier = void> {
get activeList(): TIdentifier[] {
return this.queue.map(task => task.id);
}
@@ -49,6 +49,7 @@ export interface INavNodeMoveData {
}
export interface INavNodeRenameData {
projectId?: string;
nodeId: string;
newNodeId: string;
}
@@ -264,6 +265,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
this.markOutdated(node.parentId);
await this.onNodeRename.execute({
projectId: node.projectId,
nodeId: node.id,
newNodeId,
});
@@ -0,0 +1,21 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions';
const projectProviderSymbol = Symbol('@extension/ProjectProvider');
export type IProjectProvider<T = never> = (context: T) => string | undefined;
export function projectProvider<T>(provider: IProjectProvider<T>) {
return createExtension<T>(provider, projectProviderSymbol);
}
export function isProjectProvider<T>(obj: IExtension<T>): obj is IProjectProvider<T> & IExtension<T> {
return isExtension(obj, projectProviderSymbol);
}
@@ -0,0 +1,25 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions';
const projectSetterSymbol = Symbol('@extension/ProjectSetter');
export type IProjectSetter<T = never> = (
projectId: string | null,
context: T
) => Promise<boolean> | boolean;
export function projectSetter<T>(setter: IProjectSetter<T>) {
return createExtension<T>(setter, projectSetterSymbol);
}
export function isProjectSetter<T>(obj: IExtension<T>): obj is IProjectSetter<T> & IExtension<T> {
return isExtension(obj, projectSetterSymbol);
}
@@ -0,0 +1,22 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions';
const projectSetterStateSymbol = Symbol('@extension/ProjectSetterState');
export type IProjectSetterState<T = never> = (context: T) => Promise<boolean> | boolean;
export function projectSetterState<T>(setter: IProjectSetterState<T>) {
return createExtension<T>(setter, projectSetterStateSymbol);
}
export function isProjectSetterState<T>(obj: IExtension<T>): obj is IProjectSetterState<T> & IExtension<T> {
return isExtension(obj, projectSetterStateSymbol);
}
@@ -1,3 +1,6 @@
export * from './extensions/IProjectProvider';
export * from './extensions/IProjectSetter';
export * from './extensions/IProjectSetterState';
export * from './activeProjectsContext';
export * from './ProjectInfoResource';
export * from './ProjectsService';
@@ -493,14 +493,14 @@ export abstract class CachedResource<
param: TParam,
context: TContext,
update: (param: TParam, context: TContext) => Promise<T>,
exitCheck: () => boolean
exitCheck: (param: TParam, context: TContext) => boolean
): Promise<T | undefined>;
protected async performUpdate<T>(
param: TParam,
context: TContext,
update: (param: TParam, context: TContext) => Promise<T>,
exitCheck?: () => boolean
exitCheck?: (param: TParam, context: TContext) => boolean
): Promise<T | undefined> {
const contexts = await this.beforeLoad.execute(param);
@@ -510,7 +510,7 @@ export abstract class CachedResource<
await this.scheduler.waitRelease(param);
if (exitCheck?.()) {
if (exitCheck?.(param, context)) {
return;
}
@@ -519,7 +519,7 @@ export abstract class CachedResource<
param,
async () => {
// repeated because previous task maybe has been load requested data
if (exitCheck?.()) {
if (exitCheck?.(param, context)) {
return;
}
@@ -8,6 +8,7 @@ fragment NavNodeInfo on NavigatorNodeInfo {
inline
navigable
features
projectId
object {
features
}
@@ -1,13 +1,20 @@
query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) {
query getResourceList(
$projectId: String!
$folder: String
$nameMask: String
$readProperties: Boolean
$readHistory: Boolean
) {
resources: rmListResources(
projectId: $projectId,
folder: $folder,
nameMask: $nameMask,
readProperties: $readProperties,
projectId: $projectId
folder: $folder
nameMask: $nameMask
readProperties: $readProperties
readHistory: $readHistory
) {
name,
folder,
name
folder
length
properties
}
}
@@ -0,0 +1,13 @@
mutation setResourceProperty(
$projectId: String!
$resourcePath: String!
$name: ID!
$value: String
) {
properties: rmSetResourceProperty(
projectId: $projectId
resourcePath: $resourcePath
name: $name
value: $value
)
}
+41 -6
View File
@@ -413,6 +413,7 @@ export interface Mutation {
rmDeleteResource?: Maybe<Scalars['Boolean']>;
rmMoveResource: Scalars['String'];
rmSetProjectPermissions: Scalars['Boolean'];
rmSetResourceProperty: Scalars['Boolean'];
rmSetSubjectProjectPermissions: Scalars['Boolean'];
rmWriteResourceStringContent: Scalars['String'];
setConnectionNavigatorSettings: ConnectionInfo;
@@ -612,6 +613,14 @@ export interface MutationRmSetProjectPermissionsArgs {
}
export interface MutationRmSetResourcePropertyArgs {
name: Scalars['ID'];
projectId: Scalars['String'];
resourcePath: Scalars['String'];
value?: InputMaybe<Scalars['String']>;
}
export interface MutationRmSetSubjectProjectPermissionsArgs {
permissions: Array<RmProjectPermissions>;
subjectId: Scalars['String'];
@@ -1379,6 +1388,7 @@ export interface RmResource {
folder: Scalars['Boolean'];
length: Scalars['Int'];
name: Scalars['String'];
properties?: Maybe<Scalars['Object']>;
}
export interface RmSubjectProjectPermissions {
@@ -2256,7 +2266,7 @@ export type NavGetStructContainersQueryVariables = Exact<{
}>;
export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> } };
export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> } };
export type AdminObjectGrantInfoFragment = { subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array<string> } };
@@ -2288,7 +2298,7 @@ export type ExecutionContextInfoFragment = { id: string, projectId: string, conn
export type NavNodeDbObjectInfoFragment = { id: string, object?: { type?: string, features?: Array<string>, properties?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> } };
export type NavNodeInfoFragment = { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> };
export type NavNodeInfoFragment = { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> };
export type NavNodePropertiesFragment = { id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number };
@@ -2449,7 +2459,7 @@ export type GetNodeParentsQueryVariables = Exact<{
}>;
export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> };
export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> };
export type NavDeleteNodesMutationVariables = Exact<{
nodePaths: Array<Scalars['ID']> | Scalars['ID'];
@@ -2474,7 +2484,7 @@ export type NavNodeChildrenQueryVariables = Exact<{
}>;
export type NavNodeChildrenQuery = { navNodeChildren: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }>, navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> } };
export type NavNodeChildrenQuery = { navNodeChildren: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }>, navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> } };
export type NavNodeInfoQueryVariables = Exact<{
nodePath: Scalars['ID'];
@@ -2482,7 +2492,7 @@ export type NavNodeInfoQueryVariables = Exact<{
}>;
export type NavNodeInfoQuery = { navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> } };
export type NavNodeInfoQuery = { navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, projectId?: string, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> } };
export type NavRefreshNodeQueryVariables = Exact<{
nodePath: Scalars['ID'];
@@ -2566,7 +2576,7 @@ export type GetResourceListQueryVariables = Exact<{
}>;
export type GetResourceListQuery = { resources: Array<{ name: string, folder: boolean, length: number }> };
export type GetResourceListQuery = { resources: Array<{ name: string, folder: boolean, length: number, properties?: any }> };
export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2610,6 +2620,16 @@ export type SetProjectPermissionsMutationVariables = Exact<{
export type SetProjectPermissionsMutation = { rmSetProjectPermissions: boolean };
export type SetResourcePropertyMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
name: Scalars['ID'];
value?: InputMaybe<Scalars['String']>;
}>;
export type SetResourcePropertyMutation = { properties: boolean };
export type SetSubjectProjectsPermissionsMutationVariables = Exact<{
subjectId: Scalars['String'];
permissions: Array<RmProjectPermissions> | RmProjectPermissions;
@@ -3074,6 +3094,7 @@ export const NavNodeInfoFragmentDoc = `
inline
navigable
features
projectId
object {
features
}
@@ -4146,6 +4167,7 @@ export const GetResourceListDocument = `
name
folder
length
properties
}
}
`;
@@ -4196,6 +4218,16 @@ export const SetProjectPermissionsDocument = `
rmSetProjectPermissions(projectId: $projectId, permissions: $permissions)
}
`;
export const SetResourcePropertyDocument = `
mutation setResourceProperty($projectId: String!, $resourcePath: String!, $name: ID!, $value: String) {
properties: rmSetResourceProperty(
projectId: $projectId
resourcePath: $resourcePath
name: $name
value: $value
)
}
`;
export const SetSubjectProjectsPermissionsDocument = `
mutation setSubjectProjectsPermissions($subjectId: String!, $permissions: [RMProjectPermissions!]!) {
rmSetSubjectProjectPermissions(subjectId: $subjectId, permissions: $permissions)
@@ -4747,6 +4779,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
setProjectPermissions(variables: SetProjectPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<SetProjectPermissionsMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<SetProjectPermissionsMutation>(SetProjectPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setProjectPermissions', 'mutation');
},
setResourceProperty(variables: SetResourcePropertyMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<SetResourcePropertyMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<SetResourcePropertyMutation>(SetResourcePropertyDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setResourceProperty', 'mutation');
},
setSubjectProjectsPermissions(variables: SetSubjectProjectsPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<SetSubjectProjectsPermissionsMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<SetSubjectProjectsPermissionsMutation>(SetSubjectProjectsPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setSubjectProjectsPermissions', 'mutation');
},
@@ -9,7 +9,7 @@
export function isValuesEqual<T extends string | boolean | number>(
first: T | null | undefined,
second: T | null | undefined,
defaultValue?: T
defaultValue?: T | null | undefined
): boolean {
return (first ?? defaultValue) === (second ?? defaultValue);
}
@@ -59,7 +59,17 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
items = [...items];
const connections = this.connectionsManagerService.projectConnections
.filter(connection => !connection.template)
.filter(connection => {
if (
!this.connectionSchemaManagerService.isProjectChangeable
&& this.connectionSchemaManagerService.activeProjectId
&& connection.projectId !== this.connectionSchemaManagerService.activeProjectId
) {
return false;
}
return !connection.template;
})
.sort((a, b) => {
if (a.connected === b.connected) {
return compareConnectionsInfo(a, b);
@@ -13,6 +13,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ExtensionUtils, IExtension } from '@cloudbeaver/core-extensions';
import { type IObjectNavNodeProvider, type IDataContextActiveNode, isObjectNavNodeProvider } from '@cloudbeaver/core-navigation-tree';
import { IProjectProvider, IProjectSetter, IProjectSetterState, isProjectProvider, isProjectSetter, isProjectSetterState } from '@cloudbeaver/core-projects';
import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
@@ -25,10 +26,13 @@ interface IActiveItem<T> {
id: string;
context: T;
getCurrentNavNode?: IObjectNavNodeProvider<T>;
getProjectSetterState?: IProjectSetterState<T>;
getCurrentProjectId?: IProjectProvider<T>;
getCurrentConnectionId?: IConnectionProvider<T>;
getCurrentSchemaId?: IObjectSchemaProvider<T>;
getCurrentCatalogId?: IObjectCatalogProvider<T>;
changeConnectionId?: IConnectionSetter<T>;
changeProjectId?: IProjectSetter<T>;
changeCatalogId?: IObjectCatalogSetter<T>;
changeSchemaId?: IObjectSchemaSetter<T>;
}
@@ -44,6 +48,15 @@ export class ConnectionSchemaManagerService {
return this.activeItem.getCurrentNavNode(this.activeItem.context);
}
get activeProjectId(): string | null | undefined {
if (!this.activeItem?.getCurrentProjectId) {
return null;
}
return this.activeItem.getCurrentProjectId(this.activeItem.context);
}
get activeConnectionKey(): IConnectionInfoParams | null | undefined {
if (!this.activeItem?.getCurrentConnectionId) {
@@ -53,6 +66,14 @@ export class ConnectionSchemaManagerService {
return this.activeItem.getCurrentConnectionId(this.activeItem.context);
}
get currentProjectId(): string | null | undefined {
if (this.pendingProjectId !== null) {
return this.pendingProjectId;
}
return this.activeProjectId;
}
get currentConnectionKey(): IConnectionInfoParams | null | undefined {
if (this.pendingConnectionKey !== null) {
return this.pendingConnectionKey;
@@ -130,6 +151,10 @@ export class ConnectionSchemaManagerService {
});
}
get isProjectChangeable(): boolean {
return !!this.activeItem?.changeProjectId && !!this.activeItem.getProjectSetterState?.(this.activeItem.context);
}
get isConnectionChangeable(): boolean {
return !!this.activeItem?.changeConnectionId;
}
@@ -148,6 +173,10 @@ export class ConnectionSchemaManagerService {
);
}
get isChangingProject(): boolean {
return this.changingProjectId;
}
get isChangingConnection(): boolean {
return this.changingConnection;
}
@@ -177,9 +206,11 @@ export class ConnectionSchemaManagerService {
return item;
}
private pendingProjectId: string | null;
private pendingConnectionKey: IConnectionInfoParams | null;
private pendingCatalogId: string | null | undefined;
private pendingSchemaId: string | null | undefined;
private changingProjectId: boolean;
private changingConnection: boolean;
private changingConnectionContainer: boolean;
@@ -190,17 +221,21 @@ export class ConnectionSchemaManagerService {
private readonly dbDriverResource: DBDriverResource,
private readonly notificationService: NotificationService
) {
this.changingProjectId = false;
this.changingConnection = false;
this.changingConnectionContainer = false;
this.pendingConnectionKey = null;
this.pendingProjectId = null;
this.pendingCatalogId = null;
this.pendingSchemaId = null;
makeObservable<
ConnectionSchemaManagerService,
'activeItem'
| 'changingProjectId'
| 'changingConnection'
| 'changingConnectionContainer'
| 'pendingProjectId'
| 'pendingConnectionKey'
| 'pendingCatalogId'
| 'pendingSchemaId'
@@ -218,8 +253,10 @@ export class ConnectionSchemaManagerService {
isObjectCatalogChangeable: computed,
isObjectSchemaChangeable: computed,
activeItem: computed,
changingProjectId: observable,
changingConnection: observable,
changingConnectionContainer: observable,
pendingProjectId: observable,
pendingConnectionKey: observable,
pendingCatalogId: observable,
pendingSchemaId: observable,
@@ -227,6 +264,24 @@ export class ConnectionSchemaManagerService {
});
}
async selectProjectId(projectId: string): Promise<void> {
if (!this.activeItem?.changeProjectId) {
return;
}
try {
runInAction(() => {
this.changingProjectId = true;
this.pendingProjectId = projectId;
});
await this.activeItem.changeProjectId(projectId, this.activeItem.context);
} finally {
runInAction(() => {
this.changingProjectId = false;
this.pendingProjectId = null;
});
}
}
/**
* Trigger when user select connection in dropdown
*/
@@ -236,7 +291,9 @@ export class ConnectionSchemaManagerService {
}
try {
runInAction(() => {
this.changingProjectId = true;
this.changingConnection = true;
this.pendingProjectId = connectionKey.projectId;
this.pendingConnectionKey = connectionKey;
this.pendingSchemaId = undefined;
this.pendingCatalogId = undefined;
@@ -245,7 +302,9 @@ export class ConnectionSchemaManagerService {
await this.updateContainer(connectionKey);
} finally {
runInAction(() => {
this.changingProjectId = false;
this.changingConnection = false;
this.pendingProjectId = null;
this.pendingConnectionKey = null;
this.pendingSchemaId = null;
this.pendingCatalogId = null;
@@ -361,10 +420,13 @@ export class ConnectionSchemaManagerService {
private setExtensions<T>(item: IActiveItem<T>, extensions: Array<IExtension<T>>) {
ExtensionUtils.from(extensions)
.on(isObjectNavNodeProvider, extension => { item.getCurrentNavNode = extension; })
.on(isProjectSetterState, extension => { item.getProjectSetterState = extension; })
.on(isProjectProvider, extension => { item.getCurrentProjectId = extension; })
.on(isConnectionProvider, extension => { item.getCurrentConnectionId = extension; })
.on(isObjectCatalogProvider, extension => { item.getCurrentCatalogId = extension; })
.on(isObjectSchemaProvider, extension => { item.getCurrentSchemaId = extension; })
.on(isProjectSetter, extension => { item.changeProjectId = extension; })
.on(isConnectionSetter, extension => { item.changeConnectionId = extension; })
.on(isObjectCatalogSetter, extension => { item.changeCatalogId = extension; })
.on(isObjectSchemaSetter, extension => { item.changeSchemaId = extension; });
@@ -45,7 +45,10 @@ export const ProjectSelect = observer(function ProjectSelect({
const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, {
onData: () => {
if (!value && possibleOptions.length > 0) {
if (
(!value && possibleOptions.length > 0)
|| (value && !possibleOptions.includes(value))
) {
onChange(possibleOptions[0]);
}
},
@@ -65,6 +65,23 @@ export class NavResourceNodeService {
);
}
async setProperties(resourceData: IResourceData, diff: Record<string, any>): Promise<Record<string, any>> {
return await this.resourceManagerResource.setProperties(
resourceData.key.projectId,
resourceData.resourcePath,
diff
);
}
async getProperties(resourceData: IResourceData): Promise<Record<string, any>> {
const resource = await this.resourceManagerResource.loadProperties(
resourceData.key.projectId,
resourceData.resourcePath
);
return resource;
}
getResourceData(nodeId: string): IResourceData | undefined {
if (!isRMNavNode(nodeId)) {
return;
@@ -8,6 +8,7 @@
import { injectable } from '@cloudbeaver/core-di';
import { CachedMapResource, GraphQLService, ResourceKey, ResourceKeyUtils, RmResource } from '@cloudbeaver/core-sdk';
import { isValuesEqual } from '@cloudbeaver/core-utils';
export type RmResourceInfo = RmResource;
export interface IResourceManagerParams {
@@ -23,6 +24,11 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
super();
}
getResourceName(resourcePath: string): string {
const parts = resourcePath.split('/');
return parts[parts.length - 1];
}
getFolder(resourcePath: string): string {
const parts = resourcePath.split('/');
return parts.slice(0, parts.length - 1).join('/');
@@ -30,14 +36,16 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
hasResource(key: IResourceManagerParams, resourcePath: string): boolean {
const resources = this.get(key);
const name = this.getResourceName(resourcePath);
return resources?.some(resource => resource.name === resourcePath) === true;
return resources?.some(resource => resource.name === name) === true;
}
getResource(key: IResourceManagerParams, resourcePath: string): RmResourceInfo | undefined {
const resources = this.get(key);
const name = this.getResourceName(resourcePath);
return resources?.find(resource => resource.name === resourcePath);
return resources?.find(resource => resource.name === name);
}
async move(projectId: string, oldPath: string, newPath: string): Promise<void> {
@@ -55,6 +63,81 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
// this.set();
}
async loadProperties(
projectId: string,
resourcePath: string
): Promise<Record<string, any>> {
const folder = this.getFolder(resourcePath);
const nameMask = this.getResourceName(resourcePath);
const key: IResourceManagerParams = { projectId, folder };
await this.load(key);
await this.performUpdate(key, undefined, async () => {
const { resources } = await this.graphQLService.sdk.getResourceList({
projectId,
folder,
nameMask,
readProperties: true,
});
const currentResources = this.get(key);
const resource = this.getResource(key, resourcePath);
if (resource) {
currentResources?.splice(
currentResources.indexOf(resource),
1,
...resources.map(resource => ({ properties: {}, ...resource }))
);
}
// this.dataSet(
// key,
// resources.map(resource => ({ properties: {}, ...resource }))
// );
}, () => {
const resource = this.getResource(key, resourcePath);
return resource?.properties && this.isLoaded(key) && !this.isOutdated(key);
});
return this.getResource(key, resourcePath)?.properties ?? {};
}
async setProperties(
projectId: string,
resourcePath: string,
diff: Record<string, any>
): Promise<Record<string, any>> {
const folder = this.getFolder(resourcePath);
const key: IResourceManagerParams = { projectId, folder };
const propertiesPatch: Record<string, any> = {};
const properties = await this.loadProperties(projectId, resourcePath);
await this.performUpdate(key, undefined, async () => {
for (const [name, value] of Object.entries(diff)) {
if (
properties[name] === value
|| (value === null && !(name in properties))
) {
continue;
}
await this.graphQLService.sdk.setResourceProperty({
projectId,
resourcePath,
name,
value,
});
propertiesPatch[name] = value;
}
Object.assign(properties, propertiesPatch);
});
return properties;
}
async createResource(projectId: string, resourcePath: string, folder: boolean) {
await this.graphQLService.sdk.createResource({
projectId,
@@ -62,7 +145,7 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
isFolder: folder,
});
await this.load({ projectId, folder:folder ? resourcePath : this.getFolder(resourcePath) });
await this.load({ projectId, folder: folder ? resourcePath : this.getFolder(resourcePath) });
}
async writeResource(projectId: string, resourcePath: string, data: string, forceOverwrite: boolean) {
@@ -98,7 +181,7 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
return key;
}
const ref = this.keys.find(k => k.projectId === key.projectId && k.folder === key.folder);
const ref = this.keys.find(k => k.projectId === key.projectId && isValuesEqual(k.folder, key.folder, ''));
if (ref) {
return ref;
@@ -117,7 +200,7 @@ export class ResourceManagerResource extends CachedMapResource<IResourceManagerP
folder,
});
this.dataSet(key, resources);
this.dataSet(key, resources.map(({ properties, ...resource }) => resource));
});
return this.data;
@@ -22,6 +22,7 @@ const style = css`
interface Payload {
defaultScriptName?: string;
projectId?: string | null;
}
export interface ISaveScriptDialogResult {
@@ -48,7 +49,7 @@ export const SaveScriptDialog: DialogComponent<Payload, ISaveScriptDialogResult>
const state = useObservableRef<State>(() => ({
name: payload.defaultScriptName ?? '',
projectId: null,
projectId: payload.projectId ?? null,
errorMessage: null,
validate() {
this.errorMessage = null;
@@ -116,7 +117,7 @@ export const SaveScriptDialog: DialogComponent<Payload, ISaveScriptDialogResult>
</InputField>
<ProjectSelect
value={state.projectId}
filter={p => p.canEditResources}
filter={p => (p.canEditResources && p.id === (payload.projectId ?? p.id))}
autoHide
onChange={projectId => {state.projectId = projectId;}}
/>
@@ -10,6 +10,7 @@ import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connecti
export interface IResourceNodeInfo {
nodeId: string;
projectId?: string;
parents: string[];
}
@@ -11,13 +11,14 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial
import { NotificationService } from '@cloudbeaver/core-events';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { NavNodeManagerService, NavTreeResource, NavNodeInfoResource, NodeManagerUtils, type INodeNavigationData, NavigationType } from '@cloudbeaver/core-navigation-tree';
import { ProjectsService } from '@cloudbeaver/core-projects';
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { DATA_CONTEXT_TAB_ID } from '@cloudbeaver/core-ui';
import { createPath } from '@cloudbeaver/core-utils';
import { ActionService, ACTION_SAVE, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { NavResourceNodeService, RESOURCE_NODE_TYPE, SaveScriptDialog, ResourceManagerService, RESOURCES_NODE_PATH, ResourceProjectsResource, ResourcesProjectsNavNodeService } from '@cloudbeaver/plugin-resource-manager';
import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, SqlDataSourceService, SqlEditorService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor';
import { NavResourceNodeService, RESOURCE_NODE_TYPE, SaveScriptDialog, ResourceManagerService, RESOURCES_NODE_PATH, ResourcesProjectsNavNodeService } from '@cloudbeaver/plugin-resource-manager';
import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, SqlDataSourceService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab, SqlEditorNavigatorService, SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { isScript } from './isScript';
@@ -31,14 +32,13 @@ export class PluginBootstrap extends Bootstrap {
private readonly navNodeManagerService: NavNodeManagerService,
private readonly navTreeResource: NavTreeResource,
private readonly navResourceNodeService: NavResourceNodeService,
private readonly sqlEditorService: SqlEditorService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly navigationTabsService: NavigationTabsService,
private readonly notificationService: NotificationService,
private readonly sqlEditorNavigatorService: SqlEditorNavigatorService,
private readonly resourceManagerService: ResourceManagerService,
private readonly projectsService: ProjectsService,
private readonly resourceProjectsResource: ResourceProjectsResource,
private readonly projectInfoResource: ProjectInfoResource,
private readonly sqlEditorTabResourceService: SqlEditorTabResourceService,
private readonly commonDialogService: CommonDialogService,
private readonly actionService: ActionService,
@@ -86,20 +86,33 @@ export class PluginBootstrap extends Bootstrap {
}
if (action === ACTION_SAVE) {
let projectId = dataSource.executionContext?.projectId ?? null;
await this.projectInfoResource.load(CachedMapAllKey);
const name = getSqlEditorName(state, dataSource);
if (projectId) {
const project = this.projectInfoResource.get(projectId);
if (!project?.canEditResources) {
projectId = null;
}
}
const result = await this.commonDialogService.open(SaveScriptDialog, {
defaultScriptName: name,
projectId,
});
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
if (!result.projectId) {
projectId = result.projectId;
if (!projectId) {
throw new Error('Project not selected');
}
await this.resourceProjectsResource.load();
const scriptName = `${result.name.trim()}.${SCRIPT_EXTENSION}`;
const folder = createPath(RESOURCES_NODE_PATH, result.projectId);
const folder = createPath(RESOURCES_NODE_PATH, projectId);
const resourceData = this.navResourceNodeService.getResourceData(folder);
if (!resourceData) {
@@ -109,7 +122,6 @@ export class PluginBootstrap extends Bootstrap {
const nodeId = await this.navResourceNodeService.saveScript(resourceData, scriptName, dataSource.script);
await this.navTreeResource.preloadNodeParents(NodeManagerUtils.parentsFromPath(nodeId), nodeId);
const node = await this.navNodeInfoResource.load(nodeId);
@@ -124,16 +136,16 @@ export class PluginBootstrap extends Bootstrap {
const parents = NodeManagerUtils.parentsFromPath(nodeId);
dataSource.setNodeInfo({
projectId,
nodeId,
parents,
});
if (previousDataSource) {
if (previousDataSource?.executionContext) {
dataSource.setExecutionContext(previousDataSource.executionContext);
}
}
this.sqlEditorService.setName(node.name ?? scriptName, state);
this.notificationService.logSuccess({ title: 'plugin_resource_manager_save_script_success', message: node.name });
if (!this.resourceManagerService.active) {
@@ -178,7 +190,7 @@ export class PluginBootstrap extends Bootstrap {
}
try {
const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext);
const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext);
const node = await this.navNodeInfoResource.load(data.nodeId);
if (node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) {
@@ -246,11 +258,12 @@ export class PluginBootstrap extends Bootstrap {
const parents = NodeManagerUtils.parentsFromPath(nodeId);
dataSource.setNodeInfo({
projectId: node.projectId,
nodeId,
parents,
});
if (previousDataSource) {
if (previousDataSource?.executionContext) {
dataSource.setExecutionContext(previousDataSource.executionContext);
}
@@ -6,15 +6,22 @@
* you may not use this file except in compliance with the License.
*/
import { action, computed, makeObservable, observable } from 'mobx';
import { action, computed, makeObservable, observable, runInAction, toJS } from 'mobx';
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import { IConnectionExecutionContextInfo, NOT_INITIALIZED_CONTEXT_ID } from '@cloudbeaver/core-connections';
import { TaskScheduler } from '@cloudbeaver/core-executor';
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { debounce, isArraysEqual } from '@cloudbeaver/core-utils';
import { debounce, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { BaseSqlDataSource, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor';
import type { IResourceNodeInfo, IResourceSqlDataSourceState } from './IResourceSqlDataSourceState';
interface IResourceProperties {
'default-datasource'?: string;
'default-catalog'?: string;
'default-schema'?: string;
}
interface IResourceInfo {
isReadonly?: (dataSource: ResourceSqlDataSource) => boolean;
}
@@ -23,6 +30,15 @@ interface IResourceActions {
rename(dataSource: ResourceSqlDataSource, nodeId: string, name: string): Promise<string>;
read(dataSource: ResourceSqlDataSource, nodeId: string): Promise<string>;
write(dataSource: ResourceSqlDataSource, nodeId: string, value: string): Promise<void>;
getProperties(
dataSource: ResourceSqlDataSource,
nodeId: string
): Promise<Record<string, any>>;
setProperties(
dataSource: ResourceSqlDataSource,
nodeId: string,
diff: Record<string, any>
): Promise<Record<string, any>>;
}
const VALUE_SYNC_DELAY = 1 * 1000;
@@ -42,6 +58,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
return this._script;
}
get projectId(): string | null {
return this.nodeInfo?.projectId ?? null;
}
get executionContext(): IConnectionExecutionContextInfo | undefined {
return this.state.executionContext;
}
@@ -68,9 +88,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
private info?: IResourceInfo;
private lastAction?: () => Promise<void>;
private readonly state: IResourceSqlDataSourceState;
private resourceProperties: IResourceProperties;
private loading: boolean;
private loaded: boolean;
private readonly scheduler: TaskScheduler;
constructor(
private readonly navNodeInfoResource: NavNodeInfoResource,
@@ -80,11 +101,12 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
this.state = state;
this._script = '';
this.saved = true;
this.loading = false;
this.loaded = false;
this.resourceProperties = {};
this.scheduler = new TaskScheduler(() => true);
this.debouncedWrite = debounce(this.debouncedWrite.bind(this), VALUE_SYNC_DELAY);
makeObservable<this, '_script' | 'lastAction' | 'loading' | 'loaded'>(this, {
makeObservable<this, '_script' | 'lastAction' | 'loaded' | 'resourceProperties'>(this, {
script: computed,
executionContext: computed,
nodeInfo: computed,
@@ -92,9 +114,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
equals: isArraysEqual,
}),
_script: observable,
lastAction: observable,
loading: observable,
lastAction: observable.ref,
loaded: observable,
resourceProperties: observable,
setExecutionContext: action,
setScript: action,
setNodeInfo: action,
});
@@ -113,7 +136,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
isLoading(): boolean {
return this.loading;
return this.scheduler.executing;
}
setNodeInfo(nodeInfo?: IResourceNodeInfo): void {
@@ -135,6 +158,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
this.rename(name);
}
setProject(projectId: string | null): void {
}
canRename(name: string | null): boolean {
if (this.isReadonly()) {
return false;
@@ -168,96 +195,205 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
async load(): Promise<void> {
await this.read();
await this.updateProperties();
}
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void {
this.state.executionContext = executionContext;
if (
this.nodeInfo?.projectId
&& executionContext?.projectId
&& this.nodeInfo.projectId !== executionContext.projectId
) {
console.warn('Cant change execution context because of different projects');
return;
}
if (
!isObjectsEqual(toJS(this.state.executionContext), toJS(executionContext))
) {
this.state.executionContext = toJS(executionContext);
}
if (
this.isReadonly()
|| (
this.resourceProperties['default-datasource'] === executionContext?.connectionId
&& this.resourceProperties['default-catalog'] === executionContext?.defaultCatalog
&& this.resourceProperties['default-schema'] === executionContext?.defaultSchema
)
) {
return;
}
this.resourceProperties['default-datasource'] = executionContext?.connectionId;
this.resourceProperties['default-catalog'] = executionContext?.defaultCatalog;
this.resourceProperties['default-schema'] = executionContext?.defaultSchema;
this.setProperties(toJS(this.resourceProperties));
}
async rename(name: string | null) {
await this.write();
if (
!this.actions
await this.scheduler.schedule(undefined, async () => {
if (
!this.actions
|| !this.nodeInfo
|| !this.saved
|| !name?.trim()
) {
return;
}
if (!name.toLowerCase().endsWith('.sql')) {
name += '.sql';
}
this.lastAction = this.rename.bind(this, name);
this.loading = true;
this.message = 'Renaming script...';
try {
this.exception = null;
this.nodeInfo.nodeId = await this.actions.rename(this, this.nodeInfo.nodeId, name);
if (this.nodeInfo.parents.length > 0) {
this.nodeInfo.parents.splice(this.nodeInfo.parents.length - 1, 1, this.nodeInfo.nodeId);
) {
return;
}
this.markOutdated();
this.saved = true;
this.loaded = false;
} catch (exception: any) {
this.exception = exception;
} finally {
this.loading = false;
this.message = undefined;
}
if (!name.toLowerCase().endsWith('.sql')) {
name += '.sql';
}
this.lastAction = this.rename.bind(this, name);
this.message = 'Renaming script...';
try {
this.exception = null;
this.nodeInfo.nodeId = await this.actions.rename(this, this.nodeInfo.nodeId, name);
if (this.nodeInfo.parents.length > 0) {
this.nodeInfo.parents.splice(this.nodeInfo.parents.length - 1, 1, this.nodeInfo.nodeId);
}
this.markOutdated();
this.saved = true;
this.loaded = false;
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
async read() {
if (!this.actions || !this.nodeInfo || !this.isOutdated() || !this.saved) {
return;
}
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.nodeInfo || !this.isOutdated() || !this.saved) {
return;
}
this.lastAction = this.read.bind(this);
this.loading = true;
this.message = 'Reading script...';
this.lastAction = this.read.bind(this);
this.message = 'Reading script...';
try {
this.exception = null;
this._script = await this.actions.read(this, this.nodeInfo.nodeId);
this.state.name = this.name ?? undefined;
this.markUpdated();
this.loaded = true;
super.setScript(this.script);
} catch (exception: any) {
this.exception = exception;
} finally {
this.loading = false;
this.message = undefined;
}
try {
this.exception = null;
this._script = await this.actions.read(this, this.nodeInfo.nodeId);
this.state.name = this.name ?? undefined;
this.markUpdated();
this.loaded = true;
super.setScript(this.script);
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
async write() {
if (!this.actions || !this.nodeInfo || this.saved) {
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.nodeInfo || this.saved) {
return;
}
this.lastAction = this.write.bind(this);
this.message = 'Saving script...';
try {
this.exception = null;
await this.actions.write(this, this.nodeInfo.nodeId, this.script);
this.state.name = this.name ?? undefined;
this.saved = true;
this.markUpdated();
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
async setProperties(properties: IResourceProperties) {
if (Object.keys(properties).length === 0) {
return;
}
this.lastAction = this.write.bind(this);
this.loading = true;
this.message = 'Saving script...';
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.nodeInfo) {
return;
}
try {
this.exception = null;
await this.actions.write(this, this.nodeInfo.nodeId, this.script);
this.state.name = this.name ?? undefined;
this.saved = true;
this.markUpdated();
} catch (exception: any) {
this.exception = exception;
} finally {
this.loading = false;
this.message = undefined;
this.lastAction = this.setProperties.bind(this, properties);
this.message = 'Update info...';
try {
this.exception = null;
if (!this.isReadonly()) {
await this.actions.setProperties(
this,
this.nodeInfo.nodeId,
properties
);
}
await this.updateProperties();
} catch (exception: any) {
this.exception = exception;
} finally {
this.message = undefined;
}
});
}
private async updateProperties() {
if (!this.actions || !this.nodeInfo) {
return;
}
const previousProperties = this.resourceProperties;
const resourceProperties = await this.actions.getProperties(this, this.nodeInfo.nodeId);
runInAction(() => {
this.resourceProperties = toJS(resourceProperties);
if (isObjectsEqual(toJS(previousProperties), toJS(this.resourceProperties)) && this.isReadonly()) {
return;
}
const connectionId = this.resourceProperties['default-datasource'];
const defaultCatalog = this.resourceProperties['default-catalog'];
const defaultSchema = this.resourceProperties['default-schema'];
if (!this.nodeInfo!.projectId) {
return;
}
if (connectionId) {
if (
!isValuesEqual(this.state.executionContext?.connectionId, connectionId, null)
|| !isValuesEqual(this.state.executionContext?.defaultCatalog, defaultCatalog, null)
|| !isValuesEqual(this.state.executionContext?.defaultSchema, defaultSchema, null)
|| !isValuesEqual(this.state.executionContext?.projectId, this.nodeInfo!.projectId, null)
) {
this.state.executionContext = {
id: NOT_INITIALIZED_CONTEXT_ID,
projectId: this.nodeInfo!.projectId,
connectionId,
defaultCatalog,
defaultSchema,
};
}
} else {
this.state.executionContext = undefined;
}
});
}
private debouncedWrite() {
@@ -8,7 +8,6 @@
import { action, makeObservable, observable, untracked } from 'mobx';
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -69,6 +68,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
!['undefined', 'object'].includes(typeof value.nodeInfo)
|| !['string', 'undefined'].includes(typeof value.name)
|| !['string', 'undefined'].includes(typeof value.nodeInfo?.nodeId)
|| !['string', 'undefined'].includes(typeof value.nodeInfo?.projectId)
|| !['undefined', 'object'].includes(typeof value.nodeInfo?.parents)
|| !['undefined', 'object'].includes(typeof value.executionContext)
|| !['string', 'undefined'].includes(typeof value.executionContext?.connectionId)
@@ -97,10 +97,13 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
this.navNodeInfoResource,
this.createState(
editorId,
options?.executionContext,
)
);
if (options?.executionContext) {
dataSource.setExecutionContext(options.executionContext);
}
if (options?.script) {
dataSource.setScript(options.script);
}
@@ -113,6 +116,8 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
rename: this.rename.bind(this),
read: this.read.bind(this),
write: this.write.bind(this),
getProperties: this.getProperties.bind(this),
setProperties: this.setProperties.bind(this),
});
dataSource.setInfo({
@@ -169,14 +174,12 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
private createState(
editorId: string,
executionContext?: IConnectionExecutionContextInfo,
nodeInfo?: IResourceNodeInfo
): IResourceSqlDataSourceState {
let state = this.dataSourceStateState.get(editorId);
if (!state) {
state = observable<IResourceSqlDataSourceState>({
executionContext,
nodeInfo,
});
@@ -309,6 +312,53 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
}
}
private async getProperties(
dataSource: ResourceSqlDataSource,
nodeId: string
): Promise<Record<string, any>> {
if (!dataSource.nodeInfo) {
throw new Error('Node info is not provided');
}
try {
await this.navTreeResource.preloadNodeParents(dataSource.nodeInfo.parents, dataSource.nodeInfo.nodeId);
const resourceData = this.navResourceNodeService.getResourceData(nodeId);
if (!resourceData) {
throw new Error('Can\'t find resource');
}
return await this.navResourceNodeService.getProperties(resourceData);
} catch (exception) {
this.notificationService.logException(exception as any, 'plugin_resource_manager_sync_script_error');
throw exception;
}
}
private async setProperties(
dataSource: ResourceSqlDataSource,
nodeId: string,
diff: Record<string, any>
): Promise<Record<string, any>> {
if (!this.resourceManagerService.enabled || !dataSource.nodeInfo) {
return {};
}
try {
await this.navTreeResource.preloadNodeParents(dataSource.nodeInfo.parents, dataSource.nodeInfo.nodeId);
const resourceData = this.navResourceNodeService.getResourceData(nodeId);
if (!resourceData) {
return {};
}
return await this.navResourceNodeService.setProperties(resourceData, diff);
} catch (exception) {
this.notificationService.logException(exception as any, 'plugin_resource_manager_update_script_error');
throw exception;
}
}
private async read(dataSource: ResourceSqlDataSource, nodeId: string): Promise<string> {
if (!dataSource.nodeInfo) {
throw new Error('Node info is not provided');
@@ -29,9 +29,10 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { projectProvider, projectSetter, projectSetterState } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, NavNodeInfoFragment, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { NavigationTabsService, TabHandler, ITab, ITabOptions } from '@cloudbeaver/plugin-navigation-tabs';
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor';
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab } from './isSQLEditorTab';
import { SqlEditorPanel } from './SqlEditorPanel';
@@ -75,9 +76,12 @@ export class SqlEditorTabService extends Bootstrap {
canClose: this.handleCanTabClose.bind(this),
extensions: [
objectNavNodeProvider(this.getNavNode.bind(this)),
projectSetterState(this.getProjectSetState.bind(this)),
projectProvider(this.getProjectId.bind(this)),
connectionProvider(this.getConnectionId.bind(this)),
objectCatalogProvider(this.getObjectCatalogId.bind(this)),
objectSchemaProvider(this.getObjectSchemaId.bind(this)),
projectSetter(this.setProjectId.bind(this)),
connectionSetter((connectionId, tab) => this.setConnectionId(tab, connectionId)),
objectCatalogSetter(this.setObjectCatalogId.bind(this)),
objectSchemaSetter(this.setObjectSchemaId.bind(this)),
@@ -211,7 +215,10 @@ export class SqlEditorTabService extends Bootstrap {
isSQLEditorTab(tab => {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.executionContext;
return (
!!dataSource?.executionContext
&& ResourceKeyUtils.includes(key, dataSource.executionContext.id)
);
})
);
@@ -318,6 +325,18 @@ export class SqlEditorTabService extends Bootstrap {
return true;
}
private getProjectId(tab: ITab<ISqlEditorTabState>): string | undefined {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return dataSource?.projectId ?? undefined;
}
private getProjectSetState(tab: ITab<ISqlEditorTabState>): boolean {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
return !!dataSource?.features.includes(ESqlDataSourceFeatures.setProject);
}
private getConnectionId(tab: ITab<ISqlEditorTabState>): IConnectionInfoParams | undefined {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
@@ -343,6 +362,15 @@ export class SqlEditorTabService extends Bootstrap {
return context?.defaultSchema;
}
private setProjectId(projectId: string | null, tab: ITab<ISqlEditorTabState>): boolean {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
dataSource?.setProject(projectId);
this.attachToProject(tab, projectId);
return true;
}
async setConnectionId(
tab: ITab<ISqlEditorTabState>,
connectionKey: IConnectionInfoParams,
@@ -20,6 +20,11 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
abstract get executionContext(): IConnectionExecutionContextInfo | undefined;
exception?: Error | Error[] | null | undefined;
message?: string;
get projectId(): string | null {
return null;
}
get features(): ESqlDataSourceFeatures[] {
return [];
}
@@ -91,6 +96,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.editing = state;
}
setProject(projectId: string | null): void {
}
load(): Promise<void> | void { }
dispose(): void | Promise<void> { }
}
@@ -7,5 +7,6 @@
*/
export enum ESqlDataSourceFeatures {
setName
setName,
setProject
}
@@ -15,6 +15,7 @@ import type { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures';
export interface ISqlDataSource extends ILoadableState {
readonly name: string | null;
readonly script: string;
readonly projectId: string | null;
readonly executionContext?: IConnectionExecutionContextInfo;
readonly message?: string;
readonly onSetScript: ISyncExecutor<string>;
@@ -27,6 +28,7 @@ export interface ISqlDataSource extends ILoadableState {
markUpdated(): void;
canRename(name: string | null): boolean;
setName(name: string | null): void;
setProject(projectId: string | null): void;
setScript(script: string): void;
setEditing(state: boolean): void;
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
@@ -11,7 +11,7 @@ import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { splitStyles, Split, ResizerControls, Pane, splitHorizontalStyles, Overlay, OverlayMessage, OverlayActions, Button, useMapResource, getComputed, OverlayHeader, OverlayHeaderIcon, OverlayHeaderTitle, OverlayHeaderSubTitle, useSplitUserState, Loader, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { ConnectionExecutionContextResource, ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections';
import { ConnectionExecutionContextResource, ConnectionInfoResource, createConnectionParam, DBDriverResource, getRealExecutionContextId } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree';
import { CaptureView } from '@cloudbeaver/core-view';
@@ -74,10 +74,7 @@ export const SqlEditor = observer<Props>(function SqlEditor({ state }) {
SqlEditor,
ConnectionInfoResource,
dataSource?.executionContext
? ({
projectId: dataSource.executionContext.projectId,
connectionId: dataSource.executionContext.connectionId,
})
? createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId)
: null
);
const driver = useMapResource(SqlEditor, DBDriverResource, connection.data?.driverId ?? null);
@@ -88,13 +85,13 @@ export const SqlEditor = observer<Props>(function SqlEditor({ state }) {
const context = useMapResource(
SqlEditor,
ConnectionExecutionContextResource,
connected ? (dataSource?.executionContext?.id ?? null) : null
connected ? getRealExecutionContextId(dataSource?.executionContext?.id) : null
);
const initializingContext = getComputed(() => connection.isLoading() || context.isLoading());
const initExecutionContext = getComputed(() => (
context.data === undefined
&& dataSource?.executionContext !== undefined
&& connection.data !== undefined
));
async function cancelConnection() {