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-blocks/src/Table/TableState.ts b/webapp/packages/core-blocks/src/Table/TableState.ts index e8ba799d96..2489d32f3e 100644 --- a/webapp/packages/core-blocks/src/Table/TableState.ts +++ b/webapp/packages/core-blocks/src/Table/TableState.ts @@ -9,30 +9,28 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -type Key = string | string[]; - -interface IData { - key: string; +interface IData { + key: Key; value: boolean; } -export class TableState { - readonly onExpand: IExecutor; +export class TableState { + readonly onExpand: IExecutor>; - selected: Map; - expanded: Map; + selected: Map; + expanded: Map; get itemsSelected(): boolean { return Array.from(this.selected.values()).some(v => v); } - get selectedList(): string[] { + get selectedList(): K[] { return Array.from(this.selected) .filter(([_, value]) => value) .map(([key]) => key); } - get expandedList(): string[] { + get expandedList(): K[] { return Array.from(this.expanded) .filter(([_, value]) => value) .map(([key]) => key); @@ -41,8 +39,8 @@ export class TableState { constructor() { this.onExpand = new Executor(); - this.selected = new Map(); - this.expanded = new Map(); + this.selected = new Map(); + this.expanded = new Map(); makeObservable(this, { selected: observable, @@ -55,37 +53,33 @@ export class TableState { }); } - unselect(key?: Key): Map { + unselect(key?: K | K[]): Map { if (key === undefined) { this.selected.clear(); } else { - if (typeof key === 'string') { - this.selected.delete(key); - } else { - for (const id of key) { - this.selected.delete(id); - } + const keys = Array.isArray(key) ? key : [key]; + + for (const id of keys) { + this.selected.delete(id); } } return this.selected; } - expand(key: string, value: boolean) { + expand(key: K, value: boolean) { this.expanded.set(key, value); this.onExpand.execute({ key, value }); } - collapse(key?: Key): Map { + collapse(key?: K | K[]): Map { if (key === undefined) { this.expanded.clear(); } else { - if (typeof key === 'string') { - this.expanded.delete(key); - } else { - for (const id of key) { - this.expanded.delete(id); - } + const keys = Array.isArray(key) ? key : [key]; + + for (const id of keys) { + this.expanded.delete(id); } } diff --git a/webapp/packages/core-blocks/src/Table/useTable.ts b/webapp/packages/core-blocks/src/Table/useTable.ts index 987a7fd2f9..141570b584 100644 --- a/webapp/packages/core-blocks/src/Table/useTable.ts +++ b/webapp/packages/core-blocks/src/Table/useTable.ts @@ -9,7 +9,7 @@ import { useState } from 'react'; import { TableState } from './TableState'; -export function useTable(): TableState { - const [table] = useState(() => new TableState()); +export function useTable(): TableState { + const [table] = useState(() => new TableState()); return table; } diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 4d3976c026..7d8bee27a5 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -12,7 +12,7 @@ import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { Bootstrap } from './Bootstrap'; import { Dependency } from './Dependency'; import type { DIContainer } from './DIContainer'; -import type { IServiceCollection, IServiceConstructor, IServiceInjector } from './IApp'; +import type { IServiceCollection, IServiceConstructor } from './IApp'; import { IDiWrapper, inversifyWrapper } from './inversifyWrapper'; import { IServiceProvider } from './IServiceProvider'; import type { PluginManifest } from './PluginManifest'; @@ -92,10 +92,6 @@ export class App { return this.diWrapper.collection; } - getServiceInjector(): IServiceInjector { - return this.diWrapper.injector; - } - // first phase register all dependencies private async registerServices(preload?: boolean): Promise { if (!this.isAppServiceBound) { diff --git a/webapp/packages/core-di/src/index.ts b/webapp/packages/core-di/src/index.ts index ecd20ca38e..1ae1761af5 100644 --- a/webapp/packages/core-di/src/index.ts +++ b/webapp/packages/core-di/src/index.ts @@ -16,7 +16,6 @@ export * from './DIService'; export * from './injectable'; export * from './PluginManifest'; export * from './useService'; -export * from './useController'; export * from './ITypedConstructor'; export * from './isConstructor'; export * from './IServiceProvider'; diff --git a/webapp/packages/core-di/src/useController.ts b/webapp/packages/core-di/src/useController.ts deleted file mode 100644 index 76be113d79..0000000000 --- a/webapp/packages/core-di/src/useController.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { useEffect, useMemo, useRef } from 'react'; - -import { App } from './App'; -import type { ExtractInitArgs, IDestructibleController, IInitializableController, IServiceConstructor } from './IApp'; -import { useService } from './useService'; - -/** - * @deprecated use hooks instead - */ -export function useController(ctor: IServiceConstructor, ...args: ExtractInitArgs): T; -/** - * @deprecated use hooks instead - */ -export function useController(ctor: IServiceConstructor): T; -/** - * @deprecated use hooks instead - */ -export function useController(ctor: IServiceConstructor, ...args: any[]): T { - const appService = useService(App); - const controllerRef = useRef(); - - useMemo(() => { - if (controllerRef.current && isDestructibleController(controllerRef.current)) { - controllerRef.current.destruct(); - } - - const controller = appService.getServiceInjector().resolveServiceByClass(ctor); - - if (isInitializableController(controller)) { - controller.init(...args); - } - controllerRef.current = controller; - }, [...args, args.length]); - /* we put dynamic array length as the dependency because of preact bug, - otherwise useMemo will not be triggered on array change */ - - useEffect( - () => () => { - if (isDestructibleController(controllerRef.current)) { - controllerRef.current.destruct(); - } - }, - [], - ); - - return controllerRef.current!; -} - -function isDestructibleController(obj: any): obj is IDestructibleController { - return obj && typeof obj.destruct === 'function'; -} - -function isInitializableController(obj: any): obj is IInitializableController { - return obj && typeof obj.init === 'function'; -} 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/Administration/Connections/ConnectionsAdministration.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx index 2abc992790..64816b12d7 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx @@ -22,17 +22,14 @@ import { StyleRegistry, ToolsAction, ToolsPanel, - useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useController, useService } from '@cloudbeaver/core-di'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { useService } from '@cloudbeaver/core-di'; import ConnectionsAdministrationStyle from './ConnectionsAdministration.module.css'; -import { ConnectionsAdministrationController } from './ConnectionsAdministrationController'; import { ConnectionsTable } from './ConnectionsTable/ConnectionsTable'; +import { useConnectionsTable } from './ConnectionsTable/useConnectionsTable'; import { CreateConnection } from './CreateConnection/CreateConnection'; import { CreateConnectionService } from './CreateConnectionService'; @@ -51,16 +48,11 @@ export const ConnectionsAdministration = observer @@ -73,7 +65,7 @@ export const ConnectionsAdministration = observer {translate('ui_add')} @@ -82,8 +74,8 @@ export const ConnectionsAdministration = observer {translate('ui_refresh')} @@ -91,8 +83,8 @@ export const ConnectionsAdministration = observer {translate('ui_delete')} @@ -108,13 +100,8 @@ export const ConnectionsAdministration = observer} - - + + diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts deleted file mode 100644 index dfc032294d..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 { computed, makeObservable, observable } from 'mobx'; - -import { ConfirmationDialogDelete } from '@cloudbeaver/core-blocks'; -import { - compareConnectionsInfo, - compareNewConnectionsInfo, - Connection, - ConnectionInfoActiveProjectKey, - ConnectionInfoResource, - createConnectionParam, - DatabaseConnection, - IConnectionInfoParams, -} from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { isGlobalProject, isSharedProject, ProjectInfoResource, projectInfoSortByName } from '@cloudbeaver/core-projects'; -import { resourceKeyList } from '@cloudbeaver/core-resource'; -import { isArraysEqual, isDefined, isObjectsEqual } from '@cloudbeaver/core-utils'; - -@injectable() -export class ConnectionsAdministrationController { - isProcessing = false; - readonly selectedItems = observable(new Map()); - readonly expandedItems = observable(new Map()); - - get keys(): IConnectionInfoParams[] { - return this.connections.map(createConnectionParam); - } - - get connections(): DatabaseConnection[] { - return this.connectionInfoResource - .get(ConnectionInfoActiveProjectKey) - .filter(isDefined) - .filter(connection => { - const project = this.projectInfoResource.get(connection.projectId); - - return connection.template && project && (isSharedProject(project) || isGlobalProject(project)); - }) - .sort((connectionA, connectionB) => { - const compareNew = compareNewConnectionsInfo(connectionA, connectionB); - const projectA = this.projectInfoResource.get(connectionA.projectId); - const projectB = this.projectInfoResource.get(connectionB.projectId); - - if (compareNew !== 0) { - return compareNew; - } - - if (projectA && projectB) { - const projectSort = projectInfoSortByName(projectA, projectB); - - if (projectSort !== 0) { - return projectSort; - } - } - - return compareConnectionsInfo(connectionA, connectionB); - }); - } - - get itemsSelected(): boolean { - return Array.from(this.selectedItems.values()).some(v => v); - } - - constructor( - private readonly notificationService: NotificationService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly commonDialogService: CommonDialogService, - private readonly localizationService: LocalizationService, - private readonly projectInfoResource: ProjectInfoResource, - ) { - makeObservable(this, { - isProcessing: observable, - connections: computed({ equals: (a, b) => isArraysEqual(a, b) }), - keys: computed({ equals: (a, b) => isArraysEqual(a, b, isObjectsEqual) }), - itemsSelected: computed, - }); - } - - update = async (): Promise => { - if (this.isProcessing) { - return; - } - this.isProcessing = true; - try { - await this.connectionInfoResource.refresh(ConnectionInfoActiveProjectKey); - this.connectionInfoResource.cleanNewFlags(); - this.notificationService.logSuccess({ title: 'connections_administration_tools_refresh_success' }); - } catch (exception: any) { - this.notificationService.logException(exception, 'connections_administration_tools_refresh_fail'); - } finally { - this.isProcessing = false; - } - }; - - delete = async (): Promise => { - if (this.isProcessing) { - return; - } - - const deletionList = Array.from(this.selectedItems) - .filter(([_, value]) => value) - .map(([connectionId]) => connectionId); - - if (deletionList.length === 0) { - return; - } - - const connectionNames = deletionList.map(id => this.connectionInfoResource.get(id)?.name).filter(Boolean); - const nameList = connectionNames.map(name => `"${name}"`).join(', '); - const message = `${this.localizationService.translate( - 'connections_administration_delete_confirmation', - )}${nameList}. ${this.localizationService.translate('ui_are_you_sure')}`; - - const result = await this.commonDialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message, - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - - this.isProcessing = true; - - try { - await this.connectionInfoResource.deleteConnection(resourceKeyList(deletionList)); - this.selectedItems.clear(); - - for (const id of deletionList) { - this.expandedItems.delete(id); - } - } catch (exception: any) { - this.notificationService.logException(exception, 'Connections delete failed'); - } finally { - this.isProcessing = false; - } - }; -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts index c52a5301f5..2baba73cbb 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts @@ -9,11 +9,9 @@ import React from 'react'; import { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; import { ConfirmationDialog, PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, DatabaseConnection } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { CreateConnectionService } from './CreateConnectionService'; @@ -45,9 +43,7 @@ export class ConnectionsAdministrationService extends Bootstrap { constructor( private readonly administrationItemService: AdministrationItemService, - private readonly notificationService: NotificationService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly dbDriverResource: DBDriverResource, private readonly createConnectionService: CreateConnectionService, private readonly commonDialogService: CommonDialogService, private readonly serverConfigResource: ServerConfigResource, @@ -75,7 +71,6 @@ export class ConnectionsAdministrationService extends Bootstrap { isHidden: () => this.serverConfigResource.distributed, getContentComponent: () => ConnectionsAdministration, getDrawerComponent: () => ConnectionsDrawerItem, - onActivate: this.loadConnections.bind(this), onDeActivate: this.refreshUserConnections.bind(this), }); this.connectionDetailsPlaceholder.add(Origin, 0); @@ -122,13 +117,4 @@ export class ConnectionsAdministrationService extends Bootstrap { return result !== DialogueStateResult.Rejected; } - - private async loadConnections() { - try { - await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey); - await this.dbDriverResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, 'Error occurred while loading connections'); - } - } } diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx index bd5da4b40b..8aa2311419 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx @@ -7,9 +7,21 @@ */ import { observer } from 'mobx-react-lite'; -import { Loader, Placeholder, s, StaticImage, TableColumnValue, TableItem, TableItemExpand, TableItemSelect, useS } from '@cloudbeaver/core-blocks'; -import { DatabaseConnection, DBDriverResource, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { + Loader, + Placeholder, + s, + StaticImage, + TableColumnValue, + TableItem, + TableItemExpand, + TableItemSelect, + useResource, + useS, +} from '@cloudbeaver/core-blocks'; +import { DatabaseConnection, IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { ConnectionsAdministrationService } from '../ConnectionsAdministrationService'; import styles from './Connection.module.css'; @@ -18,14 +30,16 @@ import { ConnectionEdit } from './ConnectionEdit'; interface Props { connectionKey: IConnectionInfoParams; connection: DatabaseConnection; - projectName?: string | null; + shouldDisplayProject: boolean; + icon?: string; } -export const Connection = observer(function Connection({ connectionKey, connection, projectName }) { - const driversResource = useService(DBDriverResource); - const connectionsAdministrationService = useService(ConnectionsAdministrationService); - const icon = driversResource.get(connection.driverId)?.icon; +export const Connection = observer(function Connection({ connectionKey, connection, shouldDisplayProject, icon }) { const style = useS(styles); + const connectionsAdministrationService = useService(ConnectionsAdministrationService); + const projectInfoResource = useResource(Connection, ProjectInfoResource, connectionKey.projectId, { active: shouldDisplayProject }); + + const projectName = shouldDisplayProject ? (projectInfoResource.data?.name ?? '') : undefined; return ( @@ -46,7 +60,7 @@ export const Connection = observer(function Connection({ connectionKey, c {connection.host && connection.port && `:${connection.port}`} {projectName !== undefined && ( - + {projectName} )} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx index 3f6fbd6bd8..f619e99e4f 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx @@ -7,35 +7,27 @@ */ import { observer } from 'mobx-react-lite'; -import { getComputed, Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { DatabaseConnection, IConnectionInfoParams, serializeConnectionParam } from '@cloudbeaver/core-connections'; +import { Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { DBDriverResource, serializeConnectionParam } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import { isGlobalProject, isSharedProject, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { isGlobalProject, isSharedProject, ProjectsService } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { Connection } from './Connection'; +import { IConnectionsTableState } from './useConnectionsTable'; interface Props { - keys: IConnectionInfoParams[]; - connections: DatabaseConnection[]; - selectedItems: Map; - expandedItems: Map; + state: IConnectionsTableState; } -export const ConnectionsTable = observer(function ConnectionsTable({ keys, connections, selectedItems, expandedItems }) { +export const ConnectionsTable = observer(function ConnectionsTable({ state }) { const translate = useTranslate(); const projectService = useService(ProjectsService); - const projectsLoader = useResource(ConnectionsTable, ProjectInfoResource, CachedMapAllKey); - const displayProjects = getComputed( - () => projectService.activeProjects.filter(project => isGlobalProject(project) || isSharedProject(project)).length > 1, - ); - - function getProjectName(projectId: string) { - return displayProjects ? projectsLoader.resource.get(projectId)?.name ?? null : undefined; - } + const dbDriverResource = useResource(ConnectionsTable, DBDriverResource, CachedMapAllKey); + const shouldDisplayProjects = projectService.activeProjects.filter(project => isGlobalProject(project) || isSharedProject(project)).length > 1; return ( - +
@@ -44,16 +36,17 @@ export const ConnectionsTable = observer(function ConnectionsTable({ keys {translate('connections_connection_name')} {translate('connections_connection_address')} - {displayProjects && {translate('connections_connection_project')}} + {shouldDisplayProjects && {translate('connections_connection_project')}} - {connections.map((connection, i) => ( + {state.connections.map((connection, i) => ( ))} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/useConnectionsTable.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/useConnectionsTable.tsx new file mode 100644 index 0000000000..d89709338d --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/useConnectionsTable.tsx @@ -0,0 +1,173 @@ +/* + * 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 { action, computed, observable } from 'mobx'; + +import { ConfirmationDialogDelete, TableState, useObservableRef, useResource, useTable } from '@cloudbeaver/core-blocks'; +import { + compareConnectionsInfo, + compareNewConnectionsInfo, + Connection, + ConnectionInfoActiveProjectKey, + ConnectionInfoResource, + createConnectionParam, + IConnectionInfoParams, +} from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { isGlobalProject, isSharedProject, ProjectInfoResource, projectInfoSortByName, ProjectsService } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; +import { isArraysEqual, isDefined, isObjectsEqual } from '@cloudbeaver/core-utils'; + +export interface IConnectionsTableState { + readonly connections: Connection[]; + readonly keys: IConnectionInfoParams[]; + table: TableState; + loading: boolean; + update: () => Promise; + delete: () => Promise; +} + +export function useConnectionsTable() { + const connectionInfoResource = useResource( + useConnectionsTable, + ConnectionInfoResource, + { + key: ConnectionInfoActiveProjectKey, + includes: ['customIncludeOptions'], + }, + { forceSuspense: true }, + ); + const projectInfoResource = useResource(useConnectionsTable, ProjectInfoResource, CachedMapAllKey, { forceSuspense: true }); + + const notificationService = useService(NotificationService); + const localizationService = useService(LocalizationService); + const commonDialogService = useService(CommonDialogService); + const projectService = useService(ProjectsService); + + const table = useTable(); + + const state: IConnectionsTableState = useObservableRef( + () => ({ + get connections() { + return this.connectionInfoResource.resource + .get(ConnectionInfoActiveProjectKey) + .filter((connection): connection is Connection => { + if (!isDefined(connection)) { + return false; + } + + const project = this.projectInfoResource.resource.get(connection.projectId); + + return connection.template && !!project && (isSharedProject(project) || isGlobalProject(project)); + }) + .sort((a, b) => { + const compareNew = compareNewConnectionsInfo(a, b); + const projectA = this.projectInfoResource.resource.get(a.projectId); + const projectB = this.projectInfoResource.resource.get(b.projectId); + + if (compareNew !== 0) { + return compareNew; + } + + if (projectA && projectB) { + const projectSort = projectInfoSortByName(projectA, projectB); + + if (projectSort !== 0) { + return projectSort; + } + } + + return compareConnectionsInfo(a, b); + }); + }, + get keys() { + return this.connections.map(createConnectionParam); + }, + loading: false, + async update() { + if (this.loading) { + return; + } + this.loading = true; + try { + await this.connectionInfoResource.resource.refresh(ConnectionInfoActiveProjectKey); + this.connectionInfoResource.resource.cleanNewFlags(); + this.notificationService.logSuccess({ title: 'connections_administration_tools_refresh_success' }); + } catch (exception: any) { + this.notificationService.logException(exception, 'connections_administration_tools_refresh_fail'); + } finally { + this.loading = false; + } + }, + async delete() { + if (this.loading) { + return; + } + + const deletionList = Array.from(this.table.selected) + .filter(([_, value]) => value) + .map(([connectionId]) => connectionId); + + if (deletionList.length === 0) { + return; + } + + const connectionNames = deletionList.map(id => this.connectionInfoResource.resource.get(id)?.name).filter(Boolean); + const nameList = connectionNames.map(name => `"${name}"`).join(', '); + const message = `${this.localizationService.translate( + 'connections_administration_delete_confirmation', + )}${nameList}. ${this.localizationService.translate('ui_are_you_sure')}`; + + const result = await this.commonDialogService.open(ConfirmationDialogDelete, { + title: 'ui_data_delete_confirmation', + message, + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + + this.loading = true; + + try { + await this.connectionInfoResource.resource.deleteConnection(resourceKeyList(deletionList)); + this.table.unselect(); + + for (const id of deletionList) { + this.table.expand(id, false); + } + } catch (exception: any) { + this.notificationService.logException(exception, 'connections_administration_connection_create_error'); + } finally { + this.loading = false; + } + }, + }), + { + connections: computed({ equals: (a, b) => isArraysEqual(a, b) }), + keys: computed({ equals: (a, b) => isArraysEqual(a, b, isObjectsEqual) }), + loading: observable.ref, + update: action.bound, + delete: action.bound, + }, + { + table, + connectionInfoResource, + projectInfoResource, + notificationService, + localizationService, + commonDialogService, + projectService, + }, + ); + + return state; +} 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-connections-administration/src/locales/en.ts b/webapp/packages/plugin-connections-administration/src/locales/en.ts index 8a51e05a52..521fb71806 100644 --- a/webapp/packages/plugin-connections-administration/src/locales/en.ts +++ b/webapp/packages/plugin-connections-administration/src/locales/en.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 default [ ['connections_public_connection_edit_menu_item_title', 'Edit Connection'], ['connections_public_connection_edit_cancel_title', 'Cancel confirmation'], @@ -10,4 +17,5 @@ export default [ 'templates_administration_info_message', 'The templates enable administrators to define various reusable connection parameters, subsequently allowing users to create multiple connections based on these templates.', ], + ['connections_administration_connection_create_error', 'Failed to create connection'], ]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/fr.ts b/webapp/packages/plugin-connections-administration/src/locales/fr.ts index 1db4f20421..ea916df5d3 100644 --- a/webapp/packages/plugin-connections-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-connections-administration/src/locales/fr.ts @@ -17,4 +17,5 @@ export default [ 'templates_administration_info_message', 'Les modèles permettent aux administrateurs de définir divers paramètres de connexion réutilisables, permettant ensuite aux utilisateurs de créer plusieurs connexions basées sur ces modèles.', ], + ['connections_administration_connection_create_error', 'Failed to create connection'], ]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/it.ts b/webapp/packages/plugin-connections-administration/src/locales/it.ts index 3a4a61d384..c3761b511e 100644 --- a/webapp/packages/plugin-connections-administration/src/locales/it.ts +++ b/webapp/packages/plugin-connections-administration/src/locales/it.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 default [ ['connections_public_connection_edit_menu_item_title', 'Modifica Connessione'], ['connections_public_connection_edit_cancel_title', "Conferma l'annullamento"], @@ -5,4 +12,5 @@ export default [ 'templates_administration_info_message', 'The templates enable administrators to define various reusable connection parameters, subsequently allowing users to create multiple connections based on these templates.', ], + ['connections_administration_connection_create_error', 'Failed to create connection'], ]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/ru.ts b/webapp/packages/plugin-connections-administration/src/locales/ru.ts index 2cad18e3dc..cf9559fa5f 100644 --- a/webapp/packages/plugin-connections-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-connections-administration/src/locales/ru.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 default [ ['connections_public_connection_edit_menu_item_title', 'Изменить подключение'], ['connections_public_connection_edit_cancel_title', 'Отмена редактирования'], @@ -10,4 +17,5 @@ export default [ 'templates_administration_info_message', 'Шаблоны позволяют администраторам определять различные параметры подключения, а затем позволяют пользователям создавать несколько подключений на основе этих шаблонов.', ], + ['connections_administration_connection_create_error', 'Не удалось создать подключение'], ]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/zh.ts b/webapp/packages/plugin-connections-administration/src/locales/zh.ts index ad68e6ebc9..9535cafc9b 100644 --- a/webapp/packages/plugin-connections-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-connections-administration/src/locales/zh.ts @@ -17,4 +17,5 @@ export default [ 'templates_administration_info_message', '管理员可在数据库连接模板中定义各种可重用的连接参数,之后用户可基于这些模板创建多个数据库连接。', ], + ['connections_administration_connection_create_error', 'Failed to create connection'], ]; 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) || []);