Merge remote-tracking branch 'origin/CB-2344-projects-administration' into feat/CB-2482-aws-project

This commit is contained in:
Konstantin Vasilev
2022-09-12 13:51:52 +03:00
124 changed files with 2183 additions and 345 deletions
@@ -63,7 +63,12 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode {
@Override
public String getNodeName() {
return project.getName();
return project.getDisplayName();
}
@Override
public String getLocalizedName(String locale) {
return getNodeName();
}
@Override
@@ -218,10 +218,14 @@ public class LocalResourceController implements RMController {
}
validateResourcePath(name);
RMProject project;
project = makeProjectFromPath(sharedProjectsPath.resolve(name), Set.of(), RMProject.Type.SHARED, false);
if (project == null) {
var projectPath = sharedProjectsPath.resolve(name);
if (Files.exists(projectPath)) {
throw new DBException("Project '" + name + "' already exists");
}
project = makeProjectFromPath(projectPath, Set.of(), RMProject.Type.SHARED, false);
if (project == null) {
throw new DBException("Project '" + name + "' not created");
}
try {
Files.createDirectories(getProjectPath(project.getId()));
return project;
@@ -1,7 +1,7 @@
# Metadata queries
type RMProject {
id: String!
id: ID!
name: String!
description: String
shared: Boolean!
@@ -65,8 +65,9 @@ extend type Mutation {
forceOverwrite: Boolean!): String!
rmCreateProject(
projectId: ID,
projectName: String!,
description: String): RMProject
description: String): RMProject!
rmDeleteProject(projectId: ID!): Boolean!
}
@@ -128,7 +128,9 @@ public class WebServiceRM implements DBWServiceRM {
@NotNull WebSession session, @NotNull String name, @Nullable String description
) throws DBWebException {
try {
return getResourceController(session).createProject(name, description);
RMProject rmProject = getResourceController(session).createProject(name, description);
session.createVirtualProject(rmProject);
return rmProject;
} catch (DBException e) {
throw new DBWebException("Error creating project", e);
}
@@ -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);
}
}
`;
@@ -13,12 +13,7 @@ import { IconOrImage, Link } from '@cloudbeaver/core-blocks';
import { ENotificationType } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
interface Props {
message: string | undefined | null;
status: ENotificationType | undefined;
onShowDetails?: () => void;
className?: string;
}
import { useErrorDetails } from './useErrorDetails';
const styles = css`
status-message {
@@ -43,17 +38,36 @@ const styles = css`
}
`;
export const StatusMessage = observer<Props>(function StatusMessage({ status, message, onShowDetails, className }) {
interface Props {
message?: string | null;
status?: ENotificationType;
exception?: Error | null;
onShowDetails?: () => void;
className?: string;
}
export const StatusMessage = observer<Props>(function StatusMessage({
status,
message,
exception = null,
onShowDetails,
className,
}) {
const translate = useTranslate();
message = message ? translate(message) : message;
const errorDetails = useErrorDetails(exception);
message = (message ? translate(message) : message) ?? errorDetails.details?.name;
let icon = '/icons/info_icon.svg';
if (status === ENotificationType.Error) {
if (status === ENotificationType.Error || exception !== null) {
icon = '/icons/error_icon.svg';
} else if (status === ENotificationType.Success) {
icon = '/icons/success_icon.svg';
}
if (errorDetails.details?.hasDetails && !onShowDetails) {
onShowDetails = errorDetails.open;
}
return styled(styles)(
<status-message className={className}>
{message && (
@@ -8,7 +8,7 @@
import { observable, computed, action } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useState, useCallback, useMemo } from 'react';
import { useState, useCallback, useMemo, useEffect } from 'react';
import styled, { use } from 'reshadow';
import { useObjectRef } from '../useObjectRef';
@@ -34,6 +34,23 @@ export const Table = observer<React.PropsWithChildren<Props>>(function Table({
const [selected] = useState<Map<any, boolean>>(() => selectedItems || observable(new Map()));
const [expanded] = useState<Map<any, boolean>>(() => expandedItems || observable(new Map()));
useEffect(action(() => {
if (!keys) {
return;
}
const removeSelected = Array.from(selected.keys()).filter(key => !keys.includes(key));
const removeExpanded = Array.from(expanded.keys()).filter(key => !keys.includes(key));
for (const id of removeSelected) {
selected.delete(id);
}
for (const id of removeExpanded) {
expanded.delete(id);
}
}), [keys]);
const state: ITableState = useObservableRef(() => ({
get selectableItems() {
if (!this.keys) {
@@ -14,6 +14,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { getComputed } from './getComputed';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObservableRef } from './useObservableRef';
@@ -55,6 +56,7 @@ interface IMapResourceState<TResource extends CachedMapResource<any, any, any>>
loaded: boolean;
resource: TResource;
isOutdated: () => boolean;
reload: () => void;
}
interface IMapResourceListResult<
@@ -276,6 +278,7 @@ export function useMapResource<
preloaded: computed,
exception: observable.ref,
loading: observable.ref,
resource: observable.ref,
}, {
exceptionObserved: false,
resource,
@@ -363,20 +366,22 @@ export function useMapResource<
loading: computed,
}, false);
// TODO: getComputed skips update somehow ...
const outdated = (
!result.loading
&& (result.outdated || !result.loaded)
);
const preloaded = refObj.preloaded; // make mobx subscription
const canLoad = getComputed(() => (
(
!keyRef.actual
|| result.outdated
|| !result.loaded
)
&& refObj.preloaded
&& keyRef.key !== null
&& (
result.exception === null
|| (Array.isArray(result.exception) && !result.exception.some(Boolean))
)
));
useEffect(() => {
if (!preloaded || (!outdated && keyRef.actual) || keyRef.key === null) {
return;
}
if (result.exception === null || (Array.isArray(result.exception) && !result.exception.some(Boolean))) {
if (canLoad) {
(refObj as any)[loadFunctionName]();
}
});
@@ -20,6 +20,7 @@ import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree';
import { corePluginManifest } from '@cloudbeaver/core-plugin';
import { coreProductManifest } from '@cloudbeaver/core-product';
import { coreProjectsManifest } from '@cloudbeaver/core-projects';
import { resourceManagerManifest } from '@cloudbeaver/core-resource-manager';
import { coreRootManifest } from '@cloudbeaver/core-root';
import { coreRoutingManifest } from '@cloudbeaver/core-routing';
import { coreSDKManifest } from '@cloudbeaver/core-sdk';
@@ -61,6 +62,7 @@ export const coreManifests: PluginManifest[] = [
coreConnectionsManifest,
coreAdministrationManifest,
codeDialogsManifest,
resourceManagerManifest,
coreAppManifest,
coreNavigationTree,
];
@@ -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();
}
}
}
+8 -3
View File
@@ -6,9 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import type { ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { IExecutorHandlersCollection, ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { App } from './App';
import { App } from './App';
import { dependencyInjectorContext } from './dependencyInjectorContext';
import type { IServiceConstructor, IServiceInjector } from './IApp';
import { injectable } from './injectable';
@@ -22,7 +23,11 @@ export class DIService {
private readonly app: App
) { }
dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = (
addDIContext(context: IExecutorHandlersCollection<any>): void {
context.addContextCreator(dependencyInjectorContext, this.dependencyInjectorContext);
}
private readonly dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = (
() => this.serviceInjector.getServiceByClass.bind(this)
);
}
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { IServiceConstructor } from './IApp';
export const dependencyInjectorContext: ISyncContextLoader<<T>(ctor: IServiceConstructor<T>) => T> = function dependencyInjectorContext() {
throw new Error('Implementation not provided \n Use addContextCreator to add addDIContext implementation to context');
};
+1
View File
@@ -3,6 +3,7 @@ export * from './App';
export * from './AppContext';
export * from './Bootstrap';
export * from './Dependency';
export * from './dependencyInjectorContext';
export * from './DIService';
export * from './injectable';
export * from './PluginManifest';
@@ -10,23 +10,39 @@ import type { IAsyncContextLoader, IContextLoader, IExecutionContext, ISyncConte
export class ExecutionContext<TData> implements IExecutionContext<TData> {
readonly contexts: Map<IContextLoader<any, TData>, any>;
readonly contextCreators: Map<IContextLoader<any, TData>, IContextLoader<any, TData>>;
constructor(private data: TData, context?: IExecutionContext<any>) {
this.contexts = context?.contexts || new Map<IContextLoader<any, TData>, any>();
constructor(private readonly data: TData, context?: IExecutionContext<any>) {
this.contexts = context?.contexts || new Map();
this.contextCreators = context?.contextCreators ?? new Map();
}
hasContext(loader: IContextLoader<any>): boolean {
return this.contexts.has(loader);
addContextCreators(
creators: [IContextLoader<any, TData>, IContextLoader<any, TData>][]
): void {
for (const [key, value] of creators) {
this.contextCreators.set(key, value);
}
}
getContext<T>(token: ISyncContextLoader<T, TData>): T
getContext<T>(token: IAsyncContextLoader<T, TData>): Promise<T>
hasContext(loader: IContextLoader): boolean {
return this.contextCreators.has(loader) || this.contexts.has(loader);
}
getContext<T>(token: ISyncContextLoader<T, TData>): T;
getContext<T>(token: IAsyncContextLoader<T, TData>): Promise<T>;
getContext<T>(token: IContextLoader<T, TData>): Promise<T> | T {
if (this.contexts.has(token)) {
return this.contexts.get(token);
}
const value = token(this, this.data);
let value: T | Promise<T>;
if (this.contextCreators.has(token)) {
value = this.contextCreators.get(token)!(this, this.data);
} else {
value = token(this, this.data);
}
if (value instanceof Promise) {
return this.getAsyncContext(token, value);
+15 -2
View File
@@ -9,6 +9,7 @@
import { flat } from '@cloudbeaver/core-utils';
import { ExecutionContext } from './ExecutionContext';
import { executionExceptionContext } from './executionExceptionContext';
import { ExecutorHandlersCollection } from './ExecutorHandlersCollection';
import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter';
import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext';
@@ -68,9 +69,16 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
context: IExecutionContext<T>,
scoped: Array<IExecutorHandlersCollection<T>>
): Promise<IExecutionContextProvider<T>> {
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
scoped = [...collection.collections, ...scoped];
context.addContextCreators(collection.contextCreators as any);
for (const scope of scoped) {
context.addContextCreators(scope.contextCreators as any);
}
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
await this.executeChain(collection, data, context, 'before');
for (const scope of scoped) {
@@ -83,7 +91,11 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
for (const scope of scoped) {
await this.executeHandlers(data, context, scope.handlers, interrupter);
}
} finally {
} catch (exception: any) {
const exceptionContext = context.getContext(executionExceptionContext);
exceptionContext.setException(exception);
throw exception;
} finally {
await this.executeHandlers(data, context, collection.postHandlers);
for (const scope of scoped) {
@@ -158,6 +170,7 @@ export class Executor<T = void> extends ExecutorHandlersCollection<T> implements
this.scheduler.schedule(data, async () => {
const context = new ExecutionContext(data);
context.addContextCreators(this.contextCreators as any);
try {
await handler(data, context);
@@ -8,6 +8,7 @@
import { makeObservable, observable } from 'mobx';
import type { IContextLoader, ISyncContextLoader, IAsyncContextLoader } from './IExecutionContext';
import type { IExecutorHandler } from './IExecutorHandler';
import type { ExecutorDataFilter, ExecutorDataMap, IChainLink, IExecutorHandlersCollection } from './IExecutorHandlersCollection';
@@ -16,12 +17,14 @@ implements IExecutorHandlersCollection<T, TResult> {
handlers: Array<IExecutorHandler<T, TResult>> = [];
postHandlers: Array<IExecutorHandler<T, TResult>> = [];
chain: Array<IChainLink<T, TResult>> = [];
readonly contextCreators: Map<IContextLoader<any, T>, IContextLoader<any, T>>;
readonly collections: Array<IExecutorHandlersCollection<T, TResult>>;
protected initialDataGetter: (() => T) | null;
private readonly links: Map<IExecutorHandlersCollection<any, TResult>, IExecutorHandlersCollection<T, TResult>>;
constructor() {
this.links = new Map();
this.contextCreators = new Map();
this.collections = [];
this.initialDataGetter = null;
@@ -34,6 +37,22 @@ implements IExecutorHandlersCollection<T, TResult> {
});
}
addContextCreator<TContext>(
context: ISyncContextLoader<TContext, T>,
creator: ISyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IAsyncContextLoader<TContext, T>,
creator: IAsyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IContextLoader<TContext, T>,
creator: IContextLoader<TContext, T>
): this {
this.contextCreators.set(context, creator);
return this;
}
setInitialDataGetter(getter: (() => T) | null): this {
this.initialDataGetter = getter;
return this;
@@ -17,7 +17,13 @@ export interface IExecutionContextProvider<TData> {
}
export interface IExecutionContext<TData> extends IExecutionContextProvider<TData> {
readonly parent?: IExecutionContext<any>;
readonly contexts: Map<IContextLoader<any, TData>, any>;
readonly contextCreators: Map<IContextLoader<any, TData>, IContextLoader<any, TData>>;
addContextCreators(
creators: [IContextLoader<any, TData>, IContextLoader<any, TData>][]
): void;
}
export type IAsyncContextLoader<T = any, TData = any> = (
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { IExecutionContextProvider } from './IExecutionContext';
import type { IAsyncContextLoader, IContextLoader, IExecutionContextProvider, ISyncContextLoader } from './IExecutionContext';
import type { IExecutorHandler } from './IExecutorHandler';
export type ExecutorDataFilter<T> = (data: T, contexts: IExecutionContextProvider<T>) => boolean;
@@ -25,9 +25,19 @@ export interface IExecutorHandlersCollection<T = unknown, TResult = any | Promis
readonly postHandlers: Array<IExecutorHandler<T, TResult>>;
readonly chain: Array<IChainLink<T, TResult>>;
readonly collections: Array<IExecutorHandlersCollection<T, TResult>>;
readonly contextCreators: Map<IContextLoader<any, T>, IContextLoader<any, T>>;
setInitialDataGetter(getter: () => T): this;
addContextCreator<TContext>(
context: ISyncContextLoader<TContext, T>,
creator: ISyncContextLoader<TContext, T>
): this;
addContextCreator<TContext>(
context: IAsyncContextLoader<TContext, T>,
creator: IAsyncContextLoader<TContext, T>
): this;
before: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
next: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
addCollection: (collection: IExecutorHandlersCollection<T, TResult>) => this;
@@ -9,6 +9,7 @@
import { flat } from '@cloudbeaver/core-utils';
import { ExecutionContext } from './ExecutionContext';
import { executionExceptionContext } from './executionExceptionContext';
import { ExecutorHandlersCollection } from './ExecutorHandlersCollection';
import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter';
import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext';
@@ -55,9 +56,16 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
context: IExecutionContext<T>,
scoped: Array<IExecutorHandlersCollection<T>>
): IExecutionContextProvider<T> {
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
scoped = [...collection.collections, ...scoped];
context.addContextCreators(collection.contextCreators as any);
for (const scope of scoped) {
context.addContextCreators(scope.contextCreators as any);
}
const interrupter = context.getContext(ExecutorInterrupter.interruptContext);
this.executeChain(collection, data, context, 'before');
for (const scope of scoped) {
@@ -70,6 +78,10 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
for (const scope of scoped) {
this.executeHandlers(data, context, scope.handlers, interrupter);
}
} catch (exception: any) {
const exceptionContext = context.getContext(executionExceptionContext);
exceptionContext.setException(exception);
throw exception;
} finally {
this.executeHandlers(data, context, collection.postHandlers);
@@ -144,6 +156,8 @@ export class SyncExecutor<T = void> extends ExecutorHandlersCollection<T> implem
const data = this.initialDataGetter();
const context = new ExecutionContext(data);
context.addContextCreators(this.contextCreators as any);
try {
handler(data, context);
} finally {
@@ -0,0 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export function executionExceptionContext() {
return {
exception: null as null | Error,
setException(exception: Error) {
this.exception = exception;
},
};
}
@@ -3,6 +3,7 @@ export * from './TaskScheduler/ITask';
export * from './TaskScheduler/Task';
export * from './TaskScheduler/TaskScheduler';
export * from './ExecutionContext';
export * from './executionExceptionContext';
export * from './Executor';
export * from './ExecutorHandlersCollection';
export * from './ExecutorInterrupter';
@@ -10,32 +10,37 @@ import { runInAction } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { GraphQLService, ProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk';
import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager';
import { GraphQLService, ProjectInfo as SchemaProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk';
export type Project = ProjectInfo;
export type ProjectInfo = SchemaProjectInfo;
@injectable()
export class ProjectsResource extends CachedMapResource<string, Project> {
export class ProjectInfoResource extends CachedMapResource<string, ProjectInfo> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly sharedProjectsResource: SharedProjectsResource,
private readonly userInfoResource: UserInfoResource,
) {
super([]);
this.sync(this.userInfoResource);
this.sharedProjectsResource.onDataOutdated.addHandler(this.markOutdated.bind(this));
this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated());
this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated());
this.userInfoResource.onUserChange.addPostHandler(() => {
this.clear();
});
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, Project>> {
protected async loader(key: ResourceKey<string>): Promise<Map<string, ProjectInfo>> {
const all = ResourceKeyUtils.includes(key, CachedMapAllKey);
const { projects } = await this.graphQLService.sdk.getProjectList();
runInAction(() => {
if (all) {
this.data.clear();
this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id))));
}
this.set(resourceKeyList(projects.map(project => project.id)), projects);
@@ -11,18 +11,18 @@ import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { NAV_NODE_TYPE_PROJECT } from './NAV_NODE_TYPE_PROJECT';
import { Project, ProjectsResource } from './ProjectsResource';
import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource';
@injectable()
export class ProjectsNavNodeService {
constructor(
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectsResource: ProjectsResource
private readonly projectsResource: ProjectInfoResource
) {
}
getProject(nodeId: string): Project | undefined {
getProject(nodeId: string): ProjectInfo | undefined {
const parentIds = [...this.navNodeInfoResource.getParents(nodeId), nodeId];
const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds));
@@ -35,7 +35,7 @@ export class ProjectsNavNodeService {
return this.getByNodeId(projectNode.id);
}
getByNodeId(nodeId: string): Project | undefined {
getByNodeId(nodeId: string): ProjectInfo | undefined {
return this.projectsResource.get(nodeId.replace('resource://', ''));
}
@@ -12,12 +12,12 @@ import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { Project, ProjectsResource } from './ProjectsResource';
import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource';
@injectable()
export class ProjectsService {
get activeProject(): Project | undefined {
let project: Project | undefined;
get activeProject(): ProjectInfo | undefined {
let project: ProjectInfo | undefined;
if (this.activeProjectId) {
project = this.projectsResource.get(this.activeProjectId);
@@ -41,7 +41,7 @@ export class ProjectsService {
private activeProjectId: string | null;
constructor(
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly userInfoResource: UserInfoResource
) {
this.activeProjectId = null;
@@ -51,7 +51,7 @@ export class ProjectsService {
});
}
setActiveProject(project: Project): void {
setActiveProject(project: ProjectInfo): void {
this.activeProjectId = project.id;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export * from './ProjectsResource';
export * from './ProjectInfoResource';
export * from './ProjectsService';
export * from './NAV_NODE_TYPE_PROJECT';
export * from './PROJECT_GLOBAL_ID';
@@ -9,8 +9,8 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { ProjectInfoResource } from './ProjectInfoResource';
import { ProjectsNavNodeService } from './ProjectsNavNodeService';
import { ProjectsResource } from './ProjectsResource';
import { ProjectsService } from './ProjectsService';
export const manifest: PluginManifest = {
@@ -19,7 +19,7 @@ export const manifest: PluginManifest = {
},
providers: [
ProjectsResource,
ProjectInfoResource,
ProjectsService,
LocaleService,
ProjectsNavNodeService,
@@ -0,0 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,26 @@
{
"name": "@cloudbeaver/core-resource-manager",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-navigation-tree": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0"
}
}
@@ -0,0 +1,111 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { runInAction } from 'mobx';
import { EAdminPermission } from '@cloudbeaver/core-administration';
import { injectable } from '@cloudbeaver/core-di';
import { SessionPermissionsResource } from '@cloudbeaver/core-root';
import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList } from '@cloudbeaver/core-sdk';
const newSymbol = Symbol('new-project');
export type SharedProject = RmProject;
type SharedProjectNew = SharedProject & { [newSymbol]: boolean };
interface IProjectConfig {
id: string;
name: string;
description?: string;
}
@injectable()
export class SharedProjectsResource extends CachedMapResource<string, SharedProject> {
constructor(
private readonly graphQLService: GraphQLService,
sessionPermissionsResource: SessionPermissionsResource,
) {
super([]);
sessionPermissionsResource
.require(this, EAdminPermission.admin);
this.sync(sessionPermissionsResource, () => {});
}
isNew(id: string): boolean {
if (!this.has(id)) {
return true;
}
return newSymbol in this.get(id)!;
}
async create(config: IProjectConfig): Promise<SharedProject> {
const { project } = await this.graphQLService.sdk.createProject({
projectId: config.id,
projectName: config.name,
description: config.description,
});
(project as SharedProjectNew)[newSymbol] = true;
this.set(project.id, project as SharedProject);
return this.get(project.id)!;
}
async deleteProject(id: string): Promise<void>;
async deleteProject(id: ResourceKeyList<string>): Promise<void>;
async deleteProject(key: ResourceKey<string>): Promise<void> {
const deleted: string[] = [];
try {
await this.performUpdate(key, undefined, async key => {
await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => {
await this.graphQLService.sdk.deleteProject({
projectId,
});
deleted.push(projectId);
});
});
} finally {
this.delete(resourceKeyList(deleted));
}
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, SharedProject>> {
const all = ResourceKeyUtils.includes(key, CachedMapAllKey);
if (all) {
const { projects } = await this.graphQLService.sdk.getSharedProjects();
runInAction(() => {
this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id))));
this.set(resourceKeyList(projects.map(project => project.id)), projects as SharedProject[]);
});
} else {
await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => {
const { project } = await this.graphQLService.sdk.getProject({
projectId,
});
this.set(projectId, project as SharedProject);
});
}
return this.data;
}
protected dataSet(key: string, value: SharedProject): void {
key = this.getKeyRef(key);
const data = this.data.get(key);
this.data.set(key, Object.assign(data ?? {}, value));
}
}
@@ -0,0 +1,2 @@
export * from './SharedProjectsResource';
export { resourceManagerManifest } from './manifest';
@@ -0,0 +1,22 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { PluginManifest } from '@cloudbeaver/core-di';
import { SharedProjectsResource } from './SharedProjectsResource';
export const resourceManagerManifest: PluginManifest = {
info: {
name: 'Resource Manager Core',
},
providers: [
SharedProjectsResource,
],
};
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
"references": [{ "path": "../*" }]
}
@@ -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);
}
@@ -0,0 +1,7 @@
fragment SharedProject on RMProject {
id
name
shared
description
projectPermissions
}
@@ -0,0 +1,13 @@
mutation createProject(
$projectId: ID
$projectName: String!
$description: String
) {
project: rmCreateProject(
projectId: $projectId
projectName: $projectName
description: $description
) {
...SharedProject
}
}
@@ -0,0 +1,3 @@
mutation deleteProject($projectId: ID!) {
rmDeleteProject(projectId: $projectId)
}
@@ -0,0 +1,7 @@
query getProject($projectId: String!) {
project: rmProject(
projectId: $projectId
) {
...SharedProject
}
}
@@ -0,0 +1,5 @@
query getSharedProjects {
projects: rmListSharedProjects {
...SharedProject
}
}
+92 -2
View File
@@ -330,6 +330,7 @@ export interface DriverInfo {
promotedScore?: Maybe<Scalars['Int']>;
providerId?: Maybe<Scalars['ID']>;
providerProperties: Array<ObjectPropertyInfo>;
requiresDatabaseName?: Maybe<Scalars['Boolean']>;
requiresServerName?: Maybe<Scalars['Boolean']>;
sampleURL?: Maybe<Scalars['String']>;
}
@@ -365,7 +366,7 @@ export interface Mutation {
openSession: SessionInfo;
readLobValue: Scalars['String'];
refreshSessionConnections?: Maybe<Scalars['Boolean']>;
rmCreateProject?: Maybe<ProjectInfo>;
rmCreateProject: RmProject;
rmCreateResource: Scalars['String'];
rmDeleteProject: Scalars['Boolean'];
rmDeleteResource?: Maybe<Scalars['Boolean']>;
@@ -531,6 +532,7 @@ export interface MutationReadLobValueArgs {
export interface MutationRmCreateProjectArgs {
description?: InputMaybe<Scalars['String']>;
projectId?: InputMaybe<Scalars['ID']>;
projectName: Scalars['String'];
}
@@ -861,6 +863,8 @@ export interface Query {
revokeUserRole?: Maybe<Scalars['Boolean']>;
rmListProjects: Array<RmProject>;
rmListResources: Array<RmResource>;
rmListSharedProjects: Array<RmProject>;
rmProject: RmProject;
rmReadResourceAsString: Scalars['String'];
saveAuthProviderConfiguration: AdminAuthProviderConfiguration;
saveUserMetaParameter: ObjectPropertyInfo;
@@ -1114,6 +1118,11 @@ export interface QueryRmListResourcesArgs {
}
export interface QueryRmProjectArgs {
projectId: Scalars['String'];
}
export interface QueryRmReadResourceAsStringArgs {
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -1276,7 +1285,7 @@ export interface RmProject {
createTime: Scalars['DateTime'];
creator: Scalars['String'];
description?: Maybe<Scalars['String']>;
id: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
projectPermissions: Array<Scalars['String']>;
shared: Scalars['Boolean'];
@@ -2188,6 +2197,8 @@ export type SqlScriptInfoFragment = { queries: Array<{ start: number, end: numbe
export type SessionStateFragment = { createTime: string, lastAccessTime: string, cacheExpired: boolean, locale: string, actionParameters?: any, valid: boolean, remainingTime: number };
export type SharedProjectFragment = { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> };
export type UserConnectionAuthPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array<any>, defaultValue?: any, length: ObjectPropertyLength, features: Array<string>, order: number };
export type UserConnectionNetworkHandlerPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array<any>, defaultValue?: any, order: number, length: ObjectPropertyLength, features: Array<string> };
@@ -2392,6 +2403,15 @@ export type GetProjectListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canCreateConnections: boolean, canViewConnections: boolean }> };
export type CreateProjectMutationVariables = Exact<{
projectId?: InputMaybe<Scalars['ID']>;
projectName: Scalars['String'];
description?: InputMaybe<Scalars['String']>;
}>;
export type CreateProjectMutation = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type CreateResourceMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2401,6 +2421,13 @@ export type CreateResourceMutationVariables = Exact<{
export type CreateResourceMutation = { rmCreateResource: string };
export type DeleteProjectMutationVariables = Exact<{
projectId: Scalars['ID'];
}>;
export type DeleteProjectMutation = { rmDeleteProject: boolean };
export type DeleteResourceMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2410,6 +2437,13 @@ export type DeleteResourceMutationVariables = Exact<{
export type DeleteResourceMutation = { rmDeleteResource?: boolean };
export type GetProjectQueryVariables = Exact<{
projectId: Scalars['String'];
}>;
export type GetProjectQuery = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type GetResourceListQueryVariables = Exact<{
projectId: Scalars['String'];
folder?: InputMaybe<Scalars['String']>;
@@ -2426,6 +2460,11 @@ export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never;
export type GetResourceProjectListQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> }> };
export type GetSharedProjectsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetSharedProjectsQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> }> };
export type MoveResourceMutationVariables = Exact<{
projectId: Scalars['String'];
oldPath: Scalars['String'];
@@ -2867,6 +2906,15 @@ export const SessionStateFragmentDoc = `
remainingTime
}
`;
export const SharedProjectFragmentDoc = `
fragment SharedProject on RMProject {
id
name
shared
description
projectPermissions
}
`;
export const UserConnectionNetworkHandlerPropertiesFragmentDoc = `
fragment UserConnectionNetworkHandlerProperties on ObjectPropertyInfo {
id
@@ -3847,6 +3895,17 @@ export const GetProjectListDocument = `
}
}
`;
export const CreateProjectDocument = `
mutation createProject($projectId: ID, $projectName: String!, $description: String) {
project: rmCreateProject(
projectId: $projectId
projectName: $projectName
description: $description
) {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const CreateResourceDocument = `
mutation createResource($projectId: String!, $resourcePath: String!, $isFolder: Boolean!) {
rmCreateResource(
@@ -3856,6 +3915,11 @@ export const CreateResourceDocument = `
)
}
`;
export const DeleteProjectDocument = `
mutation deleteProject($projectId: ID!) {
rmDeleteProject(projectId: $projectId)
}
`;
export const DeleteResourceDocument = `
mutation deleteResource($projectId: String!, $resourcePath: String!, $recursive: Boolean!) {
rmDeleteResource(
@@ -3865,6 +3929,13 @@ export const DeleteResourceDocument = `
)
}
`;
export const GetProjectDocument = `
query getProject($projectId: String!) {
project: rmProject(projectId: $projectId) {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const GetResourceListDocument = `
query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) {
resources: rmListResources(
@@ -3891,6 +3962,13 @@ export const GetResourceProjectListDocument = `
}
}
`;
export const GetSharedProjectsDocument = `
query getSharedProjects {
projects: rmListSharedProjects {
...SharedProject
}
}
${SharedProjectFragmentDoc}`;
export const MoveResourceDocument = `
mutation moveResource($projectId: String!, $oldPath: String!, $newPath: String!) {
rmMoveResource(
@@ -4401,18 +4479,30 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getProjectList(variables?: GetProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectListQuery>(GetProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectList', 'query');
},
createProject(variables: CreateProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreateProjectMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreateProjectMutation>(CreateProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createProject', 'mutation');
},
createResource(variables: CreateResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<CreateResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<CreateResourceMutation>(CreateResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createResource', 'mutation');
},
deleteProject(variables: DeleteProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<DeleteProjectMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<DeleteProjectMutation>(DeleteProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteProject', 'mutation');
},
deleteResource(variables: DeleteResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<DeleteResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<DeleteResourceMutation>(DeleteResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteResource', 'mutation');
},
getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectQuery>(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query');
},
getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceListQuery>(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query');
},
getResourceProjectList(variables?: GetResourceProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceProjectListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceProjectListQuery>(GetResourceProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceProjectList', 'query');
},
getSharedProjects(variables?: GetSharedProjectsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetSharedProjectsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetSharedProjectsQuery>(GetSharedProjectsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSharedProjects', 'query');
},
moveResource(variables: MoveResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<MoveResourceMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<MoveResourceMutation>(MoveResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'moveResource', 'mutation');
},
@@ -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';
@@ -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,48 @@
{
"name": "@cloudbeaver/plugin-connection-search",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-administration": "~0.1.0",
"@cloudbeaver/core-app": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-connections": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-dialogs": "~0.1.0",
"@cloudbeaver/core-events": "~0.1.0",
"@cloudbeaver/core-executor": "~0.1.0",
"@cloudbeaver/core-extensions": "~0.1.0",
"@cloudbeaver/core-localization": "~0.1.0",
"@cloudbeaver/core-notifications": "~0.1.0",
"@cloudbeaver/core-plugin": "~0.1.0",
"@cloudbeaver/core-projects": "~0.1.0",
"@cloudbeaver/core-product": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-routing": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"@cloudbeaver/core-ui": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"@cloudbeaver/core-view": "~0.1.0",
"@cloudbeaver/plugin-top-app-bar": "~0.1.0",
"@cloudbeaver/plugin-connections": "~0.1.0",
"react": "~18.x.x",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"reshadow": "~0.x.x"
}
}
@@ -0,0 +1,176 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable, makeObservable } from 'mobx';
import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor';
import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { ConnectionFormService, ConnectionFormState, IConnectionFormState } from '@cloudbeaver/plugin-connections';
import { SearchDatabase } from './SearchDatabase';
const formGetter = () => SearchDatabase;
@injectable()
export class ConnectionSearchService {
hosts = 'localhost';
databases: AdminConnectionSearchInfo[];
disabled = false;
formState: IConnectionFormState | null = null;
constructor(
private readonly notificationService: NotificationService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly connectionFormService: ConnectionFormService,
private readonly optionsPanelService: OptionsPanelService,
private readonly commonDialogService: CommonDialogService,
private readonly projectsService: ProjectsService,
) {
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
this.databases = [];
this.search = this.search.bind(this);
this.change = this.change.bind(this);
this.select = this.select.bind(this);
makeObservable(this, {
hosts: observable,
databases: observable,
disabled: observable,
formState: observable.shallow,
});
}
open(): void {
this.optionsPanelService.open(formGetter);
}
close(): void {
this.hosts = 'localhost';
this.databases = [];
}
async load(): Promise<void> {
if (this.databases.length === 0) {
await this.search();
}
}
async search(): Promise<void> {
if (this.disabled || !this.hosts || !this.hosts.trim()) {
return;
}
this.disabled = true;
try {
const hosts = this.hosts
.trim()
.replace(/[\s,|+-]+/gm, ' ')
.split(/[\s,|+-]/);
this.databases = await this.connectionInfoResource.searchDatabases(hosts);
} catch (exception: any) {
this.notificationService.logException(exception, 'Databases search failed');
} finally {
this.disabled = false;
}
}
private readonly closeHandler: IExecutorHandler<void> = async (data, contexts) => {
const isDialogClosed = await this.showUnsavedChangesDialog();
if (!isDialogClosed) {
ExecutorInterrupter.interrupt(contexts);
return;
}
this.clearFormState();
this.close();
};
private async showUnsavedChangesDialog(): Promise<boolean> {
if (
!this.formState
|| !this.optionsPanelService.isOpen(formGetter)
|| (
this.formState.config.connectionId
&& this.formState.projectId !== null
&& !this.connectionInfoResource.has(createConnectionParam(
this.formState.projectId,
this.formState.config.connectionId
))
)
) {
return true;
}
const state = await this.formState.checkFormState();
if (!state?.edited) {
return true;
}
const result = await this.commonDialogService.open(ConfirmationDialog, {
title: 'connections_public_connection_edit_cancel_title',
message: 'connections_public_connection_edit_cancel_message',
confirmActionText: 'ui_processing_ok',
});
return result !== DialogueStateResult.Rejected;
}
change(hosts: string): void {
this.hosts = hosts;
}
saveConnection() {
this.goBack();
}
goBack() {
this.clearFormState();
}
select(database: AdminConnectionSearchInfo): void {
if (!this.formState) {
this.formState = new ConnectionFormState(
this.connectionFormService,
this.connectionInfoResource
);
}
this.formState
.setOptions(
'create',
'public'
)
.setConfig(this.projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID, {
...this.connectionInfoResource.getEmptyConfig(),
driverId: database.defaultDriver,
host: database.host,
port: `${database.port}`,
})
.setAvailableDrivers(database.possibleDrivers);
this.formState.load();
}
private clearFormState() {
this.formState?.dispose();
this.formState = null;
}
}
@@ -11,19 +11,29 @@ import { observer } from 'mobx-react-lite';
import { useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, type AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { AdminConnectionSearchInfo, CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { ConnectionForm } from '@cloudbeaver/plugin-connections';
import { CustomConnection } from '../Manual/CustomConnection';
import { ConnectionSearchService } from './ConnectionSearchService';
import { DatabaseList } from './DatabaseList';
export const SearchDatabase: React.FC = observer(function SearchDatabase() {
const connectionSearchService = useService(ConnectionSearchService);
useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey);
useMapResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey);
function select(database: AdminConnectionSearchInfo) {
connectionSearchService.select(PROJECT_GLOBAL_ID, database);
connectionSearchService.select(database);
}
if (connectionSearchService.formState) {
return (
<ConnectionForm
state={connectionSearchService.formState}
onSave={() => connectionSearchService.saveConnection()}
onCancel={() => connectionSearchService.goBack()}
/>
);
}
return (
@@ -0,0 +1,49 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { EAdminPermission } from '@cloudbeaver/core-administration';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ProjectsService } from '@cloudbeaver/core-projects';
import { PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root';
import { MainMenuService, EMainMenu } from '@cloudbeaver/plugin-top-app-bar';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
@injectable()
export class SearchConnectionPluginBootstrap extends Bootstrap {
constructor(
private readonly mainMenuService: MainMenuService,
private readonly serverConfigResource: ServerConfigResource,
private readonly projectsService: ProjectsService,
private readonly permissionsService: PermissionsService,
private readonly connectionSearchService: ConnectionSearchService,
) {
super();
}
register(): void | Promise<void> {
this.mainMenuService.onConnectionClick.addHandler(() => {
this.projectsService.load();
});
this.mainMenuService.registerMenuItem(EMainMenu.mainMenuConnectionsPanel, {
id: 'searchConnection',
order: 3,
title: 'connections_connection_create_search_database',
isHidden: () => (
!this.projectsService.activeProject?.canCreateConnections
|| !this.permissionsService.has(EAdminPermission.admin)
),
onClick: () => {
this.connectionSearchService.open();
},
});
}
load(): void | Promise<void> { }
}
@@ -0,0 +1,3 @@
import { connectionSearchPlugin } from './manifest';
export default connectionSearchPlugin;
@@ -0,0 +1,14 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
import { SearchConnectionPluginBootstrap } from './SearchConnectionPluginBootstrap';
export const connectionSearchPlugin: PluginManifest = {
info: {
name: 'Search connection plugin',
},
providers: [
SearchConnectionPluginBootstrap,
ConnectionSearchService,
],
};
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
"references": [{ "path": "../*" }]
}
@@ -14,7 +14,7 @@ import { TemplateConnectionsResource } from './TemplateConnectionsResource';
export const connectionTemplate: PluginManifest = {
info: {
name: 'Connections plugin',
name: 'Template Connections plugin',
},
providers: [
@@ -9,17 +9,12 @@
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { CreateConnectionService } from '../CreateConnectionService';
import { ConnectionManualService } from './Manual/ConnectionManualService';
import { CustomConnection } from './Manual/CustomConnection';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
import { SearchDatabase } from './Search/SearchDatabase';
@injectable()
export class CreateConnectionBaseBootstrap extends Bootstrap {
constructor(
private readonly createConnectionService: CreateConnectionService,
private readonly connectionManualService: ConnectionManualService,
private readonly connectionSearchService: ConnectionSearchService
) {
super();
}
@@ -31,19 +26,6 @@ export class CreateConnectionBaseBootstrap extends Bootstrap {
order: 1,
panel: () => CustomConnection,
});
this.createConnectionService.tabsContainer.add({
key: 'search',
name: 'connections_connection_create_search_database',
order: 2,
panel: () => SearchDatabase,
onOpen: () => this.connectionSearchService.load(),
options: {
configurationWizard: {
activationPriority: 2,
},
close: () => this.connectionSearchService.close(),
},
});
}
load(): void | Promise<void> { }
@@ -13,7 +13,7 @@ import { useMemo } from 'react';
import { Loader, useMapResource } from '@cloudbeaver/core-blocks';
import { DBDriverResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { ConnectionManualService } from './ConnectionManualService';
@@ -27,7 +27,7 @@ export const CustomConnection = observer(function CustomConnection() {
dbDriverResource.resource.enabledDrivers.slice().sort(dbDriverResource.resource.compare)
)), [dbDriverResource]);
useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey);
useMapResource(CustomConnection, ProjectInfoResource, CachedMapAllKey);
function select(driverId: string) {
connectionManualService.select(PROJECT_GLOBAL_ID, driverId);
@@ -1,97 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable, makeObservable } from 'mobx';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { CreateConnectionService } from '../../CreateConnectionService';
@injectable()
export class ConnectionSearchService {
hosts = 'localhost';
databases: AdminConnectionSearchInfo[];
get disabled(): boolean {
return this.createConnectionService.disabled;
}
set disabled(value: boolean) {
this.createConnectionService.disabled = value;
}
constructor(
private readonly notificationService: NotificationService,
private readonly connectionInfoResource: ConnectionInfoResource,
private createConnectionService: CreateConnectionService,
private readonly administrationScreenService: AdministrationScreenService
) {
makeObservable(this, {
hosts: observable,
databases: observable,
});
this.databases = [];
this.search = this.search.bind(this);
this.change = this.change.bind(this);
this.select = this.select.bind(this);
}
close(): void {
this.hosts = 'localhost';
this.databases = [];
}
async load(): Promise<void> {
if (this.administrationScreenService.isConfigurationMode && this.databases.length === 0) {
await this.search();
}
}
async search(): Promise<void> {
if (this.disabled || !this.hosts || !this.hosts.trim()) {
return;
}
this.disabled = true;
try {
const hosts = this.hosts
.trim()
.replace(/[\s,|+-]+/gm, ' ')
.split(/[\s,|+-]/);
this.databases = await this.connectionInfoResource.searchDatabases(hosts);
} catch (exception: any) {
this.notificationService.logException(exception, 'Databases search failed');
} finally {
this.disabled = false;
}
}
change(hosts: string): void {
this.hosts = hosts;
}
select(projectId: string, database: AdminConnectionSearchInfo): void {
this.createConnectionService.setConnectionTemplate(
projectId,
{
...this.connectionInfoResource.getEmptyConfig(),
driverId: database.defaultDriver,
host: database.host,
port: `${database.port}`,
},
database.possibleDrivers
);
}
}
@@ -1,7 +1,6 @@
import { connectionPlugin } from './manifest';
export * from './Administration/Connections/ConnectionsAdministration';
export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
export * from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
export * from './Administration/Connections/ConnectionsAdministrationNavService';
export * from './Administration/Connections/ConnectionsAdministrationService';
export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
@@ -12,7 +12,6 @@ import { ConnectionsAdministrationNavService } from './Administration/Connection
import { ConnectionsAdministrationService } from './Administration/Connections/ConnectionsAdministrationService';
import { CreateConnectionBaseBootstrap } from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
import { ConnectionManualService } from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
import { ConnectionSearchService } from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
import { CreateConnectionService } from './Administration/Connections/CreateConnectionService';
import { ConnectionAccessTabService } from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService';
import { LocaleService } from './LocaleService';
@@ -28,7 +27,6 @@ export const connectionPlugin: PluginManifest = {
ConnectionsAdministrationNavService,
CreateConnectionService,
ConnectionManualService,
ConnectionSearchService,
CreateConnectionBaseBootstrap,
ConnectionAccessTabService,
],
@@ -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;
}
@@ -11,7 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx';
import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils';
@@ -26,7 +26,7 @@ import { Options } from './Options';
@injectable()
export class ConnectionOptionsTabService extends Bootstrap {
constructor(
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly connectionFormService: ConnectionFormService,
private readonly dbDriverResource: DBDriverResource,
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { Combobox, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { Project, ProjectsResource, ProjectsService } from '@cloudbeaver/core-projects';
import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
interface Props {
@@ -25,7 +25,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
const translate = useTranslate();
const projectsService = useService(ProjectsService);
const projectsLoader = useMapResource(ProjectSelect, ProjectsResource, CachedMapAllKey, {
const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, {
onData: () => {
if (!props.value && projectsService.activeProject) {
props.onChange(projectsService.activeProject.id);
@@ -34,8 +34,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
});
const value = props.value ?? projectsService.activeProject?.id;
const projects = projectsLoader.data as Project[];
const projects = projectsLoader.data as ProjectInfo[];
const possibleOptions = projects.filter(project => project.canCreateConnections);
@@ -17,7 +17,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectsResource } from '@cloudbeaver/core-projects';
import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectInfoResource } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { createPath } from '@cloudbeaver/core-utils';
import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view';
@@ -47,7 +47,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
private readonly commonDialogService: CommonDialogService,
private readonly notificationService: NotificationService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectsResource: ProjectsResource,
private readonly projectsResource: ProjectInfoResource,
private readonly projectsNavNodeService: ProjectsNavNodeService
) {
super();
@@ -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'
);

Some files were not shown because too many files have changed in this diff Show More