diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index abfc0a964c..6590e481e2 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -63,7 +63,7 @@ const buttonStyles = css` } &[href] { - text-decoration: none; + text-decoration: none !important; } } `; diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index 9a340c9367..512f034361 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -18,6 +18,7 @@ const styles = css` error { flex: 1; display: flex; + padding: 24px; } error-name { @@ -55,6 +56,7 @@ const styles = css` error[|inline] { align-items: center; height: 38px; + padding: 0; & error-data { display: flex; diff --git a/webapp/packages/core-blocks/src/Link.tsx b/webapp/packages/core-blocks/src/Link.tsx index c90f1f8f75..7cc7936f9b 100644 --- a/webapp/packages/core-blocks/src/Link.tsx +++ b/webapp/packages/core-blocks/src/Link.tsx @@ -27,7 +27,7 @@ const styles = css` a[|wrapper] { &, &:hover, &:focus, &:active { color: inherit; - text-decoration: none; + text-decoration: none !important; outline: none; } } diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index 9e4c481a4d..796cc5a4c5 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -328,16 +328,18 @@ export function useResource< // throw refObj.loadingPromise; // } - if (this.loading) { - throw this.resource.waitLoad(); - } + if (!this.isLoaded()) { + if (this.loading) { + throw this.resource.waitLoad(); + } - if (this.canLoad) { - throw refObj.load(); - } + if (this.canLoad) { + throw refObj.load(); + } - if (this.isError()) { - throw this.exception; + if (this.isError()) { + throw this.exception; + } } //--------------------- @@ -358,7 +360,7 @@ export function useResource< } if (this.isError()) { - return false; + return true; } return this.resource.isLoaded(propertiesRef.key, propertiesRef.includes); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index 801739741c..79e4db26d2 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -7,9 +7,9 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { - GraphQLService, CachedMapResource, ResourceKey, isResourceKeyList, resourceKeyList, ResourceKeyUtils, ResourceKeyList -} from '@cloudbeaver/core-sdk'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { GraphQLService, CachedMapResource, ResourceKey, isResourceKeyList, resourceKeyList, ResourceKeyUtils, ResourceKeyList, DetailsError } from '@cloudbeaver/core-sdk'; +import { flat } from '@cloudbeaver/core-utils'; import type { DBObject } from './EntityTypes'; import { NavNodeInfoResource } from './NavNodeInfoResource'; @@ -39,6 +39,29 @@ export class DBObjectResource extends CachedMapResource { this.navNodeInfoResource.outdateResource(this); this.navNodeInfoResource.deleteInResource(this); this.navNodeInfoResource.onDataOutdated.addHandler(this.outdateChildren.bind(this)); + this.beforeLoad.addHandler(async (originalKey, context) => { + await this.navTreeResource.waitLoad(); + if (isDBObjectParentKey(originalKey)) { + await this.navTreeResource.load(originalKey.mark); + return; + } + + const key = this.transformParam(originalKey); + const parents = [...new Set(ResourceKeyUtils + .mapArray(key, nodeId => this.navNodeInfoResource.get(nodeId)?.parentId) + .filter((nodeId): nodeId is string => nodeId !== undefined))]; + + const children = await this.navTreeResource.load(resourceKeyList(parents)); + + if ( + ResourceKeyUtils.count(key) > 0 + && !ResourceKeyUtils.includes(resourceKeyList(flat(children)), key) + ) { + ExecutorInterrupter.interrupt(context); + const cause = new DetailsError(`Entity not found: ${key.toString()}`); + throw this.markDataError(cause, key); + } + }); } protected async loader(originalKey: ResourceKey): Promise> { diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts index d16935a8c9..07a563e0ce 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts @@ -81,8 +81,10 @@ export class NavNodeInfoResource extends CachedMapResource { }); if (keyList.length > 0) { - this.set(resourceKeyList(keyList), values); + const keyListKey = resourceKeyList(keyList); + this.set(keyListKey, values); this.markUpdated(key); + this.cleanError(ResourceKeyUtils.join(keyListKey, key)); this.onItemAdd.execute(key); } } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index 35c7cf7cd0..84a33523e8 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -11,7 +11,7 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { CoreSettingsService } from '@cloudbeaver/core-app'; import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; -import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutionContext, IExecutor } from '@cloudbeaver/core-executor'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { SessionDataResource } from '@cloudbeaver/core-root'; import { @@ -141,8 +141,13 @@ export class NavTreeResource extends CachedMapResource { if (parent !== undefined && !children.includes(next)) { return false; } + await this.scheduler.waitRelease(next); - children = await this.load(next); + if (this.isLoadable(next)) { + children = await this.load(next); + } else { + children = this.get(next) || []; + } parent = next; } @@ -374,6 +379,7 @@ export class NavTreeResource extends CachedMapResource { const children = this.data.get(key) || []; childrenToRemove.push(...children.filter(navNodeId => !value.includes(navNodeId))); this.dataSet(key, value); + this.cleanError(resourceKeyList(value)); }); this.delete(resourceKeyList(childrenToRemove)); @@ -405,9 +411,9 @@ export class NavTreeResource extends CachedMapResource { } protected async preLoadData( - key: ResourceKey + key: ResourceKey, + contexts: IExecutionContext> ): Promise { - // this.performUpdate(key, undefined, async key => { await ResourceKeyUtils.forEachAsync(key, async nodeId => { if (!this.navNodeInfoResource.has(nodeId) && nodeId !== ROOT_NODE_PATH) { await this.navNodeInfoResource.loadNodeParents(nodeId); @@ -416,10 +422,11 @@ export class NavTreeResource extends CachedMapResource { if (!preloaded) { const cause = new DetailsError(`Entity not found: ${nodeId}`); - throw new ResourceError(this, key, undefined, 'Entity not found', { cause }); + const error = new ResourceError(this, key, undefined, 'Entity not found', { cause }); + ExecutorInterrupter.interrupt(contexts); + throw this.markDataError(error, key); } }); - // }); } protected async loader(key: ResourceKey): Promise> { diff --git a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts index a12fcdac56..4b0aec2197 100644 --- a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts @@ -9,7 +9,7 @@ import { observable, toJS } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, IExecutionContext, IExecutor } from '@cloudbeaver/core-executor'; import { ProjectsService } from '@cloudbeaver/core-projects'; import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root'; import { CachedMapResource, CachedResourceIncludeArgs, GetResourceListQueryVariables, GraphQLService, ICachedMapResourceMetadata, isResourceKeyList, ResourceKey, resourceKeyList, ResourceKeyUtils, RmResource } from '@cloudbeaver/core-sdk'; @@ -255,6 +255,7 @@ export class ResourceManagerResource protected async preLoadData( key: ResourceKey, + contexts: IExecutionContext>, refresh: boolean, includes: CachedResourceIncludeArgs ): Promise { diff --git a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts index a65d247c7e..0fb23470ba 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts @@ -79,7 +79,6 @@ export abstract class CachedDataResource< param: TParam, context?: T ): Promise> { - await this.preLoadData(param, false, context); await this.loadData(param, true, context); return this.data as CachedResourceValueIncludes; } @@ -88,7 +87,6 @@ export abstract class CachedDataResource< param: TParam, context?: T ): Promise> { - await this.preLoadData(param, false, context); await this.loadData(param, false, context); return this.data as CachedResourceValueIncludes; } diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index 0c8a5ca794..b142f6cc41 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -240,7 +240,7 @@ export abstract class CachedMapResource< exception: Error, key: ResourceKey, includes?: CachedResourceIncludeArgs - ): void { + ): ResourceError { if (this.isAlias(key) && !this.isAliasLoaded(key)) { this.loadedKeys.push(key); } @@ -256,6 +256,7 @@ export abstract class CachedMapResource< }); this.onDataError.execute({ param: key, exception }); + return exception as ResourceError; } markOutdated(): void; @@ -405,7 +406,6 @@ export abstract class CachedMapResource< key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes> { - await this.preLoadData(key, false, includes); await this.loadData(key, true, includes); return this.get(key) as Array> | CachedResourceValueIncludes; } @@ -426,7 +426,6 @@ export abstract class CachedMapResource< key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes> { - await this.preLoadData(key, false, includes); await this.loadData(key, false, includes); return this.get(key) as Array> | CachedResourceValueIncludes; } diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 16ba56bdc1..cd58c822da 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -9,7 +9,7 @@ import { observable, makeObservable, action, computed, toJS } from 'mobx'; import { Dependency } from '@cloudbeaver/core-di'; -import { Executor, ExecutorInterrupter, IExecutor, IExecutorHandler, ISyncExecutor, SyncExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; +import { ExecutionContext, Executor, ExecutorInterrupter, IExecutor, IExecutorHandler, IExecutionContextProvider, ISyncExecutor, SyncExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; import { MetadataMap, uuid } from '@cloudbeaver/core-utils'; import { ResourceError } from './ResourceError'; @@ -336,6 +336,10 @@ export abstract class CachedResource< abstract isLoaded(param: TParam, context?: TContext): boolean; + isLoadable(param: TParam, context?: TContext): boolean { + return !this.isLoaded(param, context) || this.isOutdated(param); + } + isAlias(key: TParam): boolean { return this.paramAliases.some(alias => { if ('isEqual' in alias && alias.isEqual) { @@ -425,7 +429,7 @@ export abstract class CachedResource< }); } - markDataError(exception: Error, param: TParam, context?: TContext): void { + markDataError(exception: Error, param: TParam, context?: TContext): ResourceError { if (this.isAlias(param) && !this.isAliasLoaded(param)) { this.loadedKeys.push(param); } @@ -437,6 +441,7 @@ export abstract class CachedResource< metadata.outdated = false; }); this.onDataError.execute({ param, exception }); + return exception as ResourceError; } cleanError(param: TParam): void { @@ -532,13 +537,11 @@ export abstract class CachedResource< } async refresh(param: TParam, context?: TContext): Promise { - await this.preLoadData(param, false, context); await this.loadData(param, true, context); return this.data; } async load(param: TParam, context?: TContext): Promise { - await this.preLoadData(param, false, context); await this.loadData(param, false, context); return this.data; } @@ -585,7 +588,6 @@ export abstract class CachedResource< } } }); - } protected setData(data: TData): void { @@ -721,8 +723,9 @@ export abstract class CachedResource< protected async preLoadData( param: TParam, + contexts: IExecutionContextProvider, refresh: boolean, - context?: TContext + context?: TContext, ): Promise { } protected async loadData( @@ -730,15 +733,17 @@ export abstract class CachedResource< refresh: boolean, context?: TContext ): Promise { + const contexts = new ExecutionContext(param); + await this.preLoadData(param, contexts, refresh, context); if (!refresh) { await this.scheduler.waitRelease(param); - if (this.isLoaded(param, context) && !this.isOutdated(param)) { + if (!this.isLoadable(param, context)) { return; } } - const contexts = await this.beforeLoad.execute(param); + await this.beforeLoad.execute(param, contexts); if (ExecutorInterrupter.isInterrupted(contexts) && !refresh) { return; @@ -749,7 +754,7 @@ export abstract class CachedResource< param, async () => { // repeated because previous task maybe has been load requested data - if (this.isLoaded(param, context) && !this.isOutdated(param)) { + if (!this.isLoadable(param, context)) { return; }