Merge remote-tracking branch 'origin/CB-2344-projects-administration' into CB-2344-projects-administration

This commit is contained in:
Aleksandr Skoblikov
2022-09-09 18:03:36 +03:00
56 changed files with 970 additions and 141 deletions
@@ -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[] {
+37 -22
View File
@@ -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<React.ButtonHTMLAttributes<any>, 'style'> {
style?: ComponentStyle;
disclosure?: boolean;
placement?: MenuInitialState['placement'];
submenu?: boolean;
modal?: boolean;
visible?: boolean;
rtl?: boolean;
@@ -48,6 +50,7 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
getHasBindings,
onVisibleSwitch,
modal,
submenu,
rtl,
...props
}, ref) {
@@ -64,18 +67,49 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(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)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props} {...children.props}>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
</ErrorBoundary>
);
}
return styled(styles)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} {...props} {...children.props}>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props}>
<box>{children}</box>
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
@@ -83,25 +117,6 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
{items}
</MenuPanel>
</MenuStateContext.Provider>
);
}
return styled(styles)(
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} {...props}>
<box>{children}</box>
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
</ErrorBoundary>
);
}));
@@ -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<IMenuItemElementProps>(function MenuItem
binding,
icon,
menu,
panelAvailable,
loading = false,
style = [],
}) {
@@ -52,7 +54,7 @@ export const MenuItemElement = observer<IMenuItemElementProps>(function MenuItem
</menu-item-binding>
<menu-item-content>
{loading && <Loader small fullSize />}
{menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
{panelAvailable !== false && menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
</menu-item-content>
</menu-panel-item>
@@ -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<IMenuPanelProps>(function MenuPanel({
}
return styled(styles)(
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(renderedChildren) === 0 && (
<MenuEmptyItem style={style} />
)}
{renderedChildren}
</menu-box>
</Menu>
<ErrorBoundary>
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(renderedChildren) === 0 && (
<MenuEmptyItem style={style} />
)}
{renderedChildren}
</menu-box>
</Menu>
</ErrorBoundary>
);
});
@@ -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);
}
}
`;
@@ -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]();
}
});
@@ -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: [
@@ -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<string, DBDriver, Driver
super();
permissionsResource.require(this, EPermission.public);
this.serverConfigResource.onDataOutdated.addHandler(this.markOutdated.bind(this));
this.serverConfigResource.onDataOutdated.addHandler(() => this.markOutdated());
makeObservable(this, {
enabledDrivers: computed,
@@ -69,19 +69,21 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
includeDriverParameters: false,
includeDriverProperties: false,
includeProviderProperties: false,
...this.getIncludesMap(driverId, includes),
...this.getIncludesMap(driverId, (all ? this.defaultIncludes : includes)),
});
if (all) {
this.resetIncludes();
this.data.clear();
}
if (driverId && !drivers.some(driver => 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<string, DBDriver, Driver
const keys = resourceKeyList(drivers.map(driver => 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]))));
}
}
+26 -24
View File
@@ -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<any>[] {
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<void> {
for (const plugin of this.plugins) {
for (const service of plugin.providers) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
for (const service of this.getServices()) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('register' in serviceInstance) {
await serviceInstance.register();
}
} else if (service.prototype instanceof Dependency) {
this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('register' in serviceInstance) {
await serviceInstance.register();
}
} else if (service.prototype instanceof Dependency) {
this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
}
}
}
async loadServices(): Promise<void> {
for (const plugin of this.plugins) {
for (const service of plugin.providers) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
for (const service of this.getServices()) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('load' in serviceInstance) {
await serviceInstance.load();
}
if ('load' in serviceInstance) {
await serviceInstance.load();
}
}
}
@@ -366,25 +366,21 @@ export abstract class CachedMapResource<
}
includes(param: ResourceKey<TKey>, key: ResourceKey<TKey>): 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);
}
@@ -98,7 +98,10 @@ export abstract class CachedResource<
this.spy(this.onDataError, 'onDataError');
}
makeObservable<CachedResource<TData, TParam, TKey, TContext>, 'loader' | 'loadedKeys'>(this, {
makeObservable<
CachedResource<TData, TParam, TKey, TContext>,
'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);
@@ -188,6 +188,10 @@ export const ResourceKeyUtils: ResourceKeyUtils = {
key: ResourceKey<TKey>,
isEqual = (keyA: TKey, keyB: TKey) => keyA === keyB
): boolean {
if (param === key) {
return true;
}
if (isResourceKeyList(param)) {
return param.includes(key);
}
@@ -100,6 +100,7 @@ export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(forwar
menuData={menuData}
rtl={rtl}
menu={menu.current}
modal={modal}
style={style}
onItemClose={handlers.handleItemClose}
/>
@@ -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;
}
@@ -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<IMenuActionElementProps>(function MenuActionElement({
@@ -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<React.ButtonHTMLAttributes<
item: IMenuItem;
menuData: IMenuData;
menu: MenuStateReturn; // from reakit useMenuState
modal?: boolean;
rtl?: boolean;
onItemClose?: () => void;
style?: ComponentStyle;
}
export const MenuItemRenderer = observer<IMenuItemRendererProps>(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<IMenuItemRendererProps>(function MenuIt
}
}, [item, onItemClose]);
if (item instanceof MenuCustomItem) {
const CustomMenuItem = item.getComponent();
return styled(styles)(
<CustomMenuItem
item={item}
menu={menu}
menuData={menuData}
style={style}
onClick={onClick}
/>
);
}
if (item instanceof MenuSubMenuItem) {
return styled(styles)(
<MenuItem
{...menu}
{...{ as: SubMenuElement }}
{...use({ hidden: item.hidden })}
id={item.id}
aria-label={item.menu.label}
itemRenderer={MenuItemRenderer}
rtl={rtl}
menuRtl={rtl}
menuData={menuData}
menuModal={modal}
subMenu={item}
style={style}
onItemClose={onItemClose}
onClick={onClick}
{...{ as: SubMenuElement }}
/>
);
}
@@ -68,6 +84,7 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
<MenuActionElement
item={item}
menu={menu}
menuData={menuData}
style={style}
onClick={onClick}
/>
@@ -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<React.ButtonHTMLAttributes<any>, 'st
menuData: IMenuData;
subMenu: IMenuSubMenuItem;
itemRenderer: React.FC<IMenuItemRendererProps>;
rtl?: boolean;
menuModal?: boolean;
menuRtl?: boolean;
onItemClose?: () => void;
style?: ComponentStyle;
}
@@ -32,7 +33,8 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
subMenu,
itemRenderer,
style,
rtl,
menuModal: modal,
menuRtl: rtl,
onItemClose,
...rest
},
@@ -43,6 +45,7 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
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<ISubMenuElementProps, HTMLButtonElement>(
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)(
<Menu
@@ -84,22 +88,28 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
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}
>
<MenuItemElement
label={subMenu.menu.label}
icon={subMenu.menu.icon}
tooltip={subMenu.menu.tooltip}
label={subMenu.label ?? subMenu.menu.label}
icon={subMenu.icon ?? subMenu.menu.icon}
tooltip={subMenu.tooltip ?? subMenu.menu.tooltip}
panelAvailable={panelAvailable}
loading={loading}
style={style}
menu
@@ -31,7 +31,7 @@ export class TabsBootstrap extends Bootstrap {
this.actionService.addHandler({
id: 'tabs-base-handler',
isActionApplicable: (context, action) => {
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);
+1
View File
@@ -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';
@@ -15,7 +15,7 @@ import type { DeleteVersionedContextCallback, IDataContext } from './IDataContex
import type { IDataContextProvider } from './IDataContextProvider';
export class DataContext implements IDataContext {
private readonly map: Map<DataContextGetter<any>, any>;
readonly map: Map<DataContextGetter<any>, any>;
private readonly versions: MetadataMap<DataContextGetter<any>, number>;
fallback?: IDataContextProvider;
@@ -40,7 +40,7 @@ export class DataContext implements IDataContext {
return this.map.has(context) || this.fallback?.has(context) || false;
}
find<T>(context: DataContextGetter<T>, value: T): boolean {
hasValue<T>(context: DataContextGetter<T>, 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<T>(context: DataContextGetter<T>, 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<T>(context: DataContextGetter<T>, value: T): DeleteVersionedContextCallback {
const data = this.map.get(context);
let version = this.versions.get(context);
@@ -15,6 +15,10 @@ export class DynamicDataContext implements IDataContext {
fallback: IDataContext;
contexts: Map<DataContextGetter<any>, 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 <T>(context: DataContextGetter<T>, value: T): boolean {
return this.fallback.find(context, value);
hasValue <T>(context: DataContextGetter<T>, value: T): boolean {
return this.fallback.hasValue(context, value);
}
find<T>(context: DataContextGetter<T>, predicate: (value: T) => boolean): T | undefined {
return this.fallback.find(context, predicate);
}
tryGet <T>(context: DataContextGetter<T>): T | undefined {
@@ -10,8 +10,10 @@ import type { DataContextGetter } from './DataContextGetter';
export interface IDataContextProvider {
fallback?: IDataContextProvider;
readonly map: Map<DataContextGetter<any>, any>;
has: (context: DataContextGetter<any>) => boolean;
get: <T>(context: DataContextGetter<T>) => T;
find: <T>(context: DataContextGetter<T>, value: T) => boolean;
find: <T>(context: DataContextGetter<T>, predicate: (item: T) => boolean) => T | undefined;
hasValue: <T>(context: DataContextGetter<T>, value: T) => boolean;
tryGet: <T>(context: DataContextGetter<T>) => T | undefined;
}
@@ -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<IMenuSubMenuItem>('submenu-item');
@@ -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<TExtraProps = unknown> {
hidden?: boolean;
disabled?: boolean;
getExtraProps?: () => TExtraProps;
}
export type ICustomMenuItemComponent<TExtraProps = unknown> = React.FC<ICustomMenuItemProps<TExtraProps> & TExtraProps>;
export interface ICustomMenuItemProps<TExtraProps = unknown> {
item: IMenuCustomItem<TExtraProps>;
style?: ComponentStyle;
className?: string;
}
export interface IMenuCustomItemOptions<TExtraProps = unknown>
extends IMenuCustomItemCommonProperties<TExtraProps> {
id: string;
getComponent: () => ICustomMenuItemComponent<TExtraProps>;
}
export interface IMenuCustomItem<TExtraProps = unknown>
extends IMenuItem, IMenuCustomItemCommonProperties<TExtraProps> {
getComponent?: () => ICustomMenuItemComponent<TExtraProps>;
}
@@ -15,5 +15,8 @@ export interface IMenuSubMenuEvents extends IMenuItemEvents {
export interface IMenuSubMenuItem extends IMenuItem {
menu: IMenu;
label?: string;
icon?: string;
tooltip?: string;
events?: IMenuSubMenuEvents;
}
@@ -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<TExtraProps = unknown> {
getExtraProps?: () => TExtraProps;
isDisabled?: () => boolean;
}
export class MenuCustomItem<TExtraProps = unknown> extends MenuItem implements IMenuCustomItem<TExtraProps> {
private readonly isDisabled?: () => boolean;
readonly hidden?: boolean;
readonly getComponent: () => ICustomMenuItemComponent<TExtraProps>;
readonly getExtraProps?: () => TExtraProps;
get disabled(): boolean {
return this.isDisabled?.() ?? false;
}
constructor(
options: IMenuCustomItemOptions<TExtraProps>,
events?: IMenuItemEvents,
getters?: IMenuCustomItemPropertyGetters<TExtraProps>,
) {
super(options.id, events);
this.getComponent = options.getComponent;
this.isDisabled = getters?.isDisabled;
this.getExtraProps = getters?.getExtraProps;
}
}
@@ -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;
}
@@ -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;
+3
View File
@@ -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';
@@ -14,7 +14,7 @@ import { TemplateConnectionsResource } from './TemplateConnectionsResource';
export const connectionTemplate: PluginManifest = {
info: {
name: 'Connections plugin',
name: 'Template Connections plugin',
},
providers: [
@@ -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<IConnectionFormProps>
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;
}
@@ -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);
@@ -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
@@ -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;
}
}
`;
@@ -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) {
@@ -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*
@@ -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"
}
}
@@ -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<string>('menu-local', '');
@@ -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<IContextMenuItemProps> {
constructor() {
super({
id: 'search-resource',
getComponent: () => SearchResourceMenuItemComponent,
});
}
}
@@ -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<IContextMenuItemProps> = 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))(
<search-box dir="ltr" className={className}>
<input
name='search'
placeholder='Search for resource...'
value={value}
autoComplete="off"
onChange={event => handleChange(event.target.value)}
/>
</search-box>
);
});
@@ -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'>(this, {
settings: observable,
});
this.autoSaveService.withAutoSave(this.settings, DEVTOOLS);
}
switch() {
this.settings.enabled = !this.settings.enabled;
}
}
@@ -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<CachedResource<any, any>>(
item.resource
);
instance.markOutdated(undefined);
},
}
),
...items,
];
},
});
}
load(): void | Promise<void> {
}
private getResources(providers: IServiceConstructor<any>[]): ResourceSubMenuItem[] {
return providers
.filter(service => service.prototype instanceof CachedResource)
.sort((a, b) => a.name.localeCompare(b.name))
.map(resource => new ResourceSubMenuItem(resource));
}
}
@@ -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',
});
@@ -0,0 +1,3 @@
import { devToolsPlugin } from './manifest';
export default devToolsPlugin;
@@ -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,
],
};
@@ -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'
);
@@ -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'
);
@@ -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'
);
@@ -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'
);
@@ -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'
);
@@ -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,
});
}
}
@@ -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<any>;
constructor(resource: IServiceConstructor<any>) {
super(MENU_RESOURCE, resource.name);
this.resource = resource;
Object.assign(this, {
id: resource.name,
});
}
}
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": [
"src"
],
"exclude": [
"**/node_modules",
"**/.*/"
],
"references": [{ "path": "../*" }]
}
@@ -14,7 +14,7 @@ import { SqlEditorTabService } from './SqlEditorTabService';
export const sqlEditorTabPluginManifest: PluginManifest = {
info: {
name: 'Sql Editor Plugin',
name: 'Sql Editor Navigation Tab Plugin',
},
providers: [
@@ -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,
@@ -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,