Merge pull request #1135 from dbeaver/CB-2344-projects-administration

CB-2567 shared projects api
This commit is contained in:
Alexey
2022-09-14 17:02:09 +03:00
committed by GitHub
62 changed files with 1029 additions and 132 deletions
@@ -63,7 +63,12 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode {
@Override
public String getNodeName() {
return project.getName();
return project.getDisplayName();
}
@Override
public String getLocalizedName(String locale) {
return getNodeName();
}
@Override
@@ -62,6 +62,7 @@ public class LocalResourceController implements RMController {
private static final Log log = Log.getLog(LocalResourceController.class);
private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+";
private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name
public static final String DEFAULT_CHANGE_ID = "0";
@@ -123,20 +124,7 @@ public class LocalResourceController implements RMController {
//TODO refactor after implement current user api in sm
var activeUserCreds = credentialsProvider.getActiveUserCredentials();
if (Files.exists(sharedProjectsPath) && activeUserCreds != null && activeUserCreds.getUserId() != null) {
var accessibleSharedProjects = smController.getAllAvailableObjectsPermissions(
activeUserCreds.getUserId(),
SMObjects.PROJECT
);
projects = accessibleSharedProjects
.stream()
.map(projectPermission -> makeProjectFromPath(
sharedProjectsPath.resolve(projectPermission.getObjectId()),
projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()),
RMProject.Type.SHARED, true)
)
.filter(Objects::nonNull)
.collect(Collectors.toList());
projects = readAccessibleSharedProjects(activeUserCreds.getUserId());
} else {
projects = new ArrayList<>();
}
@@ -160,6 +148,26 @@ public class LocalResourceController implements RMController {
return projects.toArray(new RMProject[0]);
}
private List<RMProject> readAccessibleSharedProjects(@NotNull String userId) throws DBException {
if (credentialsProvider.hasPermission(DBWConstants.PERMISSION_ADMIN) || credentialsProvider.hasPermission(RMConstants.PERMISSION_RM_ADMIN)) {
return new ArrayList<>(Arrays.asList(listAllSharedProjects()));
}
var accessibleSharedProjects = smController.getAllAvailableObjectsPermissions(
userId,
SMObjects.PROJECT
);
return accessibleSharedProjects
.stream()
.map(projectPermission -> makeProjectFromPath(
sharedProjectsPath.resolve(projectPermission.getObjectId()),
projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()),
RMProject.Type.SHARED, true)
)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private Set<RMProjectPermission> getProjectPermissions(@Nullable String projectId, RMProject.Type projectType) throws DBException {
var activeUserCreds = credentialsProvider.getActiveUserCredentials();
@@ -209,12 +217,16 @@ public class LocalResourceController implements RMController {
throw new DBException("Error creating shared project path", e);
}
}
validateResourcePath(name);
validateResourcePath(name, PROJECT_REGEX);
RMProject project;
project = makeProjectFromPath(sharedProjectsPath.resolve(name), Set.of(), RMProject.Type.SHARED, false);
if (project == null) {
var projectPath = sharedProjectsPath.resolve(name);
if (Files.exists(projectPath)) {
throw new DBException("Project '" + name + "' already exists");
}
project = makeProjectFromPath(projectPath, Set.of(), RMProject.Type.SHARED, false);
if (project == null) {
throw new DBException("Project '" + name + "' not created");
}
try {
Files.createDirectories(getProjectPath(project.getId()));
return project;
@@ -477,11 +489,15 @@ public class LocalResourceController implements RMController {
}
private void validateResourcePath(String resourcePath) throws DBException {
validateResourcePath(resourcePath, FILE_REGEX);
}
private void validateResourcePath(String resourcePath, String regex) throws DBException {
if (resourcePath.startsWith(".")) {
throw new DBException("Resource path '" + resourcePath + "' can't start with dot");
}
if (!resourcePath.matches(FILE_REGEX)) {
String illegalCharacters = resourcePath.replaceAll(FILE_REGEX, " ").strip();
if (!resourcePath.matches(regex)) {
String illegalCharacters = resourcePath.replaceAll(regex, " ").strip();
throw new DBException("Resource path '" + resourcePath + "' contains illegal characters: " + illegalCharacters);
}
}
@@ -242,7 +242,8 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
}
public synchronized boolean hasPermission(String perm) {
return getSessionPermissions().contains(perm);
return getSessionPermissions().contains(DBWConstants.PERMISSION_ADMIN) ||
getSessionPermissions().contains(perm);
}
public synchronized boolean isAuthorizedInSecurityManager() {
@@ -254,7 +254,7 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
}
// Check permissions
for (String rp : reqPermissions) {
if (!sessionPermissions.contains(rp)) {
if (!session.hasPermission(rp)) {
log.debug("Access to " + method.getName() + " denied for " + session.getUser());
throw new DBWebExceptionAccessDenied("Access denied");
}
@@ -1,7 +1,7 @@
# Metadata queries
type RMProject {
id: String!
id: ID!
name: String!
description: String
shared: Boolean!
@@ -22,6 +22,10 @@ extend type Query {
# List accessible projects
rmListProjects: [RMProject!]!
rmListSharedProjects: [RMProject!]!
rmProject(projectId: String!): RMProject!
# List accessible projects
rmListResources(
projectId: String!,
@@ -61,8 +65,9 @@ extend type Mutation {
forceOverwrite: Boolean!): String!
rmCreateProject(
projectId: ID,
projectName: String!,
description: String): ProjectInfo
description: String): RMProject!
rmDeleteProject(projectId: ID!): Boolean!
}
@@ -35,6 +35,12 @@ public interface DBWServiceRM extends DBWService {
@WebAction
RMProject[] listProjects(@NotNull WebSession webSession) throws DBWebException;
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
RMProject[] listSharedProjects(@NotNull WebSession webSession) throws DBWebException;
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
RMProject getProject(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException;
@NotNull
@WebProjectAction(
requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_VIEW
@@ -83,13 +89,13 @@ public interface DBWServiceRM extends DBWService {
@NotNull String data,
boolean forceOverwrite) throws DBException;
@WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN})
WebProjectInfo createProject(
@WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN})
RMProject createProject(
@NotNull WebSession session,
@NotNull String name,
@Nullable String description) throws DBWebException;
@WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN})
@WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN})
boolean deleteProject(
@NotNull WebSession session,
@NotNull @WebObjectId String projectId) throws DBWebException;
@@ -38,6 +38,10 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
model.getQueryType()
.dataFetcher("rmListProjects",
env -> getService(env).listProjects(getWebSession(env)))
.dataFetcher("rmListSharedProjects",
env -> getService(env).listSharedProjects(getWebSession(env)))
.dataFetcher("rmProject",
env -> getService(env).getProject(getWebSession(env), env.getArgument("projectId")))
.dataFetcher("rmListResources",
env -> getService(env).listResources(getWebSession(env),
env.getArgument("projectId"),
@@ -44,6 +44,24 @@ public class WebServiceRM implements DBWServiceRM {
}
}
@Override
public RMProject[] listSharedProjects(@NotNull WebSession webSession) throws DBWebException {
try {
return getResourceController(webSession).listAllSharedProjects();
} catch (DBException e) {
throw new DBWebException("Error reading list of accessible projects", e);
}
}
@Override
public RMProject getProject(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException {
try {
return getResourceController(webSession).getProject(projectId, false, false);
} catch (DBException e) {
throw new DBWebException("Error reading list of accessible projects", e);
}
}
@NotNull
@Override
public RMResource[] listResources(WebSession webSession, @NotNull String projectId, @Nullable String folder, @Nullable String nameMask, boolean readProperties, boolean readHistory) throws DBException {
@@ -106,13 +124,13 @@ public class WebServiceRM implements DBWServiceRM {
}
@Override
public WebProjectInfo createProject(
public RMProject createProject(
@NotNull WebSession session, @NotNull String name, @Nullable String description
) throws DBWebException {
try {
RMProject rmProject = getResourceController(session).createProject(name, description);
var project = session.createVirtualProject(rmProject);
return new WebProjectInfo(session, project);
session.createVirtualProject(rmProject);
return rmProject;
} catch (DBException e) {
throw new DBWebException("Error creating project", e);
}
@@ -24,6 +24,15 @@ import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormCon
import { FormContext } from './FormContext';
const styles = css`
field[|inline] {
display: flex;
align-items: center;
& field-label {
padding-right: 8px;
padding-bottom: 0;
}
}
field input {
margin: 0;
}
@@ -137,6 +146,7 @@ type BaseProps<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>,
iconSelector?: (item: TValue) => string | React.ReactElement | undefined;
isDisabled?: (item: TValue) => boolean;
onSwitch?: (state: boolean) => void;
inline?: boolean;
};
type ControlledProps<TKey, TValue> = BaseProps<TKey, TValue> & {
@@ -174,6 +184,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
searchable,
readOnly,
disabled,
inline,
keySelector = v => v,
valueSelector = v => v,
iconSelector,
@@ -337,7 +348,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
}
return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles))(
<field className={className}>
<field className={className} {...use({ inline })}>
{children && <field-label title={title}>{children}{rest.required && ' *'}</field-label>}
<input-box>
{(icon || loading) && (
@@ -13,12 +13,7 @@ import { IconOrImage, Link } from '@cloudbeaver/core-blocks';
import { ENotificationType } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
interface Props {
message: string | undefined | null;
status: ENotificationType | undefined;
onShowDetails?: () => void;
className?: string;
}
import { useErrorDetails } from './useErrorDetails';
const styles = css`
status-message {
@@ -43,17 +38,36 @@ const styles = css`
}
`;
export const StatusMessage = observer<Props>(function StatusMessage({ status, message, onShowDetails, className }) {
interface Props {
message?: string | null;
status?: ENotificationType;
exception?: Error | null;
onShowDetails?: () => void;
className?: string;
}
export const StatusMessage = observer<Props>(function StatusMessage({
status,
message,
exception = null,
onShowDetails,
className,
}) {
const translate = useTranslate();
message = message ? translate(message) : message;
const errorDetails = useErrorDetails(exception);
message = (message ? translate(message) : message) ?? errorDetails.details?.name;
let icon = '/icons/info_icon.svg';
if (status === ENotificationType.Error) {
if (status === ENotificationType.Error || exception !== null) {
icon = '/icons/error_icon.svg';
} else if (status === ENotificationType.Success) {
icon = '/icons/success_icon.svg';
}
if (errorDetails.details?.hasDetails && !onShowDetails) {
onShowDetails = errorDetails.open;
}
return styled(styles)(
<status-message className={className}>
{message && (
@@ -8,7 +8,7 @@
import { observable, computed, action } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useState, useCallback, useMemo } from 'react';
import { useState, useCallback, useMemo, useEffect } from 'react';
import styled, { use } from 'reshadow';
import { useObjectRef } from '../useObjectRef';
@@ -34,6 +34,23 @@ export const Table = observer<React.PropsWithChildren<Props>>(function Table({
const [selected] = useState<Map<any, boolean>>(() => selectedItems || observable(new Map()));
const [expanded] = useState<Map<any, boolean>>(() => expandedItems || observable(new Map()));
useEffect(action(() => {
if (!keys) {
return;
}
const removeSelected = Array.from(selected.keys()).filter(key => !keys.includes(key));
const removeExpanded = Array.from(expanded.keys()).filter(key => !keys.includes(key));
for (const id of removeSelected) {
selected.delete(id);
}
for (const id of removeExpanded) {
expanded.delete(id);
}
}), [keys]);
const state: ITableState = useObservableRef(() => ({
get selectableItems() {
if (!this.keys) {
@@ -129,7 +129,7 @@ export const TREE_NODE_STYLES = css`
composes: theme-typography--caption from global;
padding: 4px 12px;
[big] {
&[big] {
padding: 4px 24px;
}
}
@@ -56,6 +56,7 @@ interface IMapResourceState<TResource extends CachedMapResource<any, any, any>>
loaded: boolean;
resource: TResource;
isOutdated: () => boolean;
reload: () => void;
}
interface IMapResourceListResult<
@@ -20,6 +20,7 @@ import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree';
import { corePluginManifest } from '@cloudbeaver/core-plugin';
import { coreProductManifest } from '@cloudbeaver/core-product';
import { coreProjectsManifest } from '@cloudbeaver/core-projects';
import { resourceManagerManifest } from '@cloudbeaver/core-resource-manager';
import { coreRootManifest } from '@cloudbeaver/core-root';
import { coreRoutingManifest } from '@cloudbeaver/core-routing';
import { coreSDKManifest } from '@cloudbeaver/core-sdk';
@@ -61,6 +62,7 @@ export const coreManifests: PluginManifest[] = [
coreConnectionsManifest,
coreAdministrationManifest,
codeDialogsManifest,
resourceManagerManifest,
coreAppManifest,
coreNavigationTree,
];
+7 -2
View File
@@ -6,9 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import type { ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { IExecutorHandlersCollection, ISyncContextLoader } from '@cloudbeaver/core-executor';
import { App } from './App';
import { dependencyInjectorContext } from './dependencyInjectorContext';
import type { IServiceConstructor, IServiceInjector } from './IApp';
import { injectable } from './injectable';
@@ -22,7 +23,11 @@ export class DIService {
private readonly app: App
) { }
dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = (
addDIContext(context: IExecutorHandlersCollection<any>): void {
context.addContextCreator(dependencyInjectorContext, this.dependencyInjectorContext);
}
private readonly dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = (
() => this.serviceInjector.getServiceByClass.bind(this)
);
}
@@ -0,0 +1,15 @@
/*
* 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 type { ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { IServiceConstructor } from './IApp';
export const dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = function dependencyInjectorContext() {
throw new Error('Implementation not provided \n Use addContextCreator to add addDIContext implementation to context');
};
+1
View File
@@ -3,6 +3,7 @@ export * from './App';
export * from './AppContext';
export * from './Bootstrap';
export * from './Dependency';
export * from './dependencyInjectorContext';
export * from './DIService';
export * from './injectable';
export * from './PluginManifest';
@@ -10,23 +10,39 @@ import type { IAsyncContextLoader, IContextLoader, IExecutionContext, ISyncConte
export class ExecutionContext<TData> implements IExecutionContext<TData> {
readonly contexts: Map<IContextLoader<any, TData>, any>;
readonly contextCreators: Map<IContextLoader<any, TData>, IContextLoader<any, TData>>;
constructor(private data: TData, context?: IExecutionContext<any>) {
this.contexts = context?.contexts || new Map<IContextLoader<any, TData>, any>();
constructor(private readonly data: TData, context?: IExecutionContext<any>) {
this.contexts = context?.contexts || new Map();
this.contextCreators = context?.contextCreators ?? new Map();
}
hasContext(loader: IContextLoader<any>): boolean {
return this.contexts.has(loader);
addContextCreators(
creators: [IContextLoader<any, TData>, IContextLoader<any, TData>][]
): void {
for (const [key, value] of creators) {
this.contextCreators.set(key, value);
}
}
getContext<T>(token: ISyncContextLoader<T, TData>): T
getContext<T>(token: IAsyncContextLoader<T, TData>): Promise<T>
hasContext(loader: IContextLoader): boolean {
return this.contextCreators.has(loader) || this.contexts.has(loader);
}
getContext<T>(token: ISyncContextLoader<T, TData>): T;
getContext<T>(token: IAsyncContextLoader<T, TData>): Promise<T>;
getContext<T>(token: IContextLoader<T, TData>): Promise<T> | T {
if (this.contexts.has(token)) {
return this.contexts.get(token);
}
const value = token(this, this.data);
let value: T | Promise<T>;
if (this.contextCreators.has(token)) {
value = this.contextCreators.get(token)!(this, this.data);
} else {
value = token(this, this.data);
}
if (value instanceof Promise) {
return this.getAsyncContext(token, value);
+15 -2
View File
@@ -9,6 +9,7 @@
import { flat } from '@cloudbeaver/core-utils';
import { ExecutionContext } from './ExecutionContext';
import { executionExceptionContext } from './executionExceptionContext';
import { ExecutorHandlersCollection } from './ExecutorHandlersCollection';
import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter';
import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext';
@@ -68,9 +69,16 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
context: IExecutionContext<T>,
scoped: Array<IExecutorHandlersCollection<T>>
): Promise<IExecutionContextProvider<T>> {
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
scoped = [...collection.collections, ...scoped];
context.addContextCreators(collection.contextCreators as any);
for (const scope of scoped) {
context.addContextCreators(scope.contextCreators as any);
}
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
await this.executeChain(collection, data, context, 'before');
for (const scope of scoped) {
@@ -83,7 +91,11 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
for (const scope of scoped) {
await this.executeHandlers(data, context, scope.handlers, interrupter);
}
} finally {
} catch (exception: any) {
const exceptionContext = context.getContext(executionExceptionContext);
exceptionContext.setException(exception);
throw exception;
} finally {
await this.executeHandlers(data, context, collection.postHandlers);
for (const scope of scoped) {
@@ -158,6 +170,7 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
this.scheduler.schedule(data, async () => {
const context = new ExecutionContext(data);
context.addContextCreators(this.contextCreators as any);
try {
await handler(data, context);
@@ -8,6 +8,7 @@
import { makeObservable, observable } from 'mobx';
import type { IContextLoader, ISyncContextLoader, IAsyncContextLoader } from './IExecutionContext';
import type { IExecutorHandler } from './IExecutorHandler';
import type { ExecutorDataFilter, ExecutorDataMap, IChainLink, IExecutorHandlersCollection } from './IExecutorHandlersCollection';
@@ -16,12 +17,14 @@ implements IExecutorHandlersCollection<T, TResult> {
handlers: Array<IExecutorHandler<T, TResult>> = [];
postHandlers: Array<IExecutorHandler<T, TResult>> = [];
chain: Array<IChainLink<T, TResult>> = [];
readonly contextCreators: Map<IContextLoader<any, T>, IContextLoader<any, T>>;
readonly collections: Array<IExecutorHandlersCollection<T, TResult>>;
protected initialDataGetter: (() => T) | null;
private readonly links: Map<IExecutorHandlersCollection<any, TResult>, IExecutorHandlersCollection<T, TResult>>;
constructor() {
this.links = new Map();
this.contextCreators = new Map();
this.collections = [];
this.initialDataGetter = null;
@@ -34,6 +37,22 @@ implements IExecutorHandlersCollection<T, TResult> {
});
}
addContextCreator<TContext>(
context: ISyncContextLoader<TContext, T>,
creator: ISyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IAsyncContextLoader<TContext, T>,
creator: IAsyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IContextLoader<TContext, T>,
creator: IContextLoader<TContext, T>
): this {
this.contextCreators.set(context, creator);
return this;
}
setInitialDataGetter(getter: (() => T) | null): this {
this.initialDataGetter = getter;
return this;
@@ -17,7 +17,13 @@ export interface IExecutionContextProvider<TData> {
}
export interface IExecutionContext<TData> extends IExecutionContextProvider<TData> {
readonly parent?: IExecutionContext<any>;
readonly contexts: Map<IContextLoader<any, TData>, any>;
readonly contextCreators: Map<IContextLoader<any, TData>, IContextLoader<any, TData>>;
addContextCreators(
creators: [IContextLoader<any, TData>, IContextLoader<any, TData>][]
): void;
}
export type IAsyncContextLoader<T = any, TData = any> = (
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { IExecutionContextProvider } from './IExecutionContext';
import type { IAsyncContextLoader, IContextLoader, IExecutionContextProvider, ISyncContextLoader } from './IExecutionContext';
import type { IExecutorHandler } from './IExecutorHandler';
export type ExecutorDataFilter<T> = (data: T, contexts: IExecutionContextProvider<T>) => boolean;
@@ -25,9 +25,19 @@ export interface IExecutorHandlersCollection<T = unknown, TResult = any | Promis
readonly postHandlers: Array<IExecutorHandler<T, TResult>>;
readonly chain: Array<IChainLink<T, TResult>>;
readonly collections: Array<IExecutorHandlersCollection<T, TResult>>;
readonly contextCreators: Map<IContextLoader<any, T>, IContextLoader<any, T>>;
setInitialDataGetter(getter: () => T): this;
addContextCreator<TContext>(
context: ISyncContextLoader<TContext, T>,
creator: ISyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IAsyncContextLoader<TContext, T>,
creator: IAsyncContextLoader<TContext, T>
): this;
before: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
next: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
addCollection: (collection: IExecutorHandlersCollection<T, TResult>) => this;
@@ -9,6 +9,7 @@
import { flat } from '@cloudbeaver/core-utils';
import { ExecutionContext } from './ExecutionContext';
import { executionExceptionContext } from './executionExceptionContext';
import { ExecutorHandlersCollection } from './ExecutorHandlersCollection';
import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter';
import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext';
@@ -55,9 +56,16 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
context: IExecutionContext<T>,
scoped: Array<IExecutorHandlersCollection<T>>
): IExecutionContextProvider<T> {
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
scoped = [...collection.collections, ...scoped];
context.addContextCreators(collection.contextCreators as any);
for (const scope of scoped) {
context.addContextCreators(scope.contextCreators as any);
}
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
this.executeChain(collection, data, context, 'before');
for (const scope of scoped) {
@@ -70,6 +78,10 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
for (const scope of scoped) {
this.executeHandlers(data, context, scope.handlers, interrupter);
}
} catch (exception: any) {
const exceptionContext = context.getContext(executionExceptionContext);
exceptionContext.setException(exception);
throw exception;
} finally {
this.executeHandlers(data, context, collection.postHandlers);
@@ -144,6 +156,8 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
const data = this.initialDataGetter();
const context = new ExecutionContext(data);
context.addContextCreators(this.contextCreators as any);
try {
handler(data, context);
} finally {
@@ -0,0 +1,16 @@
/*
* 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.
*/
export function executionExceptionContext() {
return {
exception: null as null | Error,
setException(exception: Error) {
this.exception = exception;
},
};
}
@@ -3,6 +3,7 @@ export * from './TaskScheduler/ITask';
export * from './TaskScheduler/Task';
export * from './TaskScheduler/TaskScheduler';
export * from './ExecutionContext';
export * from './executionExceptionContext';
export * from './Executor';
export * from './ExecutorHandlersCollection';
export * from './ExecutorInterrupter';
@@ -10,32 +10,37 @@ import { runInAction } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { GraphQLService, ProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk';
import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager';
import { GraphQLService, ProjectInfo as SchemaProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk';
export type Project = ProjectInfo;
export type ProjectInfo = SchemaProjectInfo;
@injectable()
export class ProjectsResource extends CachedMapResource<string, Project> {
export class ProjectInfoResource extends CachedMapResource<string, ProjectInfo> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly sharedProjectsResource: SharedProjectsResource,
private readonly userInfoResource: UserInfoResource,
) {
super([]);
this.sync(this.userInfoResource);
this.sharedProjectsResource.onDataOutdated.addHandler(this.markOutdated.bind(this));
this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated());
this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated());
this.userInfoResource.onUserChange.addPostHandler(() => {
this.clear();
});
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, Project>> {
protected async loader(key: ResourceKey<string>): Promise<Map<string, ProjectInfo>> {
const all = ResourceKeyUtils.includes(key, CachedMapAllKey);
const { projects } = await this.graphQLService.sdk.getProjectList();
runInAction(() => {
if (all) {
this.data.clear();
this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id))));
}
this.set(resourceKeyList(projects.map(project => project.id)), projects);
@@ -11,18 +11,18 @@ import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { NAV_NODE_TYPE_PROJECT } from './NAV_NODE_TYPE_PROJECT';
import { Project, ProjectsResource } from './ProjectsResource';
import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource';
@injectable()
export class ProjectsNavNodeService {
constructor(
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectsResource: ProjectsResource
private readonly projectsResource: ProjectInfoResource
) {
}
getProject(nodeId: string): Project | undefined {
getProject(nodeId: string): ProjectInfo | undefined {
const parentIds = [...this.navNodeInfoResource.getParents(nodeId), nodeId];
const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds));
@@ -35,7 +35,7 @@ export class ProjectsNavNodeService {
return this.getByNodeId(projectNode.id);
}
getByNodeId(nodeId: string): Project | undefined {
getByNodeId(nodeId: string): ProjectInfo | undefined {
return this.projectsResource.get(nodeId.replace('resource://', ''));
}
@@ -12,12 +12,12 @@ import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { Project, ProjectsResource } from './ProjectsResource';
import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource';
@injectable()
export class ProjectsService {
get activeProject(): Project | undefined {
let project: Project | undefined;
get activeProject(): ProjectInfo | undefined {
let project: ProjectInfo | undefined;
if (this.activeProjectId) {
project = this.projectsResource.get(this.activeProjectId);
@@ -41,7 +41,7 @@ export class ProjectsService {
private activeProjectId: string | null;
constructor(
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly userInfoResource: UserInfoResource
) {
this.activeProjectId = null;
@@ -51,7 +51,7 @@ export class ProjectsService {
});
}
setActiveProject(project: Project): void {
setActiveProject(project: ProjectInfo): void {
this.activeProjectId = project.id;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export * from './ProjectsResource';
export * from './ProjectInfoResource';
export * from './ProjectsService';
export * from './NAV_NODE_TYPE_PROJECT';
export * from './PROJECT_GLOBAL_ID';
@@ -9,8 +9,8 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { ProjectInfoResource } from './ProjectInfoResource';
import { ProjectsNavNodeService } from './ProjectsNavNodeService';
import { ProjectsResource } from './ProjectsResource';
import { ProjectsService } from './ProjectsService';
export const manifest: PluginManifest = {
@@ -19,7 +19,7 @@ export const manifest: PluginManifest = {
},
providers: [
ProjectsResource,
ProjectInfoResource,
ProjectsService,
LocaleService,
ProjectsNavNodeService,
@@ -0,0 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,26 @@
{
"name": "@cloudbeaver/core-resource-manager",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-navigation-tree": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0"
}
}
@@ -0,0 +1,111 @@
/*
* 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 { runInAction } from 'mobx';
import { EAdminPermission } from '@cloudbeaver/core-administration';
import { injectable } from '@cloudbeaver/core-di';
import { SessionPermissionsResource } from '@cloudbeaver/core-root';
import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList } from '@cloudbeaver/core-sdk';
const newSymbol = Symbol('new-project');
export type SharedProject = RmProject;
type SharedProjectNew = SharedProject & { [newSymbol]: boolean };
interface IProjectConfig {
id: string;
name: string;
description?: string;
}
@injectable()
export class SharedProjectsResource extends CachedMapResource<string, SharedProject> {
constructor(
private readonly graphQLService: GraphQLService,
sessionPermissionsResource: SessionPermissionsResource,
) {
super([]);
sessionPermissionsResource
.require(this, EAdminPermission.admin);
this.sync(sessionPermissionsResource, () => {});
}
isNew(id: string): boolean {
if (!this.has(id)) {
return true;
}
return newSymbol in this.get(id)!;
}
async create(config: IProjectConfig): Promise<SharedProject> {
const { project } = await this.graphQLService.sdk.createProject({
projectId: config.id,
projectName: config.name,
description: config.description,
});
(project as SharedProjectNew)[newSymbol] = true;
this.set(project.id, project as SharedProject);
return this.get(project.id)!;
}
async deleteProject(id: string): Promise<void>;
async deleteProject(id: ResourceKeyList<string>): Promise<void>;
async deleteProject(key: ResourceKey<string>): Promise<void> {
const deleted: string[] = [];
try {
await this.performUpdate(key, undefined, async key => {
await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => {
await this.graphQLService.sdk.deleteProject({
projectId,
});
deleted.push(projectId);
});
});
} finally {
this.delete(resourceKeyList(deleted));
}
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, SharedProject>> {
const all = ResourceKeyUtils.includes(key, CachedMapAllKey);
if (all) {
const { projects } = await this.graphQLService.sdk.getSharedProjects();
runInAction(() => {
this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id))));
this.set(resourceKeyList(projects.map(project => project.id)), projects as SharedProject[]);
});
} else {
await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => {
const { project } = await this.graphQLService.sdk.getProject({
projectId,
});
this.set(projectId, project as SharedProject);
});
}
return this.data;
}
protected dataSet(key: string, value: SharedProject): void {
key = this.getKeyRef(key);
const data = this.data.get(key);
this.data.set(key, Object.assign(data ?? {}, value));
}
}
@@ -0,0 +1,2 @@
export * from './SharedProjectsResource';
export { resourceManagerManifest } from './manifest';
@@ -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 type { PluginManifest } from '@cloudbeaver/core-di';
import { SharedProjectsResource } from './SharedProjectsResource';
export const resourceManagerManifest: PluginManifest = {
info: {
name: 'Resource Manager Core',
},
providers: [
SharedProjectsResource,
],
};
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
"references": [{ "path": "../*" }]
}
@@ -0,0 +1,7 @@
fragment SharedProject on RMProject {
id
name
shared
description
projectPermissions
}
@@ -0,0 +1,13 @@
mutation createProject(
$projectId: ID
$projectName: String!
$description: String
) {
project: rmCreateProject(
projectId: $projectId
projectName: $projectName
description: $description
) {
...SharedProject
}
}
@@ -0,0 +1,3 @@
mutation deleteProject($projectId: ID!) {
rmDeleteProject(projectId: $projectId)
}
@@ -0,0 +1,7 @@
query getProject($projectId: String!) {
project: rmProject(
projectId: $projectId
) {
...SharedProject
}
}
@@ -0,0 +1,5 @@
query getSharedProjects {
projects: rmListSharedProjects {
...SharedProject
}
}
+91 -2
View File
@@ -366,7 +366,7 @@ export interface Mutation {
openSession: SessionInfo;
readLobValue: Scalars['String'];
refreshSessionConnections?: Maybe<Scalars['Boolean']>;
rmCreateProject?: Maybe<ProjectInfo>;
rmCreateProject: RmProject;
rmCreateResource: Scalars['String'];
rmDeleteProject: Scalars['Boolean'];
rmDeleteResource?: Maybe<Scalars['Boolean']>;
@@ -532,6 +532,7 @@ export interface MutationReadLobValueArgs {
export interface MutationRmCreateProjectArgs {
description?: InputMaybe<Scalars['String']>;
projectId?: InputMaybe<Scalars['ID']>;
projectName: Scalars['String'];
}
@@ -862,6 +863,8 @@ export interface Query {
revokeUserRole?: Maybe<Scalars['Boolean']>;
rmListProjects: Array<RmProject>;
rmListResources: Array<RmResource>;
rmListSharedProjects: Array<RmProject>;
rmProject: RmProject;
rmReadResourceAsString: Scalars['String'];
saveAuthProviderConfiguration: AdminAuthProviderConfiguration;
saveUserMetaParameter: ObjectPropertyInfo;
@@ -1116,6 +1119,11 @@ export interface QueryRmListResourcesArgs {
}
export interface QueryRmProjectArgs {
projectId: Scalars['String'];
}
export interface QueryRmReadResourceAsStringArgs {
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -1278,7 +1286,7 @@ export interface RmProject {
createTime: Scalars['DateTime'];
creator: Scalars['String'];
description?: Maybe<Scalars['String']>;
id: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
projectPermissions: Array<Scalars['String']>;
shared: Scalars['Boolean'];
@@ -2191,6 +2199,8 @@ export type SqlScriptInfoFragment = { queries: Array<{ start: number, end: numbe
export type SessionStateFragment = { createTime: string, lastAccessTime: string, cacheExpired: boolean, locale: string, actionParameters?: any, valid: boolean, remainingTime: number };
export type SharedProjectFragment = { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> };
export type UserConnectionAuthPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array<any>, defaultValue?: any, length: ObjectPropertyLength, features: Array<string>, order: number };
export type UserConnectionNetworkHandlerPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array<any>, defaultValue?: any, order: number, length: ObjectPropertyLength, features: Array<string> };
@@ -2395,6 +2405,15 @@ export type GetProjectListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canCreateConnections: boolean, canViewConnections: boolean }> };
export type CreateProjectMutationVariables = Exact<{
projectId?: InputMaybe<Scalars['ID']>;
projectName: Scalars['String'];
description?: InputMaybe<Scalars['String']>;
}>;
export type CreateProjectMutation = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type CreateResourceMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2404,6 +2423,13 @@ export type CreateResourceMutationVariables = Exact<{
export type CreateResourceMutation = { rmCreateResource: string };
export type DeleteProjectMutationVariables = Exact<{
projectId: Scalars['ID'];
}>;
export type DeleteProjectMutation = { rmDeleteProject: boolean };
export type DeleteResourceMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2413,6 +2439,13 @@ export type DeleteResourceMutationVariables = Exact<{
export type DeleteResourceMutation = { rmDeleteResource?: boolean };
export type GetProjectQueryVariables = Exact<{
projectId: Scalars['String'];
}>;
export type GetProjectQuery = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type GetResourceListQueryVariables = Exact<{
projectId: Scalars['String'];
folder?: InputMaybe<Scalars['String']>;
@@ -2429,6 +2462,11 @@ export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never;
export type GetResourceProjectListQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> }> };
export type GetSharedProjectsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetSharedProjectsQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> }> };
export type MoveResourceMutationVariables = Exact<{
projectId: Scalars['String'];
oldPath: Scalars['String'];
@@ -2870,6 +2908,15 @@ export const SessionStateFragmentDoc = `
remainingTime
}
`;
export const SharedProjectFragmentDoc = `
fragment SharedProject on RMProject {
id
name
shared
description
projectPermissions
}
`;
export const UserConnectionNetworkHandlerPropertiesFragmentDoc = `
fragment UserConnectionNetworkHandlerProperties on ObjectPropertyInfo {
id
@@ -3850,6 +3897,17 @@ export const GetProjectListDocument = `
}
}
`;
export const CreateProjectDocument = `
mutation createProject($projectId: ID, $projectName: String!, $description: String) {
project: rmCreateProject(
projectId: $projectId
projectName: $projectName
description: $description
) {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const CreateResourceDocument = `
mutation createResource($projectId: String!, $resourcePath: String!, $isFolder: Boolean!) {
rmCreateResource(
@@ -3859,6 +3917,11 @@ export const CreateResourceDocument = `
)
}
`;
export const DeleteProjectDocument = `
mutation deleteProject($projectId: ID!) {
rmDeleteProject(projectId: $projectId)
}
`;
export const DeleteResourceDocument = `
mutation deleteResource($projectId: String!, $resourcePath: String!, $recursive: Boolean!) {
rmDeleteResource(
@@ -3868,6 +3931,13 @@ export const DeleteResourceDocument = `
)
}
`;
export const GetProjectDocument = `
query getProject($projectId: String!) {
project: rmProject(projectId: $projectId) {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const GetResourceListDocument = `
query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) {
resources: rmListResources(
@@ -3894,6 +3964,13 @@ export const GetResourceProjectListDocument = `
}
}
`;
export const GetSharedProjectsDocument = `
query getSharedProjects {
projects: rmListSharedProjects {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const MoveResourceDocument = `
mutation moveResource($projectId: String!, $oldPath: String!, $newPath: String!) {
rmMoveResource(
@@ -4404,18 +4481,30 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getProjectList(variables?: GetProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectListQuery>(GetProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectList', 'query');
},
createProject(variables: CreateProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreateProjectMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreateProjectMutation>(CreateProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createProject', 'mutation');
},
createResource(variables: CreateResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreateResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreateResourceMutation>(CreateResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createResource', 'mutation');
},
deleteProject(variables: DeleteProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<DeleteProjectMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<DeleteProjectMutation>(DeleteProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteProject', 'mutation');
},
deleteResource(variables: DeleteResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<DeleteResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<DeleteResourceMutation>(DeleteResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteResource', 'mutation');
},
getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectQuery>(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query');
},
getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceListQuery>(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query');
},
getResourceProjectList(variables?: GetResourceProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceProjectListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceProjectListQuery>(GetResourceProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceProjectList', 'query');
},
getSharedProjects(variables?: GetSharedProjectsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetSharedProjectsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetSharedProjectsQuery>(GetSharedProjectsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSharedProjects', 'query');
},
moveResource(variables: MoveResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<MoveResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<MoveResourceMutation>(MoveResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'moveResource', 'mutation');
},
@@ -8,8 +8,11 @@
import { observer } from 'mobx-react-lite';
import { useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { AdminConnectionSearchInfo, CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { ConnectionForm } from '@cloudbeaver/plugin-connections';
import { ConnectionSearchService } from './ConnectionSearchService';
@@ -17,6 +20,7 @@ import { DatabaseList } from './DatabaseList';
export const SearchDatabase: React.FC = observer(function SearchDatabase() {
const connectionSearchService = useService(ConnectionSearchService);
useMapResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey);
function select(database: AdminConnectionSearchInfo) {
connectionSearchService.select(database);
@@ -13,7 +13,7 @@ import { useMemo } from 'react';
import { Loader, useMapResource } from '@cloudbeaver/core-blocks';
import { DBDriverResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { ConnectionManualService } from './ConnectionManualService';
@@ -27,7 +27,7 @@ export const CustomConnection = observer(function CustomConnection() {
dbDriverResource.resource.enabledDrivers.slice().sort(dbDriverResource.resource.compare)
)), [dbDriverResource]);
useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey);
useMapResource(CustomConnection, ProjectInfoResource, CachedMapAllKey);
function select(driverId: string) {
connectionManualService.select(PROJECT_GLOBAL_ID, driverId);
@@ -11,7 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx';
import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils';
@@ -26,7 +26,7 @@ import { Options } from './Options';
@injectable()
export class ConnectionOptionsTabService extends Bootstrap {
constructor(
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly connectionFormService: ConnectionFormService,
private readonly dbDriverResource: DBDriverResource,
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
@@ -34,9 +34,9 @@ import {
import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { Project, ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { usePermission } from '@cloudbeaver/core-root';
import { CachedMapAllKey, CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
@@ -44,6 +44,7 @@ import { useAuthenticationAction } from '@cloudbeaver/core-ui';
import { ConnectionFormService } from '../ConnectionFormService';
import type { IConnectionFormProps } from '../IConnectionFormProps';
import { ParametersForm } from './ParametersForm';
import { ProjectSelect } from './ProjectSelect';
import { useOptions } from './useOptions';
const styles = css`
@@ -116,16 +117,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
optionsHook.setAuthModel(model);
}, []);
const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey);
const projects = projectsLoader.data as Project[];
function handleProjectSelect(projectId: string) {
const project = projectsLoader.resource.get(projectId);
if (project?.canCreateConnections) {
state.setProject(projectId);
}
}
const driverMap = useMapResource(
Options,
@@ -304,24 +295,12 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
>
{translate('connections_connection_name')}
</InputField>
<Combobox
name='projectId'
<ProjectSelect
value={state.projectId ?? PROJECT_GLOBAL_ID}
items={projects}
keySelector={project => project.id}
valueSelector={project => project.name}
titleSelector={project => project.description}
isDisabled={project => !project.canCreateConnections}
searchable={projects.length > 10}
readOnly={readonly || edit || projects.length < 2}
readOnly={readonly || edit}
disabled={disabled}
loading={projectsLoader.isLoading()}
tiny
fill
onSelect={handleProjectSelect}
>
{translate('connections_connection_project')}
</Combobox>
onChange={projectId => state.setProject(projectId)}
/>
{!config.template && (
<InputField
type="text"
@@ -0,0 +1,70 @@
/*
* 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 { observer } from 'mobx-react-lite';
import { Combobox, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
interface Props {
value: string | null;
onChange: (value: string) => void;
readOnly?: boolean;
disabled?: boolean;
inline?: boolean;
}
export const ProjectSelect = observer(function ProjectSelect(props: Props) {
const translate = useTranslate();
const projectsService = useService(ProjectsService);
const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, {
onData: () => {
if (!props.value && projectsService.activeProject) {
props.onChange(projectsService.activeProject.id);
}
},
});
const value = props.value ?? projectsService.activeProject?.id;
const projects = projectsLoader.data as ProjectInfo[];
const possibleOptions = projects.filter(project => project.canCreateConnections);
function handleProjectSelect(projectId: string) {
const project = projectsLoader.resource.get(projectId);
if (project?.canCreateConnections) {
props.onChange(projectId);
}
}
return (
<Combobox
name='projectId'
value={value ?? ''}
items={projects}
keySelector={project => project.id}
valueSelector={project => project.name}
titleSelector={project => project.description}
isDisabled={project => !project.canCreateConnections}
readOnly={props.readOnly || possibleOptions.length <= 1}
searchable={projects.length > 10}
disabled={props.disabled}
loading={projectsLoader.isLoading()}
inline={props.inline}
tiny
fill
onSelect={handleProjectSelect}
>
{translate('connections_connection_project')}
</Combobox>
);
});
@@ -17,7 +17,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectsResource } from '@cloudbeaver/core-projects';
import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectInfoResource } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { createPath } from '@cloudbeaver/core-utils';
import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view';
@@ -47,7 +47,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
private readonly commonDialogService: CommonDialogService,
private readonly notificationService: NotificationService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly projectsNavNodeService: ProjectsNavNodeService
) {
super();
@@ -2,6 +2,7 @@ import { connectionPlugin } from './manifest';
export * from './ConnectionAuthentication/IConnectionAuthenticationConfig';
export * from './ConnectionAuthentication/ConnectionAuthenticationForm';
export * from './ConnectionForm/Options/ConnectionOptionsTabService';
export * from './ConnectionForm/Options/ProjectSelect';
export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService';
export * from './ConnectionForm/SSH/ConnectionSSHTabService';
export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService';
@@ -26,6 +26,7 @@ export type NavTreeControlProps = {
dndPlaceholder?: boolean;
expanded?: boolean;
className?: string;
style?: ComponentStyle;
ref?: React.Ref<HTMLDivElement> | undefined;
};
@@ -113,6 +113,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
<Control
ref={setRef}
dndElement={dndData.state.isDragging}
style={style}
node={node}
/>
{(expanded || externalExpanded) && (
@@ -59,6 +59,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
node,
dndElement,
dndPlaceholder,
className,
}, ref) {
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
@@ -83,7 +84,12 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
}
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
<TreeNodeControl
ref={ref}
onClick={onClickHandler}
{...use({ outdated, editing, dragging: dndElement })}
className={className}
>
<TreeNodeName title={node.name}>
{editing ? (
<NavigationNodeEditor node={node} onClose={() => setEditing(false)} />
@@ -14,7 +14,6 @@ import { Translate } from '@cloudbeaver/core-localization';
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import { useChildren } from '../../NodesManager/useChildren';
import { useNode } from '../../NodesManager/useNode';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer';
@@ -24,9 +23,14 @@ import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
const nestedStyles = css`
TreeNode {
margin-top: 8px;
}
NavigationNodeNested {
padding-left: 8px !important;
&:only-child Control {
display: none;
}
& NavigationNodeNested {
padding-left: 0 !important;
}
}
`;
@@ -1,11 +1,14 @@
export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/KEY_BINDING_ENABLE_FILTER';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeRenderer';
export * from './NavigationTree/ElementsTree/ACTION_LINK_OBJECT';
export * from './NavigationTree/ElementsTree/DATA_CONTEXT_ELEMENTS_TREE';
export * from './NavigationTree/ElementsTree/ElementsTree';
export * from './NavigationTree/ElementsTree/ElementsTreeContext';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl';
export * from './NavigationTree/ElementsTree/KEY_BINDING_COLLAPSE_ALL';
export * from './NavigationTree/ElementsTree/KEY_BINDING_LINK_OBJECT';
export * from './NavigationTree/ElementsTree/MENU_NAV_TREE';
@@ -13,8 +16,8 @@ export * from './NavigationTree/ElementsTree/NavigationNodeComponent';
export * from './NavigationTree/ElementsTree/NavNodeFilterCompareFn';
export * from './NavigationTree/ElementsTree/useElementsTree';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS';
export * from './NavigationTree/NavigationTreeBootstrap';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService';
export * from './NavigationTree/NavigationTreeBootstrap';
export * from './NavigationTree/NavigationTreeService';
export * from './NavigationTree/NavigationTree';
export * from './NodesManager/NavNodeView/IFolderTransform';
@@ -10,6 +10,7 @@ import { computed, makeObservable } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager';
import { GraphQLService, CachedDataResource, RmProject } from '@cloudbeaver/core-sdk';
export type Project = Omit<RmProject, 'creator' | 'createTime' | 'canEditConnection' | 'canEditResource'>;
@@ -23,6 +24,7 @@ export class ResourceProjectsResource extends CachedDataResource<Project[]> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly userInfoResource: UserInfoResource,
private readonly sharedProjectsResource: SharedProjectsResource,
) {
super([]);
@@ -31,6 +33,10 @@ export class ResourceProjectsResource extends CachedDataResource<Project[]> {
this.markOutdated();
});
this.sharedProjectsResource.onDataOutdated.addHandler(() => this.markOutdated());
this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated());
this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated());
makeObservable(this, {
userProject: computed,
});
@@ -11,9 +11,12 @@ import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus, useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs';
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { useStyles } from '@cloudbeaver/core-theming';
import { ProjectSelect } from '@cloudbeaver/plugin-connections/src/ConnectionForm/Options/ProjectSelect';
const style = css`
fill {
@@ -25,8 +28,12 @@ interface Payload {
defaultScriptName?: string;
}
interface State {
value: string;
export interface ISaveScriptDialogResult {
name: string;
projectId: string;
}
interface State extends ISaveScriptDialogResult {
errorMessage: string | null;
validate: () => void;
submit: () => Promise<void>;
@@ -34,7 +41,7 @@ interface State {
const regex = /^(?!\.)[\p{L}\w\-$.\s()@]+$/u;
export const SaveScriptDialog: DialogComponent<Payload, string> = observer(function SaveScriptDialog({
export const SaveScriptDialog: DialogComponent<Payload, ISaveScriptDialogResult> = observer(function SaveScriptDialog({
payload,
resolveDialog,
rejectDialog,
@@ -42,14 +49,16 @@ export const SaveScriptDialog: DialogComponent<Payload, string> = observer(funct
}) {
const translate = useTranslate();
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const projectsService = useService(ProjectsService);
const state = useObservableRef<State>(() => ({
value: payload.defaultScriptName ?? '',
name: payload.defaultScriptName ?? '',
projectId: projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID,
errorMessage: null,
validate() {
this.errorMessage = null;
const valid = regex.test(this.value.trim());
const valid = regex.test(this.name.trim());
if (!valid) {
this.errorMessage = translate('plugin_resource_manager_script_name_invalid_characters_message');
@@ -59,11 +68,12 @@ export const SaveScriptDialog: DialogComponent<Payload, string> = observer(funct
this.validate();
if (!this.errorMessage) {
resolveDialog(this.value);
resolveDialog(this);
}
},
}), {
value: observable.ref,
name: observable.ref,
projectId: observable.ref,
errorMessage: observable.ref,
validate: action.bound,
submit: action.bound,
@@ -89,7 +99,7 @@ export const SaveScriptDialog: DialogComponent<Payload, string> = observer(funct
<Button
type="button"
mod={['unelevated']}
disabled={!state.value.trim()}
disabled={!state.name.trim()}
onClick={state.submit}
>
<Translate token='ui_processing_save' />
@@ -100,15 +110,19 @@ export const SaveScriptDialog: DialogComponent<Payload, string> = observer(funct
onReject={rejectDialog}
>
<SubmittingForm ref={focusedRef} onSubmit={state.submit}>
<Container center>
<Container center gap>
<InputField
name='value'
name='name'
state={state}
error={!!state.errorMessage}
description={state.errorMessage ?? undefined}
>
{translate('ui_name') + ':'}
</InputField>
<ProjectSelect
value={state.projectId}
onChange={projectId => {state.projectId = projectId;}}
/>
</Container>
</SubmittingForm>
</CommonDialogWrapper>
@@ -0,0 +1,105 @@
/*
* 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 { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
import { NavigationNodeEditor, NavTreeControlComponent, NavTreeControlProps, TreeNodeMenu } from '@cloudbeaver/plugin-navigation-tree';
const styles = css`
TreeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected=true]) > portal,
portal:focus-within {
visibility: visible;
}
portal {
position: relative;
}
TreeNodeName {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
height: 100%;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
portal {
box-sizing: border-box;
margin-left: auto !important;
margin-right: 16px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
export const NavigationNodeProjectControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(forwardRef(function NavigationNodeProjectControl({
node,
dndElement,
dndPlaceholder,
className,
}, ref) {
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
const selected = treeNodeContext.selected;
const [editing, setEditing] = useState(false);
const nodeActions = useObjectRef<INodeActions>({
rename: () => {
setEditing(true);
},
});
function handlePortalClick(event: React.MouseEvent<HTMLDivElement>) {
EventContext.set(event, EventStopPropagationFlag);
treeNodeContext.select();
}
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl
ref={ref}
onClick={onClickHandler}
{...use({ outdated, editing, dragging: dndElement })}
className={className}
>
<TreeNodeName title={node.name}>
{editing ? (
<NavigationNodeEditor node={node} onClose={() => setEditing(false)} />
) : (
<name-box>{node.name}</name-box>
)}
</TreeNodeName>
{!editing && !dndPlaceholder && (
<portal onClick={handlePortalClick}>
<TreeNodeMenu node={node} actions={nodeActions} selected={selected} />
</portal>
)}
</TreeNodeControl>
);
}));
@@ -0,0 +1,34 @@
/*
* 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 type { NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import type { IElementsTreeFilter } from '@cloudbeaver/plugin-navigation-tree';
import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT';
import { RESOURCES_NODE_PATH } from '../../RESOURCES_NODE_PATH';
export function navigationTreeProjectFilter(
navNodeInfoResource: NavNodeInfoResource,
navTreeResource: NavTreeResource,
): IElementsTreeFilter {
return (filter: string, node: NavNode, children: string[]) => {
if (node.id !== RESOURCES_NODE_PATH) {
return children;
}
const nodes = navNodeInfoResource
.get(resourceKeyList(children))
.filter<NavNode>((node => node !== undefined) as (node: NavNode | undefined) => node is NavNode)
.filter(node => node.nodeType !== NAV_NODE_TYPE_RM_PROJECT || navTreeResource.get(node.id)?.length)
.map(node => node.id);
return nodes;
};
}
@@ -0,0 +1,31 @@
/*
* 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 type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import type { IElementsTreeNodeExpandInfoGetter } from '@cloudbeaver/plugin-navigation-tree';
import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT';
export function navigationTreeProjectsExpandStateGetter(
navNodeInfoResource: NavNodeInfoResource
): IElementsTreeNodeExpandInfoGetter {
return nodeId => {
const node = navNodeInfoResource.get(nodeId);
if (node?.nodeType !== NAV_NODE_TYPE_RM_PROJECT) {
return null;
}
return {
expanded: true,
expandable: false,
};
};
}
@@ -0,0 +1,79 @@
/*
* 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 { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { Translate } from '@cloudbeaver/core-localization';
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { type IElementsTreeCustomRenderer, type NavigationNodeRendererComponent, useNode, NavigationNodeRenderer } from '@cloudbeaver/plugin-navigation-tree';
import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT';
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
const nestedStyles = css`
TreeNode {
margin-top: 8px;
&:only-child Control {
display: none;
}
& NavigationNodeNested {
padding-left: 0 !important;
}
}
`;
export function navigationTreeProjectsRendererRenderer(
navNodeInfoResource: NavNodeInfoResource
): IElementsTreeCustomRenderer {
return nodeId => {
const node = navNodeInfoResource.get(nodeId);
if (node?.nodeType === NAV_NODE_TYPE_RM_PROJECT) {
return ProjectRenderer;
}
return undefined;
};
}
const ProjectRenderer: NavigationNodeRendererComponent = observer(function ManageableGroup({
nodeId,
path,
dragging,
component,
className,
expanded,
}) {
const { node } = useNode(nodeId);
if (!node) {
return styled(TREE_NODE_STYLES)(
<TreeNodeNestedMessage>
<Translate token='app_navigationTree_node_not_found' />
</TreeNodeNestedMessage>
);
}
return (
<NavigationNodeRenderer
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={className}
control={NavigationNodeProjectControl}
style={nestedStyles}
component={component}
/>
);
});
@@ -7,17 +7,21 @@
*/
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { Loader, useDataResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { createPath } from '@cloudbeaver/core-utils';
import { NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { CaptureView } from '@cloudbeaver/core-view';
import { NavigationTreeService, ElementsTree, NavigationNodeControl } from '@cloudbeaver/plugin-navigation-tree';
import { NavigationTreeService, ElementsTree } from '@cloudbeaver/plugin-navigation-tree';
import { ResourceProjectsResource } from '../ResourceProjectsResource';
import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter';
import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer';
const styles = css`
CaptureView {
@@ -54,18 +58,35 @@ const styles = css`
export const ResourceManagerTree = observer(function ResourceManagerTree() {
const translate = useTranslate();
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeResource = useService(NavTreeResource);
const navTreeService = useService(NavigationTreeService);
const { resource } = useDataResource(ResourceManagerTree, ResourceProjectsResource, undefined);
const projectsRendererRenderer = useMemo(
() => navigationTreeProjectsRendererRenderer(navNodeInfoResource),
[navNodeInfoResource]
);
const projectsExpandStateGetter = useMemo(
() => navigationTreeProjectsExpandStateGetter(navNodeInfoResource),
[navNodeInfoResource]
);
const projectFilter = useMemo(
() => navigationTreeProjectFilter(navNodeInfoResource, navTreeResource),
[navNodeInfoResource, navTreeResource]
);
return styled(styles)(
<Loader state={resource}>
<CaptureView view={navTreeService}>
<ElementsTree
root={createPath(RESOURCES_NODE_PATH, resource.userProject?.id)}
root={RESOURCES_NODE_PATH}
getChildren={navTreeService.getChildren}
loadChildren={navTreeService.loadNestedNodes}
control={NavigationNodeControl}
filters={[projectFilter]}
renderers={[projectsRendererRenderer]}
expandStateGetters={[projectsExpandStateGetter]}
emptyPlaceholder={() => styled(styles)(
<center>
<message>
@@ -86,8 +86,8 @@ export class PluginBootstrap extends Bootstrap {
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
await this.resourceProjectsResource.load();
const scriptName = `${result.trim()}.${SCRIPT_EXTENSION}`;
const folder = createPath(RESOURCES_NODE_PATH, this.resourceProjectsResource.userProject?.id);
const scriptName = `${result.name.trim()}.${SCRIPT_EXTENSION}`;
const folder = createPath(RESOURCES_NODE_PATH, result.projectId);
const resourceData = this.navResourceNodeService.getResourceData(folder);
if (!resourceData) {