diff --git a/webapp/packages/core-blocks/src/StatusMessage.tsx b/webapp/packages/core-blocks/src/StatusMessage.tsx index a2006d549b..1ef76d3178 100644 --- a/webapp/packages/core-blocks/src/StatusMessage.tsx +++ b/webapp/packages/core-blocks/src/StatusMessage.tsx @@ -13,12 +13,7 @@ import { IconOrImage, Link } from '@cloudbeaver/core-blocks'; import { ENotificationType } from '@cloudbeaver/core-events'; import { useTranslate } from '@cloudbeaver/core-localization'; -interface Props { - message: string | undefined | null; - status: ENotificationType | undefined; - onShowDetails?: () => void; - className?: string; -} +import { useErrorDetails } from './useErrorDetails'; const styles = css` status-message { @@ -43,17 +38,36 @@ const styles = css` } `; -export const StatusMessage = observer(function StatusMessage({ status, message, onShowDetails, className }) { +interface Props { + message?: string | null; + status?: ENotificationType; + exception?: Error | null; + onShowDetails?: () => void; + className?: string; +} + +export const StatusMessage = observer(function StatusMessage({ + status, + message, + exception = null, + onShowDetails, + className, +}) { const translate = useTranslate(); - message = message ? translate(message) : message; + const errorDetails = useErrorDetails(exception); + message = (message ? translate(message) : message) ?? errorDetails.details?.name; let icon = '/icons/info_icon.svg'; - if (status === ENotificationType.Error) { + if (status === ENotificationType.Error || exception !== null) { icon = '/icons/error_icon.svg'; } else if (status === ENotificationType.Success) { icon = '/icons/success_icon.svg'; } + if (errorDetails.details?.hasDetails && !onShowDetails) { + onShowDetails = errorDetails.open; + } + return styled(styles)( {message && ( diff --git a/webapp/packages/core-blocks/src/Table/Table.tsx b/webapp/packages/core-blocks/src/Table/Table.tsx index 5b3ae0bfa1..2e5b3a194e 100644 --- a/webapp/packages/core-blocks/src/Table/Table.tsx +++ b/webapp/packages/core-blocks/src/Table/Table.tsx @@ -8,7 +8,7 @@ import { observable, computed, action } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import styled, { use } from 'reshadow'; import { useObjectRef } from '../useObjectRef'; @@ -34,6 +34,23 @@ export const Table = observer>(function Table({ const [selected] = useState>(() => selectedItems || observable(new Map())); const [expanded] = useState>(() => expandedItems || observable(new Map())); + useEffect(action(() => { + if (!keys) { + return; + } + + const removeSelected = Array.from(selected.keys()).filter(key => !keys.includes(key)); + const removeExpanded = Array.from(expanded.keys()).filter(key => !keys.includes(key)); + + for (const id of removeSelected) { + selected.delete(id); + } + + for (const id of removeExpanded) { + expanded.delete(id); + } + }), [keys]); + const state: ITableState = useObservableRef(() => ({ get selectableItems() { if (!this.keys) { diff --git a/webapp/packages/core-di/src/DIService.ts b/webapp/packages/core-di/src/DIService.ts index d9e12f8672..1b185da8d8 100644 --- a/webapp/packages/core-di/src/DIService.ts +++ b/webapp/packages/core-di/src/DIService.ts @@ -6,9 +6,10 @@ * you may not use this file except in compliance with the License. */ -import type { ISyncContextLoader } from '@cloudbeaver/core-executor'; +import type { IExecutorHandlersCollection, ISyncContextLoader } from '@cloudbeaver/core-executor'; -import type { App } from './App'; +import { App } from './App'; +import { dependencyInjectorContext } from './dependencyInjectorContext'; import type { IServiceConstructor, IServiceInjector } from './IApp'; import { injectable } from './injectable'; @@ -22,7 +23,11 @@ export class DIService { private readonly app: App ) { } - dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = ( + addDIContext(context: IExecutorHandlersCollection): void { + context.addContextCreator(dependencyInjectorContext, this.dependencyInjectorContext); + } + + private readonly dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = ( () => this.serviceInjector.getServiceByClass.bind(this) ); } \ No newline at end of file diff --git a/webapp/packages/core-di/src/dependencyInjectorContext.ts b/webapp/packages/core-di/src/dependencyInjectorContext.ts new file mode 100644 index 0000000000..40bd182d1c --- /dev/null +++ b/webapp/packages/core-di/src/dependencyInjectorContext.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ISyncContextLoader } from '@cloudbeaver/core-executor'; + +import type { IServiceConstructor } from './IApp'; + +export const dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = function dependencyInjectorContext() { + throw new Error('Implementation not provided \n Use addContextCreator to add addDIContext implementation to context'); +}; \ No newline at end of file diff --git a/webapp/packages/core-di/src/index.ts b/webapp/packages/core-di/src/index.ts index 8096df9cf7..ef435878ce 100644 --- a/webapp/packages/core-di/src/index.ts +++ b/webapp/packages/core-di/src/index.ts @@ -3,6 +3,7 @@ export * from './App'; export * from './AppContext'; export * from './Bootstrap'; export * from './Dependency'; +export * from './dependencyInjectorContext'; export * from './DIService'; export * from './injectable'; export * from './PluginManifest'; diff --git a/webapp/packages/core-executor/src/ExecutionContext.ts b/webapp/packages/core-executor/src/ExecutionContext.ts index d27501ccb8..032981993d 100644 --- a/webapp/packages/core-executor/src/ExecutionContext.ts +++ b/webapp/packages/core-executor/src/ExecutionContext.ts @@ -10,23 +10,39 @@ import type { IAsyncContextLoader, IContextLoader, IExecutionContext, ISyncConte export class ExecutionContext implements IExecutionContext { readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; - constructor(private data: TData, context?: IExecutionContext) { - this.contexts = context?.contexts || new Map, any>(); + constructor(private readonly data: TData, context?: IExecutionContext) { + this.contexts = context?.contexts || new Map(); + this.contextCreators = context?.contextCreators ?? new Map(); } - hasContext(loader: IContextLoader): boolean { - return this.contexts.has(loader); + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void { + for (const [key, value] of creators) { + this.contextCreators.set(key, value); + } } - getContext(token: ISyncContextLoader): T - getContext(token: IAsyncContextLoader): Promise + hasContext(loader: IContextLoader): boolean { + return this.contextCreators.has(loader) || this.contexts.has(loader); + } + + getContext(token: ISyncContextLoader): T; + getContext(token: IAsyncContextLoader): Promise; getContext(token: IContextLoader): Promise | T { if (this.contexts.has(token)) { return this.contexts.get(token); } - const value = token(this, this.data); + let value: T | Promise; + + if (this.contextCreators.has(token)) { + value = this.contextCreators.get(token)!(this, this.data); + } else { + value = token(this, this.data); + } if (value instanceof Promise) { return this.getAsyncContext(token, value); diff --git a/webapp/packages/core-executor/src/Executor.ts b/webapp/packages/core-executor/src/Executor.ts index f4fb934f20..d5cf73bb5c 100644 --- a/webapp/packages/core-executor/src/Executor.ts +++ b/webapp/packages/core-executor/src/Executor.ts @@ -9,6 +9,7 @@ import { flat } from '@cloudbeaver/core-utils'; import { ExecutionContext } from './ExecutionContext'; +import { executionExceptionContext } from './executionExceptionContext'; import { ExecutorHandlersCollection } from './ExecutorHandlersCollection'; import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter'; import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext'; @@ -68,9 +69,16 @@ export class Executor extends ExecutorHandlersCollection implements context: IExecutionContext, scoped: Array> ): Promise> { - const interrupter = context.getContext(ExecutorInterrupter.interruptContext); scoped = [...collection.collections, ...scoped]; + context.addContextCreators(collection.contextCreators as any); + + for (const scope of scoped) { + context.addContextCreators(scope.contextCreators as any); + } + + const interrupter = context.getContext(ExecutorInterrupter.interruptContext); + await this.executeChain(collection, data, context, 'before'); for (const scope of scoped) { @@ -83,7 +91,11 @@ export class Executor extends ExecutorHandlersCollection implements for (const scope of scoped) { await this.executeHandlers(data, context, scope.handlers, interrupter); } - } finally { + } catch (exception: any) { + const exceptionContext = context.getContext(executionExceptionContext); + exceptionContext.setException(exception); + throw exception; + } finally { await this.executeHandlers(data, context, collection.postHandlers); for (const scope of scoped) { @@ -158,6 +170,7 @@ export class Executor extends ExecutorHandlersCollection implements this.scheduler.schedule(data, async () => { const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); try { await handler(data, context); diff --git a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts index 4bc69c555a..26504a4812 100644 --- a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts @@ -8,6 +8,7 @@ import { makeObservable, observable } from 'mobx'; +import type { IContextLoader, ISyncContextLoader, IAsyncContextLoader } from './IExecutionContext'; import type { IExecutorHandler } from './IExecutorHandler'; import type { ExecutorDataFilter, ExecutorDataMap, IChainLink, IExecutorHandlersCollection } from './IExecutorHandlersCollection'; @@ -16,12 +17,14 @@ implements IExecutorHandlersCollection { handlers: Array> = []; postHandlers: Array> = []; chain: Array> = []; + readonly contextCreators: Map, IContextLoader>; readonly collections: Array>; protected initialDataGetter: (() => T) | null; private readonly links: Map, IExecutorHandlersCollection>; constructor() { this.links = new Map(); + this.contextCreators = new Map(); this.collections = []; this.initialDataGetter = null; @@ -34,6 +37,22 @@ implements IExecutorHandlersCollection { }); } + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + addContextCreator( + context: IContextLoader, + creator: IContextLoader + ): this { + this.contextCreators.set(context, creator); + return this; + } + setInitialDataGetter(getter: (() => T) | null): this { this.initialDataGetter = getter; return this; diff --git a/webapp/packages/core-executor/src/IExecutionContext.ts b/webapp/packages/core-executor/src/IExecutionContext.ts index 7f4f5d3cc8..2feade714e 100644 --- a/webapp/packages/core-executor/src/IExecutionContext.ts +++ b/webapp/packages/core-executor/src/IExecutionContext.ts @@ -17,7 +17,13 @@ export interface IExecutionContextProvider { } export interface IExecutionContext extends IExecutionContextProvider { + readonly parent?: IExecutionContext; readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; + + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void; } export type IAsyncContextLoader = ( diff --git a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts index 2cc29324f0..505ae0dad7 100644 --- a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IExecutionContextProvider } from './IExecutionContext'; +import type { IAsyncContextLoader, IContextLoader, IExecutionContextProvider, ISyncContextLoader } from './IExecutionContext'; import type { IExecutorHandler } from './IExecutorHandler'; export type ExecutorDataFilter = (data: T, contexts: IExecutionContextProvider) => boolean; @@ -25,9 +25,19 @@ export interface IExecutorHandlersCollection>; readonly chain: Array>; readonly collections: Array>; + readonly contextCreators: Map, IContextLoader>; setInitialDataGetter(getter: () => T): this; + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + before: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; next: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; addCollection: (collection: IExecutorHandlersCollection) => this; diff --git a/webapp/packages/core-executor/src/SyncExecutor.ts b/webapp/packages/core-executor/src/SyncExecutor.ts index 37b62e8bd8..eccd4e4143 100644 --- a/webapp/packages/core-executor/src/SyncExecutor.ts +++ b/webapp/packages/core-executor/src/SyncExecutor.ts @@ -9,6 +9,7 @@ import { flat } from '@cloudbeaver/core-utils'; import { ExecutionContext } from './ExecutionContext'; +import { executionExceptionContext } from './executionExceptionContext'; import { ExecutorHandlersCollection } from './ExecutorHandlersCollection'; import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter'; import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext'; @@ -55,9 +56,16 @@ export class SyncExecutor extends ExecutorHandlersCollection implem context: IExecutionContext, scoped: Array> ): IExecutionContextProvider { - const interrupter = context.getContext(ExecutorInterrupter.interruptContext); scoped = [...collection.collections, ...scoped]; + context.addContextCreators(collection.contextCreators as any); + + for (const scope of scoped) { + context.addContextCreators(scope.contextCreators as any); + } + + const interrupter = context.getContext(ExecutorInterrupter.interruptContext); + this.executeChain(collection, data, context, 'before'); for (const scope of scoped) { @@ -70,6 +78,10 @@ export class SyncExecutor extends ExecutorHandlersCollection implem for (const scope of scoped) { this.executeHandlers(data, context, scope.handlers, interrupter); } + } catch (exception: any) { + const exceptionContext = context.getContext(executionExceptionContext); + exceptionContext.setException(exception); + throw exception; } finally { this.executeHandlers(data, context, collection.postHandlers); @@ -144,6 +156,8 @@ export class SyncExecutor extends ExecutorHandlersCollection implem const data = this.initialDataGetter(); const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); + try { handler(data, context); } finally { diff --git a/webapp/packages/core-executor/src/executionExceptionContext.ts b/webapp/packages/core-executor/src/executionExceptionContext.ts new file mode 100644 index 0000000000..819e73a140 --- /dev/null +++ b/webapp/packages/core-executor/src/executionExceptionContext.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function executionExceptionContext() { + return { + exception: null as null | Error, + setException(exception: Error) { + this.exception = exception; + }, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-executor/src/index.ts b/webapp/packages/core-executor/src/index.ts index 22f248d2d2..d9aeff7deb 100644 --- a/webapp/packages/core-executor/src/index.ts +++ b/webapp/packages/core-executor/src/index.ts @@ -3,6 +3,7 @@ export * from './TaskScheduler/ITask'; export * from './TaskScheduler/Task'; export * from './TaskScheduler/TaskScheduler'; export * from './ExecutionContext'; +export * from './executionExceptionContext'; export * from './Executor'; export * from './ExecutorHandlersCollection'; export * from './ExecutorInterrupter'; diff --git a/webapp/packages/core-projects/src/ProjectInfoResource.ts b/webapp/packages/core-projects/src/ProjectInfoResource.ts index 3918b39dca..7fb80adf94 100644 --- a/webapp/packages/core-projects/src/ProjectInfoResource.ts +++ b/webapp/packages/core-projects/src/ProjectInfoResource.ts @@ -10,6 +10,7 @@ import { runInAction } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; +import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager'; import { GraphQLService, ProjectInfo as SchemaProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk'; export type ProjectInfo = SchemaProjectInfo; @@ -18,11 +19,15 @@ export type ProjectInfo = SchemaProjectInfo; export class ProjectInfoResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, + private readonly sharedProjectsResource: SharedProjectsResource, private readonly userInfoResource: UserInfoResource, ) { super([]); this.sync(this.userInfoResource); + this.sharedProjectsResource.onDataOutdated.addHandler(this.markOutdated.bind(this)); + this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated()); this.userInfoResource.onUserChange.addPostHandler(() => { this.clear(); });