From 9e657d2280f32cd08112a6bed79f36ae19a05a1e Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Sep 2024 17:18:27 +0300 Subject: [PATCH] CB-5511 fix: pagination (#2906) * CB-5511 fix: pagination * CB-5511 chore: remove unused code --------- Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- .vscode/extensions.json | 1 - .../core-authentication/src/UsersResource.ts | 16 +++- .../src/ResourcesHooks/useOffsetPagination.ts | 44 ++++++---- .../src/NodesManager/DBObjectResource.ts | 27 ++++-- .../src/NodesManager/NavTreeResource.ts | 20 +++-- .../src/Resource/CachedResource.ts | 87 +++++++++++++------ .../Resource/CachedResourceOffsetPageKeys.ts | 86 +++++++++--------- .../OffsetPagination/IResourceOffsetPage.ts | 26 ++++++ .../OffsetPagination/ResourceOffsetPage.ts | 83 ++++++++++++++++++ .../core-resource/src/Resource/Resource.ts | 9 +- .../src/Resource/ResourceAlias.ts | 13 ++- .../src/Resource/ResourceMetadata.ts | 7 +- .../src/Resource/ResourceOffsetPagination.ts | 35 +++++--- webapp/packages/core-resource/src/index.ts | 8 ++ .../Users/Teams/GrantedUsers/GrantedUsers.tsx | 2 +- .../Users/UsersTable/useUsersTable.tsx | 7 +- .../ConnectionAccess/ConnectionAccess.tsx | 2 +- .../elementsTreeLimitFilter.ts | 6 +- .../elementsTreeLimitRenderer.tsx | 10 ++- .../ElementsTree/useElementsTree.ts | 16 +++- .../NavigationTree/NavigationTreeService.ts | 12 ++- .../VirtualFolder/VirtualFolderPanel.tsx | 2 +- .../ObjectPropertyTable.tsx | 2 +- 23 files changed, 377 insertions(+), 144 deletions(-) create mode 100644 webapp/packages/core-resource/src/Resource/OffsetPagination/IResourceOffsetPage.ts create mode 100644 webapp/packages/core-resource/src/Resource/OffsetPagination/ResourceOffsetPage.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1db4798379..b0cfd8d15d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -15,7 +15,6 @@ "streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker-russian", "syler.sass-indented", - "VisualStudioExptTeam.intellicode-api-usage-examples", "VisualStudioExptTeam.vscodeintellicode", "yzhang.markdown-all-in-one", "GraphQL.vscode-graphql-syntax", diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index a28b2571aa..6e790e75a3 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -5,6 +5,8 @@ * 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 { injectable } from '@cloudbeaver/core-di'; import { CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, @@ -240,6 +242,7 @@ export class UsersResource extends CachedMapResource[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { let userId: string | undefined; @@ -290,12 +293,21 @@ export class UsersResource extends CachedMapResource user.userId), + users.length === limit, + ]); } }); const key = resourceKeyList(usersList.map(user => user.userId)); - this.set(key, usersList); + runInAction(() => { + this.set(key, usersList); + for (const pageArgs of pages) { + this.offsetPagination.setPage(...pageArgs); + } + }); return this.data; } diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useOffsetPagination.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useOffsetPagination.ts index 0d60e43648..9d1365325b 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useOffsetPagination.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useOffsetPagination.ts @@ -14,8 +14,10 @@ import { CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, getNextPageOffset, ICachedResourceOffsetPageOptions, + isResourceAlias, ResourceKey, ResourceKeyAlias, ResourceKeyList, @@ -30,7 +32,10 @@ interface IOptions> { } interface IOffsetPagination { - key: TKey extends ResourceKeyListAlias | ResourceKeyList + currentPage: TKey extends ResourceKeyListAlias | ResourceKeyList + ? ResourceKeyListAlias> + : ResourceKeyAlias>; + allPages: TKey extends ResourceKeyListAlias | ResourceKeyList ? ResourceKeyListAlias> : ResourceKeyAlias>; hasNextPage: boolean; @@ -42,6 +47,7 @@ interface IOffsetPaginationPrivate> extends IOffse offset: number; resource: CachedMapResource; _key: ResourceKeyAlias> | ResourceKeyListAlias>; + _target: TKey | undefined; } export function useOffsetPagination, TKey extends ResourceKey>( @@ -61,31 +67,37 @@ export function useOffsetPagination ({ offset, _key: createPageKey(offset, pageSize, targetKey), - get key() { - const pageInfo = resource.offsetPagination.getPageInfo(createPageKey(0, 0, this._key.target)); - - for (const page of pageInfo?.pages || []) { - if (page.outdated && page.from < this._key.options.offset) { - return createPageKey(page.from, this._key.options.limit, this._key.target); + _target: targetKey, + get currentPage() { + for (let i = 0; i < this.offset; i += this._key.options.limit) { + const key = createPageKey(i, this._key.options.limit, this._target); + if (resource.isOutdated(key)) { + return key; } } + return this._key as any; }, + get allPages(): any { + return createPageKey(0, this._key.options.offset + this._key.options.limit, this._target); + }, get hasNextPage(): boolean { return this.resource.offsetPagination.hasNextPage(this._key); }, loadMore() { if (this.hasNextPage) { - this._key = createPageKey(this._key.options.offset + this._key.options.limit, this._key.options.limit, this._key.target); + this._key = createPageKey(this._key.options.offset + this._key.options.limit, this._key.options.limit, this._target); } }, refresh() { - this.resource.markOutdated(this._key.target); + this.resource.markOutdated(this._target); }, }), { _key: observable.ref, - key: computed, + offset: observable.ref, + currentPage: computed, + allPages: computed, hasNextPage: computed, loadMore: action.bound, refresh: action.bound, @@ -93,8 +105,9 @@ export function useOffsetPagination, + next: ResourceKey, ): ResourceKeyAlias> | ResourceKeyListAlias> { - if (target instanceof ResourceKeyList || target instanceof ResourceKeyListAlias) { - return CachedResourceOffsetPageListKey(offset, limit).setTarget(target); + const parent = isResourceAlias(next) ? next : CachedResourceOffsetPageTargetKey(next); + if (next instanceof ResourceKeyList || next instanceof ResourceKeyListAlias) { + return CachedResourceOffsetPageListKey(offset, limit).setParent(parent); } - return CachedResourceOffsetPageKey(offset, limit).setTarget(target); + return CachedResourceOffsetPageKey(offset, limit).setParent(parent); } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index b118e85a26..17ef52750d 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -14,6 +14,7 @@ import { CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, isResourceAlias, type ResourceKey, resourceKeyList, @@ -47,7 +48,8 @@ export class DBObjectResource extends CachedMapResource { const pageAlias = this.aliases.isAlias(nodeId, CachedResourceOffsetPageKey) || this.aliases.isAlias(nodeId, CachedResourceOffsetPageListKey); if (pageAlias) { - this.markOutdated(DBObjectParentKey(pageAlias.target)); + const pageTarget = this.aliases.isAlias(nodeId, CachedResourceOffsetPageTargetKey); + this.markOutdated(DBObjectParentKey(pageTarget?.options.target)); } if (!isResourceAlias(nodeId)) { @@ -70,7 +72,9 @@ export class DBObjectResource extends CachedMapResource { } if (parentKey) { - await this.navTreeResource.load(CachedResourceOffsetPageKey(offset, limit).setTarget(parentKey.options.parentId)); + await this.navTreeResource.load( + CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(parentKey.options.parentId)), + ); return; } @@ -105,14 +109,21 @@ export class DBObjectResource extends CachedMapResource { if (parentKey) { const nodeId = parentKey.options.parentId; - await this.loadFromChildren(nodeId, offset, limit); + const dbObjects = await this.loadFromChildren(nodeId, offset, limit); runInAction(() => { - this.offsetPagination.setPageEnd( - CachedResourceOffsetPageKey(offset, limit).setTarget(originalKey), - this.navTreeResource.offsetPagination.hasNextPage(CachedResourceOffsetPageKey(offset, limit).setTarget(nodeId)), + const keys = dbObjects.map(dbObject => dbObject.id); + this.set(resourceKeyList(keys), dbObjects); + + this.offsetPagination.setPage( + CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(originalKey)), + keys, + this.navTreeResource.offsetPagination.hasNextPage( + CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + ), ); }); + return this.data; } @@ -128,14 +139,14 @@ export class DBObjectResource extends CachedMapResource { return this.data; } - private async loadFromChildren(parentId: string, offset: number, limit: number) { + private async loadFromChildren(parentId: string, offset: number, limit: number): Promise { const { dbObjects } = await this.graphQLService.sdk.getChildrenDBObjectInfo({ navNodeId: parentId, offset, limit, }); - this.set(resourceKeyList(dbObjects.map(dbObject => dbObject.id)), dbObjects); + return dbObjects; } private async loadDBObjectInfo(navNodeId: string): Promise { diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index b32c1283f5..4f52088d9a 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -17,6 +17,7 @@ import { CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, type ICachedResourceMetadata, isResourceAlias, isResourceKeyList, @@ -463,6 +464,7 @@ export class NavTreeResource extends CachedMapResource[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { - const nodeId = pageKey?.target ?? key; + const nodeId = pageTarget?.options?.target ?? key; const navNodeChildren = await this.loadNodeChildren(nodeId, offset, limit); values.push(navNodeChildren); - this.offsetPagination.setPageEnd( - CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setTarget(nodeId), + pages.push([ + CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + navNodeChildren.navNodeChildren.map(node => node.id), navNodeChildren.navNodeChildren.length === limit, - ); + ]); }); - this.setNavObject(values, offset, limit); + runInAction(() => { + this.setNavObject(values, offset, limit); + + for (const pageArgs of pages) { + this.offsetPagination.setPage(...pageArgs); + } + }); return this.data; } diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 69070eb405..50f8a914c9 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -23,7 +23,7 @@ import { getFirstException, isContainsException } from '@cloudbeaver/core-utils' import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, - expandOffsetPageRange, + CachedResourceOffsetPageTargetKey, isOffsetPageInRange, isOffsetPageOutdated, } from './CachedResourceOffsetPageKeys'; @@ -75,7 +75,7 @@ export abstract class CachedResource< constructor(defaultKey: ResourceKey, defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { super(defaultValue, defaultIncludes); - this.offsetPagination = new ResourceOffsetPagination(this.metadata); + this.offsetPagination = new ResourceOffsetPagination(this.metadata, this.getKeyRef.bind(this)); this.loadingTask = this.loadingTask.bind(this); @@ -90,8 +90,42 @@ export abstract class CachedResource< this.aliases.add(CachedResourceParamKey, () => defaultKey); this.aliases.add(CachedResourceListEmptyKey, () => resourceKeyList([])); - this.aliases.add(CachedResourceOffsetPageKey, key => key.target); - this.aliases.add(CachedResourceOffsetPageListKey, key => key.target ?? CachedResourceListEmptyKey); + this.aliases.add(CachedResourceOffsetPageTargetKey, key => key.options.target); + this.aliases.add(CachedResourceOffsetPageKey, key => { + const keys = []; + const pageInfo = this.offsetPagination.getPageInfo(key); + + if (pageInfo) { + const from = key.options.offset; + const to = key.options.offset + key.options.limit; + + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + keys.push(...page.get(from, to)); + } + } + } + + // todo: return single element? + return resourceKeyList([...new Set(keys)]); + }); + this.aliases.add(CachedResourceOffsetPageListKey, key => { + const keys = []; + const pageInfo = this.offsetPagination.getPageInfo(key); + + if (pageInfo) { + const from = key.options.offset; + const to = key.options.offset + key.options.limit; + + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + keys.push(...page.get(from, to)); + } + } + } + + return resourceKeyList([...new Set(keys)]); + }); // this.logger.spy(this.beforeLoad, 'beforeLoad'); // this.logger.spy(this.onDataOutdated, 'onDataOutdated'); @@ -316,18 +350,9 @@ export abstract class CachedResource< } markLoaded(param: ResourceKey, includes?: TInclude): void { - const pageKey = this.aliases.isAlias(param, CachedResourceOffsetPageKey) || this.aliases.isAlias(param, CachedResourceOffsetPageListKey); - this.metadata.update(param, metadata => { metadata.loaded = true; - if (pageKey) { - metadata.offsetPage = observable({ - ...metadata.offsetPage, - pages: expandOffsetPageRange(metadata.offsetPage?.pages || [], pageKey.options, false), - }); - } - if (includes) { this.commitIncludes(metadata, includes); } @@ -353,13 +378,17 @@ export abstract class CachedResource< metadata.outdated = false; if (pageKey) { - metadata.offsetPage = observable({ - ...metadata.offsetPage, - pages: expandOffsetPageRange(metadata.offsetPage?.pages || [], pageKey.options, false), + const from = pageKey.options.offset; + const to = from + pageKey.options.limit; + + metadata.offsetPage?.pages.forEach(page => { + if (page.isInRange(from, to)) { + page.setOutdated(false); + } }); } else { metadata.offsetPage?.pages.forEach(page => { - page.outdated = false; + page.setOutdated(false); }); } }); @@ -405,9 +434,13 @@ export abstract class CachedResource< metadata.outdated = false; if (pageKey) { - metadata.offsetPage = observable({ - ...metadata.offsetPage, - pages: expandOffsetPageRange(metadata.offsetPage?.pages || [], pageKey.options, false), + const from = pageKey.options.offset; + const to = from + pageKey.options.limit; + + metadata.offsetPage?.pages.forEach(page => { + if (page.isInRange(from, to)) { + page.setOutdated(false); + } }); } }); @@ -541,13 +574,17 @@ export abstract class CachedResource< metadata.outdatedIncludes = observable([...metadata.includes]); if (pageKey) { - metadata.offsetPage = observable({ - ...metadata.offsetPage, - pages: expandOffsetPageRange(metadata.offsetPage?.pages || [], pageKey.options, true), + const from = pageKey.options.offset; + const to = from + pageKey.options.limit; + + metadata.offsetPage?.pages.forEach(page => { + if (page.isHasCommonSegment(from, to)) { + page.setOutdated(true); + } }); } else { metadata.offsetPage?.pages.forEach(page => { - page.outdated = true; + page.setOutdated(true); }); } }); @@ -559,7 +596,7 @@ export abstract class CachedResource< metadata.outdated = true; metadata.outdatedIncludes = observable([...metadata.includes]); metadata.offsetPage?.pages.forEach(page => { - page.outdated = true; + page.setOutdated(true); }); }); } diff --git a/webapp/packages/core-resource/src/Resource/CachedResourceOffsetPageKeys.ts b/webapp/packages/core-resource/src/Resource/CachedResourceOffsetPageKeys.ts index 3fbc498942..28846e9e3d 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResourceOffsetPageKeys.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResourceOffsetPageKeys.ts @@ -5,6 +5,9 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { IResourceOffsetPage } from './OffsetPagination/IResourceOffsetPage'; +import { ResourceOffsetPage } from './OffsetPagination/ResourceOffsetPage'; +import { ResourceKey } from './ResourceKey'; import { resourceKeyAliasFactory } from './ResourceKeyAlias'; import { resourceKeyListAliasFactory } from './ResourceKeyListAlias'; @@ -13,12 +16,6 @@ interface IOffsetPageInfo { limit: number; } -interface IResourceOffsetPage { - from: number; - to: number; - outdated: boolean; -} - export interface ICachedResourceOffsetPage { totalCount?: number; end?: number; @@ -31,6 +28,7 @@ export interface ICachedResourceOffsetPageOptions extends IOffsetPageInfo {} export const CACHED_RESOURCE_DEFAULT_PAGE_OFFSET = 0; export const CACHED_RESOURCE_DEFAULT_PAGE_LIMIT = 100; +export const CachedResourceOffsetPageTargetKey = resourceKeyAliasFactory('@cached-resource/param-chain', (target: ResourceKey) => ({ target })); export const CachedResourceOffsetPageListKey = resourceKeyListAliasFactory< any, [offset: number, limit: number], @@ -66,8 +64,10 @@ export function getNextPageOffset(info: ICachedResourceOffsetPage): number { } export function isOffsetPageOutdated(pages: IResourceOffsetPage[], info: IOffsetPageInfo): boolean { - for (const { from, to, outdated } of pages) { - if (outdated && info.offset >= from && info.offset + info.limit <= to) { + const from = info.offset; + const to = info.offset + info.limit; + for (const page of pages) { + if (page.isHasCommonSegment(from, to) && page.isOutdated()) { return true; } } @@ -101,52 +101,48 @@ export function isOffsetPageInRange({ pages, end }: ICachedResourceOffsetPage, i return false; } -export function limitOffsetPages(pages: IResourceOffsetPage[], limit: number): IResourceOffsetPage[] { - const result: IResourceOffsetPage[] = []; - +export function expandOffsetPageRange( + pages: IResourceOffsetPage[], + info: IOffsetPageInfo, + items: any[], + outdated: boolean, + hasNextPage: boolean, +): void { + const from = info.offset; + const to = info.offset + info.limit; + + let pageInserted = false; for (const page of pages) { - if (page.from >= limit) { - break; + if (page.to <= from) { + continue; } - result.push({ ...page, to: Math.min(limit, page.to) }); - } - - return result; -} - -export function expandOffsetPageRange(pages: IResourceOffsetPage[], info: IOffsetPageInfo, outdated: boolean): IResourceOffsetPage[] { - pages = [...pages, { from: info.offset, to: info.offset + info.limit, outdated, end: false }].sort((a, b) => a.from - b.from); - const result: IResourceOffsetPage[] = []; - let previous: IResourceOffsetPage | undefined; - for (const { from, to, outdated } of pages) { - if (!previous) { - previous = { from, to, outdated }; - continue; + if (!hasNextPage) { + if (page.from >= to) { + pages.splice(pages.indexOf(page)); + break; + } } - if (from <= previous.from + previous.to) { - if (previous.outdated === outdated) { - previous.to = Math.max(previous.to, to); + if (page.from <= from && !pageInserted) { + if (page.from < from) { + page.setSize(page.from, from); + pages.splice(pages.indexOf(page) + 1, 0, new ResourceOffsetPage().setSize(from, to).update(from, items).setOutdated(outdated)); } else { - if (previous.from < from) { - result.push({ ...previous, to: from }); - } - if (previous.to > to) { - result.push({ from, to, outdated }); - previous = { ...previous, from: to }; - } else { - previous = { from, to, outdated }; - } + page.setSize(from, to).update(from, items).setOutdated(outdated); } - } else { - result.push(previous); - previous = { from, to, outdated }; + pageInserted = true; + continue; + } + + if (page.isInRange(from, to)) { + pages.splice(pages.indexOf(page), 1); } } - if (previous) { - result.push(previous); + const lastPage = pages[pages.length - 1]; + + if (!lastPage || lastPage.to <= from) { + pages.push(new ResourceOffsetPage().setSize(from, to).update(from, items).setOutdated(outdated)); } - return result; } diff --git a/webapp/packages/core-resource/src/Resource/OffsetPagination/IResourceOffsetPage.ts b/webapp/packages/core-resource/src/Resource/OffsetPagination/IResourceOffsetPage.ts new file mode 100644 index 0000000000..a9da410ddf --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/OffsetPagination/IResourceOffsetPage.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 interface IResourceOffsetPage { + from: number; + to: number; + items: any[]; + outdated: boolean; + + get(from: number, to: number): any[]; + + isOutdated(): boolean; + isHasCommonSegment(from: number, to: number): boolean; + isInRange(from: number, to: number): boolean; + + setSize(from: number, to: number): this; + + setOutdated(outdated: boolean): this; + + update(from: number, items: any[]): this; +} diff --git a/webapp/packages/core-resource/src/Resource/OffsetPagination/ResourceOffsetPage.ts b/webapp/packages/core-resource/src/Resource/OffsetPagination/ResourceOffsetPage.ts new file mode 100644 index 0000000000..90d1433e5a --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/OffsetPagination/ResourceOffsetPage.ts @@ -0,0 +1,83 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { IResourceOffsetPage } from './IResourceOffsetPage'; + +export class ResourceOffsetPage implements IResourceOffsetPage { + from: number; + to: number; + items: any[]; + outdated: boolean; + + constructor() { + this.from = 0; + this.to = 0; + this.items = []; + this.outdated = false; + + makeObservable(this, { + from: true, + to: true, + items: observable.shallow, + outdated: observable, + }); + } + + get(from: number, to: number): any[] { + return this.items.slice(from - this.from, to - this.from); + } + + isOutdated(): boolean { + return this.outdated; + } + + isHasCommonSegment(from: number, to: number): boolean { + return !(to < this.from || this.to <= from); + } + + isInRange(from: number, to: number): boolean { + return this.from >= from && this.to <= to; + } + + setSize(from: number, to: number): this { + const prevForm = this.from; + const prevTo = this.to; + + this.from = from; + this.to = to; + + if (from >= prevForm) { + this.items.splice(0, from - prevForm); + } else { + this.items.unshift(...new Array(prevForm - from)); + this.setOutdated(true); + } + + if (to - from <= prevTo - prevForm) { + this.items.splice(to - from); + } else { + this.items.push(...new Array(to - from - (prevTo - prevForm))); + this.setOutdated(true); + } + + return this; + } + + update(from: number, items: any[]): this { + this.items.splice(from - this.from, items.length, ...items); + + return this; + } + + setOutdated(outdated: boolean): this { + this.outdated = outdated; + + return this; + } +} diff --git a/webapp/packages/core-resource/src/Resource/Resource.ts b/webapp/packages/core-resource/src/Resource/Resource.ts index 0e451dac95..3804a0bf4a 100644 --- a/webapp/packages/core-resource/src/Resource/Resource.ts +++ b/webapp/packages/core-resource/src/Resource/Resource.ts @@ -40,7 +40,10 @@ export abstract class Resource< protected readonly logger: ResourceLogger; protected readonly metadata: ResourceMetadata; - constructor(protected readonly defaultValue: () => TData, protected defaultIncludes: TInclude = [] as any) { + constructor( + protected readonly defaultValue: () => TData, + protected defaultIncludes: TInclude = [] as any, + ) { super(); this.isKeyEqual = this.isKeyEqual.bind(this); this.isIntersect = this.isIntersect.bind(this); @@ -96,7 +99,7 @@ export abstract class Resource< key = this.aliases.transformToAlias(key); nextKey = this.aliases.transformToAlias(nextKey); - return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target); + return key.isEqual(nextKey); } else if (isResourceAlias(key) || isResourceAlias(nextKey)) { return true; } @@ -123,7 +126,7 @@ export abstract class Resource< param = this.aliases.transformToAlias(param); second = this.aliases.transformToAlias(second); - return param.isEqual(second) && this.isEqual(param.target, second.target); + return param.isEqual(second); } if (isResourceAlias(param) || isResourceAlias(second)) { diff --git a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts index 46358f1d00..ed23a86b95 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts @@ -7,15 +7,16 @@ */ import { isObjectsEqual } from '@cloudbeaver/core-utils'; +import { ResourceKey } from './ResourceKey'; + export type ResourceAliasOptionsKey = string | number; -export type ResourceAliasOptionsValueTypes = string | number | boolean | null | undefined; +export type ResourceAliasOptionsValueTypes = string | number | boolean | ResourceKey | null | undefined; export type ResourceAliasOptionsValue = ResourceAliasOptionsValueTypes | Array; export type ResourceAliasOptions = Readonly> | undefined; export abstract class ResourceAlias { readonly id: string; readonly options: TOptions; - target: any; parent?: ResourceAlias; private readonly typescriptHack: TKey; abstract readonly name: string; @@ -41,14 +42,10 @@ export abstract class ResourceAlias return undefined; } - setTarget(target: any): this { - this.target = target; - return this; - } - setParent(parent: ResourceAlias): this { + parent = this.parent ? this.parent.setParent(parent) : parent; const copy = new (this.constructor as any)(this.id, this.options, parent) as this; - return copy.setTarget(this.target); + return copy; } isEqual(key: ResourceAlias): boolean { diff --git a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts index 55510057ca..b5c9c67039 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts @@ -7,8 +7,9 @@ */ import { observable } from 'mobx'; -import { DefaultValueGetter, isNotNullDefined, isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; +import { DefaultValueGetter, isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; +import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; import { isResourceAlias } from './ResourceAlias'; import type { ResourceAliases } from './ResourceAliases'; @@ -193,8 +194,8 @@ export class ResourceMetadata { if (isResourceAlias(key)) { key = this.aliases.transformToAlias(key); - if (isNotNullDefined(key.target)) { - return this.getMetadataKeyRef(key.target); + if (this.aliases.isAlias(key, CachedResourceOffsetPageKey) || this.aliases.isAlias(key, CachedResourceOffsetPageListKey)) { + return this.getMetadataKeyRef(key.parent as any); } return key.toString() as TKey; diff --git a/webapp/packages/core-resource/src/Resource/ResourceOffsetPagination.ts b/webapp/packages/core-resource/src/Resource/ResourceOffsetPagination.ts index 7744a1ac5a..d07cdad696 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceOffsetPagination.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceOffsetPagination.ts @@ -8,17 +8,20 @@ import { observable } from 'mobx'; import { + expandOffsetPageRange, ICachedResourceOffsetPage, type ICachedResourceOffsetPageOptions, isOffsetPageInRange, - limitOffsetPages, } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; import type { ResourceAlias } from './ResourceAlias'; import type { ResourceMetadata } from './ResourceMetadata'; export class ResourceOffsetPagination { - constructor(protected metadata: ResourceMetadata) { + constructor( + protected metadata: ResourceMetadata, + private readonly getStableKey: (key: TKey) => TKey, + ) { this.metadata = metadata; } @@ -47,29 +50,35 @@ export class ResourceOffsetPagination>, hasNextPage: boolean): void { - const count = key.options.offset + key.options.limit; + setPage(key: ResourceAlias>, items: any[], hasNextPage: boolean) { + const offset = key.options.offset; + const limit = offset + key.options.limit; this.metadata.update(key as TKey, metadata => { let end = metadata.offsetPage?.end; if (hasNextPage) { - if (end !== undefined && end <= count) { + if (end !== undefined && end <= limit) { end = undefined; } } else { - end = count; + end = limit; } - metadata.offsetPage = observable({ - pages: [], - ...metadata.offsetPage, - end, - }); + if (!metadata.offsetPage) { + metadata.offsetPage = observable({ + pages: [], + end, + }); + } + + metadata.offsetPage.end = end; - if (!hasNextPage) { - metadata.offsetPage.pages = limitOffsetPages(metadata.offsetPage?.pages || [], count); + if (!metadata.offsetPage.pages) { + metadata.offsetPage.pages = []; } + + expandOffsetPageRange(metadata.offsetPage.pages, key.options, items.map(this.getStableKey), false, hasNextPage); }); } } diff --git a/webapp/packages/core-resource/src/index.ts b/webapp/packages/core-resource/src/index.ts index de996d0233..6ff9ce3edd 100644 --- a/webapp/packages/core-resource/src/index.ts +++ b/webapp/packages/core-resource/src/index.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 * from './Resource/CachedDataResource'; export * from './Resource/CachedMapResource'; export * from './Resource/CachedResource'; @@ -7,6 +14,7 @@ export { CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, getNextPageOffset, type ICachedResourceOffsetPageOptions, } from './Resource/CachedResourceOffsetPageKeys'; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx index bc75486716..a6769d3b2a 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx @@ -30,7 +30,7 @@ export const GrantedUsers: TabContainerPanelComponent = observer const serverConfigResource = useResource(UserList, ServerConfigResource, undefined, { active: selected }); const isDefaultTeam = formState.config.teamId === serverConfigResource.data?.defaultUserTeam; - const users = useResource(GrantedUsers, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setTarget(UsersResourceFilterKey()), { + const users = useResource(GrantedUsers, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), { active: selected && !isDefaultTeam, }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx index 72af2f2b57..19cf506a0d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx @@ -34,7 +34,7 @@ export function useUsersTable(filters: IUserFilters) { const pagination = useOffsetPagination(UsersResource, { key: UsersResourceFilterKey(filters.search.toLowerCase(), filters.status === 'true' ? true : filters.status === 'false' ? false : undefined), }); - const usersLoader = useResource(useUsersTable, usersResource, pagination.key); + const usersLoader = useResource(useUsersTable, usersResource, pagination.currentPage); const notificationService = useService(NotificationService); const commonDialogService = useService(CommonDialogService); @@ -47,7 +47,10 @@ export function useUsersTable(filters: IUserFilters) { }, get users() { const users = Array.from( - new Set([...this.usersLoader.resource.get(UsersResourceNewUsers), ...usersLoader.tryGetData.filter(isDefined).sort(compareUsers)]), + new Set([ + ...this.usersLoader.resource.get(UsersResourceNewUsers), + ...usersResource.get(pagination.allPages).filter(isDefined).sort(compareUsers), + ]), ); return filters.filterUsers(users.filter(isDefined)); }, diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index cafa9f0eb0..1e6d294002 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -43,7 +43,7 @@ export const ConnectionAccess: TabContainerPanelComponent useAutoLoad(ConnectionAccess, state, selected); - const users = useResource(ConnectionAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setTarget(UsersResourceFilterKey()), { + const users = useResource(ConnectionAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), { active: selected, }); const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts index 824c929957..9c8f11ccc5 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import type { NavTreeResource } from '@cloudbeaver/core-navigation-tree'; -import { CachedResourceOffsetPageKey } from '@cloudbeaver/core-resource'; +import { CachedResourceOffsetPageKey, CachedResourceOffsetPageTargetKey } from '@cloudbeaver/core-resource'; import type { IElementsTreeFilter } from '../useElementsTree'; @@ -16,7 +16,9 @@ export const NAVIGATION_TREE_LIMIT = { export function elementsTreeLimitFilter(navTreeResource: NavTreeResource): IElementsTreeFilter { return (tree, filter, node, children) => { - const pageInfo = navTreeResource.offsetPagination.getPageInfo(CachedResourceOffsetPageKey(0, 0).setTarget(node.id)); + const pageInfo = navTreeResource.offsetPagination.getPageInfo( + CachedResourceOffsetPageKey(0, 0).setParent(CachedResourceOffsetPageTargetKey(node.id)), + ); if (pageInfo && pageInfo.end === undefined) { return [...children, NAVIGATION_TREE_LIMIT.limit]; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx index fa19d63236..3a4b6c0877 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { Link, s, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavTreeResource } from '@cloudbeaver/core-navigation-tree'; -import { CachedResourceOffsetPageKey, getNextPageOffset } from '@cloudbeaver/core-resource'; +import { CachedResourceOffsetPageKey, CachedResourceOffsetPageTargetKey, getNextPageOffset } from '@cloudbeaver/core-resource'; import type { NavigationNodeRendererComponent } from '../NavigationNodeComponent'; import { NAVIGATION_TREE_LIMIT } from './elementsTreeLimitFilter'; @@ -31,9 +31,13 @@ const NavTreeLimitMessage: NavigationNodeRendererComponent = observer(function N function loadMore() { const parentNodeId = path[path.length - 1]; - const pageInfo = navTreeResource.offsetPagination.getPageInfo(CachedResourceOffsetPageKey(0, 0).setTarget(parentNodeId)); + const pageInfo = navTreeResource.offsetPagination.getPageInfo( + CachedResourceOffsetPageKey(0, 0).setParent(CachedResourceOffsetPageTargetKey(parentNodeId)), + ); if (pageInfo) { - navTreeResource.load(CachedResourceOffsetPageKey(getNextPageOffset(pageInfo), limit).setTarget(parentNodeId)); + navTreeResource.load( + CachedResourceOffsetPageKey(getNextPageOffset(pageInfo), limit).setParent(CachedResourceOffsetPageTargetKey(parentNodeId)), + ); } } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index 3abc6e4f57..3e64eaf56c 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -15,7 +15,13 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, CachedResourceOffsetPageKey, getNextPageOffset, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { + CachedMapAllKey, + CachedResourceOffsetPageKey, + CachedResourceOffsetPageTargetKey, + getNextPageOffset, + ResourceKeyUtils, +} from '@cloudbeaver/core-resource'; import type { IDNDData } from '@cloudbeaver/core-ui'; import { ILoadableState, MetadataMap, throttle } from '@cloudbeaver/core-utils'; @@ -231,12 +237,16 @@ export function useElementsTree(options: IOptions): IElementsTree { await navNodeInfoResource.load(nodeId); - const pageInfo = navTreeResource.offsetPagination.getPageInfo(CachedResourceOffsetPageKey(0, 0).setTarget(nodeId)); + const pageInfo = navTreeResource.offsetPagination.getPageInfo( + CachedResourceOffsetPageKey(0, 0).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + ); if (pageInfo) { const lastOffset = getNextPageOffset(pageInfo); for (let offset = 0; offset < lastOffset; offset += navTreeResource.childrenLimit) { - await navTreeResource.load(CachedResourceOffsetPageKey(offset, navTreeResource.childrenLimit).setTarget(nodeId)); + await navTreeResource.load( + CachedResourceOffsetPageKey(offset, navTreeResource.childrenLimit).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + ); } } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts index 3d6ebdd34a..ad867e50f2 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreeService.ts @@ -17,7 +17,13 @@ import { import { injectable } from '@cloudbeaver/core-di'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { EObjectFeature, NavNodeInfoResource, NavNodeManagerService, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; -import { CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedResourceOffsetPageKey, ResourceKey, resourceKeyList } from '@cloudbeaver/core-resource'; +import { + CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, + CachedResourceOffsetPageKey, + CachedResourceOffsetPageTargetKey, + ResourceKey, + resourceKeyList, +} from '@cloudbeaver/core-resource'; import { MetadataMap } from '@cloudbeaver/core-utils'; import { ACTION_COLLAPSE_ALL, ACTION_FILTER, IActiveView, View } from '@cloudbeaver/core-view'; @@ -105,7 +111,9 @@ export class NavigationTreeService extends View { } await this.navTreeResource.load( - CachedResourceOffsetPageKey(CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, this.navTreeResource.childrenLimit).setTarget(id), + CachedResourceOffsetPageKey(CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, this.navTreeResource.childrenLimit).setParent( + CachedResourceOffsetPageTargetKey(id), + ), ); return true; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx index 2ab08908e1..f868ae582e 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx @@ -30,7 +30,7 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi pageSize: tree.resource.childrenLimit, }); - const dbObjectLoader = useResource(VirtualFolderPanel, DBObjectResource, pagination.key); + const dbObjectLoader = useResource(VirtualFolderPanel, DBObjectResource, pagination.currentPage); const { nodes, duplicates } = navNodeViewService.filterDuplicates(dbObjectLoader.data.filter(isDefined).map(node => node?.id) || []); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index 5a5fa9e73d..aa75b83337 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -33,7 +33,7 @@ export const ObjectPropertyTable = observer(function O pageSize: navTreeResource.resource.childrenLimit, }); - const dbObjectLoader = useResource(ObjectPropertyTable, DBObjectResource, pagination.key); + const dbObjectLoader = useResource(ObjectPropertyTable, DBObjectResource, pagination.currentPage); const { nodes, duplicates } = navNodeViewService.filterDuplicates(dbObjectLoader.data.filter(isDefined).map(node => node?.id) || []);