Merge pull request #1067 from dbeaver/fix/CB-2420/cached-resources

CB-2420 CB-2431 cached resources
This commit is contained in:
Alexey
2022-08-24 13:28:09 +03:00
committed by GitHub
12 changed files with 212 additions and 75 deletions
@@ -72,7 +72,7 @@ export const Loader = observer<Props>(function Loader({
fullSize,
className,
loader,
loading = true,
loading,
inlineException,
state,
style,
@@ -87,30 +87,57 @@ export const Loader = observer<Props>(function Loader({
let exception: Error | null = null;
let reload: (() => void) | undefined;
const loadingUndefined = loading === undefined;
if (loadingUndefined) {
loading = true;
}
let loaded = !loading;
if (state) {
state = Array.isArray(state) ? state : [state];
for (const element of state) {
for (let i = 0; i < state.length; i++) {
const element = state[i];
if (
'isLoaded' in element
&& 'isLoading' in element
) {
loaded = element.isLoaded();
loading = element.isLoading();
if (i === 0 && loadingUndefined) {
loaded = element.isLoaded();
loading = element.isLoading();
} else {
loaded &&= element.isLoaded();
loading ||= element.isLoading();
}
if (loading) {
if (element.cancel) {
onCancel = element.cancel;
onCancel = () => {
onCancel?.();
element.cancel?.();
};
}
if (element.isCancelled) {
cancelDisabled = element.isCancelled();
if (i == 0 && cancelDisabled === undefined) {
cancelDisabled = element.isCancelled();
} else {
cancelDisabled ||= element.isCancelled();
}
}
}
} else {
loading = element.loading;
loaded = !loading;
if (i === 0 && loadingUndefined) {
loading = element.loading;
loaded = !loading;
} else {
loading ||= element.loading;
loaded &&= !loading;
}
}
if ('exception' in element && element.exception) {
@@ -20,10 +20,13 @@ interface KeyWithIncludes<TKey, TIncludes> {
includes: TIncludes;
}
interface IActions<TResource> {
interface IActions<
TResource,
TKeyArg extends CachedDataResourceParam<TResource>,
> {
active?: boolean;
isActive?: (resource: TResource) => Promise<boolean> | boolean;
onLoad?: (resource: TResource) => Promise<any> | any;
onLoad?: (resource: TResource, key: TKeyArg | null) => Promise<any> | any;
onData?: (
data: CachedResourceData<TResource>,
resource: TResource,
@@ -55,7 +58,7 @@ export function useDataResource<
keyObj: TResource extends any
? TKeyArg | null | KeyWithIncludes<TKeyArg, TIncludes>
: never,
actions?: IActions<TResource>
actions?: IActions<TResource, TKeyArg>
): IMapResourceResult<TResource, TIncludes>;
export function useDataResource<
@@ -71,7 +74,7 @@ export function useDataResource<
keyObj: TResource extends any
? TKeyArg | null | KeyWithIncludes<TKeyArg, TIncludes>
: never,
actions?: IActions<TResource>
actions?: IActions<TResource, TKeyArg>
): IMapResourceResult<TResource, TIncludes> {
// eslint-disable-next-line react-hooks/rules-of-hooks
const resource = ctor instanceof CachedDataResource ? ctor : useService(ctor);
@@ -113,7 +116,7 @@ export function useDataResource<
this.firstRender = false;
this.loading = true;
await actions?.onLoad?.(resource);
await actions?.onLoad?.(resource, key);
if (key === null) {
return;
@@ -24,7 +24,7 @@ interface IActions<
> {
active?: boolean;
isActive?: (resource: TResource) => Promise<boolean> | boolean;
onLoad?: (resource: TResource) => Promise<boolean | void> | boolean | void;
onLoad?: (resource: TResource, key: TKeyArg | null) => Promise<boolean | void> | boolean | void;
onData?: (
data: CachedMapResourceLoader<
TKeyArg,
@@ -150,7 +150,7 @@ export function useMapResource<
return true;
}
if (this.loadedKey === null) {
if (this.loadedKey === null || this.key === null) {
return false;
}
@@ -176,11 +176,15 @@ export function useMapResource<
});
}
if (!untracked(() => resource.includes(key, keyRef.key))) {
untracked(() => {
untracked(() => {
if (
key === null
|| keyRef.key === null
|| !resource.includes(key, keyRef.key)
) {
keyRef.key = key;
});
}
}
});
const refObj = useObservableRef(() => ({
loading: false,
@@ -227,7 +231,7 @@ export function useMapResource<
keyRef.loadedKey = key;
this.loading = true;
const prevent = await actions?.onLoad?.(resource);
const prevent = await actions?.onLoad?.(resource, key);
if (key === null || prevent === true) {
setException(null);
@@ -269,6 +273,7 @@ export function useMapResource<
}
},
}), {
preloaded: computed,
exception: observable.ref,
loading: observable.ref,
}, {
@@ -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 { IServiceConstructor } from '@cloudbeaver/core-di';
import type { IApplication } from './createApp';
export function getService<T>(app: IApplication, ctor: IServiceConstructor<T>): T {
return app.injector.getServiceByClass(ctor);
}
@@ -8,13 +8,19 @@
import { injectable } from '@cloudbeaver/core-di';
import {
GraphQLService, CachedMapResource, ResourceKey, isResourceKeyList, resourceKeyList, ResourceKeyUtils
GraphQLService, CachedMapResource, ResourceKey, isResourceKeyList, resourceKeyList, ResourceKeyUtils, ResourceKeyList
} from '@cloudbeaver/core-sdk';
import type { DBObject } from './EntityTypes';
import { NavNodeInfoResource } from './NavNodeInfoResource';
import { NavTreeResource } from './NavTreeResource';
const dbObjectParentKeySymbol = Symbol('@db-object/parent') as unknown as string;
export const DBObjectParentKey = (parentId: string) => resourceKeyList<string>(
[dbObjectParentKeySymbol],
parentId
);
@injectable()
export class DBObjectResource extends CachedMapResource<string, DBObject> {
constructor(
@@ -24,24 +30,25 @@ export class DBObjectResource extends CachedMapResource<string, DBObject> {
) {
super();
this.addAlias(
isDBObjectParentKey,
param => resourceKeyList(navTreeResource.get(param.mark) || []),
true
);
// this.preloadResource(this.navNodeInfoResource);
this.navNodeInfoResource.outdateResource(this);
this.navNodeInfoResource.deleteInResource(this);
this.navNodeInfoResource.onDataOutdated.addHandler(this.outdateChildren.bind(this));
}
async loadChildren(parentId: string, key: ResourceKey<string>): Promise<Map<string, DBObject>> {
await this.performUpdate(
key,
[],
() => this.loadFromChildren(parentId, 0, this.navTreeResource.childrenLimit + 1),
() => this.isLoaded(key) && !this.isOutdated(key)
);
protected async loader(originalKey: ResourceKey<string>): Promise<Map<string, DBObject>> {
const key = this.transformParam(originalKey);
return this.data;
}
if (isDBObjectParentKey(originalKey)) {
await this.loadFromChildren(originalKey.mark, 0, this.navTreeResource.childrenLimit + 1);
return this.data;
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, DBObject>> {
if (isResourceKeyList(key)) {
const values: DBObject[] = [];
for (const navNodeId of key.list) {
@@ -87,3 +94,9 @@ export class DBObjectResource extends CachedMapResource<string, DBObject> {
// }
}
}
function isDBObjectParentKey(
param: ResourceKey<string>
): param is ResourceKeyList<string> {
return isResourceKeyList(param) && param.list.includes(dbObjectParentKeySymbol);
}
@@ -374,6 +374,10 @@ export abstract class CachedMapResource<
return this.isAliasEqual(param, key);
}
if (ResourceKeyUtils.isEmpty(param) || ResourceKeyUtils.isEmpty(key)) {
return ResourceKeyUtils.isEmpty(param) && ResourceKeyUtils.isEmpty(key);
}
param = ResourceKeyUtils.mapKey(param, this.getKeyRef.bind(this));
key = ResourceKeyUtils.mapKey(key, this.getKeyRef.bind(this));
@@ -41,6 +41,7 @@ interface MapFnc {
}
export interface ResourceKeyUtils {
isEmpty: <TKey>(key: ResourceKey<TKey>) => boolean;
hasMark: <TKey>(key: ResourceKey<TKey>, mark: any) => boolean;
count: <TKey>(key: ResourceKey<TKey>) => number;
first: <TKey>(key: ResourceKey<TKey>) => TKey;
@@ -65,6 +66,16 @@ export interface ResourceKeyUtils {
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ResourceKeyUtils: ResourceKeyUtils = {
isEmpty<TKey>(
key: ResourceKey<TKey>
): boolean {
if (isResourceKeyList(key)) {
return key.list.length === 0;
} else {
return false;
}
},
hasMark<TKey>(
key: ResourceKey<TKey>,
mark: any
@@ -12,7 +12,8 @@
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix",
"test": "core-cli-test"
},
"peerDependencies": {
"@cloudbeaver/core-administration": "~0.1.0",
@@ -0,0 +1,64 @@
/*
* 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 '@testing-library/jest-dom';
import { createApp } from '@cloudbeaver/core-cli/tests/utils/createApp';
import { getService } from '@cloudbeaver/core-cli/tests/utils/getService';
import datasourceContextSwitchPlugin from '@cloudbeaver/plugin-datasource-context-switch';
import navigationTabsPlugin from '@cloudbeaver/plugin-navigation-tabs';
import topAppBarPlugin from '@cloudbeaver/plugin-top-app-bar';
import { navigationTreePlugin } from '../../manifest';
import { NavNodeViewService } from './NavNodeViewService';
const app = createApp(
topAppBarPlugin,
navigationTabsPlugin,
datasourceContextSwitchPlugin,
navigationTreePlugin
);
beforeAll(() => app.init());
describe('filterDuplicates', () => {
test('Filter duplicates', async () => {
const navNodeViewService = getService(app, NavNodeViewService);
const { nodes, duplicates } = navNodeViewService.filterDuplicates(['0', '1', '2', '3', '0', '3']);
expect(nodes).toEqual(['1', '2']);
expect(duplicates).toEqual(['0', '3']);
});
test('No duplicates', async () => {
const navNodeViewService = getService(app, NavNodeViewService);
const { nodes, duplicates } = navNodeViewService.filterDuplicates(['1', '2', '0']);
expect(nodes).toEqual(['1', '2', '0']);
expect(duplicates).toEqual([]);
});
test('Empty list', async () => {
const navNodeViewService = getService(app, NavNodeViewService);
const { nodes, duplicates } = navNodeViewService.filterDuplicates([]);
expect(nodes).toEqual([]);
expect(duplicates).toEqual([]);
});
test('Only duplicates', async () => {
const navNodeViewService = getService(app, NavNodeViewService);
const { nodes, duplicates } = navNodeViewService.filterDuplicates(['0', '1', '0', '1', '2', '3', '2', '3']);
expect(nodes).toEqual([]);
expect(duplicates).toEqual(['0', '1', '2', '3']);
});
});
@@ -113,8 +113,10 @@ export class NavNodeViewService {
const duplicates: string[] = [];
for (const child of nodes) {
if (nextChildren.includes(child)) {
if (!duplicates.includes(child)) {
const isDuplicate = duplicates.includes(child);
if (nextChildren.includes(child) || isDuplicate) {
if (!isDuplicate) {
duplicates.push(child);
nextChildren.splice(nextChildren.indexOf(child), 1);
}
@@ -6,14 +6,15 @@
* you may not use this file except in compliance with the License.
*/
import { untracked } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { NavNodeInfoResource, NavTreeResource, DBObjectResource, type DBObject } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { NavNodeInfoResource, NavTreeResource, DBObjectResource, type DBObject, DBObjectParentKey } from '@cloudbeaver/core-navigation-tree';
import { type NavNodeTransformViewComponent, NavNodeViewService } from '@cloudbeaver/plugin-navigation-tree';
import { TableLoader } from '../../ObjectPropertyTable/Table/TableLoader';
@@ -42,24 +43,26 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi
onLoad: async resource => !(await resource.preloadNodeParents(parents, nodeId)),
});
const { nodes, truncated } = navNodeViewService.limit(tree.data || []);
const key = resourceKeyList(nodes);
const dbObject = useMapResource(VirtualFolderPanel, DBObjectResource, key, {
async onLoad(resource: DBObjectResource) {
const preloaded = await tree.resource.preloadNodeParents(parents, nodeId);
const limited = navNodeViewService.limit(tree.data || []);
if (!preloaded) {
return true;
}
const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes);
await resource.loadChildren(nodeId, key);
return true;
},
const dbObject = useMapResource(VirtualFolderPanel, DBObjectResource, DBObjectParentKey(nodeId), {
preload: [tree],
});
useEffect(() => {
untracked(() => {
navNodeViewService.logDuplicates(nodeId, duplicates);
});
});
const objects = dbObject.data
.filter(object => object && navNodeInfoResource.get(object.id)?.nodeType === nodeType) as DBObject[];
.filter(object => (
object
&& nodes.includes(object.id)
&& navNodeInfoResource.get(object.id)?.nodeType === nodeType
)) as DBObject[];
return styled(style)(
<Loader state={[tree, dbObject]}>{() => styled(style)(
@@ -68,7 +71,7 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<tab-wrapper>
<TableLoader objects={objects} truncated={truncated > 0} />
<TableLoader objects={objects} truncated={limited.truncated > 0} />
</tab-wrapper>
)}
</>
@@ -14,8 +14,7 @@ import styled, { css } from 'reshadow';
import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { NavTreeResource, DBObjectResource, type DBObject } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { NavTreeResource, DBObjectResource, type DBObject, DBObjectParentKey } from '@cloudbeaver/core-navigation-tree';
import { NavNodeViewService } from '@cloudbeaver/plugin-navigation-tree';
import { TableLoader } from './Table/TableLoader';
@@ -54,18 +53,7 @@ export const ObjectPropertyTable = observer<ObjectPropertyTableProps>(function O
const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes);
const key = resourceKeyList(nodes);
const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, key, {
async onLoad(resource: DBObjectResource) {
const preloaded = await tree.resource.preloadNodeParents(parents, objectId);
if (!preloaded) {
return true;
}
await resource.loadChildren(objectId, key);
return true;
},
const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, DBObjectParentKey(objectId), {
preload: [tree],
});
@@ -75,20 +63,21 @@ export const ObjectPropertyTable = observer<ObjectPropertyTableProps>(function O
});
});
const objects = dbObject.data as DBObject[];
const objects = dbObject.data.filter(object => nodes.includes(object?.id ?? '')) as DBObject[];
return styled(styles)(
<Loader state={[tree, dbObject]} style={styles}>{() => styled(styles)(
<>
{nodes.length === 0 ? (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<div>
<TableLoader objects={objects} truncated={limited.truncated > 0} />
</div>
)}
</>
)}
<Loader state={[tree, dbObject]} style={styles}>
{() => styled(styles)(
<>
{nodes.length === 0 ? (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<div>
<TableLoader objects={objects} truncated={limited.truncated > 0} />
</div>
)}
</>
)}
</Loader>
);
});