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/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 2b51949149..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'; @@ -277,6 +278,7 @@ export function useMapResource< preloaded: computed, exception: observable.ref, loading: observable.ref, + resource: observable.ref, }, { exceptionObserved: false, resource, @@ -364,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-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-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-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-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/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-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-sql-editor-navigation-tab/src/manifest.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts index 6d3e28b5e4..86b08d8878 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts @@ -14,7 +14,7 @@ import { SqlEditorTabService } from './SqlEditorTabService'; export const sqlEditorTabPluginManifest: PluginManifest = { info: { - name: 'Sql Editor Plugin', + name: 'Sql Editor Navigation Tab Plugin', }, providers: [ diff --git a/webapp/packages/plugin-sql-editor/src/SqlGenerators/SqlGeneratorsBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlGenerators/SqlGeneratorsBootstrap.ts index bdea89e574..077b533bcf 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlGenerators/SqlGeneratorsBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlGenerators/SqlGeneratorsBootstrap.ts @@ -58,7 +58,7 @@ export class SqlGeneratorsBootstrap extends Bootstrap { return false; } - return !context.find(DATA_CONTEXT_MENU, MENU_SQL_GENERATORS) && !context.has(DATA_CONTEXT_MENU_NESTED); + return !context.hasValue(DATA_CONTEXT_MENU, MENU_SQL_GENERATORS) && !context.has(DATA_CONTEXT_MENU_NESTED); }, getItems: (context, items) => [ ...items, diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index 0b75ef65e3..ec1f91b20d 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -14,6 +14,7 @@ import spreadsheetNew from '@cloudbeaver/plugin-data-spreadsheet-new'; import dataViewer from '@cloudbeaver/plugin-data-viewer'; import datasourceContextSwitch from '@cloudbeaver/plugin-datasource-context-switch'; import ddlViewer from '@cloudbeaver/plugin-ddl-viewer'; +import devTools from '@cloudbeaver/plugin-devtools'; import gisViewer from '@cloudbeaver/plugin-gis-viewer'; import help from '@cloudbeaver/plugin-help'; import localization from '@cloudbeaver/plugin-localization'; @@ -42,6 +43,7 @@ import versionUpdate from '@cloudbeaver/plugin-version-update-administration'; import { defaultProductManifest } from './manifest'; const PLUGINS: PluginManifest[] = [ + devTools, administration, spreadsheetNew, authentication,