diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java index 5d638eab22..127f10e8f7 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java @@ -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 diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 5dc79c8373..2b8aca87b9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -218,10 +218,14 @@ public class LocalResourceController implements RMController { } validateResourcePath(name); 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; diff --git a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls index 53e824ac57..3d3e6f8050 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -1,7 +1,7 @@ # Metadata queries type RMProject { - id: String! + id: ID! name: String! description: String shared: Boolean! @@ -65,8 +65,9 @@ extend type Mutation { forceOverwrite: Boolean!): String! rmCreateProject( + projectId: ID, projectName: String!, - description: String): RMProject + description: String): RMProject! rmDeleteProject(projectId: ID!): Boolean! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 5fad3e0820..ed3403c1f0 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -128,7 +128,9 @@ public class WebServiceRM implements DBWServiceRM { @NotNull WebSession session, @NotNull String name, @Nullable String description ) throws DBWebException { try { - return getResourceController(session).createProject(name, description); + RMProject rmProject = getResourceController(session).createProject(name, description); + session.createVirtualProject(rmProject); + return rmProject; } catch (DBException e) { throw new DBWebException("Error creating project", e); } diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 447e2d7ee8..f7952ae74a 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { computed, makeObservable, runInAction } from 'mobx'; +import { computed, makeObservable, observable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { SyncExecutor, ISyncExecutor, ITask, AutoRunningTask, whileTask } from '@cloudbeaver/core-executor'; @@ -253,7 +253,7 @@ UserInfoIncludes protected resetIncludes(): void { const metadata = this.getMetadata(); - metadata.includes = [...this.defaultIncludes]; + metadata.includes = observable([...this.defaultIncludes]); } getIncludes(key: void): string[] { diff --git a/webapp/packages/core-blocks/src/Menu/Menu.tsx b/webapp/packages/core-blocks/src/Menu/Menu.tsx index 95bd5ac53d..da3ab5dcfa 100644 --- a/webapp/packages/core-blocks/src/Menu/Menu.tsx +++ b/webapp/packages/core-blocks/src/Menu/Menu.tsx @@ -13,6 +13,7 @@ import styled from 'reshadow'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; +import { ErrorBoundary } from '../ErrorBoundary'; import { useObjectRef } from '../useObjectRef'; import { MenuPanel } from './MenuPanel'; import { menuPanelStyles } from './menuPanelStyles'; @@ -25,6 +26,7 @@ interface IMenuProps extends Omit, 'style'> { style?: ComponentStyle; disclosure?: boolean; placement?: MenuInitialState['placement']; + submenu?: boolean; modal?: boolean; visible?: boolean; rtl?: boolean; @@ -48,6 +50,7 @@ export const Menu = observer(forwardRef(function getHasBindings, onVisibleSwitch, modal, + submenu, rtl, ...props }, ref) { @@ -64,18 +67,49 @@ export const Menu = observer(forwardRef(function propsRef.onVisibleSwitch?.(menu.visible); }, [menu.visible]); + let menuVisible = menu.visible; + + if (panelAvailable === false) { + menuVisible = false; + } + if (React.isValidElement(children) && disclosure) { return styled(styles)( + + + + {disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })} + + + {items} + + + + ); + } + + return styled(styles)( + - - {disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })} + + {children} (forwardRef(function {items} - ); - } - - return styled(styles)( - - - {children} - - - {items} - - + ); })); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx b/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx index 28ff20906e..419ea30066 100644 --- a/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx +++ b/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx @@ -22,6 +22,7 @@ interface IMenuItemElementProps { icon?: string | React.ReactNode; menu?: boolean; loading?: boolean; + panelAvailable?: boolean; style?: ComponentStyle; } @@ -31,6 +32,7 @@ export const MenuItemElement = observer(function MenuItem binding, icon, menu, + panelAvailable, loading = false, style = [], }) { @@ -52,7 +54,7 @@ export const MenuItemElement = observer(function MenuItem {loading && } - {menu && !loading && } + {panelAvailable !== false && menu && !loading && } diff --git a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx index fa54995b1e..60d979b5d9 100644 --- a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx +++ b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx @@ -13,6 +13,7 @@ import styled, { use } from 'reshadow'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; +import { ErrorBoundary } from '../ErrorBoundary'; import { getComputed } from '../getComputed'; import { MenuEmptyItem } from './MenuEmptyItem'; import { menuPanelStyles } from './menuPanelStyles'; @@ -26,6 +27,7 @@ export interface IMenuPanelProps { getHasBindings?: () => boolean; children: React.ReactNode | (() => React.ReactNode); rtl?: boolean; + submenu?: boolean; style?: ComponentStyle; className?: string; } @@ -59,13 +61,15 @@ export const MenuPanel = observer(function MenuPanel({ } return styled(styles)( - - - {Children.count(renderedChildren) === 0 && ( - - )} - {renderedChildren} - - + + + + {Children.count(renderedChildren) === 0 && ( + + )} + {renderedChildren} + + + ); }); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts index bc7508bd56..08933a5cfc 100644 --- a/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts +++ b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts @@ -39,6 +39,9 @@ export const menuPanelStyles = css` outline: none; z-index: 999; } + MenuPanel[submenu] { + margin-top: -12px; + } menu-box { composes: theme-typography--body2 theme-elevation-z5 from global; min-width: 140px; @@ -58,6 +61,12 @@ export const menuPanelStyles = css` } } + Menu[modal] menu-box { + overflow: auto; + max-height: 80vh; + overflow-x: hidden; + } + MenuItem, MenuItemCheckbox, MenuItemRadio { display: flex; align-items: center; @@ -109,6 +118,7 @@ export const menuPanelStyles = css` align-items: center; height: 30px; padding: 0 4px; + & menu-item-text, & menu-item-binding { display: block; padding: 0 4px; @@ -145,4 +155,13 @@ export const menuPanelStyles = css` width: 16px; } } + + Menu[rtl] { + & menu-item-text { + text-align: right; + } + & menu-panel-item menu-item-content Icon { + transform: rotate(90deg); + } + } `; diff --git a/webapp/packages/core-blocks/src/StatusMessage.tsx b/webapp/packages/core-blocks/src/StatusMessage.tsx index a2006d549b..1ef76d3178 100644 --- a/webapp/packages/core-blocks/src/StatusMessage.tsx +++ b/webapp/packages/core-blocks/src/StatusMessage.tsx @@ -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(function StatusMessage({ status, message, onShowDetails, className }) { +interface Props { + message?: string | null; + status?: ENotificationType; + exception?: Error | null; + onShowDetails?: () => void; + className?: string; +} + +export const StatusMessage = observer(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)( {message && ( diff --git a/webapp/packages/core-blocks/src/Table/Table.tsx b/webapp/packages/core-blocks/src/Table/Table.tsx index 5b3ae0bfa1..2e5b3a194e 100644 --- a/webapp/packages/core-blocks/src/Table/Table.tsx +++ b/webapp/packages/core-blocks/src/Table/Table.tsx @@ -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>(function Table({ const [selected] = useState>(() => selectedItems || observable(new Map())); const [expanded] = useState>(() => 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) { diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 2a3c55b40d..9c43522b0d 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -14,6 +14,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk'; import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { getComputed } from './getComputed'; import type { ILoadableState } from './Loader/ILoadableState'; import { useObservableRef } from './useObservableRef'; @@ -55,6 +56,7 @@ interface IMapResourceState> loaded: boolean; resource: TResource; isOutdated: () => boolean; + reload: () => void; } interface IMapResourceListResult< @@ -276,6 +278,7 @@ export function useMapResource< preloaded: computed, exception: observable.ref, loading: observable.ref, + resource: observable.ref, }, { exceptionObserved: false, resource, @@ -363,20 +366,22 @@ export function useMapResource< loading: computed, }, false); - // TODO: getComputed skips update somehow ... - const outdated = ( - !result.loading - && (result.outdated || !result.loaded) - ); - - const preloaded = refObj.preloaded; // make mobx subscription + const canLoad = getComputed(() => ( + ( + !keyRef.actual + || result.outdated + || !result.loaded + ) + && refObj.preloaded + && keyRef.key !== null + && ( + result.exception === null + || (Array.isArray(result.exception) && !result.exception.some(Boolean)) + ) + )); useEffect(() => { - if (!preloaded || (!outdated && keyRef.actual) || keyRef.key === null) { - return; - } - - if (result.exception === null || (Array.isArray(result.exception) && !result.exception.some(Boolean))) { + if (canLoad) { (refObj as any)[loadFunctionName](); } }); diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 394e2fafaa..9f930b14f5 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -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, ]; diff --git a/webapp/packages/core-cli/configs/webpack.product.config.js b/webapp/packages/core-cli/configs/webpack.product.config.js index 2908e0e90b..0d3e4a7b01 100644 --- a/webapp/packages/core-cli/configs/webpack.product.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.config.js @@ -74,6 +74,10 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), { minimizer: [new TerserPlugin({ extractComments: /Copyright \(C\)/i, + terserOptions: { + keep_classnames: true, + keep_fnames: true, + }, })], }, plugins: [ diff --git a/webapp/packages/core-connections/src/DBDriverResource.ts b/webapp/packages/core-connections/src/DBDriverResource.ts index a853b9e832..08b14aff81 100644 --- a/webapp/packages/core-connections/src/DBDriverResource.ts +++ b/webapp/packages/core-connections/src/DBDriverResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { computed, makeObservable } from 'mobx'; +import { computed, makeObservable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { EPermission, SessionPermissionsResource, ServerConfigResource } from '@cloudbeaver/core-root'; @@ -37,7 +37,7 @@ export class DBDriverResource extends CachedMapResource this.markOutdated()); makeObservable(this, { enabledDrivers: computed, @@ -69,19 +69,21 @@ export class DBDriverResource extends CachedMapResource driver.id === driverId)) { throw new Error('Driver is not found'); } - this.updateDriver(...drivers); + runInAction(() => { + if (all) { + const removedDrivers = this.keys.filter(key => !drivers.some(driver => driver.id === key)); + this.delete(resourceKeyList(removedDrivers)); + } + + this.updateDriver(...drivers); + }); }); return this.data; @@ -91,6 +93,6 @@ export class DBDriverResource extends CachedMapResource driver.id)); const oldDriver = this.get(keys); - this.set(keys, oldDriver.map((oldDriver, i) => ({ ...oldDriver, ...drivers[i] }))); + this.set(keys, oldDriver.map((oldDriver, i) => (Object.assign(oldDriver ?? {}, drivers[i])))); } } diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 2c3edc1b5c..88feb6db4a 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -6,10 +6,12 @@ * you may not use this file except in compliance with the License. */ +import { flat } from '@cloudbeaver/core-utils'; + import { Bootstrap } from './Bootstrap'; import { Dependency } from './Dependency'; import type { DIContainer } from './DIContainer'; -import type { IServiceCollection, IServiceInjector } from './IApp'; +import type { IServiceCollection, IServiceConstructor, IServiceInjector } from './IApp'; import { IDiWrapper, inversifyWrapper } from './inversifyWrapper'; import type { PluginManifest } from './PluginManifest'; @@ -24,6 +26,14 @@ export class App { this.getServiceCollection().addServiceByClass(App, this); } + getPlugins(): PluginManifest[] { + return [...this.plugins]; + } + + getServices(): IServiceConstructor[] { + return flat(this.plugins.map(plugin => plugin.providers)); + } + registerChildContainer(container: DIContainer): void { this.diWrapper.registerChildContainer(container); } @@ -42,41 +52,33 @@ export class App { // first phase register all dependencies registerServices(): void { - for (const plugin of this.plugins) { - if (plugin.providers.length) { - plugin.providers.forEach(provider => { - // console.log('provider', provider.name); - this.diWrapper.collection.addServiceByClass(provider); - }); - } + for (const service of this.getServices()) { + // console.log('provider', provider.name); + this.diWrapper.collection.addServiceByClass(service); } } async initializeServices(): Promise { - for (const plugin of this.plugins) { - for (const service of plugin.providers) { - if (service.prototype instanceof Bootstrap) { - const serviceInstance = this.diWrapper.injector.getServiceByClass(service); + for (const service of this.getServices()) { + if (service.prototype instanceof Bootstrap) { + const serviceInstance = this.diWrapper.injector.getServiceByClass(service); - if ('register' in serviceInstance) { - await serviceInstance.register(); - } - } else if (service.prototype instanceof Dependency) { - this.diWrapper.injector.getServiceByClass(service); + if ('register' in serviceInstance) { + await serviceInstance.register(); } + } else if (service.prototype instanceof Dependency) { + this.diWrapper.injector.getServiceByClass(service); } } } async loadServices(): Promise { - for (const plugin of this.plugins) { - for (const service of plugin.providers) { - if (service.prototype instanceof Bootstrap) { - const serviceInstance = this.diWrapper.injector.getServiceByClass(service); + for (const service of this.getServices()) { + if (service.prototype instanceof Bootstrap) { + const serviceInstance = this.diWrapper.injector.getServiceByClass(service); - if ('load' in serviceInstance) { - await serviceInstance.load(); - } + if ('load' in serviceInstance) { + await serviceInstance.load(); } } } diff --git a/webapp/packages/core-di/src/DIService.ts b/webapp/packages/core-di/src/DIService.ts index d9e12f8672..1b185da8d8 100644 --- a/webapp/packages/core-di/src/DIService.ts +++ b/webapp/packages/core-di/src/DIService.ts @@ -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 type { App } from './App'; +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<(ctor: IServiceConstructor) => T> = ( + addDIContext(context: IExecutorHandlersCollection): void { + context.addContextCreator(dependencyInjectorContext, this.dependencyInjectorContext); + } + + private readonly dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = ( () => this.serviceInjector.getServiceByClass.bind(this) ); } \ No newline at end of file diff --git a/webapp/packages/core-di/src/dependencyInjectorContext.ts b/webapp/packages/core-di/src/dependencyInjectorContext.ts new file mode 100644 index 0000000000..40bd182d1c --- /dev/null +++ b/webapp/packages/core-di/src/dependencyInjectorContext.ts @@ -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<(ctor: IServiceConstructor) => T> = function dependencyInjectorContext() { + throw new Error('Implementation not provided \n Use addContextCreator to add addDIContext implementation to context'); +}; \ No newline at end of file diff --git a/webapp/packages/core-di/src/index.ts b/webapp/packages/core-di/src/index.ts index 8096df9cf7..ef435878ce 100644 --- a/webapp/packages/core-di/src/index.ts +++ b/webapp/packages/core-di/src/index.ts @@ -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'; diff --git a/webapp/packages/core-executor/src/ExecutionContext.ts b/webapp/packages/core-executor/src/ExecutionContext.ts index d27501ccb8..032981993d 100644 --- a/webapp/packages/core-executor/src/ExecutionContext.ts +++ b/webapp/packages/core-executor/src/ExecutionContext.ts @@ -10,23 +10,39 @@ import type { IAsyncContextLoader, IContextLoader, IExecutionContext, ISyncConte export class ExecutionContext implements IExecutionContext { readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; - constructor(private data: TData, context?: IExecutionContext) { - this.contexts = context?.contexts || new Map, any>(); + constructor(private readonly data: TData, context?: IExecutionContext) { + this.contexts = context?.contexts || new Map(); + this.contextCreators = context?.contextCreators ?? new Map(); } - hasContext(loader: IContextLoader): boolean { - return this.contexts.has(loader); + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void { + for (const [key, value] of creators) { + this.contextCreators.set(key, value); + } } - getContext(token: ISyncContextLoader): T - getContext(token: IAsyncContextLoader): Promise + hasContext(loader: IContextLoader): boolean { + return this.contextCreators.has(loader) || this.contexts.has(loader); + } + + getContext(token: ISyncContextLoader): T; + getContext(token: IAsyncContextLoader): Promise; getContext(token: IContextLoader): Promise | T { if (this.contexts.has(token)) { return this.contexts.get(token); } - const value = token(this, this.data); + let value: T | Promise; + + 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); diff --git a/webapp/packages/core-executor/src/Executor.ts b/webapp/packages/core-executor/src/Executor.ts index f4fb934f20..d5cf73bb5c 100644 --- a/webapp/packages/core-executor/src/Executor.ts +++ b/webapp/packages/core-executor/src/Executor.ts @@ -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 extends ExecutorHandlersCollection implements context: IExecutionContext, scoped: Array> ): Promise> { - 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 extends ExecutorHandlersCollection 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 extends ExecutorHandlersCollection implements this.scheduler.schedule(data, async () => { const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); try { await handler(data, context); diff --git a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts index 4bc69c555a..26504a4812 100644 --- a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts @@ -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 { handlers: Array> = []; postHandlers: Array> = []; chain: Array> = []; + readonly contextCreators: Map, IContextLoader>; readonly collections: Array>; protected initialDataGetter: (() => T) | null; private readonly links: Map, IExecutorHandlersCollection>; constructor() { this.links = new Map(); + this.contextCreators = new Map(); this.collections = []; this.initialDataGetter = null; @@ -34,6 +37,22 @@ implements IExecutorHandlersCollection { }); } + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + addContextCreator( + context: IContextLoader, + creator: IContextLoader + ): this { + this.contextCreators.set(context, creator); + return this; + } + setInitialDataGetter(getter: (() => T) | null): this { this.initialDataGetter = getter; return this; diff --git a/webapp/packages/core-executor/src/IExecutionContext.ts b/webapp/packages/core-executor/src/IExecutionContext.ts index 7f4f5d3cc8..2feade714e 100644 --- a/webapp/packages/core-executor/src/IExecutionContext.ts +++ b/webapp/packages/core-executor/src/IExecutionContext.ts @@ -17,7 +17,13 @@ export interface IExecutionContextProvider { } export interface IExecutionContext extends IExecutionContextProvider { + readonly parent?: IExecutionContext; readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; + + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void; } export type IAsyncContextLoader = ( diff --git a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts index 2cc29324f0..505ae0dad7 100644 --- a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts @@ -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 = (data: T, contexts: IExecutionContextProvider) => boolean; @@ -25,9 +25,19 @@ export interface IExecutorHandlersCollection>; readonly chain: Array>; readonly collections: Array>; + readonly contextCreators: Map, IContextLoader>; setInitialDataGetter(getter: () => T): this; + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + before: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; next: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; addCollection: (collection: IExecutorHandlersCollection) => this; diff --git a/webapp/packages/core-executor/src/SyncExecutor.ts b/webapp/packages/core-executor/src/SyncExecutor.ts index 37b62e8bd8..eccd4e4143 100644 --- a/webapp/packages/core-executor/src/SyncExecutor.ts +++ b/webapp/packages/core-executor/src/SyncExecutor.ts @@ -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 extends ExecutorHandlersCollection implem context: IExecutionContext, scoped: Array> ): IExecutionContextProvider { - 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 extends ExecutorHandlersCollection 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 extends ExecutorHandlersCollection implem const data = this.initialDataGetter(); const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); + try { handler(data, context); } finally { diff --git a/webapp/packages/core-executor/src/executionExceptionContext.ts b/webapp/packages/core-executor/src/executionExceptionContext.ts new file mode 100644 index 0000000000..819e73a140 --- /dev/null +++ b/webapp/packages/core-executor/src/executionExceptionContext.ts @@ -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; + }, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-executor/src/index.ts b/webapp/packages/core-executor/src/index.ts index 22f248d2d2..d9aeff7deb 100644 --- a/webapp/packages/core-executor/src/index.ts +++ b/webapp/packages/core-executor/src/index.ts @@ -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'; diff --git a/webapp/packages/core-projects/src/ProjectsResource.ts b/webapp/packages/core-projects/src/ProjectInfoResource.ts similarity index 55% rename from webapp/packages/core-projects/src/ProjectsResource.ts rename to webapp/packages/core-projects/src/ProjectInfoResource.ts index 767ca16554..7fb80adf94 100644 --- a/webapp/packages/core-projects/src/ProjectsResource.ts +++ b/webapp/packages/core-projects/src/ProjectInfoResource.ts @@ -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 { +export class ProjectInfoResource extends CachedMapResource { 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): Promise> { + protected async loader(key: ResourceKey): Promise> { 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); diff --git a/webapp/packages/core-projects/src/ProjectsNavNodeService.ts b/webapp/packages/core-projects/src/ProjectsNavNodeService.ts index 611adf2ac9..f32f32fafc 100644 --- a/webapp/packages/core-projects/src/ProjectsNavNodeService.ts +++ b/webapp/packages/core-projects/src/ProjectsNavNodeService.ts @@ -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://', '')); } diff --git a/webapp/packages/core-projects/src/ProjectsService.ts b/webapp/packages/core-projects/src/ProjectsService.ts index 1f2baf2d44..3416330894 100644 --- a/webapp/packages/core-projects/src/ProjectsService.ts +++ b/webapp/packages/core-projects/src/ProjectsService.ts @@ -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; } diff --git a/webapp/packages/core-projects/src/index.ts b/webapp/packages/core-projects/src/index.ts index bc23ee59bf..e64f70432d 100644 --- a/webapp/packages/core-projects/src/index.ts +++ b/webapp/packages/core-projects/src/index.ts @@ -1,4 +1,4 @@ -export * from './ProjectsResource'; +export * from './ProjectInfoResource'; export * from './ProjectsService'; export * from './NAV_NODE_TYPE_PROJECT'; export * from './PROJECT_GLOBAL_ID'; diff --git a/webapp/packages/core-projects/src/manifest.ts b/webapp/packages/core-projects/src/manifest.ts index aba58ae383..990808529f 100644 --- a/webapp/packages/core-projects/src/manifest.ts +++ b/webapp/packages/core-projects/src/manifest.ts @@ -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, diff --git a/webapp/packages/core-resource-manager/.gitignore b/webapp/packages/core-resource-manager/.gitignore new file mode 100644 index 0000000000..99902db863 --- /dev/null +++ b/webapp/packages/core-resource-manager/.gitignore @@ -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* diff --git a/webapp/packages/core-resource-manager/package.json b/webapp/packages/core-resource-manager/package.json new file mode 100644 index 0000000000..638e6fc3ef --- /dev/null +++ b/webapp/packages/core-resource-manager/package.json @@ -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" + } +} diff --git a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts new file mode 100644 index 0000000000..2af647bd86 --- /dev/null +++ b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts @@ -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 { + 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 { + 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; + async deleteProject(id: ResourceKeyList): Promise; + async deleteProject(key: ResourceKey): Promise { + 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): Promise> { + 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)); + } +} \ No newline at end of file diff --git a/webapp/packages/core-resource-manager/src/index.ts b/webapp/packages/core-resource-manager/src/index.ts new file mode 100644 index 0000000000..1a1224277b --- /dev/null +++ b/webapp/packages/core-resource-manager/src/index.ts @@ -0,0 +1,2 @@ +export * from './SharedProjectsResource'; +export { resourceManagerManifest } from './manifest'; diff --git a/webapp/packages/core-resource-manager/src/manifest.ts b/webapp/packages/core-resource-manager/src/manifest.ts new file mode 100644 index 0000000000..beeba9dc78 --- /dev/null +++ b/webapp/packages/core-resource-manager/src/manifest.ts @@ -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, + ], +}; diff --git a/webapp/packages/core-resource-manager/tsconfig.json b/webapp/packages/core-resource-manager/tsconfig.json new file mode 100644 index 0000000000..2a4cc370f7 --- /dev/null +++ b/webapp/packages/core-resource-manager/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/.*/"], + "references": [{ "path": "../*" }] +} diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index 4a38af4b34..4ef124c396 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -366,25 +366,21 @@ export abstract class CachedMapResource< } includes(param: ResourceKey, key: ResourceKey): boolean { - if (param === key) { + if + ( + this.isAliasEqual(param, key) + || (ResourceKeyUtils.isEmpty(param) && ResourceKeyUtils.isEmpty(key)) + ) { return true; } if (this.isAlias(param) || this.isAlias(key)) { - return this.isAliasEqual(param, key); - } - - if (ResourceKeyUtils.isEmpty(param) || ResourceKeyUtils.isEmpty(key)) { - return ResourceKeyUtils.isEmpty(param) && ResourceKeyUtils.isEmpty(key); + return true; } param = ResourceKeyUtils.mapKey(param, this.getKeyRef.bind(this)); key = ResourceKeyUtils.mapKey(key, this.getKeyRef.bind(this)); - if (param === key) { - return true; - } - return ResourceKeyUtils.includes(param, key, this.isKeyEqual); } @@ -454,6 +450,7 @@ export abstract class CachedMapResource< if (key === undefined) { key = ResourceKeyUtils.join(resourceKeyList(this.keys), ...this.loadedKeys.map(key => this.transformParam(key))); this.loadedKeys = []; + this.resetIncludes(); } else { if (this.isAlias(key)) { const index = this.loadedKeys.findIndex(loadedKey => this.isAliasEqual(key!, loadedKey)); @@ -466,10 +463,10 @@ export abstract class CachedMapResource< key = this.transformParam(key); } - runInAction(() => ResourceKeyUtils.forEach(key!, key => { + ResourceKeyUtils.forEach(key, key => { const metadata = this.getMetadata(key); metadata.outdated = true; - })); + }); this.onDataOutdated.execute(key); } diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 80fe862dab..a4fc7956ad 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -98,7 +98,10 @@ export abstract class CachedResource< this.spy(this.onDataError, 'onDataError'); } - makeObservable, 'loader' | 'loadedKeys'>(this, { + makeObservable< + CachedResource, + 'loader' | 'loadedKeys' | 'commitIncludes' | 'resetIncludes' | 'markOutdatedSync' + >(this, { loadedKeys: observable, data: observable, loader: action, @@ -107,6 +110,9 @@ export abstract class CachedResource< markDataError: action, markOutdated: action, markUpdated: action, + commitIncludes: action, + markOutdatedSync: action, + resetIncludes: action, }); } @@ -360,7 +366,7 @@ export abstract class CachedResource< break transform; } } else { - if (this.includes(alias.param, param)) { + if (alias.param === param) { param = alias.getAlias(param); deep++; // eslint-disable-next-line no-labels @@ -406,7 +412,7 @@ export abstract class CachedResource< protected resetIncludes(): void { for (const metadata of this.metadata.values()) { - metadata.includes = [...this.defaultIncludes]; + metadata.includes = observable([...this.defaultIncludes]); } } @@ -441,12 +447,16 @@ export abstract class CachedResource< } isAliasEqual(param: TParam, second: TParam): boolean { + if (param === second) { + return true; + } + return this.paramAliases.some(alias => { if ('getter' in alias && alias.getter) { return alias.param(param) && alias.param(second); - } else { - return alias.param === param && alias.param === second; } + + return false; }); } @@ -455,14 +465,10 @@ export abstract class CachedResource< } protected includes(param: TParam, second: TParam): boolean { - if (param === second) { + if (this.isAliasEqual(param, second)) { return true; } - if (this.isAlias(param) || this.isAlias(second)) { - return this.isAliasEqual(param, second); - } - param = this.transformParam(param); second = this.transformParam(second); return this.isKeyEqual(param, second); diff --git a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts index 44f8ca306f..ca23605a08 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts @@ -188,6 +188,10 @@ export const ResourceKeyUtils: ResourceKeyUtils = { key: ResourceKey, isEqual = (keyA: TKey, keyB: TKey) => keyA === keyB ): boolean { + if (param === key) { + return true; + } + if (isResourceKeyList(param)) { return param.includes(key); } diff --git a/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql b/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql new file mode 100644 index 0000000000..8cc0ddb4bd --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql @@ -0,0 +1,7 @@ +fragment SharedProject on RMProject { + id + name + shared + description + projectPermissions +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql new file mode 100644 index 0000000000..61a4ee7018 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql @@ -0,0 +1,13 @@ +mutation createProject( + $projectId: ID + $projectName: String! + $description: String +) { + project: rmCreateProject( + projectId: $projectId + projectName: $projectName + description: $description + ) { + ...SharedProject + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql new file mode 100644 index 0000000000..609c981045 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql @@ -0,0 +1,3 @@ +mutation deleteProject($projectId: ID!) { + rmDeleteProject(projectId: $projectId) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql new file mode 100644 index 0000000000..22e99e24c2 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql @@ -0,0 +1,7 @@ +query getProject($projectId: String!) { + project: rmProject( + projectId: $projectId + ) { + ...SharedProject + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql b/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql new file mode 100644 index 0000000000..c685b0e137 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql @@ -0,0 +1,5 @@ +query getSharedProjects { + projects: rmListSharedProjects { + ...SharedProject + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 5ae56d4f47..af8f1e5f66 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -330,6 +330,7 @@ export interface DriverInfo { promotedScore?: Maybe; providerId?: Maybe; providerProperties: Array; + requiresDatabaseName?: Maybe; requiresServerName?: Maybe; sampleURL?: Maybe; } @@ -365,7 +366,7 @@ export interface Mutation { openSession: SessionInfo; readLobValue: Scalars['String']; refreshSessionConnections?: Maybe; - rmCreateProject?: Maybe; + rmCreateProject: RmProject; rmCreateResource: Scalars['String']; rmDeleteProject: Scalars['Boolean']; rmDeleteResource?: Maybe; @@ -531,6 +532,7 @@ export interface MutationReadLobValueArgs { export interface MutationRmCreateProjectArgs { description?: InputMaybe; + projectId?: InputMaybe; projectName: Scalars['String']; } @@ -861,6 +863,8 @@ export interface Query { revokeUserRole?: Maybe; rmListProjects: Array; rmListResources: Array; + rmListSharedProjects: Array; + rmProject: RmProject; rmReadResourceAsString: Scalars['String']; saveAuthProviderConfiguration: AdminAuthProviderConfiguration; saveUserMetaParameter: ObjectPropertyInfo; @@ -1114,6 +1118,11 @@ export interface QueryRmListResourcesArgs { } +export interface QueryRmProjectArgs { + projectId: Scalars['String']; +} + + export interface QueryRmReadResourceAsStringArgs { projectId: Scalars['String']; resourcePath: Scalars['String']; @@ -1276,7 +1285,7 @@ export interface RmProject { createTime: Scalars['DateTime']; creator: Scalars['String']; description?: Maybe; - id: Scalars['String']; + id: Scalars['ID']; name: Scalars['String']; projectPermissions: Array; shared: Scalars['Boolean']; @@ -2188,6 +2197,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 }; + export type UserConnectionAuthPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array, defaultValue?: any, length: ObjectPropertyLength, features: Array, order: number }; export type UserConnectionNetworkHandlerPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array, defaultValue?: any, order: number, length: ObjectPropertyLength, features: Array }; @@ -2392,6 +2403,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; + projectName: Scalars['String']; + description?: InputMaybe; +}>; + + +export type CreateProjectMutation = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array } }; + export type CreateResourceMutationVariables = Exact<{ projectId: Scalars['String']; resourcePath: Scalars['String']; @@ -2401,6 +2421,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']; @@ -2410,6 +2437,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 } }; + export type GetResourceListQueryVariables = Exact<{ projectId: Scalars['String']; folder?: InputMaybe; @@ -2426,6 +2460,11 @@ export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never; export type GetResourceProjectListQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array }> }; +export type GetSharedProjectsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetSharedProjectsQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array }> }; + export type MoveResourceMutationVariables = Exact<{ projectId: Scalars['String']; oldPath: Scalars['String']; @@ -2867,6 +2906,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 @@ -3847,6 +3895,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( @@ -3856,6 +3915,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( @@ -3865,6 +3929,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( @@ -3891,6 +3962,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( @@ -4401,18 +4479,30 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getProjectList(variables?: GetProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectList', 'query'); }, + createProject(variables: CreateProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(CreateProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createProject', 'mutation'); + }, createResource(variables: CreateResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(CreateResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createResource', 'mutation'); }, + deleteProject(variables: DeleteProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(DeleteProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteProject', 'mutation'); + }, deleteResource(variables: DeleteResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(DeleteResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteResource', 'mutation'); }, + getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query'); + }, getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query'); }, getResourceProjectList(variables?: GetResourceProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetResourceProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceProjectList', 'query'); }, + getSharedProjects(variables?: GetSharedProjectsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetSharedProjectsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSharedProjects', 'query'); + }, moveResource(variables: MoveResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(MoveResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'moveResource', 'mutation'); }, diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx index 261d55cd3a..e5a9fc1021 100644 --- a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx @@ -100,6 +100,7 @@ export const ContextMenu = observer(forwar menuData={menuData} rtl={rtl} menu={menu.current} + modal={modal} style={style} onItemClose={handlers.handleItemClose} /> diff --git a/webapp/packages/core-ui/src/ContextMenu/IContextMenuItemProps.ts b/webapp/packages/core-ui/src/ContextMenu/IContextMenuItemProps.ts new file mode 100644 index 0000000000..9d92816f10 --- /dev/null +++ b/webapp/packages/core-ui/src/ContextMenu/IContextMenuItemProps.ts @@ -0,0 +1,20 @@ +/* + * 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 { MenuStateReturn } from 'reakit/ts'; + +import type { ComponentStyle } from '@cloudbeaver/core-theming'; +import type { IMenuData, IMenuItem } from '@cloudbeaver/core-view'; + +export interface IContextMenuItemProps { + item: IMenuItem; + menu: MenuStateReturn; + menuData: IMenuData; + style?: ComponentStyle; + onClick: () => void; +} \ No newline at end of file diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx index fd3e68b92e..5481e28718 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx @@ -8,18 +8,17 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import { MenuItem, MenuItemCheckbox, MenuItemRadio, MenuStateReturn } from 'reakit/Menu'; +import { MenuItem, MenuItemCheckbox, MenuItemRadio } from 'reakit/Menu'; import styled, { use } from 'reshadow'; import { Checkbox, MenuItemElement, menuPanelStyles, Radio } from '@cloudbeaver/core-blocks'; -import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; +import { useStyles } from '@cloudbeaver/core-theming'; import type { IMenuActionItem } from '@cloudbeaver/core-view'; -interface IMenuActionElementProps { +import type { IContextMenuItemProps } from './IContextMenuItemProps'; + +interface IMenuActionElementProps extends IContextMenuItemProps { item: IMenuActionItem; - menu: MenuStateReturn; - style?: ComponentStyle; - onClick: () => void; } export const MenuActionElement = observer(function MenuActionElement({ diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuItemRenderer.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuItemRenderer.tsx index 0ecacc403a..7531a13433 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuItemRenderer.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuItemRenderer.tsx @@ -13,7 +13,7 @@ import styled, { use } from 'reshadow'; import { MenuItemElement, menuPanelStyles } from '@cloudbeaver/core-blocks'; import { ComponentStyle, joinStyles, useStyles } from '@cloudbeaver/core-theming'; -import { IMenuItem, IMenuData, MenuSubMenuItem, MenuSeparatorItem, MenuActionItem, MenuBaseItem } from '@cloudbeaver/core-view'; +import { IMenuItem, IMenuData, MenuSubMenuItem, MenuSeparatorItem, MenuActionItem, MenuBaseItem, MenuCustomItem } from '@cloudbeaver/core-view'; import { MenuActionElement } from './MenuActionElement'; import { SubMenuElement } from './SubMenuElement'; @@ -23,13 +23,14 @@ export interface IMenuItemRendererProps extends Omit void; style?: ComponentStyle; } export const MenuItemRenderer = observer(function MenuItemRenderer({ - item, rtl, menuData, menu, onItemClose, style, + item, modal, rtl, menuData, menu, onItemClose, style, }) { const styles = useStyles(menuPanelStyles, style); const onClick = useCallback(() => { @@ -40,21 +41,36 @@ export const MenuItemRenderer = observer(function MenuIt } }, [item, onItemClose]); + if (item instanceof MenuCustomItem) { + const CustomMenuItem = item.getComponent(); + + return styled(styles)( + + ); + } + if (item instanceof MenuSubMenuItem) { return styled(styles)( ); } @@ -68,6 +84,7 @@ export const MenuItemRenderer = observer(function MenuIt diff --git a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx index 8948d3ac7a..563b7cad9c 100644 --- a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx @@ -13,7 +13,7 @@ import styled from 'reshadow'; import { getComputed, IMenuState, Menu, MenuItemElement, menuPanelStyles, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; -import { DATA_CONTEXT_MENU_NESTED, IMenuData, IMenuSubMenuItem, MenuActionItem, MenuService, useMenu } from '@cloudbeaver/core-view'; +import { DATA_CONTEXT_MENU_NESTED, DATA_CONTEXT_SUBMENU_ITEM, IMenuData, IMenuSubMenuItem, MenuActionItem, MenuService, useMenu } from '@cloudbeaver/core-view'; import type { IMenuItemRendererProps } from './MenuItemRenderer'; @@ -21,7 +21,8 @@ interface ISubMenuElementProps extends Omit, 'st menuData: IMenuData; subMenu: IMenuSubMenuItem; itemRenderer: React.FC; - rtl?: boolean; + menuModal?: boolean; + menuRtl?: boolean; onItemClose?: () => void; style?: ComponentStyle; } @@ -32,7 +33,8 @@ export const SubMenuElement = observer( subMenu, itemRenderer, style, - rtl, + menuModal: modal, + menuRtl: rtl, onItemClose, ...rest }, @@ -43,6 +45,7 @@ export const SubMenuElement = observer( const styles = useStyles(menuPanelStyles, style); const subMenuData = useMenu({ menu: subMenu.menu, context: menuData.context }); subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true); + subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, subMenu); const handler = menuService.getHandler(subMenuData.context); const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context)); @@ -72,6 +75,7 @@ export const SubMenuElement = observer( const loading = getComputed(() => handler?.isLoading?.(subMenuData.context)); const disabled = getComputed(() => handler?.isDisabled?.(subMenuData.context)); const MenuItemRenderer = itemRenderer; + const panelAvailable = subMenuData.available && !loading; return styled(styles)( ( item={item} menuData={subMenuData} menu={menu.current} + rtl={rtl} + modal={modal} style={style} onItemClose={handlers.handleItemClose} /> ))} rtl={rtl} + modal={modal} style={style} - panelAvailable={subMenuData.available && !loading} + placement={rtl ? 'left-start' : 'right-start'} + panelAvailable={panelAvailable} disabled={disabled} getHasBindings={handlers.hasBindings} + submenu onVisibleSwitch={handlers.handleVisibleSwitch} {...rest} > { - const menu = context.find(DATA_CONTEXT_MENU, MENU_TAB); + const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_TAB); const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT); const tab = context.tryGet(DATA_CONTEXT_TAB_ID); diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 742487adde..0ea0dd819b 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -2,6 +2,7 @@ export * from './Clipboard/ClipboardBootstrap'; export * from './Clipboard/ClipboardService'; export * from './ContextMenu/ContextMenu'; +export * from './ContextMenu/IContextMenuItemProps'; export * from './ContextMenu/MenuBar/MenuBar'; export * from './ContextMenu/MenuBar/MENU_BAR_DEFAULT_STYLES'; export * from './ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES'; diff --git a/webapp/packages/core-view/src/DataContext/DataContext.ts b/webapp/packages/core-view/src/DataContext/DataContext.ts index 04f47348ac..6c476cda2e 100644 --- a/webapp/packages/core-view/src/DataContext/DataContext.ts +++ b/webapp/packages/core-view/src/DataContext/DataContext.ts @@ -15,7 +15,7 @@ import type { DeleteVersionedContextCallback, IDataContext } from './IDataContex import type { IDataContextProvider } from './IDataContextProvider'; export class DataContext implements IDataContext { - private readonly map: Map, any>; + readonly map: Map, any>; private readonly versions: MetadataMap, number>; fallback?: IDataContextProvider; @@ -40,7 +40,7 @@ export class DataContext implements IDataContext { return this.map.has(context) || this.fallback?.has(context) || false; } - find(context: DataContextGetter, value: T): boolean { + hasValue(context: DataContextGetter, value: T): boolean { // eslint-disable-next-line @typescript-eslint/no-this-alias let provider: IDataContextProvider = this; @@ -57,6 +57,25 @@ export class DataContext implements IDataContext { } } + find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let provider: IDataContextProvider = this; + + while (true) { + const value = provider.map.get(context); + + if (predicate(value)) { + return value; + } + + if (provider.fallback) { + provider = provider.fallback; + } else { + return undefined; + } + } + } + set(context: DataContextGetter, value: T): DeleteVersionedContextCallback { const data = this.map.get(context); let version = this.versions.get(context); diff --git a/webapp/packages/core-view/src/DataContext/DynamicDataContext.ts b/webapp/packages/core-view/src/DataContext/DynamicDataContext.ts index 9686833c00..6048085556 100644 --- a/webapp/packages/core-view/src/DataContext/DynamicDataContext.ts +++ b/webapp/packages/core-view/src/DataContext/DynamicDataContext.ts @@ -15,6 +15,10 @@ export class DynamicDataContext implements IDataContext { fallback: IDataContext; contexts: Map, DeleteVersionedContextCallback>; + get map() { + return this.fallback.map; + } + constructor(fallback: IDataContext) { this.fallback = fallback; this.contexts = new Map(); @@ -56,8 +60,12 @@ export class DynamicDataContext implements IDataContext { return this.fallback.get(context); } - find (context: DataContextGetter, value: T): boolean { - return this.fallback.find(context, value); + hasValue (context: DataContextGetter, value: T): boolean { + return this.fallback.hasValue(context, value); + } + + find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { + return this.fallback.find(context, predicate); } tryGet (context: DataContextGetter): T | undefined { diff --git a/webapp/packages/core-view/src/DataContext/IDataContextProvider.ts b/webapp/packages/core-view/src/DataContext/IDataContextProvider.ts index b38f41bd94..3b0e12e875 100644 --- a/webapp/packages/core-view/src/DataContext/IDataContextProvider.ts +++ b/webapp/packages/core-view/src/DataContext/IDataContextProvider.ts @@ -10,8 +10,10 @@ import type { DataContextGetter } from './DataContextGetter'; export interface IDataContextProvider { fallback?: IDataContextProvider; + readonly map: Map, any>; has: (context: DataContextGetter) => boolean; get: (context: DataContextGetter) => T; - find: (context: DataContextGetter, value: T) => boolean; + find: (context: DataContextGetter, predicate: (item: T) => boolean) => T | undefined; + hasValue: (context: DataContextGetter, value: T) => boolean; tryGet: (context: DataContextGetter) => T | undefined; } diff --git a/webapp/packages/core-view/src/Menu/DATA_CONTEXT_SUBMENU_ITEM.ts b/webapp/packages/core-view/src/Menu/DATA_CONTEXT_SUBMENU_ITEM.ts new file mode 100644 index 0000000000..e9250d1fa1 --- /dev/null +++ b/webapp/packages/core-view/src/Menu/DATA_CONTEXT_SUBMENU_ITEM.ts @@ -0,0 +1,12 @@ +/* + * 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 { createDataContext } from '../DataContext/createDataContext'; +import type { IMenuSubMenuItem } from './MenuItem/IMenuSubMenuItem'; + +export const DATA_CONTEXT_SUBMENU_ITEM = createDataContext('submenu-item'); diff --git a/webapp/packages/core-view/src/Menu/MenuItem/IMenuCustomItem.ts b/webapp/packages/core-view/src/Menu/MenuItem/IMenuCustomItem.ts new file mode 100644 index 0000000000..25bda90e12 --- /dev/null +++ b/webapp/packages/core-view/src/Menu/MenuItem/IMenuCustomItem.ts @@ -0,0 +1,36 @@ +/* + * 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 { ComponentStyle } from '@cloudbeaver/core-theming'; + +import type { IMenuItem } from './IMenuItem'; + +interface IMenuCustomItemCommonProperties { + hidden?: boolean; + disabled?: boolean; + getExtraProps?: () => TExtraProps; +} + +export type ICustomMenuItemComponent = React.FC & TExtraProps>; + +export interface ICustomMenuItemProps { + item: IMenuCustomItem; + style?: ComponentStyle; + className?: string; +} + +export interface IMenuCustomItemOptions + extends IMenuCustomItemCommonProperties { + id: string; + getComponent: () => ICustomMenuItemComponent; +} + +export interface IMenuCustomItem + extends IMenuItem, IMenuCustomItemCommonProperties { + getComponent?: () => ICustomMenuItemComponent; +} diff --git a/webapp/packages/core-view/src/Menu/MenuItem/IMenuSubMenuItem.ts b/webapp/packages/core-view/src/Menu/MenuItem/IMenuSubMenuItem.ts index ef0da95d36..b4f4e983c3 100644 --- a/webapp/packages/core-view/src/Menu/MenuItem/IMenuSubMenuItem.ts +++ b/webapp/packages/core-view/src/Menu/MenuItem/IMenuSubMenuItem.ts @@ -15,5 +15,8 @@ export interface IMenuSubMenuEvents extends IMenuItemEvents { export interface IMenuSubMenuItem extends IMenuItem { menu: IMenu; + label?: string; + icon?: string; + tooltip?: string; events?: IMenuSubMenuEvents; } diff --git a/webapp/packages/core-view/src/Menu/MenuItem/MenuCustomItem.ts b/webapp/packages/core-view/src/Menu/MenuItem/MenuCustomItem.ts new file mode 100644 index 0000000000..5d764e5966 --- /dev/null +++ b/webapp/packages/core-view/src/Menu/MenuItem/MenuCustomItem.ts @@ -0,0 +1,39 @@ +/* + * 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 { ICustomMenuItemComponent, IMenuCustomItem, IMenuCustomItemOptions } from './IMenuCustomItem'; +import type { IMenuItemEvents } from './IMenuItem'; +import { MenuItem } from './MenuItem'; + +interface IMenuCustomItemPropertyGetters { + getExtraProps?: () => TExtraProps; + isDisabled?: () => boolean; +} + +export class MenuCustomItem extends MenuItem implements IMenuCustomItem { + private readonly isDisabled?: () => boolean; + + readonly hidden?: boolean; + readonly getComponent: () => ICustomMenuItemComponent; + readonly getExtraProps?: () => TExtraProps; + + get disabled(): boolean { + return this.isDisabled?.() ?? false; + } + + constructor( + options: IMenuCustomItemOptions, + events?: IMenuItemEvents, + getters?: IMenuCustomItemPropertyGetters, + ) { + super(options.id, events); + this.getComponent = options.getComponent; + this.isDisabled = getters?.isDisabled; + this.getExtraProps = getters?.getExtraProps; + } +} diff --git a/webapp/packages/core-view/src/Menu/MenuItem/MenuSubMenuItem.ts b/webapp/packages/core-view/src/Menu/MenuItem/MenuSubMenuItem.ts index 331ba4dcd7..31f76d75a5 100644 --- a/webapp/packages/core-view/src/Menu/MenuItem/MenuSubMenuItem.ts +++ b/webapp/packages/core-view/src/Menu/MenuItem/MenuSubMenuItem.ts @@ -12,15 +12,24 @@ import { MenuItem } from './MenuItem'; export class MenuSubMenuItem extends MenuItem implements IMenuSubMenuItem { readonly menu: IMenu; + readonly label?: string; + readonly icon?: string; + readonly tooltip?: string; readonly events?: IMenuSubMenuEvents; readonly hidden: boolean; constructor( menu: IMenu, + label?: string, + icon?: string, + tooltip?: string, events?: IMenuSubMenuEvents ) { super(menu.id); this.menu = menu; + this.label = label; + this.icon = icon; + this.tooltip = tooltip; this.hidden = false; this.events = events; } diff --git a/webapp/packages/core-view/src/Menu/MenuService.ts b/webapp/packages/core-view/src/Menu/MenuService.ts index a8c994c99b..c77af8d911 100644 --- a/webapp/packages/core-view/src/Menu/MenuService.ts +++ b/webapp/packages/core-view/src/Menu/MenuService.ts @@ -114,7 +114,7 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC } if (creator.menus) { - const applicable = creator.menus.some(menu => contexts.find(DATA_CONTEXT_MENU, menu)); + const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu)); if (!applicable) { return false; diff --git a/webapp/packages/core-view/src/index.ts b/webapp/packages/core-view/src/index.ts index 6edaadd9b3..9e6b171e15 100644 --- a/webapp/packages/core-view/src/index.ts +++ b/webapp/packages/core-view/src/index.ts @@ -30,10 +30,12 @@ export * from './DataContext/useDataContext'; export * from './DataContext/useDynamicDataContext'; export * from './Menu/MenuItem/IMenuActionItem'; export * from './Menu/MenuItem/IMenuBaseItem'; +export * from './Menu/MenuItem/IMenuCustomItem'; export * from './Menu/MenuItem/IMenuItem'; export * from './Menu/MenuItem/IMenuSubMenuItem'; export * from './Menu/MenuItem/MenuActionItem'; export * from './Menu/MenuItem/MenuBaseItem'; +export * from './Menu/MenuItem/MenuCustomItem'; export * from './Menu/MenuItem/MenuGroupItem'; export * from './Menu/MenuItem/MenuLazyItem'; export * from './Menu/MenuItem/MenuSeparatorItem'; @@ -42,6 +44,7 @@ export * from './Menu/createMenu'; export * from './Menu/DATA_CONTEXT_MENU_LOCAL'; export * from './Menu/DATA_CONTEXT_MENU_NESTED'; export * from './Menu/DATA_CONTEXT_MENU'; +export * from './Menu/DATA_CONTEXT_SUBMENU_ITEM'; export * from './Menu/IMenu'; export * from './Menu/menuExtractActions'; export * from './Menu/MenuService'; diff --git a/webapp/packages/plugin-connection-search/.gitignore b/webapp/packages/plugin-connection-search/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-connection-search/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-connection-search/package.json b/webapp/packages/plugin-connection-search/package.json new file mode 100644 index 0000000000..b49f2e35ac --- /dev/null +++ b/webapp/packages/plugin-connection-search/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cloudbeaver/plugin-connection-search", + "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-administration": "~0.1.0", + "@cloudbeaver/core-app": "~0.1.0", + "@cloudbeaver/core-authentication": "~0.1.0", + "@cloudbeaver/core-blocks": "~0.1.0", + "@cloudbeaver/core-connections": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-dialogs": "~0.1.0", + "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-extensions": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-notifications": "~0.1.0", + "@cloudbeaver/core-plugin": "~0.1.0", + "@cloudbeaver/core-projects": "~0.1.0", + "@cloudbeaver/core-product": "~0.1.0", + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-routing": "~0.1.0", + "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", + "@cloudbeaver/core-view": "~0.1.0", + "@cloudbeaver/plugin-top-app-bar": "~0.1.0", + "@cloudbeaver/plugin-connections": "~0.1.0", + "react": "~18.x.x", + "mobx": "~6.x.x", + "mobx-react-lite": "~3.x.x", + "reshadow": "~0.x.x" + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts new file mode 100644 index 0000000000..d16c9575cc --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts @@ -0,0 +1,176 @@ +/* + * 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 { observable, makeObservable } from 'mobx'; + +import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; +import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { ConnectionFormService, ConnectionFormState, IConnectionFormState } from '@cloudbeaver/plugin-connections'; + +import { SearchDatabase } from './SearchDatabase'; + +const formGetter = () => SearchDatabase; + +@injectable() +export class ConnectionSearchService { + hosts = 'localhost'; + databases: AdminConnectionSearchInfo[]; + + disabled = false; + + formState: IConnectionFormState | null = null; + + constructor( + private readonly notificationService: NotificationService, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionFormService: ConnectionFormService, + private readonly optionsPanelService: OptionsPanelService, + private readonly commonDialogService: CommonDialogService, + private readonly projectsService: ProjectsService, + ) { + this.optionsPanelService.closeTask.addHandler(this.closeHandler); + + this.databases = []; + this.search = this.search.bind(this); + this.change = this.change.bind(this); + this.select = this.select.bind(this); + + makeObservable(this, { + hosts: observable, + databases: observable, + disabled: observable, + formState: observable.shallow, + }); + } + + open(): void { + this.optionsPanelService.open(formGetter); + } + + close(): void { + this.hosts = 'localhost'; + this.databases = []; + } + + async load(): Promise { + if (this.databases.length === 0) { + await this.search(); + } + } + + async search(): Promise { + if (this.disabled || !this.hosts || !this.hosts.trim()) { + return; + } + + this.disabled = true; + + try { + const hosts = this.hosts + .trim() + .replace(/[\s,|+-]+/gm, ' ') + .split(/[\s,|+-]/); + + this.databases = await this.connectionInfoResource.searchDatabases(hosts); + } catch (exception: any) { + this.notificationService.logException(exception, 'Databases search failed'); + } finally { + this.disabled = false; + } + } + + private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + const isDialogClosed = await this.showUnsavedChangesDialog(); + + if (!isDialogClosed) { + ExecutorInterrupter.interrupt(contexts); + return; + } + + this.clearFormState(); + this.close(); + }; + + private async showUnsavedChangesDialog(): Promise { + if ( + !this.formState + || !this.optionsPanelService.isOpen(formGetter) + || ( + this.formState.config.connectionId + && this.formState.projectId !== null + && !this.connectionInfoResource.has(createConnectionParam( + this.formState.projectId, + this.formState.config.connectionId + )) + ) + ) { + return true; + } + + const state = await this.formState.checkFormState(); + + if (!state?.edited) { + return true; + } + + const result = await this.commonDialogService.open(ConfirmationDialog, { + title: 'connections_public_connection_edit_cancel_title', + message: 'connections_public_connection_edit_cancel_message', + confirmActionText: 'ui_processing_ok', + }); + + return result !== DialogueStateResult.Rejected; + } + + change(hosts: string): void { + this.hosts = hosts; + } + + saveConnection() { + this.goBack(); + } + + goBack() { + this.clearFormState(); + } + + select(database: AdminConnectionSearchInfo): void { + if (!this.formState) { + this.formState = new ConnectionFormState( + this.connectionFormService, + this.connectionInfoResource + ); + } + + this.formState + .setOptions( + 'create', + 'public' + ) + .setConfig(this.projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID, { + ...this.connectionInfoResource.getEmptyConfig(), + driverId: database.defaultDriver, + host: database.host, + port: `${database.port}`, + }) + .setAvailableDrivers(database.possibleDrivers); + + this.formState.load(); + } + + private clearFormState() { + this.formState?.dispose(); + this.formState = null; + } +} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/Database.tsx b/webapp/packages/plugin-connection-search/src/Search/Database.tsx similarity index 100% rename from webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/Database.tsx rename to webapp/packages/plugin-connection-search/src/Search/Database.tsx diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/DatabaseList.tsx b/webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx similarity index 100% rename from webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/DatabaseList.tsx rename to webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx similarity index 62% rename from webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx rename to webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx index f0ddab4d99..0234ff5e0d 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx +++ b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx @@ -11,19 +11,29 @@ import { observer } from 'mobx-react-lite'; import { useMapResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, 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 { CustomConnection } from '../Manual/CustomConnection'; import { ConnectionSearchService } from './ConnectionSearchService'; import { DatabaseList } from './DatabaseList'; export const SearchDatabase: React.FC = observer(function SearchDatabase() { const connectionSearchService = useService(ConnectionSearchService); - useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey); + useMapResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey); function select(database: AdminConnectionSearchInfo) { - connectionSearchService.select(PROJECT_GLOBAL_ID, database); + connectionSearchService.select(database); + } + + if (connectionSearchService.formState) { + return ( + connectionSearchService.saveConnection()} + onCancel={() => connectionSearchService.goBack()} + /> + ); } return ( diff --git a/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts new file mode 100644 index 0000000000..2eb1023f7e --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts @@ -0,0 +1,49 @@ +/* + * 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 { EAdminPermission } from '@cloudbeaver/core-administration'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { ProjectsService } from '@cloudbeaver/core-projects'; +import { PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; +import { MainMenuService, EMainMenu } from '@cloudbeaver/plugin-top-app-bar'; + +import { ConnectionSearchService } from './Search/ConnectionSearchService'; + +@injectable() +export class SearchConnectionPluginBootstrap extends Bootstrap { + constructor( + private readonly mainMenuService: MainMenuService, + private readonly serverConfigResource: ServerConfigResource, + private readonly projectsService: ProjectsService, + private readonly permissionsService: PermissionsService, + private readonly connectionSearchService: ConnectionSearchService, + ) { + super(); + } + + register(): void | Promise { + this.mainMenuService.onConnectionClick.addHandler(() => { + this.projectsService.load(); + }); + + this.mainMenuService.registerMenuItem(EMainMenu.mainMenuConnectionsPanel, { + id: 'searchConnection', + order: 3, + title: 'connections_connection_create_search_database', + isHidden: () => ( + !this.projectsService.activeProject?.canCreateConnections + || !this.permissionsService.has(EAdminPermission.admin) + ), + onClick: () => { + this.connectionSearchService.open(); + }, + }); + } + + load(): void | Promise { } +} diff --git a/webapp/packages/plugin-connection-search/src/index.ts b/webapp/packages/plugin-connection-search/src/index.ts new file mode 100644 index 0000000000..b239f1cea3 --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/index.ts @@ -0,0 +1,3 @@ +import { connectionSearchPlugin } from './manifest'; + +export default connectionSearchPlugin; \ No newline at end of file diff --git a/webapp/packages/plugin-connection-search/src/manifest.ts b/webapp/packages/plugin-connection-search/src/manifest.ts new file mode 100644 index 0000000000..14ed2f493b --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/manifest.ts @@ -0,0 +1,14 @@ +import type { PluginManifest } from '@cloudbeaver/core-di'; + +import { ConnectionSearchService } from './Search/ConnectionSearchService'; +import { SearchConnectionPluginBootstrap } from './SearchConnectionPluginBootstrap'; + +export const connectionSearchPlugin: PluginManifest = { + info: { + name: 'Search connection plugin', + }, + providers: [ + SearchConnectionPluginBootstrap, + ConnectionSearchService, + ], +}; \ No newline at end of file diff --git a/webapp/packages/plugin-connection-search/tsconfig.json b/webapp/packages/plugin-connection-search/tsconfig.json new file mode 100644 index 0000000000..2a4cc370f7 --- /dev/null +++ b/webapp/packages/plugin-connection-search/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/.*/"], + "references": [{ "path": "../*" }] +} diff --git a/webapp/packages/plugin-connection-template/src/manifest.ts b/webapp/packages/plugin-connection-template/src/manifest.ts index ab335adb47..029a0a942e 100644 --- a/webapp/packages/plugin-connection-template/src/manifest.ts +++ b/webapp/packages/plugin-connection-template/src/manifest.ts @@ -14,7 +14,7 @@ import { TemplateConnectionsResource } from './TemplateConnectionsResource'; export const connectionTemplate: PluginManifest = { info: { - name: 'Connections plugin', + name: 'Template Connections plugin', }, providers: [ diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts index 0194de1ae8..78de8687f1 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts @@ -9,17 +9,12 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { CreateConnectionService } from '../CreateConnectionService'; -import { ConnectionManualService } from './Manual/ConnectionManualService'; import { CustomConnection } from './Manual/CustomConnection'; -import { ConnectionSearchService } from './Search/ConnectionSearchService'; -import { SearchDatabase } from './Search/SearchDatabase'; @injectable() export class CreateConnectionBaseBootstrap extends Bootstrap { constructor( private readonly createConnectionService: CreateConnectionService, - private readonly connectionManualService: ConnectionManualService, - private readonly connectionSearchService: ConnectionSearchService ) { super(); } @@ -31,19 +26,6 @@ export class CreateConnectionBaseBootstrap extends Bootstrap { order: 1, panel: () => CustomConnection, }); - this.createConnectionService.tabsContainer.add({ - key: 'search', - name: 'connections_connection_create_search_database', - order: 2, - panel: () => SearchDatabase, - onOpen: () => this.connectionSearchService.load(), - options: { - configurationWizard: { - activationPriority: 2, - }, - close: () => this.connectionSearchService.close(), - }, - }); } load(): void | Promise { } diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx index f42d3a6b95..f09fe57db1 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx @@ -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); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/ConnectionSearchService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/ConnectionSearchService.ts deleted file mode 100644 index 6b558fa861..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/ConnectionSearchService.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 { observable, makeObservable } from 'mobx'; - -import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; - -import { CreateConnectionService } from '../../CreateConnectionService'; - -@injectable() -export class ConnectionSearchService { - hosts = 'localhost'; - databases: AdminConnectionSearchInfo[]; - - get disabled(): boolean { - return this.createConnectionService.disabled; - } - - set disabled(value: boolean) { - this.createConnectionService.disabled = value; - } - - constructor( - private readonly notificationService: NotificationService, - private readonly connectionInfoResource: ConnectionInfoResource, - private createConnectionService: CreateConnectionService, - private readonly administrationScreenService: AdministrationScreenService - ) { - makeObservable(this, { - hosts: observable, - databases: observable, - }); - - this.databases = []; - this.search = this.search.bind(this); - this.change = this.change.bind(this); - this.select = this.select.bind(this); - } - - close(): void { - this.hosts = 'localhost'; - this.databases = []; - } - - async load(): Promise { - if (this.administrationScreenService.isConfigurationMode && this.databases.length === 0) { - await this.search(); - } - } - - async search(): Promise { - if (this.disabled || !this.hosts || !this.hosts.trim()) { - return; - } - - this.disabled = true; - - try { - const hosts = this.hosts - .trim() - .replace(/[\s,|+-]+/gm, ' ') - .split(/[\s,|+-]/); - - this.databases = await this.connectionInfoResource.searchDatabases(hosts); - } catch (exception: any) { - this.notificationService.logException(exception, 'Databases search failed'); - } finally { - this.disabled = false; - } - } - - change(hosts: string): void { - this.hosts = hosts; - } - - select(projectId: string, database: AdminConnectionSearchInfo): void { - this.createConnectionService.setConnectionTemplate( - projectId, - { - ...this.connectionInfoResource.getEmptyConfig(), - driverId: database.defaultDriver, - host: database.host, - port: `${database.port}`, - }, - database.possibleDrivers - ); - } -} diff --git a/webapp/packages/plugin-connections-administration/src/index.ts b/webapp/packages/plugin-connections-administration/src/index.ts index 46a8d31ba9..515700d344 100644 --- a/webapp/packages/plugin-connections-administration/src/index.ts +++ b/webapp/packages/plugin-connections-administration/src/index.ts @@ -1,7 +1,6 @@ import { connectionPlugin } from './manifest'; export * from './Administration/Connections/ConnectionsAdministration'; export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService'; -export * from './Administration/Connections/CreateConnection/Search/ConnectionSearchService'; export * from './Administration/Connections/ConnectionsAdministrationNavService'; export * from './Administration/Connections/ConnectionsAdministrationService'; export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap'; diff --git a/webapp/packages/plugin-connections-administration/src/manifest.ts b/webapp/packages/plugin-connections-administration/src/manifest.ts index 371d7be6f4..4cc5d755de 100644 --- a/webapp/packages/plugin-connections-administration/src/manifest.ts +++ b/webapp/packages/plugin-connections-administration/src/manifest.ts @@ -12,7 +12,6 @@ import { ConnectionsAdministrationNavService } from './Administration/Connection import { ConnectionsAdministrationService } from './Administration/Connections/ConnectionsAdministrationService'; import { CreateConnectionBaseBootstrap } from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap'; import { ConnectionManualService } from './Administration/Connections/CreateConnection/Manual/ConnectionManualService'; -import { ConnectionSearchService } from './Administration/Connections/CreateConnection/Search/ConnectionSearchService'; import { CreateConnectionService } from './Administration/Connections/CreateConnectionService'; import { ConnectionAccessTabService } from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService'; import { LocaleService } from './LocaleService'; @@ -28,7 +27,6 @@ export const connectionPlugin: PluginManifest = { ConnectionsAdministrationNavService, CreateConnectionService, ConnectionManualService, - ConnectionSearchService, CreateConnectionBaseBootstrap, ConnectionAccessTabService, ], diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx index eb11f8eeb4..4d7ac0dec2 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -11,10 +11,10 @@ import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; import styled, { css } from 'reshadow'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, IProperty, Loader, PropertiesTable, useMapResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useStyles } from '@cloudbeaver/core-theming'; +import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { uuid } from '@cloudbeaver/core-utils'; import type { IConnectionFormProps } from '../IConnectionFormProps'; @@ -66,10 +66,10 @@ export const DriverProperties: TabContainerPanelComponent DBDriverResource, { key: (selected && formState.config.driverId) || null, includes: ['includeDriverProperties'] }, { - isActive: () => selected && !!formState.config.driverId, + active: selected && !!formState.config.driverId, onData: driver => { for (const key of Object.keys(formState.config.properties)) { - if (driver.driverProperties?.some(property => property.id === key) + if (driver.driverProperties.some(property => property.id === key) || state.propertiesList.some(property => property.key === key)) { continue; } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index acc6f743e9..b0712e1f95 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -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, diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx index a498122ec1..804bb3e9d8 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx @@ -10,7 +10,7 @@ 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 { Project, ProjectsResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; interface Props { @@ -25,7 +25,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) { const translate = useTranslate(); const projectsService = useService(ProjectsService); - const projectsLoader = useMapResource(ProjectSelect, ProjectsResource, CachedMapAllKey, { + const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, { onData: () => { if (!props.value && projectsService.activeProject) { props.onChange(projectsService.activeProject.id); @@ -34,8 +34,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) { }); const value = props.value ?? projectsService.activeProject?.id; - - const projects = projectsLoader.data as Project[]; + const projects = projectsLoader.data as ProjectInfo[]; const possibleOptions = projects.filter(project => project.canCreateConnections); diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 5f1b4513b3..b47d89ae75 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -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(); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index 5f47b3a73b..8d40977f0d 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -42,7 +42,7 @@ export class TableHeaderService extends Bootstrap { this.actionService.addHandler({ id: 'table-header-menu-base-handler', isActionApplicable(context, action) { - const menu = context.find(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU); + const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU); const model = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL); const resultIndex = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX); diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index 70b179112d..6034ed8cb0 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -42,7 +42,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { this.menuService.setHandler({ id: 'connection-selector-base', - isApplicable: context => context.find(DATA_CONTEXT_MENU, MENU_CONNECTION_SELECTOR), + isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_SELECTOR), isLoading: () => this.connectionSelectorLoading, isDisabled: () => ( !this.connectionSchemaManagerService.isConnectionChangeable @@ -104,7 +104,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { this.menuService.setHandler({ id: 'connection-data-container-selector-base', - isApplicable: context => context.find(DATA_CONTEXT_MENU, MENU_CONNECTION_DATA_CONTAINER_SELECTOR), + isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_DATA_CONTAINER_SELECTOR), isDisabled: () => ( !this.connectionSchemaManagerService.currentConnection?.connected || this.connectionSelectorLoading diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx index bbc24e58d7..5f185ac80e 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx @@ -27,15 +27,17 @@ import { MENU_CONNECTION_SELECTOR } from '../MENU_CONNECTION_SELECTOR'; const menuStyles = css` Menu { - max-height: 400px; - overflow: auto; + & menu-box { + max-height: 80vh; + overflow: auto; + } & menu-panel-item { - overflow-x: hidden; + overflow-x: hidden; } & menu-item-text { - max-width: 400px; - overflow-x: hidden; - text-overflow: ellipsis; + max-width: 400px; + overflow-x: hidden; + text-overflow: ellipsis; } } `; diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts index 1aa0616c08..3ff7ad052f 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts @@ -31,7 +31,7 @@ export class DDLViewerFooterService { this.actionsService.addHandler({ id: 'ddl-viewer-footer-base-handler', isActionApplicable(context, action) { - const menu = context.find(DATA_CONTEXT_MENU, MENU_DDL_VIEWER_FOOTER); + const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_DDL_VIEWER_FOOTER); const node = context.tryGet(DATA_CONTEXT_DDL_VIEWER_NODE); if (!menu || !node) { diff --git a/webapp/packages/plugin-devtools/.gitignore b/webapp/packages/plugin-devtools/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-devtools/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-devtools/package.json b/webapp/packages/plugin-devtools/package.json new file mode 100644 index 0000000000..5914cd45f0 --- /dev/null +++ b/webapp/packages/plugin-devtools/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cloudbeaver/plugin-devtools", + "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-administration": "~0.1.0", + "@cloudbeaver/core-app": "~0.1.0", + "@cloudbeaver/core-authentication": "~0.1.0", + "@cloudbeaver/core-blocks": "~0.1.0", + "@cloudbeaver/core-connections": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-dialogs": "~0.1.0", + "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-extensions": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-notifications": "~0.1.0", + "@cloudbeaver/core-plugin": "~0.1.0", + "@cloudbeaver/core-product": "~0.1.0", + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-routing": "~0.1.0", + "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", + "@cloudbeaver/core-view": "~0.1.0", + "@cloudbeaver/plugin-top-app-bar": "~0.1.0", + "@cloudbeaver/plugin-user-profile": "~0.1.0", + "mobx": "~6.x.x", + "mobx-react-lite": "~3.x.x", + "react": "~18.x.x", + "reshadow": "~0.x.x" + } +} diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts new file mode 100644 index 0000000000..8448f823a0 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { createDataContext } from '@cloudbeaver/core-view'; + +export const DATA_CONTEXT_MENU_SEARCH = createDataContext('menu-local', ''); diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts new file mode 100644 index 0000000000..3ad5dffc4e --- /dev/null +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; +import { MenuCustomItem } from '@cloudbeaver/core-view'; + +import { SearchResourceMenuItemComponent } from './SearchResourceMenuItemComponent'; + +export class SearchResourceMenuItem extends MenuCustomItem { + constructor() { + super({ + id: 'search-resource', + getComponent: () => SearchResourceMenuItemComponent, + }); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx new file mode 100644 index 0000000000..bd4a647c5b --- /dev/null +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx @@ -0,0 +1,48 @@ +/* + * 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 { useStyles } from '@cloudbeaver/core-theming'; +import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; +import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_MENU_SEARCH } from './DATA_CONTEXT_MENU_SEARCH'; + +const styles = css` + search-box { + padding: 8px 12px; + } +`; + +export const SearchResourceMenuItemComponent: ICustomMenuItemComponent = observer(function SearchResourceMenuItemComponent({ + item, + menu, + onClick, + menuData, + className, + style, +}) { + const value = menuData.context.tryGet(DATA_CONTEXT_MENU_SEARCH) ?? ''; + function handleChange(value: string) { + menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value); + } + + return styled(useStyles(style, styles))( + + handleChange(event.target.value)} + /> + + ); +}); \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts new file mode 100644 index 0000000000..081f2374ea --- /dev/null +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -0,0 +1,44 @@ +/* + * 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 { makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; + +interface IDevToolsSettings { + enabled: boolean; +} + +const DEVTOOLS = 'devtools'; + +@injectable() +export class DevToolsService { + get isEnabled(): boolean { + return this.settings.enabled; + } + + private readonly settings: IDevToolsSettings; + + constructor( + private readonly autoSaveService: LocalStorageSaveService, + ) { + this.settings = { + enabled: false, + }; + + makeObservable(this, { + settings: observable, + }); + this.autoSaveService.withAutoSave(this.settings, DEVTOOLS); + } + + switch() { + this.settings.enabled = !this.settings.enabled; + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts new file mode 100644 index 0000000000..55b7e8ef8f --- /dev/null +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -0,0 +1,217 @@ +/* + * 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 { EAdminPermission } from '@cloudbeaver/core-administration'; +import { App, Bootstrap, DIService, injectable, IServiceConstructor } from '@cloudbeaver/core-di'; +import { PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; +import { CachedResource } from '@cloudbeaver/core-sdk'; +import { ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_SUBMENU_ITEM, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; +import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; +import { MainMenuService } from '@cloudbeaver/plugin-top-app-bar'; +import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile'; + +import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS'; +import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH'; +import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem'; +import { DevToolsService } from './DevToolsService'; +import { MENU_DEVTOOLS } from './menu/MENU_DEVTOOLS'; +import { MENU_PLUGIN } from './menu/MENU_PLUGIN'; +import { MENU_PLUGINS } from './menu/MENU_PLUGINS'; +import { MENU_RESOURCE } from './menu/MENU_RESOURCE'; +import { MENU_RESOURCES } from './menu/MENU_RESOURCES'; +import { PluginSubMenuItem } from './menu/PluginSubMenuItem'; +import { ResourceSubMenuItem } from './menu/ResourceSubMenuItem'; + +@injectable() +export class PluginBootstrap extends Bootstrap { + constructor( + private readonly app: App, + private readonly diService: DIService, + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly mainMenuService: MainMenuService, + private readonly devToolsService: DevToolsService, + private readonly permissionsService: PermissionsService, + private readonly serverConfigResource: ServerConfigResource + ) { + super(); + } + + register(): void { + this.menuService.addCreator({ + isApplicable: context => { + if (!this.permissionsService.has(EAdminPermission.admin)) { + return false; + } + return context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU; + }, + getItems: (context, items) => [ + ACTION_DEVTOOLS, + ...items, + ], + }); + + + this.actionService.addHandler({ + id: 'devtools', + isActionApplicable: (context, action) => [ + ACTION_DEVTOOLS, + ].includes(action), + isChecked: (context, action) => { + + switch (action) { + case ACTION_DEVTOOLS: { + return this.devToolsService.isEnabled; + } + } + + return false; + }, + handler: (context, action) => { + switch (action) { + case ACTION_DEVTOOLS: { + this.devToolsService.switch(); + break; + } + } + }, + }); + + this.menuService.addCreator({ + isApplicable: context => { + if (!this.devToolsService.isEnabled) { + return false; + } + return context.get(DATA_CONTEXT_MENU) === MENU_USER_PROFILE; + }, + getItems: (context, items) => [ + MENU_DEVTOOLS, + ...items, + ], + }); + + this.menuService.addCreator({ + isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_DEVTOOLS, + getItems: (context, items) => { + const search = context.tryGet(DATA_CONTEXT_MENU_SEARCH); + + if (search) { + + return [ + new SearchResourceMenuItem(), + ...this.getResources(this.app + .getServices() + .filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))), + ]; + } + + return [ + new SearchResourceMenuItem(), + MENU_PLUGINS, + ...items, + ]; + }, + }); + + this.menuService.addCreator({ + isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS, + getItems: (context, items) => [ + ...this.app + .getPlugins() + .sort((a, b) => a.info.name.localeCompare(b.info.name)) + .map(plugin => new PluginSubMenuItem(plugin)), + ...items, + ], + }); + + this.menuService.addCreator({ + isApplicable: context => { + if (context.get(DATA_CONTEXT_MENU) !== MENU_PLUGIN) { + return false; + } + const item = context.tryGet(DATA_CONTEXT_SUBMENU_ITEM); + + if (item instanceof PluginSubMenuItem) { + return item.plugin.providers.some(provider => provider.prototype instanceof CachedResource); + } + + return false; + }, + getItems: (_, items) => [ + MENU_RESOURCES, + ...items, + ], + }); + + this.menuService.addCreator({ + isApplicable: context => ( + context.get(DATA_CONTEXT_MENU) === MENU_RESOURCES + && context.has(DATA_CONTEXT_SUBMENU_ITEM) + ), + getItems: (context, items) => { + const item = context.find(DATA_CONTEXT_SUBMENU_ITEM, item => item instanceof PluginSubMenuItem); + + if (!item) { + return items; + } + + const plugin = this.app + .getPlugins() + .find(plugin => plugin.info.name === item.id); + + if (!plugin) { + return items; + } + + return [ + ...this.getResources(plugin.providers), + ...items, + ]; + }, + }); + + this.menuService.addCreator({ + isApplicable: context => ( + context.get(DATA_CONTEXT_MENU) === MENU_RESOURCE + && context.get(DATA_CONTEXT_SUBMENU_ITEM) instanceof ResourceSubMenuItem + ), + getItems: (context, items) => { + const item = context.get(DATA_CONTEXT_SUBMENU_ITEM) as ResourceSubMenuItem; + + return [ + new MenuBaseItem( + { + id: 'markOutdated', + label: 'Mark outdated', + tooltip: 'Outdate resource', + }, + { + onSelect: () => { + const instance = this.diService.serviceInjector.getServiceByClass>( + item.resource + ); + instance.markOutdated(undefined); + }, + } + ), + ...items, + ]; + }, + }); + } + + load(): void | Promise { + } + + private getResources(providers: IServiceConstructor[]): ResourceSubMenuItem[] { + return providers + .filter(service => service.prototype instanceof CachedResource) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(resource => new ResourceSubMenuItem(resource)); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts new file mode 100644 index 0000000000..75ce8853a2 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts @@ -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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DEVTOOLS = createAction('devtools', { + type: 'checkbox', + label: 'DevTools', + tooltip: 'Show DevTools menu', +}); \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/index.ts b/webapp/packages/plugin-devtools/src/index.ts new file mode 100644 index 0000000000..76453a8b8e --- /dev/null +++ b/webapp/packages/plugin-devtools/src/index.ts @@ -0,0 +1,3 @@ +import { devToolsPlugin } from './manifest'; + +export default devToolsPlugin; \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/manifest.ts b/webapp/packages/plugin-devtools/src/manifest.ts new file mode 100644 index 0000000000..da4f216277 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/manifest.ts @@ -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 { DevToolsService } from './DevToolsService'; +import { PluginBootstrap } from './PluginBootstrap'; + +export const devToolsPlugin: PluginManifest = { + info: { + name: 'DevTools plugin', + }, + providers: [ + PluginBootstrap, + DevToolsService, + ], +}; \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts b/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts new file mode 100644 index 0000000000..2b46988d41 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts @@ -0,0 +1,14 @@ +/* + * 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DEVTOOLS = createMenu( + 'devtools', + 'DevTools' +); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts new file mode 100644 index 0000000000..471b1c2d79 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts @@ -0,0 +1,14 @@ +/* + * 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_PLUGIN = createMenu( + 'plugin', + 'Plugin' +); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts new file mode 100644 index 0000000000..b3a906bee1 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts @@ -0,0 +1,14 @@ +/* + * 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_PLUGINS = createMenu( + 'plugins', + 'Plugins' +); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts new file mode 100644 index 0000000000..053458caa4 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts @@ -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. + */ + +import { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_RESOURCE = createMenu( + 'resource', + 'Resource', + undefined, + 'Resource actions' +); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts new file mode 100644 index 0000000000..713925a237 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts @@ -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. + */ + +import { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_RESOURCES = createMenu( + 'resources-list', + 'Resources', + undefined, + 'List of registered resources' +); diff --git a/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts b/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts new file mode 100644 index 0000000000..7cb7535b65 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts @@ -0,0 +1,26 @@ +/* + * 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 { MenuSubMenuItem } from '@cloudbeaver/core-view'; + +import { MENU_PLUGIN } from './MENU_PLUGIN'; + +export class PluginSubMenuItem extends MenuSubMenuItem { + readonly plugin: PluginManifest; + + constructor(plugin: PluginManifest) { + super(MENU_PLUGIN, plugin.info.name); + + this.plugin = plugin; + + Object.assign(this, { + id: plugin.info.name, + }); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts b/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts new file mode 100644 index 0000000000..c6d82c193f --- /dev/null +++ b/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts @@ -0,0 +1,26 @@ +/* + * 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 { IServiceConstructor } from '@cloudbeaver/core-di'; +import { MenuSubMenuItem } from '@cloudbeaver/core-view'; + +import { MENU_RESOURCE } from './MENU_RESOURCE'; + +export class ResourceSubMenuItem extends MenuSubMenuItem { + readonly resource: IServiceConstructor; + + constructor(resource: IServiceConstructor) { + super(MENU_RESOURCE, resource.name); + + this.resource = resource; + + Object.assign(this, { + id: resource.name, + }); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/tsconfig.json b/webapp/packages/plugin-devtools/tsconfig.json new file mode 100644 index 0000000000..ad14f68a20 --- /dev/null +++ b/webapp/packages/plugin-devtools/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": [ + "src" + ], + "exclude": [ + "**/node_modules", + "**/.*/" + ], + "references": [{ "path": "../*" }] +} \ No newline at end of file diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts index 0b07f34691..23ae9ce148 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts @@ -26,6 +26,7 @@ export type NavTreeControlProps = { dndPlaceholder?: boolean; expanded?: boolean; className?: string; + style?: ComponentStyle; ref?: React.Ref | undefined; }; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx index d44dbb20ce..8cc356dfab 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx @@ -113,6 +113,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat {(expanded || externalExpanded) && ( diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx index f9a2977de1..c13380196b 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -59,6 +59,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer + {editing ? ( setEditing(false)} /> diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx index f6839cb2e5..73a0d3565d 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -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'; @@ -25,8 +24,16 @@ const nestedStyles = css` TreeNode { margin-top: 8px; } - NavigationNodeNested { + /*NavigationNodeNested { padding-left: 8px !important; + }*/ + TreeNode { + & Control { + /*display: none;*/ + } + & NavigationNodeNested { + padding-left: 0 !important; + } } `; diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index a4f4e4bc10..010ece5893 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -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'; diff --git a/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts b/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts index 58f8381c0d..5b74ed68b5 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts @@ -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; @@ -23,6 +24,7 @@ export class ResourceProjectsResource extends CachedDataResource { constructor( private readonly graphQLService: GraphQLService, private readonly userInfoResource: UserInfoResource, + private readonly sharedProjectsResource: SharedProjectsResource, ) { super([]); @@ -31,6 +33,10 @@ export class ResourceProjectsResource extends CachedDataResource { this.markOutdated(); }); + this.sharedProjectsResource.onDataOutdated.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated()); + makeObservable(this, { userProject: computed, }); diff --git a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx index b30d2e9597..ab18bf5d9b 100644 --- a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx +++ b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx @@ -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; @@ -34,7 +41,7 @@ interface State { const regex = /^(?!\.)[\p{L}\w\-$.\s()@]+$/u; -export const SaveScriptDialog: DialogComponent = observer(function SaveScriptDialog({ +export const SaveScriptDialog: DialogComponent = observer(function SaveScriptDialog({ payload, resolveDialog, rejectDialog, @@ -42,14 +49,16 @@ export const SaveScriptDialog: DialogComponent = observer(funct }) { const translate = useTranslate(); const [focusedRef] = useFocus({ focusFirstChild: true }); + const projectsService = useService(ProjectsService); const state = useObservableRef(() => ({ - 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 = 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 = observer(funct