mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #1067 from dbeaver/fix/CB-2420/cached-resources
CB-2420 CB-2431 cached resources
This commit is contained in:
@@ -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",
|
||||
|
||||
+64
@@ -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']);
|
||||
});
|
||||
});
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
+18
-15
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
+15
-26
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user