mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 06:40:05 +08:00
Merge pull request #1135 from dbeaver/CB-2344-projects-administration
CB-2567 shared projects api
This commit is contained in:
+6
-1
@@ -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
|
||||
|
||||
+35
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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() {
|
||||
|
||||
+1
-1
@@ -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!
|
||||
}
|
||||
+9
-3
@@ -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;
|
||||
|
||||
+4
@@ -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"),
|
||||
|
||||
+21
-3
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
-5
@@ -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,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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ export type NavTreeControlProps = {
|
||||
dndPlaceholder?: boolean;
|
||||
expanded?: boolean;
|
||||
className?: string;
|
||||
style?: ComponentStyle;
|
||||
ref?: React.Ref<HTMLDivElement> | undefined;
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -113,6 +113,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
<Control
|
||||
ref={setRef}
|
||||
dndElement={dndData.state.isDragging}
|
||||
style={style}
|
||||
node={node}
|
||||
/>
|
||||
{(expanded || externalExpanded) && (
|
||||
|
||||
+7
-1
@@ -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)} />
|
||||
|
||||
+8
-4
@@ -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>
|
||||
|
||||
+105
@@ -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>
|
||||
);
|
||||
}));
|
||||
+34
@@ -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;
|
||||
};
|
||||
}
|
||||
+31
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
+79
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user