From 5ce49faea029f2a609eeda3df08e7ef56f767c7f Mon Sep 17 00:00:00 2001 From: daelynum Date: Fri, 13 Sep 2024 17:09:37 +0200 Subject: [PATCH 01/14] dbeaver/tech-docs#607 updated images in README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f4f8107f95..d57d026f26 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# CloudBeaver Community + - +# CloudBeaver Community Cloud Database Manager - Community Edition. CloudBeaver is a web server which provides rich web interface. Server itself is a Java application, web part is written on TypeScript and React. It is free to use and open-source (licensed under [Apache 2](https://github.com/dbeaver/cloudbeaver/blob/devel/LICENSE) license). See out [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. -![](https://github.com/dbeaver/cloudbeaver/wiki/images/demo_screenshot_1.png) + + + + ## Run in Docker From 461e2517531207a21cbd429eddeeef28b90bb2e1 Mon Sep 17 00:00:00 2001 From: daelynum Date: Mon, 16 Sep 2024 14:29:02 +0200 Subject: [PATCH 02/14] dbeaver/tech-docs#607 updated images in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fde20e181d..5bcf48ae8f 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ CloudBeaver is a web server that provides a rich web interface. The server itsel It is free to use and open-source (licensed under [Apache 2](https://github.com/dbeaver/cloudbeaver/blob/devel/LICENSE) license). See our [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. + + - - ## Run in Docker From 9e657d2280f32cd08112a6bed79f36ae19a05a1e Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Sep 2024 17:18:27 +0300 Subject: [PATCH 03/14] 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) || []); From 03d0f26530825e05532642fa72fc2ad4a9b2e87a Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:17:47 +0200 Subject: [PATCH 04/14] CB-5391 remove controller from the plugin-connections-administration (#2876) * CB-5391 remove controller from the plugin-connections-administration * CB-5391 remove use controller * CB-5391 review fixes --------- Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> Co-authored-by: Alexey --- .../core-blocks/src/Table/TableState.ts | 48 +++-- .../core-blocks/src/Table/useTable.ts | 4 +- webapp/packages/core-di/src/App.ts | 6 +- webapp/packages/core-di/src/index.ts | 1 - webapp/packages/core-di/src/useController.ts | 62 ------- .../Connections/ConnectionsAdministration.tsx | 37 ++-- .../ConnectionsAdministrationController.ts | 148 --------------- .../ConnectionsAdministrationService.ts | 16 +- .../ConnectionsTable/Connection.tsx | 30 ++- .../ConnectionsTable/ConnectionsTable.tsx | 37 ++-- .../ConnectionsTable/useConnectionsTable.tsx | 173 ++++++++++++++++++ .../src/locales/en.ts | 8 + .../src/locales/fr.ts | 1 + .../src/locales/it.ts | 8 + .../src/locales/ru.ts | 8 + .../src/locales/zh.ts | 1 + 16 files changed, 273 insertions(+), 315 deletions(-) delete mode 100644 webapp/packages/core-di/src/useController.ts delete mode 100644 webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts create mode 100644 webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/useConnectionsTable.tsx 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/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/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'], ]; From 12e1fb57d7b4be429b12de6d4a5077855291c06c Mon Sep 17 00:00:00 2001 From: Greg Miller Date: Tue, 17 Sep 2024 14:04:02 +0200 Subject: [PATCH 05/14] dbeaver/dbeaver-devops#1483 Added docker apt-get upgrade --- deploy/docker/cloudbeaver-ce/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile index 361df92c0a..4679467a03 100644 --- a/deploy/docker/cloudbeaver-ce/Dockerfile +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -2,6 +2,10 @@ FROM dbeaver/base-java MAINTAINER DBeaver Corp, devops@dbeaver.com +RUN set -eux; \ + apt-get update; \ + apt-get upgrade -y; + COPY cloudbeaver /opt/cloudbeaver EXPOSE 8978 From ea2246af31553478a538d2396232baf1cc2836e3 Mon Sep 17 00:00:00 2001 From: Greg Miller Date: Tue, 17 Sep 2024 14:09:01 +0200 Subject: [PATCH 06/14] dbeaver/dbeaver-devops#1483 Remove sed before apt-get --- deploy/docker/cloudbeaver-ce/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile index 4679467a03..95bdcc6812 100644 --- a/deploy/docker/cloudbeaver-ce/Dockerfile +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -2,8 +2,7 @@ FROM dbeaver/base-java MAINTAINER DBeaver Corp, devops@dbeaver.com -RUN set -eux; \ - apt-get update; \ +RUN apt-get update; \ apt-get upgrade -y; COPY cloudbeaver /opt/cloudbeaver From 30e74ce09f24ae7133238c9e126dc9f032938bd6 Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:44:44 +0200 Subject: [PATCH 07/14] CB-5450 purge cache if quota error happens (#2917) * CB-5450 purge cache if quota error happens * CB-5450 cache only images from the same origin * CB-5450 change window to self * CB-5450 revert purgeOnQuotaError --------- Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- webapp/packages/core-browser/src/service-worker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/packages/core-browser/src/service-worker.ts b/webapp/packages/core-browser/src/service-worker.ts index 4849f1b029..2d104d07df 100644 --- a/webapp/packages/core-browser/src/service-worker.ts +++ b/webapp/packages/core-browser/src/service-worker.ts @@ -117,7 +117,7 @@ registerRoute( ); registerRoute( - ({ request }) => request.destination === 'image', + ({ request, url }) => url.origin === self.location.origin && request.destination === 'image', new CacheFirst({ cacheName: 'images', plugins: [ @@ -127,6 +127,7 @@ registerRoute( new ExpirationPlugin({ maxEntries: 1000, maxAgeSeconds: 7 * 24 * 60 * 60, + purgeOnQuotaError: true, }), ], }), From 5b527872bd7e70fd877b323004f85a281a5ca7d7 Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:30:19 +0400 Subject: [PATCH 08/14] CB-4095. Added env variables for configurations parameters (#2905) * CB-4095. Added env variables for configurations parameters * CB-4095. Refactor after review * CB-4095. Refactor after review, added sub group for envs * CB-4095. Remove unusable env variable --------- Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Alexander Skoblikov --- .../DefaultConfiguration/cloudbeaver.conf | 41 ++++++++-------- .../SQLiteConfiguration/cloudbeaver.conf | 39 ++++++++------- .../service/sql/WebSQLConstants.java | 1 - .../workspace/conf/cloudbeaver.conf | 47 +++++++++---------- 4 files changed, 62 insertions(+), 66 deletions(-) diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf index 08112c854e..e063bf739b 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf @@ -1,8 +1,8 @@ { server: { - serverPort: 8978, + serverPort: "${CLOUDBEAVER_SERVICE_PORT:8978}", - workspaceLocation: "workspace", + workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "web", driversLocation: "drivers", @@ -27,9 +27,9 @@ sql.proposals.insert.table.alias: PLAIN }, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", enableSecurityManager: false, @@ -64,29 +64,28 @@ }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - defaultUserTeam: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + grantConnectionsAccessToAnonymousTeam: "${CLOUDBEAVER_APP_GRANT_CONNECTIONS_ACCESS_TO_ANONYMOUS_TEAM:false}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:false}", + showReadOnlyConnectionInfo: "${CLOUDBEAVER_APP_READ_ONLY_CONNECTION_INFO:false}", systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}", - forwardProxy: false, + forwardProxy: "${CLOUDBEAVER_APP_FORWARD_PROXY:false}", - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, + publicCredentialsSaveEnabled: "${CLOUDBEAVER_APP_PUBLIC_CREDENTIALS_SAVE_ENABLED:true}", + adminCredentialsSaveEnabled: "${CLOUDBEAVER_APP_ADMIN_CREDENTIALS_SAVE_ENABLED:true}", - resourceManagerEnabled: true, + resourceManagerEnabled: "${CLOUDBEAVER_APP_RESOURCE_MANAGER_ENABLED:true}", resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + resourceManagerFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_RESOURCE_MANAGER_FILE_SIZE_LIMIT:500000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}" }, enabledAuthProviders: [ "local" diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf index efc8a5732f..3c96c0389e 100644 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf @@ -1,8 +1,8 @@ { server: { - serverPort: 8978, + serverPort: "${CLOUDBEAVER_SERVICE_PORT:8978}", - workspaceLocation: "workspace", + workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "web", driversLocation: "drivers", @@ -25,9 +25,9 @@ sql.proposals.insert.table.alias: PLAIN }, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", enableSecurityManager: false, @@ -61,28 +61,27 @@ }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + grantConnectionsAccessToAnonymousTeam: "${CLOUDBEAVER_APP_GRANT_CONNECTIONS_ACCESS_TO_ANONYMOUS_TEAM:false}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:false}", + showReadOnlyConnectionInfo: "${CLOUDBEAVER_APP_READ_ONLY_CONNECTION_INFO:false}", systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}", - forwardProxy: false, + forwardProxy: "${CLOUDBEAVER_APP_FORWARD_PROXY:false}", - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, + publicCredentialsSaveEnabled: "${CLOUDBEAVER_APP_PUBLIC_CREDENTIALS_SAVE_ENABLED:true}", + adminCredentialsSaveEnabled: "${CLOUDBEAVER_APP_ADMIN_CREDENTIALS_SAVE_ENABLED:true}", - resourceManagerEnabled: true, + resourceManagerEnabled: "${CLOUDBEAVER_APP_RESOURCE_MANAGER_ENABLED:true}", resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + resourceManagerFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_RESOURCE_MANAGER_FILE_SIZE_LIMIT:500000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}" }, enabledAuthProviders: [ "local" diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java index a8ff1bd845..561162394c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java @@ -22,7 +22,6 @@ public class WebSQLConstants { public static final String QUOTA_PROP_ROW_LIMIT = "sqlResultSetRowsLimit"; - public static final String QUOTA_PROP_MEMORY_LIMIT = "sqlResultSetMemoryLimit"; public static final String QUOTA_PROP_QUERY_LIMIT = "sqlMaxRunningQueries"; public static final String QUOTA_PROP_SQL_QUERY_TIMEOUT = "sqlQueryTimeout"; public static final String QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH = "sqlTextPreviewMaxLength"; diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index 6ba3e1b6ff..4511bd491c 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -1,9 +1,9 @@ { server: { serverPort: "${CLOUDBEAVER_TEST_PORT:18978}", - serverName: "CloudBeaver CE Test Server", + serverName: "${CLOUDBEAVER_SERVER_NAME:CloudBeaver CE Test Server}", - workspaceLocation: "workspace", + workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "workspace/web", driversLocation: "../../../deploy/", @@ -12,37 +12,37 @@ productSettings: {}, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", sm: { enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:false}" }, database: { - driver="h2_embedded_v2", - url: "jdbc:h2:mem:testdb", + driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}", + url: "${CLOUDBEAVER_DB_URL:jdbc:h2:mem:testdb}", - createDatabase: true, + createDatabase: "${CLOUDBEAVER_CREATE_DATABASE:true}", - initialDataConfiguration: "workspace/conf/initial-data.conf", + initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:workspace/conf/initial-data.conf}", pool: { - minIdleConnections: 4, - maxIdleConnections: 10, - maxConnections: 100, - validationQuery: "SELECT 1" + minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}", + maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}", + maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}", + validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}" } } }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - defaultUserTeam: "user", - supportsCustomConnections: true, - enableReverseProxyAuth: true, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:true}", + enableReverseProxyAuth: "${CLOUDBEAVER_APP_ENABLE_REVERSE_PROXY_AUTH:true}", enabledAuthProviders: [ "local", "reverseProxy" @@ -52,13 +52,12 @@ ], resourceQuotas: { - dataExportFileSizeLimit: 10000000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120, - sqlQueryTimeout: 5 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}", + sqlQueryTimeout: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_QUERY_TIMEOUT:5}" }, disabledDrivers: [ From 4f0db5b854f634074893136bac31fcd7dc656014 Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:50:04 +0200 Subject: [PATCH 09/14] CB-4896 migrate to jetty 12 (#2847) * CB-4896 migrate to jetty 12 * CB-4896 fix root uri * CB-4896 fix proxy handler paths * CB-4896 resolve conflicts * CB-4896 fix hidden gql errors * CB-4896 redirect welcome mode (redirect from '/' to index.html) * CB-4896 fix url 'blinking' --------- Co-authored-by: Alexander Skoblikov Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../io.cloudbeaver.model/META-INF/MANIFEST.MF | 2 +- .../model/session/WebHttpRequestInfo.java | 57 ++++++ .../cloudbeaver/model/session/WebSession.java | 15 +- .../server/graphql/GraphQLEndpoint.java | 2 + .../server/jetty/CBJettyServer.java | 76 ++++---- .../server/jetty/CBJettyServletContext.java | 4 +- .../server/jetty/CBSessionHandler.java | 169 +++++++++++------- .../CBSymLinkContentAllowedAliasChecker.java | 38 ++++ .../server/servlets/CBStaticServlet.java | 59 +----- .../server/servlets/CBStatusServlet.java | 3 +- .../server/servlets/ProxyResourceHandler.java | 80 +++++++++ .../websockets/CBAbstractWebSocket.java | 24 +-- .../server/websockets/CBEventsWebSocket.java | 12 +- .../websockets/CBExpiredSessionWebSocket.java | 4 +- .../websockets/CBJettyWebSocketManager.java | 42 +++-- .../websockets/WebSocketPingPongCallback.java | 8 +- .../service/session/WebSessionManager.java | 41 +++-- .../service/sql/WebSQLFileLoaderServlet.java | 4 +- .../service/sql/WebSQLResultServlet.java | 30 +++- .../service/fs/model/WebFSServlet.java | 4 +- 20 files changed, 430 insertions(+), 244 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 6a557347eb..b1a8d173c5 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -19,7 +19,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.bundle.graphql.java;visibility:=reexport, org.jkiss.bundle.apache.dbcp, com.google.gson;visibility:=reexport, - jakarta.servlet;visibility:=reexport + jakarta.servlet-api;bundle-version:="6.0.0";visibility:=reexport Export-Package: io.cloudbeaver, io.cloudbeaver.auth, io.cloudbeaver.auth.provider, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java new file mode 100644 index 0000000000..a6dde6469d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java @@ -0,0 +1,57 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model.session; + +import jakarta.servlet.http.HttpServletRequest; + +public class WebHttpRequestInfo { + + private final String id; + private final Object locale; + private final String lastRemoteAddress; + private final String lastRemoteUserAgent; + + public WebHttpRequestInfo(HttpServletRequest request) { + this.id = request.getSession().getId(); + this.locale = request.getAttribute("locale"); + this.lastRemoteAddress = request.getRemoteAddr(); + this.lastRemoteUserAgent = request.getHeader("User-Agent"); + } + + public WebHttpRequestInfo(String id, Object locale, String lastRemoteAddress, String lastRemoteUserAgent) { + this.id = id; + this.locale = locale; + this.lastRemoteAddress = lastRemoteAddress; + this.lastRemoteUserAgent = lastRemoteUserAgent; + } + + public String getId() { + return id; + } + + public Object getLocale() { + return locale; + } + + public String getLastRemoteAddress() { + return lastRemoteAddress; + } + + public String getLastRemoteUserAgent() { + return lastRemoteUserAgent; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index 5b1d052433..bf0bc809c9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -33,7 +33,6 @@ import io.cloudbeaver.utils.CBModelConstants; import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebDataSourceUtils; -import jakarta.servlet.http.HttpServletRequest; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -124,13 +123,13 @@ public class WebSession extends BaseWebSession private final Map sessionHandlers; public WebSession( - @NotNull HttpServletRequest request, + @NotNull WebHttpRequestInfo requestInfo, @NotNull WebAuthApplication application, @NotNull Map sessionHandlers ) throws DBException { - super(request.getSession().getId(), application); + super(requestInfo.getId(), application); this.lastAccessTime = this.createTime; - setLocale(CommonUtils.toString(request.getSession().getAttribute(ATTR_LOCALE), this.locale)); + setLocale(CommonUtils.toString(requestInfo.getLocale(), this.locale)); this.sessionHandlers = sessionHandlers; //force authorization of anonymous session to avoid access error, //because before authorization could be called by any request, @@ -138,7 +137,7 @@ public WebSession( //and the order of requests is not guaranteed. //look at CB-4747 refreshSessionAuth(); - updateSessionParameters(request); + updateSessionParameters(requestInfo); } @Nullable @@ -558,9 +557,9 @@ public synchronized void updateInfo(boolean isOldHttpSessionUsed) { } } - public synchronized void updateSessionParameters(HttpServletRequest request) { - this.lastRemoteAddr = request.getRemoteAddr(); - this.lastRemoteUserAgent = request.getHeader("User-Agent"); + public synchronized void updateSessionParameters(WebHttpRequestInfo requestInfo) { + this.lastRemoteAddr = requestInfo.getLastRemoteAddress(); + this.lastRemoteUserAgent = requestInfo.getLastRemoteUserAgent(); this.cacheExpired = false; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java index a0ee694bae..c3d9bf0f91 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java @@ -23,6 +23,7 @@ import graphql.language.SourceLocation; import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLSchema; +import graphql.schema.PropertyDataFetcherHelper; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; @@ -77,6 +78,7 @@ public class GraphQLEndpoint extends HttpServlet { public GraphQLEndpoint() { GraphQLSchema schema = buildSchema(); + PropertyDataFetcherHelper.setUseLambdaFactory(false); graphQL = GraphQL .newGraphQL(schema) .instrumentation(new SimplePerformantInstrumentation()) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index f30a4a1e46..ddf67a7d43 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -23,18 +23,16 @@ import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.CBStatusServlet; +import io.cloudbeaver.server.servlets.ProxyResourceHandler; import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; +import org.eclipse.jetty.ee10.servlet.*; import org.eclipse.jetty.server.*; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.DefaultSessionIdManager; -import org.eclipse.jetty.server.session.NullSessionDataStore; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; -import org.eclipse.jetty.util.resource.PathResource; -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.DefaultSessionIdManager; +import org.eclipse.jetty.session.NullSessionDataStore; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.eclipse.jetty.xml.XmlConfiguration; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -66,15 +64,15 @@ public CBJettyServer(@NotNull CBApplication application) { public void runServer() { try { CBServerConfig serverConfiguration = application.getServerConfiguration(); - JettyServer server; + Server server; int serverPort = serverConfiguration.getServerPort(); String serverHost = serverConfiguration.getServerHost(); Path sslPath = getSslConfigurationPath(); boolean sslConfigurationExists = sslPath != null && Files.exists(sslPath); if (sslConfigurationExists) { - server = new JettyServer(); - XmlConfiguration sslConfiguration = new XmlConfiguration(new PathResource(sslPath)); + server = new Server(); + XmlConfiguration sslConfiguration = new XmlConfiguration(ResourceFactory.of(server).newResource(sslPath)); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // method sslConfiguration.configure() does not see the context class of the Loader, // so we have to configure it manually, then return the old classLoader. @@ -83,23 +81,30 @@ public void runServer() { Thread.currentThread().setContextClassLoader(classLoader); } else { if (CommonUtils.isEmpty(serverHost)) { - server = new JettyServer(serverPort); + server = new Server(serverPort); } else { - server = new JettyServer( + server = new Server( InetSocketAddress.createUnresolved(serverHost, serverPort)); } } { // Handler configuration + Path contentRootPath = Path.of(serverConfiguration.getContentRoot()); ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); - servletContextHandler.setResourceBase(serverConfiguration.getContentRoot()); + servletContextHandler.setBaseResourceAsPath(contentRootPath); String rootURI = serverConfiguration.getRootURI(); servletContextHandler.setContextPath(rootURI); ServletHolder staticServletHolder = new ServletHolder("static", new CBStaticServlet()); staticServletHolder.setInitParameter("dirAllowed", "false"); - servletContextHandler.addServlet(staticServletHolder, "/*"); + staticServletHolder.setInitParameter("cacheControl", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); + servletContextHandler.addServlet(staticServletHolder, "/"); + servletContextHandler.insertHandler(new ProxyResourceHandler(Path.of(serverConfiguration.getContentRoot()))); + + if (Files.isSymbolicLink(contentRootPath)) { + servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); + } ServletHolder imagesServletHolder = new ServletHolder("images", new CBImageServlet()); servletContextHandler.addServlet(imagesServletHolder, serverConfiguration.getServicesURI() + "images/*"); @@ -120,12 +125,7 @@ public void runServer() { } } - initSessionManager(this.application, servletContextHandler); - - server.setHandler(servletContextHandler); - - JettyWebSocketServletContainerInitializer.configure(servletContextHandler, - (context, wsContainer) -> { + WebSocketUpgradeHandler webSocketHandler = WebSocketUpgradeHandler.from(server, servletContextHandler, (wsContainer) -> { wsContainer.setIdleTimeout(Duration.ofMinutes(5)); // Add websockets wsContainer.addMapping( @@ -134,6 +134,12 @@ public void runServer() { ); } ); + servletContextHandler.insertHandler(webSocketHandler); + + initSessionManager(this.application, server, servletContextHandler); + + server.setHandler(servletContextHandler); + ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); //errorHandler.addErrorPage(404, "/missing.html"); servletContextHandler.setErrorHandler(errorHandler); @@ -180,10 +186,11 @@ private Path getSslConfigurationPath() { private void initSessionManager( @NotNull CBApplication application, + @NotNull Server server, @NotNull ServletContextHandler servletContextHandler ) { // Init sessions persistence - CBSessionHandler sessionHandler = new CBSessionHandler(application); + SessionHandler sessionHandler = new SessionHandler(); var maxIdleTime = application.getMaxSessionIdleTime(); int intMaxIdleSeconds; if (maxIdleTime > Integer.MAX_VALUE) { @@ -198,27 +205,10 @@ private void initSessionManager( sessionCache.setSessionDataStore(new NullSessionDataStore()); sessionHandler.setSessionCache(sessionCache); servletContextHandler.setSessionHandler(sessionHandler); - } - - public static class JettyServer extends Server { - public JettyServer(int serverPort) { - super(serverPort); - } - public JettyServer() { - super(); - } - public JettyServer(InetSocketAddress addr) { - super(addr); - } + DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); + idMgr.setWorkerName(null); + server.addBean(idMgr, true); - @Override - public void setSessionIdManager(SessionIdManager sessionIdManager) { - if (sessionIdManager instanceof DefaultSessionIdManager) { - // Nullify worker name to avoid dummy prefixes in session ID cookie - ((DefaultSessionIdManager) sessionIdManager).setWorkerName(null); - } - super.setSessionIdManager(sessionIdManager); - } } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java index 0579ff22b0..b04374442c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java @@ -19,8 +19,8 @@ import io.cloudbeaver.service.DBWServletContext; import jakarta.servlet.http.HttpServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; public class CBJettyServletContext implements DBWServletContext { private final ServletContextHandler contextHandler; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java index 6a091573c2..907c9e3758 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java @@ -18,8 +18,13 @@ import io.cloudbeaver.server.CBApplication; import jakarta.servlet.SessionCookieConfig; -import org.eclipse.jetty.http.Syntax; -import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.SessionHandler; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; public class CBSessionHandler extends SessionHandler { private final CBCookieConfig cbCookieConfig; @@ -37,107 +42,147 @@ public SessionCookieConfig getSessionCookieConfig() { } - //mostly copy of org.eclipse.jetty.server.session.CookieConfig but allows to use dynamic setSecure flag + //mostly copy of org.eclipse.jetty.ee10.servlet.CookieConfig but allows to use dynamic setSecure flag public final class CBCookieConfig implements SessionCookieConfig { - public CBCookieConfig() { + + @Override + public boolean isSecure() { + var serverUrl = CBSessionHandler.this.application.getServerURL(); + return serverUrl != null && serverUrl.startsWith("https://"); } + @Override public String getComment() { - return CBSessionHandler.this._sessionComment; + return getSessionComment(); } + @Override public String getDomain() { - return CBSessionHandler.this._sessionDomain; + return getSessionDomain(); } + @Override public int getMaxAge() { - return CBSessionHandler.this._maxCookieAge; + return getMaxCookieAge(); } + @Override + public void setAttribute(String name, String value) { + checkState(); + String lcase = name.toLowerCase(Locale.ENGLISH); + + switch (lcase) { + case "name" -> setName(value); + case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value)); + case "comment" -> setComment(value); + case "domain" -> setDomain(value); + case "httponly" -> setHttpOnly(Boolean.parseBoolean(value)); + case "secure" -> setSecure(Boolean.parseBoolean(value)); + case "path" -> setPath(value); + default -> setSessionCookieAttribute(name, value); + } + } + + @Override + public String getAttribute(String name) { + String lcase = name.toLowerCase(Locale.ENGLISH); + return switch (lcase) { + case "name" -> getName(); + case "max-age" -> Integer.toString(getMaxAge()); + case "comment" -> getComment(); + case "domain" -> getDomain(); + case "httponly" -> String.valueOf(isHttpOnly()); + case "secure" -> String.valueOf(isSecure()); + case "path" -> getPath(); + default -> getSessionCookieAttribute(name); + }; + } + + /** + * According to the SessionCookieConfig javadoc, the attributes must also include + * all values set by explicit setters. + * + * @see SessionCookieConfig + */ + @Override + public Map getAttributes() { + Map specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + specials.put("name", getAttribute("name")); + specials.put("max-age", getAttribute("max-age")); + specials.put("comment", getAttribute("comment")); + specials.put("domain", getAttribute("domain")); + specials.put("httponly", getAttribute("httponly")); + specials.put("secure", getAttribute("secure")); + specials.put("path", getAttribute("path")); + specials.putAll(getSessionCookieAttributes()); + return Collections.unmodifiableMap(specials); + } + + @Override public String getName() { - return CBSessionHandler.this._sessionCookie; + return getSessionCookie(); } + @Override public String getPath() { - return CBSessionHandler.this._sessionPath; + return getSessionPath(); } + @Override public boolean isHttpOnly() { - return CBSessionHandler.this._httpOnly; - } - - public boolean isSecure() { - var serverUrl = CBSessionHandler.this.application.getServerURL(); - return serverUrl != null && serverUrl.startsWith("https://"); + return CBSessionHandler.this.isHttpOnly(); } + @Override public void setComment(String comment) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._sessionComment = comment; - } + checkState(); + CBSessionHandler.this.setSessionComment(comment); } + @Override public void setDomain(String domain) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._sessionDomain = domain; - } + checkState(); + CBSessionHandler.this.setSessionDomain(domain); } + @Override public void setHttpOnly(boolean httpOnly) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._httpOnly = httpOnly; - } + checkState(); + CBSessionHandler.this.setHttpOnly(httpOnly); } + @Override public void setMaxAge(int maxAge) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._maxCookieAge = maxAge; - } + checkState(); + CBSessionHandler.this.setMaxCookieAge(maxAge); } + @Override public void setName(String name) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else if ("".equals(name)) { - throw new IllegalArgumentException("Blank cookie name"); - } else { - if (name != null) { - Syntax.requireValidRFC2616Token(name, "Bad Session cookie name"); - } - - CBSessionHandler.this._sessionCookie = name; - } + checkState(); + CBSessionHandler.this.setSessionCookie(name); } + @Override public void setPath(String path) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._sessionPath = path; - } + checkState(); + CBSessionHandler.this.setSessionPath(path); } + @Override public void setSecure(boolean secure) { - if (CBSessionHandler.this._context != null && CBSessionHandler.this._context.getContextHandler() - .isAvailable()) { + checkState(); + CBSessionHandler.this.setSecureCookies(secure); + } + + private void checkState() { + //It is allowable to call the CookieConfig.setXX methods after the SessionHandler has started, + //but before the context has fully started. Ie it is allowable for ServletContextListeners + //to call these methods in contextInitialized(). + ServletContextHandler handler = ServletContextHandler.getCurrentServletContextHandler(); + if (handler != null && handler.isAvailable()) throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - } else { - CBSessionHandler.this._secureCookies = secure; - } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java new file mode 100644 index 0000000000..ecfc108878 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jetty; + +import org.eclipse.jetty.server.AliasCheck; +import org.eclipse.jetty.util.resource.Resource; +import org.jkiss.code.NotNull; + +import java.nio.file.Path; + +public class CBSymLinkContentAllowedAliasChecker implements AliasCheck { + @NotNull + private final Path contentRootPath; + + public CBSymLinkContentAllowedAliasChecker(@NotNull Path contentRootPath) { + this.contentRootPath = contentRootPath; + } + + @Override + public boolean checkAlias(String pathInContext, Resource resource) { + Path resourcePath = resource.getPath(); + return resourcePath != null && resourcePath.startsWith(contentRootPath); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index 11ad45034e..b191027991 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -29,28 +29,19 @@ import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.server.CBServerConfig; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpContent; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.ResourceService; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.IOUtils; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.Enumeration; +import java.io.IOException; import java.util.Map; @WebServlet(urlPatterns = "/") @@ -62,10 +53,6 @@ public class CBStaticServlet extends DefaultServlet { private static final Log log = Log.getLog(CBStaticServlet.class); - public CBStaticServlet() { - super(makeResourceService()); - } - @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { @@ -189,46 +176,4 @@ private boolean processSessionStart(HttpServletRequest request, HttpServletRespo return false; } - private static ResourceService makeResourceService() { - ResourceService resourceService = new ProxyResourceService(); - resourceService.setCacheControl(new HttpField(HttpHeader.CACHE_CONTROL, "public, max-age=" + STATIC_CACHE_SECONDS)); - return resourceService; - } - - - private static class ProxyResourceService extends ResourceService { - @Override - protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, HttpContent content, Enumeration reqRanges) throws IOException { - String resourceName = content.getResource().getName(); - if (resourceName.endsWith("index.html") || resourceName.endsWith("sso.html")) { - return patchIndexHtml(response, content); - } - return super.sendData(request, response, include, content, reqRanges); - } - - private boolean patchIndexHtml(HttpServletResponse response, HttpContent content) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Resource resource = content.getResource(); - File file = resource.getFile(); - try (InputStream fis = new FileInputStream(file)) { - IOUtils.copyStream(fis, baos); - } - String indexContents = baos.toString(StandardCharsets.UTF_8); - CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); - indexContents = indexContents - .replace("{ROOT_URI}", serverConfig.getRootURI()) - .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); - byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); - - putHeaders(response, content, indexBytes.length); - // Disable cache for index.html - response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.setHeader(HttpHeader.EXPIRES.toString(), "0"); - - response.getOutputStream().write(indexBytes); - - return true; - } - } - } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java index ceb4fdf748..53979a462c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java @@ -23,8 +23,7 @@ import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.jkiss.dbeaver.DBException; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.utils.GeneralUtils; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java new file mode 100644 index 0000000000..12b89242f5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java @@ -0,0 +1,80 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.servlets; + +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBServerConfig; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.jkiss.code.NotNull; +import org.jkiss.utils.IOUtils; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ProxyResourceHandler extends Handler.Wrapper { + @NotNull + private final Path contentRoot; + + public ProxyResourceHandler(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; + } + + public boolean handle(Request request, Response response, Callback callback) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + String pathInContext = Request.getPathInContext(request); + + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; + } + + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + return super.handle(request, response, callback); + } + + if (pathInContext.startsWith("/")) { + pathInContext = pathInContext.substring(1); + } + var filePath = contentRoot.resolve(pathInContext); + try (InputStream fis = Files.newInputStream(filePath)) { + IOUtils.copyStream(fis, baos); + } + String indexContents = baos.toString(StandardCharsets.UTF_8); + CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); + indexContents = indexContents + .replace("{ROOT_URI}", serverConfig.getRootURI()) + .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + // Disable cache for index.html + response.getHeaders().put(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); + response.getHeaders().put(HttpHeader.EXPIRES.toString(), "0"); + + response.write(true, ByteBuffer.wrap(indexBytes), callback); + return true; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java index 807814ea19..2dac868a86 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java @@ -17,29 +17,31 @@ package io.cloudbeaver.server.websockets; import com.google.gson.Gson; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSEvent; -import java.io.IOException; - -public class CBAbstractWebSocket extends WebSocketAdapter { +public class CBAbstractWebSocket extends Session.Listener.AbstractAutoDemanding { private static final Log log = Log.getLog(CBAbstractWebSocket.class); protected static final Gson gson = WSUtils.gson; public void handleEvent(WSEvent event) { - if (isNotConnected()) { + if (!isOpen()) { return; } - try { - getRemote().sendString(gson.toJson(event)); - } catch (IOException e) { - handleEventException(e); - } + Session session = getSession(); + session.sendText(gson.toJson(event), new Callback() { + @Override + public void fail(Throwable e) { + handleEventException(e); + } + }); + } - protected void handleEventException(Exception e) { + protected void handleEventException(Throwable e) { log.error("Failed to send websocket message", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index 697011c5d4..b99ff76e93 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -20,8 +20,8 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.websocket.CBWebSessionEventHandler; +import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WriteCallback; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; @@ -36,7 +36,7 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi @NotNull private final BaseWebSession webSession; @NotNull - private final WriteCallback callback; + private final Callback callback; public CBEventsWebSocket(@NotNull BaseWebSession webSession) { this.webSession = webSession; @@ -45,8 +45,8 @@ public CBEventsWebSocket(@NotNull BaseWebSession webSession) { } @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); + public void onWebSocketOpen(Session session) { + super.onWebSocketOpen(session); this.webSession.addEventHandler(this); handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); @@ -109,7 +109,7 @@ public void handleWebSessionEvent(WSEvent event) { super.handleEvent(event); } @Override - protected void handleEventException(Exception e) { + protected void handleEventException(Throwable e) { super.handleEventException(e); webSession.addSessionError(e); } @@ -120,7 +120,7 @@ public BaseWebSession getWebSession() { } @NotNull - public WriteCallback getCallback() { + public Callback getCallback() { return callback; } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java index ec294713eb..643197d7e3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java @@ -21,8 +21,8 @@ public class CBExpiredSessionWebSocket extends CBAbstractWebSocket { @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); + public void onWebSocketOpen(Session session) { + super.onWebSocketOpen(session); handleEvent(new WSAccessTokenExpiredEvent()); close(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index 02c730dd10..26b958fbb2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -18,18 +18,19 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.session.WebSessionManager; -import jakarta.servlet.http.HttpServletRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.server.ServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.ServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.WebSocketCreator; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; -import org.jkiss.dbeaver.runtime.DBWorkbench; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -38,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -public class CBJettyWebSocketManager implements JettyWebSocketCreator { +public class CBJettyWebSocketManager implements WebSocketCreator { private static final Log log = Log.getLog(CBJettyWebSocketManager.class); private final Map> socketBySessionId = new ConcurrentHashMap<>(); private final WebSessionManager webSessionManager; @@ -51,17 +52,22 @@ public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) { @Nullable @Override - public Object createWebSocket(@NotNull JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { - var httpRequest = request.getHttpServletRequest(); - var webSession = webSessionManager.getOrRestoreSession(httpRequest); + public Object createWebSocket(@NotNull ServerUpgradeRequest request, ServerUpgradeResponse resp, Callback callback) { + var webSession = webSessionManager.getOrRestoreSession(request); + var requestInfo = new WebHttpRequestInfo( + request.getId(), + request.getAttribute("locale"), + Request.getRemoteAddr(request), + request.getHeaders().get("User-Agent") + ); if (webSession != null) { - webSession.updateSessionParameters(httpRequest); + webSession.updateSessionParameters(requestInfo); // web client session return createNewEventsWebSocket(webSession); } // possible desktop client session try { - var headlessSession = createHeadlessSession(httpRequest); + var headlessSession = createHeadlessSession(request); if (headlessSession == null) { log.debug("Couldn't create headless session"); return null; @@ -86,21 +92,21 @@ private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSe } @Nullable - private WebHeadlessSession createHeadlessSession(@NotNull HttpServletRequest request) throws DBException { - var httpSession = request.getSession(false); - if (httpSession == null) { + private WebHeadlessSession createHeadlessSession(@NotNull Request request) throws DBException { + var requestSession = request.getSession(false); + if (requestSession == null) { log.debug("CloudBeaver web session not exist, try to create headless session"); } else { - log.debug("CloudBeaver session not found with id " + httpSession.getId() + ", try to create headless session"); + log.debug("CloudBeaver session not found with id " + requestSession.getId() + ", try to create headless session"); } - return webSessionManager.getHeadlessSession(request, true); + return webSessionManager.getHeadlessSession(request, requestSession, true); } public void sendPing() { //remove expired sessions socketBySessionId.entrySet() .removeIf(entry -> { - entry.getValue().removeIf(ws -> !ws.isConnected()); + entry.getValue().removeIf(ws -> !ws.isOpen()); return webSessionManager.getSession(entry.getKey()) == null || entry.getValue().isEmpty(); } @@ -115,7 +121,7 @@ public void sendPing() { var webSockets = entry.getValue(); for (CBEventsWebSocket webSocket : webSockets) { try { - webSocket.getRemote().sendPing( + webSocket.getSession().sendPing( ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)), webSocket.getCallback() ); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java index 8530e963c1..b19741df9a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 DBeaver Corp and others + * Copyright (C) 2010-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; -import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.Callback; import org.jkiss.code.NotNull; -public class WebSocketPingPongCallback implements WriteCallback { +public class WebSocketPingPongCallback implements Callback { @NotNull private final BaseWebSession webSession; @@ -30,7 +30,7 @@ public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) { } @Override - public void writeSuccess() { + public void succeed() { if (webSession instanceof WebHeadlessSession) { webSession.touchSession(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index 40dc20ae00..3380cd4872 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -18,18 +18,18 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.SMTokenCredentialProvider; -import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebHeadlessSession; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.model.session.WebSessionAuthProcessor; +import io.cloudbeaver.model.session.*; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.events.WSWebUtils; import io.cloudbeaver.service.DBWSessionHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -85,7 +85,8 @@ protected CBApplication getApplication() { public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { WebSession webSession = getWebSession(request, response, false); - webSession.updateSessionParameters(request); + var requestInfo = new WebHttpRequestInfo(request); + webSession.updateSessionParameters(requestInfo); webSession.updateInfo(!request.getSession().isNew()); return true; } @@ -109,14 +110,14 @@ public WebSession getWebSession( var baseWebSession = sessionMap.get(sessionId); if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { try { - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } sessionMap.put(sessionId, webSession); } else if (baseWebSession == null) { try { - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } @@ -154,13 +155,15 @@ public WebSession getWebSession( * @return WebSession object or null, if session expired or invalid */ @Nullable - public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { - var httpSession = request.getSession(); - if (httpSession == null) { + public WebSession getOrRestoreSession(@NotNull Request request) { + var sessionIdCookie = Request.getCookies(request).stream().filter( + c -> c.getName().equals(CBConstants.CB_SESSION_COOKIE_NAME) + ).findAny().orElse(null); + if (sessionIdCookie == null) { log.debug("Http session is null. No Web Session returned"); return null; } - var sessionId = httpSession.getId(); + var sessionId = sessionIdCookie.getValue(); WebSession webSession; synchronized (sessionMap) { if (sessionMap.containsKey(sessionId)) { @@ -178,7 +181,12 @@ public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { return null; } - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo( + request.getId(), + request.getAttribute("locale"), + Request.getRemoteAddr(request), + request.getHeaders().get("User-Agent") + )); restorePreviousUserSession(webSession, oldAuthInfo); sessionMap.put(sessionId, webSession); @@ -212,7 +220,7 @@ private void restorePreviousUserSession( } @NotNull - protected WebSession createWebSessionImpl(@NotNull HttpServletRequest request) throws DBException { + protected WebSession createWebSessionImpl(@NotNull WebHttpRequestInfo request) throws DBException { return new WebSession(request, application, getSessionHandlers()); } @@ -281,16 +289,15 @@ public Collection getAllActiveSessions() { } @Nullable - public WebHeadlessSession getHeadlessSession(HttpServletRequest request, boolean create) throws DBException { - String smAccessToken = request.getHeader(WSConstants.WS_AUTH_HEADER); + public WebHeadlessSession getHeadlessSession(Request request, Session session, boolean create) throws DBException { + String smAccessToken = request.getHeaders().get(WSConstants.WS_AUTH_HEADER); if (CommonUtils.isEmpty(smAccessToken)) { return null; } synchronized (sessionMap) { - var httpSession = request.getSession(); var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); - var sessionId = httpSession != null ? httpSession.getId() + var sessionId = session != null ? session.getId() : authPermissions.getSessionId(); var existSession = sessionMap.get(sessionId); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java index 5ea8b52716..bc52ac542a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -29,7 +29,7 @@ import jakarta.servlet.annotation.MultipartConfig; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; @@ -84,7 +84,7 @@ protected void processServiceRequest( .resolve(session.getSessionId()); MultipartConfigElement multiPartConfig = new MultipartConfigElement(tempFolder.toString()); - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multiPartConfig); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, multiPartConfig); Map variables = gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java index a0d231525e..31c5ff26f3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java @@ -1,3 +1,19 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.cloudbeaver.service.sql; import io.cloudbeaver.DBWebException; @@ -5,18 +21,18 @@ import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.service.WebServiceServletBase; -import org.eclipse.jetty.server.Request; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.IOUtils; - import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.MultipartConfig; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -42,7 +58,7 @@ public WebSQLResultServlet(CBApplication application, DBWServiceSQL sqlService) @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); String fileName = UUID.randomUUID().toString(); for (Part part : request.getParts()) { part.write(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER + "/" + fileName); diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java index 53ac1a5dad..fa2514c98f 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java @@ -27,7 +27,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; -import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; @@ -79,7 +79,7 @@ private void doGet(WebSession session, HttpServletRequest request, HttpServletRe private void doPost(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { // we need to set this attribute to get parts - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement("")); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement("")); Map variables = getVariables(request); String parentNodePath = JSONUtils.getString(variables, "toParentNodePath"); if (CommonUtils.isEmpty(parentNodePath)) { From 2d1bde22a2ec463848fb208a627661a70cc1579f Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Tue, 17 Sep 2024 19:14:34 +0200 Subject: [PATCH 10/14] Cb 5562 Fix bad this.setState for the new form API (#2903) * CB-5478 adds useAdministrationUserFormState with flexible id field * CB-5478 simplifies logic fork form state hooks * CB-5562 refactors form state and form parts * CB-5562 fixes disabled param for formState * CB-5562 comments cleanup * CB-5562 fixes getting of exceptions * CB-5562 baseform bad set state fix * CB-5562 removes load and configure method for forms * CB-5562 reverts some state.exception changes * CB-5562 removes bad set state from user form * CB-5562 cleanup * CB-5562 cleanup * CB-5562 removes bad set state from user form * CB-5562 fixes bad set state for user profile * CB-5562 pr fixes * CB-5562 pr fixes --------- Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../core-ui/src/Form/Components/BaseForm.tsx | 9 +- webapp/packages/core-ui/src/Form/FormPart.ts | 22 +++- webapp/packages/core-ui/src/Form/FormState.ts | 115 +++++------------- webapp/packages/core-ui/src/Form/IFormPart.ts | 4 +- .../packages/core-ui/src/Form/IFormState.ts | 19 +-- .../Users/UserForm/AdministrationUserForm.tsx | 4 +- .../UserFormConnectionAccessPart.ts | 2 +- .../Users/UserForm/Info/UserFormInfoPart.ts | 17 +-- .../UserProfileFormAuthenticationPart.ts | 2 +- .../UserInfoPart/UserProfileFormInfoPart.ts | 2 +- .../UserProfileForm/UserProfileFormPanel.tsx | 2 +- 11 files changed, 68 insertions(+), 130 deletions(-) diff --git a/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx index e9de4a144b..96f18cc7c6 100644 --- a/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx +++ b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { Button, Container, Form, s, StatusMessage, useAutoLoad, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, Container, Form, getComputed, s, StatusMessage, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { getFirstException } from '@cloudbeaver/core-utils'; import { TabList } from '../../Tabs/TabList'; @@ -22,7 +22,8 @@ export const BaseForm = observer>(function BaseForm({ servic const translate = useTranslate(); const editing = state.mode === FormMode.Edit; - const changed = state.isChanged(); + const changed = state.isChanged; + const error = getComputed(() => getFirstException(state.exception)); const form = useForm({ async onSubmit() { @@ -36,15 +37,13 @@ export const BaseForm = observer>(function BaseForm({ servic }, }); - useAutoLoad(BaseForm, state); - return (
- + diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 2dcf2063d7..c789e0df64 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, observable, toJS } from 'mobx'; +import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { executorHandlerFilter, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { isObjectsEqual } from '@cloudbeaver/core-utils'; @@ -16,6 +16,7 @@ import type { IFormState } from './IFormState'; export abstract class FormPart implements IFormPart { state: TPartState; initialState: TPartState; + isSaving: boolean; exception: Error | null; promise: Promise | null; @@ -29,6 +30,7 @@ export abstract class FormPart implements IFormPar ) { this.initialState = initialState; this.state = toJS(this.initialState); + this.isSaving = false; this.exception = null; this.promise = null; @@ -37,7 +39,6 @@ export abstract class FormPart implements IFormPar this.loading = false; this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this))); - this.formState.configureTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.configure.bind(this))); this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.format.bind(this))); this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.validate.bind(this))); @@ -46,12 +47,19 @@ export abstract class FormPart implements IFormPar state: observable, exception: observable.ref, promise: observable.ref, + isSaving: observable.ref, loaded: observable, loading: observable, setInitialState: action, + isDisabled: computed, + isChanged: computed, }); } + get isDisabled(): boolean { + return this.isSaving || this.isLoading(); + } + isLoading(): boolean { return this.loading; } @@ -68,7 +76,7 @@ export abstract class FormPart implements IFormPar return this.exception !== null; } - isChanged(): boolean { + get isChanged(): boolean { if (!this.loaded || this.initialState === this.state) { return false; } @@ -85,10 +93,12 @@ export abstract class FormPart implements IFormPar try { await this.loader(); - if (!this.isChanged()) { + if (!this.isChanged) { return; } + this.isSaving = true; + await this.saveChanges(data, contexts); if (ExecutorInterrupter.isInterrupted(contexts)) { return; @@ -100,6 +110,7 @@ export abstract class FormPart implements IFormPar this.exception = exception; throw exception; } finally { + this.isSaving = false; this.loading = false; } } @@ -136,7 +147,7 @@ export abstract class FormPart implements IFormPar protected setInitialState(initialState: TPartState) { this.initialState = initialState; - if (this.isChanged()) { + if (this.isChanged) { return; } @@ -147,7 +158,6 @@ export abstract class FormPart implements IFormPar this.state = state; } - protected configure(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected format(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected validate(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 2b651eda81..93bd21daa2 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -11,7 +11,7 @@ import { DataContext, dataContextAddDIProvider, DataContextGetter, type IDataCon import type { IServiceProvider } from '@cloudbeaver/core-di'; import type { ENotificationType } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; -import { isLoadableStateHasException, MetadataMap, uuid } from '@cloudbeaver/core-utils'; +import { isArraysEqual, isNotNullDefined, MetadataMap, uuid } from '@cloudbeaver/core-utils'; import { DATA_CONTEXT_LOADABLE_STATE, loadableStateContext } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_FORM_STATE } from './DATA_CONTEXT_FORM_STATE'; @@ -25,23 +25,24 @@ export class FormState implements IFormState { mode: FormMode; parts: MetadataMap>; state: TState; - isSaving: boolean; statusMessage: string | string[] | null; statusType: ENotificationType | null; - exception: Error | (Error | null)[] | null; promise: Promise | null; get isDisabled(): boolean { - return this.isSaving || this.isLoading(); + return this.partsValues.some(part => part.isSaving || part?.isLoading?.()); + } + + get isSaving(): boolean { + return this.partsValues.some(part => part.isSaving); } readonly id: string; readonly service: FormBaseService; readonly dataContext: IDataContext; - readonly configureTask: IExecutor>; readonly formStateTask: IExecutor; readonly fillDefaultConfigTask: IExecutor>; readonly submitTask: IExecutor>; @@ -56,17 +57,12 @@ export class FormState implements IFormState { this.mode = FormMode.Create; this.parts = new MetadataMap(); this.state = state; - this.isSaving = false; this.statusMessage = null; this.statusType = null; - this.exception = null; this.promise = null; - this.configureTask = new Executor(this as IFormState, () => true); - this.configureTask.addCollection(service.onConfigure); - this.formStateTask = new Executor(state, () => true); this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this)); @@ -90,42 +86,43 @@ export class FormState implements IFormState { mode: observable, parts: observable.ref, promise: observable.ref, - exception: observable.ref, - isSaving: observable.ref, state: observable, + isSaving: computed, + exception: computed, isDisabled: computed, setMode: action, setPartsState: action, - setException: action, setState: action, + isChanged: computed, + partsValues: computed[]>({ + equals: isArraysEqual, + }), + isError: computed, + isCancelled: computed, }); } - isLoading(): boolean { - return this.promise !== null || this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isLoading()); - } - - isLoaded(): boolean { - if (this.promise) { - return false; - } - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.every(loader => loader.isLoaded()); + get partsValues() { + return Array.from(this.parts.values()); } - isError(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isError()); + get exception(): Error | (Error | null)[] | null { + return this.partsValues + .map(part => part?.exception) + .flat() + .filter(isNotNullDefined); } - isOutdated(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isOutdated?.() === true); + get isError(): boolean { + return this.partsValues.some(part => part.isError()); } - isCancelled(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isCancelled?.() === true); + get isCancelled(): boolean { + return this.partsValues.some(part => part?.isCancelled?.()); } - isChanged(): boolean { - return Array.from(this.parts.values()).some(part => part.isChanged()); + get isChanged(): boolean { + return this.partsValues.some(part => part.isChanged); } getPart>(getter: DataContextGetter, init: (context: IDataContext, id: string) => T): T { @@ -140,50 +137,6 @@ export class FormState implements IFormState { }) as T; } - async load(refresh?: boolean): Promise { - if (this.promise !== null) { - return this.promise; - } - - if (this.isLoaded() && !this.isOutdated() && !refresh) { - return; - } - - this.promise = (async () => { - try { - await this.configureTask.execute(this); - - const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders; - - for (const loader of loaders) { - if (isLoadableStateHasException(loader)) { - continue; - } - - if (!loader.isLoaded() || loader.isOutdated?.() === true) { - try { - await loader.load(); - } catch { - return; - } - } - } - - await this.fillDefaultConfigTask.execute(this); - this.exception = null; - } catch (exception: any) { - this.exception = exception; - throw exception; - } finally { - this.promise = null; - } - })(); - } - - async reload(): Promise { - await this.load(true); - } - cancel(): void { const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders; @@ -210,11 +163,6 @@ export class FormState implements IFormState { return this; } - setException(exception: Error | (Error | null)[] | null): this { - this.exception = exception; - return this; - } - setState(state: TState): this { this.state = state; return this; @@ -222,20 +170,15 @@ export class FormState implements IFormState { async save(): Promise { try { - this.isSaving = true; const context = await this.submitTask.execute(this); if (ExecutorInterrupter.isInterrupted(context)) { return false; } - this.exception = null; return true; - } catch (exception: any) { - this.exception = exception; - } finally { - this.isSaving = false; - } + } catch (exception: any) {} + return false; } diff --git a/webapp/packages/core-ui/src/Form/IFormPart.ts b/webapp/packages/core-ui/src/Form/IFormPart.ts index faa9e63117..446c33fd97 100644 --- a/webapp/packages/core-ui/src/Form/IFormPart.ts +++ b/webapp/packages/core-ui/src/Form/IFormPart.ts @@ -10,8 +10,10 @@ import type { ILoadableState } from '@cloudbeaver/core-utils'; export interface IFormPart extends ILoadableState { readonly state: TState; readonly initialState: TState; + isSaving: boolean; + readonly isDisabled: boolean; - isChanged(): boolean; + readonly isChanged: boolean; load(): Promise; reset(): void; diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 67b2029e1f..050332649b 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -8,13 +8,13 @@ import type { DataContextGetter, IDataContext } from '@cloudbeaver/core-data-context'; import type { ENotificationType } from '@cloudbeaver/core-events'; import type { IExecutor } from '@cloudbeaver/core-executor'; -import type { ILoadableState, MetadataMap } from '@cloudbeaver/core-utils'; +import type { MetadataMap } from '@cloudbeaver/core-utils'; import type { FormBaseService } from './FormBaseService'; import type { FormMode } from './FormMode'; import type { IFormPart } from './IFormPart'; -export interface IFormState extends ILoadableState { +export interface IFormState { readonly id: string; readonly service: FormBaseService; readonly dataContext: IDataContext; @@ -23,14 +23,13 @@ export interface IFormState extends ILoadableState { readonly parts: MetadataMap; readonly state: TState; readonly isDisabled: boolean; + readonly exception: Error | (Error | null)[] | null; readonly promise: Promise | null; - readonly exception: Error | (Error | null)[] | null; readonly statusMessage: string | string[] | null; readonly statusType: ENotificationType | null; - readonly configureTask: IExecutor>; readonly formStateTask: IExecutor; readonly fillDefaultConfigTask: IExecutor>; readonly submitTask: IExecutor>; @@ -39,20 +38,14 @@ export interface IFormState extends ILoadableState { setMode(mode: FormMode): this; setPartsState(state: MetadataMap): this; - setException(exception: Error | (Error | null)[] | null): this; setState(state: TState): this; getPart>(getter: DataContextGetter, init: (context: IDataContext, id: string) => T): T; - isLoading(): boolean; - isLoaded(): boolean; - isError(): boolean; - isOutdated(): boolean; - isCancelled(): boolean; - isChanged(): boolean; + isError: boolean; + isCancelled: boolean; + isChanged: boolean; - load(): Promise; - reload(): Promise; save(): Promise; reset(): void; cancel(): void; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx index 5c006048a7..99bd4c784e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx @@ -59,7 +59,7 @@ export const AdministrationUserForm = observer(function AdministrationUse }, }); - useAutoLoad(AdministrationUserForm, state); + useAutoLoad(AdministrationUserForm, [userFormInfoPart]); return ( @@ -69,7 +69,7 @@ export const AdministrationUserForm = observer(function AdministrationUse diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts index 5e7d6ea83b..6cc4e3cf2a 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts @@ -24,7 +24,7 @@ export class UserFormConnectionAccessPart extends FormPart [ - getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined), - getCachedDataResourceLoaderState(this.authRolesResource, () => undefined), - ]); - } private async updateCredentials() { const password = this.state.password; @@ -220,7 +211,7 @@ export class UserFormInfoPart extends FormPart Date: Tue, 17 Sep 2024 20:46:54 +0200 Subject: [PATCH 11/14] 24.2.1 version bump --- server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.model/pom.xml | 2 +- server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.product.ce/pom.xml | 2 +- .../META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml | 2 +- server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.server/pom.xml | 2 +- .../bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.admin/pom.xml | 2 +- .../bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.auth/pom.xml | 2 +- .../io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.data.transfer/pom.xml | 2 +- server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.fs/pom.xml | 2 +- .../io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.metadata/pom.xml | 2 +- .../io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.rm.nio/pom.xml | 2 +- server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.rm/pom.xml | 2 +- .../io.cloudbeaver.service.security/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.security/pom.xml | 2 +- server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.slf4j/pom.xml | 2 +- server/features/io.cloudbeaver.ce.drivers.feature/feature.xml | 2 +- server/features/io.cloudbeaver.ce.drivers.feature/pom.xml | 2 +- server/features/io.cloudbeaver.product.ce.feature/feature.xml | 2 +- server/features/io.cloudbeaver.product.ce.feature/pom.xml | 2 +- server/features/io.cloudbeaver.server.feature/feature.xml | 2 +- server/features/io.cloudbeaver.server.feature/pom.xml | 2 +- server/features/io.cloudbeaver.ws.feature/feature.xml | 2 +- server/features/io.cloudbeaver.ws.feature/pom.xml | 2 +- server/pom.xml | 2 +- server/product/web-server/CloudbeaverServer.product | 2 +- server/product/web-server/pom.xml | 2 +- 37 files changed, 50 insertions(+), 50 deletions(-) diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index b1a8d173c5..226b75a8b0 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Model Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true -Bundle-Version: 1.0.61.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.62.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.model/pom.xml b/server/bundles/io.cloudbeaver.model/pom.xml index feddc91ae6..421eb0e899 100644 --- a/server/bundles/io.cloudbeaver.model/pom.xml +++ b/server/bundles/io.cloudbeaver.model/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.model - 1.0.61-SNAPSHOT + 1.0.62-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF index e352f3816b..f10b82c773 100644 --- a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Community Product Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 24.2.1.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 24.2.2.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.product.ce/pom.xml b/server/bundles/io.cloudbeaver.product.ce/pom.xml index daafc53e12..74fb225380 100644 --- a/server/bundles/io.cloudbeaver.product.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.product.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce - 24.2.1-SNAPSHOT + 24.2.2-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF index 8cbfa5fc84..1a26e7e53e 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Base JDBC drivers Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true -Bundle-Version: 1.0.106.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.107.qualifier +Bundle-Release-Date: 20241007 Bundle-Vendor: DBeaver Corp Bundle-ActivationPolicy: lazy Automatic-Module-Name: io.cloudbeaver.resources.drivers.base diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 6325eb04b9..8831414e5b 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.resources.drivers.base - 1.0.106-SNAPSHOT + 1.0.107-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 15de9b77ce..93455b8266 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Server Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 24.2.1.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 24.2.2.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index cf53c858bd..1b33a40193 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 24.2.1-SNAPSHOT + 24.2.2-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF index 9602219ee4..6b10865c0b 100644 --- a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Administration Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.105.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.106.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.admin/pom.xml b/server/bundles/io.cloudbeaver.service.admin/pom.xml index f200105f1a..cc6d869b22 100644 --- a/server/bundles/io.cloudbeaver.service.admin/pom.xml +++ b/server/bundles/io.cloudbeaver.service.admin/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.admin - 1.0.105-SNAPSHOT + 1.0.106-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF index 3481f6c816..fbc470b0f7 100644 --- a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Authentication Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.105.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.106.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.auth/pom.xml b/server/bundles/io.cloudbeaver.service.auth/pom.xml index b3c214f1f2..7bff8ea860 100644 --- a/server/bundles/io.cloudbeaver.service.auth/pom.xml +++ b/server/bundles/io.cloudbeaver.service.auth/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.auth - 1.0.105-SNAPSHOT + 1.0.106-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF index bcddeb66ac..f5250bd4cc 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Data Transfer Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.106.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.107.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml index aa3b788ab9..185a1d5222 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml +++ b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.data.transfer - 1.0.106-SNAPSHOT + 1.0.107-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF index 25acfd3be3..f87beec385 100644 --- a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - File System Bundle-SymbolicName: io.cloudbeaver.service.fs;singleton:=true -Bundle-Version: 1.0.23.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.24.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.fs/pom.xml b/server/bundles/io.cloudbeaver.service.fs/pom.xml index 8f6b7cae02..52d9291e87 100644 --- a/server/bundles/io.cloudbeaver.service.fs/pom.xml +++ b/server/bundles/io.cloudbeaver.service.fs/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.fs - 1.0.23-SNAPSHOT + 1.0.24-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF index 26f8bbe9d5..8f2fa9480d 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Metadata Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.109.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.110.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml index 52997551fa..f97daaaaaa 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/pom.xml +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.metadata - 1.0.109-SNAPSHOT + 1.0.110-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF index 737a39ad87..c2fbcd9eac 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Resource manager NIO implementation Bundle-SymbolicName: io.cloudbeaver.service.rm.nio;singleton:=true -Bundle-Version: 1.0.23.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.24.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml index aae7ab9f48..73a645ff48 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm.nio - 1.0.23-SNAPSHOT + 1.0.24-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF index c9829ed966..31ae2dab47 100644 --- a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Resource manager Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true -Bundle-Version: 1.0.58.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.59.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm/pom.xml b/server/bundles/io.cloudbeaver.service.rm/pom.xml index 8951e0b0f0..a46196fd8e 100644 --- a/server/bundles/io.cloudbeaver.service.rm/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm - 1.0.58-SNAPSHOT + 1.0.59-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF index 8a8a30cb1a..514653ed5f 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: Cloudbeaver Web Service - Security Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true -Bundle-Version: 1.0.61.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.62.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.security/pom.xml b/server/bundles/io.cloudbeaver.service.security/pom.xml index beea19d866..939a65afd6 100644 --- a/server/bundles/io.cloudbeaver.service.security/pom.xml +++ b/server/bundles/io.cloudbeaver.service.security/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.security - 1.0.61-SNAPSHOT + 1.0.62-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF index cfe182e73f..ed1d71160f 100644 --- a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: CloudBeaver SLF4j Binding Bundle-SymbolicName: io.cloudbeaver.slf4j;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20240923 +Bundle-Version: 1.0.22.qualifier +Bundle-Release-Date: 20241007 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.slf4j/pom.xml b/server/bundles/io.cloudbeaver.slf4j/pom.xml index ecf17e15b3..cbcfbb5d15 100644 --- a/server/bundles/io.cloudbeaver.slf4j/pom.xml +++ b/server/bundles/io.cloudbeaver.slf4j/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.slf4j - 1.0.21-SNAPSHOT + 1.0.22-SNAPSHOT eclipse-plugin diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml index f92eae44dc..77b67fe77c 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml index 38579f0ce9..b2d7dbda84 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.ce.drivers.feature - 1.0.129-SNAPSHOT + 1.0.130-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.product.ce.feature/feature.xml b/server/features/io.cloudbeaver.product.ce.feature/feature.xml index 2859ff7450..284670b63a 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/feature.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.product.ce.feature/pom.xml b/server/features/io.cloudbeaver.product.ce.feature/pom.xml index b711e83dba..0b891199c2 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/pom.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce.feature - 24.2.1-SNAPSHOT + 24.2.2-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index e3873e6d98..159f75d39a 100644 --- a/server/features/io.cloudbeaver.server.feature/feature.xml +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index d1eda2207a..3b8fb58670 100644 --- a/server/features/io.cloudbeaver.server.feature/pom.xml +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.server.feature - 24.2.1-SNAPSHOT + 24.2.2-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.ws.feature/feature.xml b/server/features/io.cloudbeaver.ws.feature/feature.xml index 7e39d57fb1..714ab2baf7 100644 --- a/server/features/io.cloudbeaver.ws.feature/feature.xml +++ b/server/features/io.cloudbeaver.ws.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ws.feature/pom.xml b/server/features/io.cloudbeaver.ws.feature/pom.xml index de5b10caaf..36705b3d43 100644 --- a/server/features/io.cloudbeaver.ws.feature/pom.xml +++ b/server/features/io.cloudbeaver.ws.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.ws.feature - 1.0.59-SNAPSHOT + 1.0.60-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index 840742f3a6..4337a2507b 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,7 +19,7 @@ CloudBeaver CE - 24.2.1 + 24.2.2 diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 72baeb5698..179c684197 100644 --- a/server/product/web-server/CloudbeaverServer.product +++ b/server/product/web-server/CloudbeaverServer.product @@ -2,7 +2,7 @@ diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index 22f71d98a5..ef175dce7d 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 24.2.1-SNAPSHOT + 24.2.2-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product From 3c5aa6b7e85371e41cf4ec96859644af1dab8a5b Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 18 Sep 2024 21:17:59 +0800 Subject: [PATCH 12/14] CB-5658 CB-5659 fix: pagination resolving (#2922) * CB-5658 CB-5659 fix: pagination resolving * CB-5658 fix resources pages * CB-5667 fix: node rename behavior * CB-5658 chore: refactor repeated code --------- Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../core-authentication/src/UsersResource.ts | 17 ++--- .../src/NodesManager/DBObjectResource.ts | 19 ++--- .../src/NodesManager/NavTreeResource.ts | 17 +++-- .../src/Resource/CachedResource.ts | 72 ++++++++++++------- .../src/Resource/ResourceAlias.ts | 2 +- .../src/Resource/ResourceAliases.ts | 45 +++++++++--- .../src/Resource/ResourceMetadata.ts | 8 ++- .../src/Resource/getOffsetPageKeyInfo.ts | 46 ++++++++++++ webapp/packages/core-resource/src/index.ts | 1 + 9 files changed, 153 insertions(+), 74 deletions(-) create mode 100644 webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 6e790e75a3..ea81f6d1b0 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -9,12 +9,11 @@ import { runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { - CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, - CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedMapAllKey, CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + getOffsetPageKeyInfo, isResourceAlias, type ResourceKey, resourceKeyList, @@ -260,19 +259,11 @@ export class UsersResource extends CachedMapResource user.userId), users.length === limit, ]); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index 17ef52750d..90f8d9c0d1 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -15,6 +15,7 @@ import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, CachedResourceOffsetPageTargetKey, + getOffsetPageKeyInfo, isResourceAlias, type ResourceKey, resourceKeyList, @@ -96,16 +97,8 @@ export class DBObjectResource extends CachedMapResource { } protected async loader(originalKey: ResourceKey): Promise> { - let limit = this.navTreeResource.childrenLimit; - let offset = CACHED_RESOURCE_DEFAULT_PAGE_OFFSET; const parentKey = this.aliases.isAlias(originalKey, DBObjectParentKey); - const pageKey = - this.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || this.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); - - if (pageKey) { - limit = pageKey.options.limit; - offset = pageKey.options.offset; - } + const { isPageListKey, offset, limit } = getOffsetPageKeyInfo(this, originalKey, undefined, this.navTreeResource.childrenLimit); if (parentKey) { const nodeId = parentKey.options.parentId; @@ -116,11 +109,11 @@ export class DBObjectResource extends CachedMapResource { this.set(resourceKeyList(keys), dbObjects); this.offsetPagination.setPage( - CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(originalKey)), + isPageListKey + ? CachedResourceOffsetPageListKey(offset, limit).setParent(parentKey || CachedResourceOffsetPageTargetKey(nodeId)) + : CachedResourceOffsetPageKey(offset, limit).setParent(parentKey || CachedResourceOffsetPageTargetKey(nodeId)), keys, - this.navTreeResource.offsetPagination.hasNextPage( - CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(nodeId)), - ), + keys.length === limit, ); }); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index 4f52088d9a..5f996e2976 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -12,12 +12,12 @@ import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutionContext, IExecutor } from '@cloudbeaver/core-executor'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { - CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedMapAllKey, CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, CachedResourceOffsetPageTargetKey, + getOffsetPageKeyInfo, type ICachedResourceMetadata, isResourceAlias, isResourceKeyList, @@ -275,9 +275,10 @@ export class NavTreeResource extends CachedMapResource): Promise> { - const pageKey = - this.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || this.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); + const { isPageListKey, pageTargetKey, offset, limit } = getOffsetPageKeyInfo(this, originalKey, undefined, this.childrenLimit); const allKey = this.aliases.isAlias(originalKey, CachedMapAllKey); - const pageTarget = this.aliases.isAlias(originalKey, CachedResourceOffsetPageTargetKey); if (allKey) { throw new Error('Loading all nodes is prohibited'); } - const offset = pageKey?.options.offset ?? CACHED_RESOURCE_DEFAULT_PAGE_OFFSET; - const limit = pageKey?.options.limit ?? this.childrenLimit; const values: NavNodeChildrenQuery[] = []; const pages: Parameters[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { - const nodeId = pageTarget?.options?.target ?? key; + const nodeId = pageTargetKey ?? key; const navNodeChildren = await this.loadNodeChildren(nodeId, offset, limit); values.push(navNodeChildren); pages.push([ - CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + isPageListKey + ? CachedResourceOffsetPageListKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)) + : CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)), navNodeChildren.navNodeChildren.map(node => node.id), navNodeChildren.navNodeChildren.length === limit, ]); diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 50f8a914c9..e37a73e0ed 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -34,7 +34,7 @@ import { isResourceAlias } from './ResourceAlias'; import { ResourceError } from './ResourceError'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { resourceKeyAlias } from './ResourceKeyAlias'; -import { resourceKeyList } from './ResourceKeyList'; +import { isResourceKeyList, resourceKeyList } from './ResourceKeyList'; import { resourceKeyListAlias } from './ResourceKeyListAlias'; import { ResourceOffsetPagination } from './ResourceOffsetPagination'; @@ -91,41 +91,58 @@ export abstract class CachedResource< this.aliases.add(CachedResourceParamKey, () => defaultKey); this.aliases.add(CachedResourceListEmptyKey, () => resourceKeyList([])); this.aliases.add(CachedResourceOffsetPageTargetKey, key => key.options.target); - this.aliases.add(CachedResourceOffsetPageKey, key => { - const keys = []; - const pageInfo = this.offsetPagination.getPageInfo(key); + this.aliases.add( + CachedResourceOffsetPageListKey, + key => key.parent! as any, + (param, key) => { + if (!isResourceKeyList(key)) { + return key as any; + } - if (pageInfo) { - const from = key.options.offset; - const to = key.options.offset + key.options.limit; + const keys = new Set(); + const pageInfo = this.offsetPagination.getPageInfo(param); - for (const page of pageInfo.pages) { - if (page.isHasCommonSegment(from, to)) { - keys.push(...page.get(from, to)); + if (pageInfo) { + const from = param.options.offset; + const to = param.options.offset + param.options.limit; + + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + for (const pageKey of page.get(from, to)) { + keys.add(pageKey); + } + } } } - } + return resourceKeyList(key.filter(value => keys.has(value))); + }, + ); + this.aliases.add( + CachedResourceOffsetPageKey, + key => key.parent! as any, + (param, key) => { + if (!isResourceKeyList(key)) { + return key as any; + } - // todo: return single element? - return resourceKeyList([...new Set(keys)]); - }); - this.aliases.add(CachedResourceOffsetPageListKey, key => { - const keys = []; - const pageInfo = this.offsetPagination.getPageInfo(key); + const keys = new Set(); + const pageInfo = this.offsetPagination.getPageInfo(param); - if (pageInfo) { - const from = key.options.offset; - const to = key.options.offset + key.options.limit; + if (pageInfo) { + const from = param.options.offset; + const to = param.options.offset + param.options.limit; - for (const page of pageInfo.pages) { - if (page.isHasCommonSegment(from, to)) { - keys.push(...page.get(from, to)); + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + for (const pageKey of page.get(from, to)) { + keys.add(pageKey); + } + } } } - } - - return resourceKeyList([...new Set(keys)]); - }); + return resourceKeyList(key.filter(value => keys.has(value))); + }, + ); // this.logger.spy(this.beforeLoad, 'beforeLoad'); // this.logger.spy(this.onDataOutdated, 'onDataOutdated'); @@ -329,6 +346,7 @@ export abstract class CachedResource< } const pageKey = this.aliases.isAlias(param, CachedResourceOffsetPageKey) || this.aliases.isAlias(param, CachedResourceOffsetPageListKey); + if (pageKey) { const pageInfo = this.offsetPagination.getPageInfo(pageKey); diff --git a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts index ed23a86b95..ca8f17372f 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts @@ -42,7 +42,7 @@ export abstract class ResourceAlias return undefined; } - setParent(parent: ResourceAlias): this { + setParent(parent: ResourceAlias | undefined): 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; diff --git a/webapp/packages/core-resource/src/Resource/ResourceAliases.ts b/webapp/packages/core-resource/src/Resource/ResourceAliases.ts index fa608783c9..c58f340918 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceAliases.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceAliases.ts @@ -8,7 +8,7 @@ import { toJS } from 'mobx'; import { isResourceAlias, type ResourceAlias, ResourceAliasFactory, type ResourceAliasOptions } from './ResourceAlias'; -import type { ResourceKey } from './ResourceKey'; +import type { ResourceKey, ResourceKeySimple } from './ResourceKey'; import type { ResourceKeyAlias } from './ResourceKeyAlias'; import { isResourceKeyList, ResourceKeyList } from './ResourceKeyList'; import type { ResourceKeyListAlias } from './ResourceKeyListAlias'; @@ -16,14 +16,25 @@ import type { ResourceLogger } from './ResourceLogger'; export type IParamAlias = { id: string; - getAlias: (param: ResourceAlias) => ResourceKey; + getAlias: ResourceAliasResolver; + transformKey?: ResourceAliasKeyTransformer; }; +export type ResourceAliasResolver = (param: ResourceAlias) => ResourceKey; + +export type ResourceAliasKeyTransformer = >( + param: ResourceAlias, + key: T, +) => T; + export class ResourceAliases { protected paramAliases: Array>; private captureAliasGetterExecution: boolean; - constructor(private readonly logger: ResourceLogger, private readonly validateKey: (key: TKey) => boolean) { + constructor( + private readonly logger: ResourceLogger, + private readonly validateKey: (key: TKey) => boolean, + ) { this.paramAliases = []; this.captureAliasGetterExecution = false; @@ -52,21 +63,23 @@ export class ResourceAliases { add( param: ResourceAlias | ResourceAliasFactory, - getAlias: (param: ResourceAlias) => ResourceKey, + getAlias: ResourceAliasResolver, + transformKey?: ResourceAliasKeyTransformer, ): void { - this.paramAliases.push({ id: param.id, getAlias }); + this.paramAliases.push({ id: param.id, getAlias, transformKey }); } replace( param: ResourceAlias | ResourceAliasFactory, - getAlias: (param: ResourceAlias) => ResourceKey, + getAlias: ResourceAliasResolver, + transformKey?: ResourceAliasKeyTransformer, ): void { const indexOf = this.paramAliases.findIndex(aliasInfo => aliasInfo.id === param.id); if (indexOf === -1) { - this.add(param, getAlias); + this.add(param, getAlias, transformKey); } else { - this.paramAliases.splice(indexOf, 1, { id: param.id, getAlias }); + this.paramAliases.splice(indexOf, 1, { id: param.id, getAlias, transformKey }); } } @@ -111,6 +124,7 @@ export class ResourceAliases { transformToKey(param: ResourceKey): TKey | ResourceKeyList { let deep = 0; + const transforms: Array<{ key: ResourceKeyAlias | ResourceKeyListAlias; alias: IParamAlias }> = []; while (deep < 10) { if (!this.validateResourceKey(param)) { let paramString = JSON.stringify(toJS(param)); @@ -124,7 +138,15 @@ export class ResourceAliases { if (isResourceAlias(param)) { for (const alias of this.paramAliases) { if (alias.id === param.id) { - param = this.captureGetAlias(alias, param); + transforms.push({ key: param, alias }); + const nextParam = this.captureGetAlias(alias, param); + + if (isResourceAlias(nextParam)) { + param = nextParam.setParent(param); + } else { + param = nextParam; + } + deep++; break; } @@ -141,6 +163,11 @@ export class ResourceAliases { if (isResourceAlias(param)) { throw new Error(`Alias ${param.toString()} is not registered in ${this.logger.getName()}`); } + + for (const { key, alias } of transforms) { + param = alias.transformKey ? alias.transformKey(key, param) : param; + } + return param; } diff --git a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts index b5c9c67039..74179e1d87 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts @@ -11,7 +11,7 @@ import { DefaultValueGetter, isPrimitive, MetadataMap } from '@cloudbeaver/core- import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; -import { isResourceAlias } from './ResourceAlias'; +import { isResourceAlias, ResourceAlias } from './ResourceAlias'; import type { ResourceAliases } from './ResourceAliases'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { isResourceKeyList, ResourceKeyList } from './ResourceKeyList'; @@ -111,6 +111,8 @@ export class ResourceMetadata { if (this.some(param, predicate)) { result = true; } + } else if (predicate(this.get(param))) { + result = true; } } @@ -194,11 +196,11 @@ export class ResourceMetadata { if (isResourceAlias(key)) { key = this.aliases.transformToAlias(key); - if (this.aliases.isAlias(key, CachedResourceOffsetPageKey) || this.aliases.isAlias(key, CachedResourceOffsetPageListKey)) { + if (isResourceAlias(key, CachedResourceOffsetPageKey) || isResourceAlias(key, CachedResourceOffsetPageListKey)) { return this.getMetadataKeyRef(key.parent as any); } - return key.toString() as TKey; + return (key as ResourceAlias).toString() as TKey; } if (isPrimitive(key)) { diff --git a/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts b/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts new file mode 100644 index 0000000000..c51ceae077 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts @@ -0,0 +1,46 @@ +/* + * 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 { CachedResource } from './CachedResource'; +import { + CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, + CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, + CachedResourceOffsetPageKey, + CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, +} from './CachedResourceOffsetPageKeys'; +import { CachedResourceKey } from './IResource'; + +interface IOffsetPageKeyInfo { + limit: number; + offset: number; + isPageListKey: boolean; + pageTargetKey?: any; +} + +export function getOffsetPageKeyInfo( + resource: CachedResource, + originalKey: CachedResourceKey, + offset = CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, + limit = CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, +): IOffsetPageKeyInfo { + const pageListKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); + const pageKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || pageListKey; + const pageTargetKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageTargetKey); + + if (pageKey) { + limit = pageKey.options.limit; + offset = pageKey.options.offset; + } + + return { + limit, + offset, + isPageListKey: !!pageListKey, + pageTargetKey: pageTargetKey?.options.target, + }; +} diff --git a/webapp/packages/core-resource/src/index.ts b/webapp/packages/core-resource/src/index.ts index 6ff9ce3edd..ca7ebd42d6 100644 --- a/webapp/packages/core-resource/src/index.ts +++ b/webapp/packages/core-resource/src/index.ts @@ -18,6 +18,7 @@ export { getNextPageOffset, type ICachedResourceOffsetPageOptions, } from './Resource/CachedResourceOffsetPageKeys'; +export * from './Resource/getOffsetPageKeyInfo'; export * from './Resource/CachedTreeResource/CachedTreeResource'; export * from './Resource/CachedTreeResource/ICachedTreeMoveData'; export * from './Resource/ICachedResourceMetadata'; From 12e0107617f78588341c4d88381569bc07c7c794 Mon Sep 17 00:00:00 2001 From: serge-rider Date: Wed, 18 Sep 2024 15:40:15 +0200 Subject: [PATCH 13/14] 24.2.2 version bump --- webapp/packages/product-default/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 95ad13e66a..4bd8b88606 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -5,7 +5,7 @@ "src/**/*.scss", "public/**/*" ], - "version": "24.2.1", + "version": "24.2.2", "description": "CloudBeaver Community", "license": "Apache-2.0", "main": "dist/index.js", From 46309e9505d060008088e934a12a7c133376df43 Mon Sep 17 00:00:00 2001 From: Anastasiya <45152336+LonwoLonwo@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:58:39 +0300 Subject: [PATCH 14/14] Update README.md link (#2923) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5bcf48ae8f..2149dc9704 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,6 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Contribution As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests. -[We are happy to reward](https://dbeaver.com/help-beaver/) our most active contributors every major sprint. +[We are happy to reward](https://dbeaver.com/help-dbeaver/) our most active contributors every major sprint. The most significant contribution to our code for the major release 24.2.0 was made by: 1. [matthieukhl](https://github.com/matthieukhl)