From 8f740b732bad48f311c1ebf276b43c348baaac75 Mon Sep 17 00:00:00 2001 From: Georgii Date: Tue, 2 Jul 2024 17:02:30 +0400 Subject: [PATCH 01/15] dbeaver/dbeaver-infra#98 build imls for optional plugins (#2752) --- osgi-app.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osgi-app.properties b/osgi-app.properties index 1a657ff267..527e3a5737 100644 --- a/osgi-app.properties +++ b/osgi-app.properties @@ -23,4 +23,6 @@ testBundlePaths=\ dbeaver/test;\ cloudbeaver/server/test; additionalModuleRoots=\ - opt; \ No newline at end of file + opt; +optionalFeatureRepositories=\ + dbeaver/product/repositories \ No newline at end of file From 6efbd6904aaa85904338a578592a4635648a7112 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Tue, 2 Jul 2024 16:04:58 +0300 Subject: [PATCH 02/15] dbeaver/pro#2651 Add all resource properties updating (#2747) * dbeaver/pro#2651 Add all resource properties updating * dbeaver/pro#2651 Fix code style --------- Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com> --- .../rm/local/LocalResourceController.java | 21 +++++++++++++++++++ .../service/rm/impl/WebServiceRM.java | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 1a20c860cc..86b865013c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -836,6 +836,27 @@ public String setResourceProperty( } } + @NotNull + @Override + public String setResourceProperties( + @NotNull String projectId, + @NotNull String resourcePath, + @NotNull Map properties + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) { + validateResourcePath(resourcePath); + BaseWebProjectImpl webProject = getWebProject(projectId, false); + doFileWriteOperation(projectId, webProject.getMetadataFilePath(), + () -> { + log.debug("Updating resource '" + resourcePath + "' properties in project '" + projectId + "'"); + webProject.setResourceProperties(resourcePath, properties); + return null; + } + ); + return DEFAULT_CHANGE_ID; + } + } + private void validateResourcePath(String resourcePath) throws DBException { var fullPath = Paths.get(resourcePath); for (Path path : fullPath) { diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 19d73fd036..8af78a4819 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -105,7 +105,7 @@ public RMResource[] listResources(@NotNull WebSession webSession, * @param resourcePath the resource path * @param propertyName the property name * @param propertyValue the property value - * @return the resource property + * @return true on success * @throws DBException the db exception */ @NotNull From faafcee1a412c6993fae6227ba2df46a5a1d4861 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Tue, 2 Jul 2024 15:48:37 +0200 Subject: [PATCH 03/15] CB-4961 Control files downloading ability (#2744) * CB-4961 adds "disable download" setting to data viewer * CB-4691 removes unneeded alias for new disable download setting * CB-4691 pr fixes * CB-4961 reverts image value presentation canSave logic * CB-4961 disables download from image context menu * CB-4691 fixes unit tests * CB-4691 change new setting to existing one * CB-4691 renames disable download -> disable export * CB-4691 pr fixes * CB-4691 updates tests for new export setting --------- Co-authored-by: Alexey Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- webapp/packages/core-sdk/package.json | 1 + .../src/DATA_EXPORT_SETTINGS_GROUP.ts | 10 -- .../src/DataExportMenuService.ts | 20 +--- .../src/DataExportSettingsService.test.ts | 109 ------------------ .../src/DataExportSettingsService.ts | 58 ---------- .../plugin-data-export/src/manifest.ts | 1 - .../DataGridContextMenuSaveContentService.ts | 12 +- .../src/DataViewerService.ts | 4 + .../src/DataViewerSettingsService.test.ts | 7 ++ .../src/DataViewerSettingsService.ts | 20 ++++ .../ImageValue/ImageValuePresentation.tsx | 19 ++- .../ImageValue/useValuePanelImageValue.ts | 8 +- .../TextValue/TextValuePresentation.tsx | 4 +- .../plugin-data-viewer/src/locales/de.ts | 2 + .../plugin-data-viewer/src/locales/en.ts | 2 + .../plugin-data-viewer/src/locales/fr.ts | 2 + .../plugin-data-viewer/src/locales/it.ts | 2 + .../plugin-data-viewer/src/locales/ru.ts | 2 + .../plugin-data-viewer/src/locales/zh.ts | 2 + 19 files changed, 85 insertions(+), 200 deletions(-) delete mode 100644 webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts delete mode 100644 webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts delete mode 100644 webapp/packages/plugin-data-export/src/DataExportSettingsService.ts diff --git a/webapp/packages/core-sdk/package.json b/webapp/packages/core-sdk/package.json index a2e8b435b8..529d724252 100644 --- a/webapp/packages/core-sdk/package.json +++ b/webapp/packages/core-sdk/package.json @@ -25,6 +25,7 @@ "@cloudbeaver/core-executor": "^0", "@cloudbeaver/core-utils": "^0", "axios": "^1", + "graphql": "^16", "graphql-request": "^6", "mobx": "^6" }, diff --git a/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts b/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts deleted file mode 100644 index 991978c9df..0000000000 --- a/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts +++ /dev/null @@ -1,10 +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 { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; - -export const DATA_EXPORT_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_data_export_data_export_settings_group'); diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index a488fb5c6e..f42885a63b 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -11,7 +11,6 @@ import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigation-tree'; -import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { withTimestamp } from '@cloudbeaver/core-utils'; import { ACTION_EXPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { @@ -20,24 +19,22 @@ import { DATA_CONTEXT_DV_PRESENTATION, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, DataViewerPresentationType, + DataViewerService, IDatabaseDataSource, IDataContainerOptions, } from '@cloudbeaver/plugin-data-viewer'; import type { IDataQueryOptions } from '@cloudbeaver/plugin-sql-editor'; -import { DataExportSettingsService } from './DataExportSettingsService'; - const DataExportDialog = importLazyComponent(() => import('./Dialog/DataExportDialog').then(module => module.DataExportDialog)); @injectable() export class DataExportMenuService { constructor( private readonly commonDialogService: CommonDialogService, - private readonly dataExportSettingsService: DataExportSettingsService, private readonly actionService: ActionService, private readonly menuService: MenuService, - private readonly sessionPermissionsResource: SessionPermissionsResource, private readonly localizationService: LocalizationService, + private readonly dataViewerService: DataViewerService, ) {} register(): void { @@ -46,7 +43,7 @@ export class DataExportMenuService { contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], isApplicable: context => { const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); - return !this.isExportDisabled() && (!presentation || presentation.type === DataViewerPresentationType.Data); + return this.dataViewerService.canExportData && (!presentation || presentation.type === DataViewerPresentationType.Data); }, getItems(context, items) { return [...items, ACTION_EXPORT]; @@ -60,6 +57,7 @@ export class DataExportMenuService { id: 'data-export-base-handler', menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isHidden: (context, action) => !this.dataViewerService.canExportData, actions: [ACTION_EXPORT], isDisabled(context) { const model = context.get(DATA_CONTEXT_DV_DDM)!; @@ -118,7 +116,7 @@ export class DataExportMenuService { return false; } - return !this.isExportDisabled() && context.has(DATA_CONTEXT_CONNECTION); + return this.dataViewerService.canExportData && context.has(DATA_CONTEXT_CONNECTION); }, getItems: (context, items) => [...items, ACTION_EXPORT], }); @@ -141,12 +139,4 @@ export class DataExportMenuService { }, }); } - - private isExportDisabled() { - if (this.sessionPermissionsResource.has(EAdminPermission.admin)) { - return false; - } - - return this.dataExportSettingsService.disabled; - } } diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts deleted file mode 100644 index b8bbf3fcfb..0000000000 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts +++ /dev/null @@ -1,109 +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 '@testing-library/jest-dom'; - -import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; -import { coreAppManifest } from '@cloudbeaver/core-app'; -import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; -import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; -import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; -import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; -import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { coreProjectsManifest } from '@cloudbeaver/core-projects'; -import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; -import { coreRoutingManifest } from '@cloudbeaver/core-routing'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; -import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { - expectDeprecatedSettingMessage, - expectNoDeprecatedSettingMessage, -} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; -import { coreStorageManifest } from '@cloudbeaver/core-storage'; -import { coreUIManifest } from '@cloudbeaver/core-ui'; -import { coreViewManifest } from '@cloudbeaver/core-view'; -import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; -import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; -import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; -import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; -import { createApp } from '@cloudbeaver/tests-runner'; - -import { DataExportSettingsService } from './DataExportSettingsService'; -import { dataExportManifest } from './manifest'; - -const endpoint = createGQLEndpoint(); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); -const app = createApp( - dataExportManifest, - coreLocalizationManifest, - coreEventsManifest, - coreRootManifest, - coreSDKManifest, - coreBrowserManifest, - coreSettingsManifest, - coreStorageManifest, - coreViewManifest, - coreAuthenticationManifest, - coreProjectsManifest, - coreUIManifest, - coreRoutingManifest, - coreAdministrationManifest, - coreConnectionsManifest, - coreDialogsManifest, - coreNavigationTree, - coreAppManifest, - datasourceContextSwitchPluginManifest, - navigationTreePlugin, - navigationTabsPlugin, - objectViewerManifest, - coreClientActivityManifest, -); - -const testValueA = true; -const testValueB = true; - -const deprecatedSettings = { - 'plugin_data_export.disabled': testValueB, -}; - -const newSettings = { - ...deprecatedSettings, - 'plugin.data-export.disabled': testValueA, -}; - -test('New settings override deprecated', async () => { - const settings = app.injector.getServiceByClass(DataExportSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(newSettings))); - - await config.refresh(); - - expect(settings.disabled).toBe(testValueA); - expectNoDeprecatedSettingMessage(); -}); - -test('Deprecated settings are used if new settings are not defined', async () => { - const settings = app.injector.getServiceByClass(DataExportSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(deprecatedSettings))); - - await config.refresh(); - - expect(settings.disabled).toBe(testValueB); - expectDeprecatedSettingMessage(); -}); diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts deleted file mode 100644 index 5210503545..0000000000 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts +++ /dev/null @@ -1,58 +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 { Dependency, injectable } from '@cloudbeaver/core-di'; -import { - createSettingsAliasResolver, - ROOT_SETTINGS_LAYER, - SettingsManagerService, - SettingsProvider, - SettingsProviderService, - SettingsResolverService, -} from '@cloudbeaver/core-settings'; -import { schema, schemaExtra } from '@cloudbeaver/core-utils'; - -const defaultSettings = schema.object({ - 'plugin.data-export.disabled': schemaExtra.stringedBoolean().default(false), -}); - -export type DataExportSettings = schema.infer; - -@injectable() -export class DataExportSettingsService extends Dependency { - get disabled(): boolean { - return this.settings.getValue('plugin.data-export.disabled'); - } - readonly settings: SettingsProvider; - - constructor( - private readonly settingsProviderService: SettingsProviderService, - private readonly settingsManagerService: SettingsManagerService, - private readonly settingsResolverService: SettingsResolverService, - ) { - super(); - this.settings = this.settingsProviderService.createSettings(defaultSettings); - this.settingsResolverService.addResolver( - ROOT_SETTINGS_LAYER, - /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'plugin.data-export.disabled': 'plugin_data_export.disabled' }), - ); - - this.registerSettings(); - } - - private registerSettings() { - this.settingsManagerService.registerSettings(this.settings, () => [ - // { - // group: DATA_EXPORT_SETTINGS_GROUP, - // key: 'disabled', - // type: ESettingsValueType.Checkbox, - // name: 'Disable data export', - // }, - ]); - } -} diff --git a/webapp/packages/plugin-data-export/src/manifest.ts b/webapp/packages/plugin-data-export/src/manifest.ts index 2c58f37f4f..c2a8e77884 100644 --- a/webapp/packages/plugin-data-export/src/manifest.ts +++ b/webapp/packages/plugin-data-export/src/manifest.ts @@ -15,7 +15,6 @@ export const dataExportManifest: PluginManifest = { providers: [ () => import('./Bootstrap').then(m => m.Bootstrap), () => import('./DataExportMenuService').then(m => m.DataExportMenuService), - () => import('./DataExportSettingsService').then(m => m.DataExportSettingsService), () => import('./DataExportService').then(m => m.DataExportService), () => import('./DataExportProcessService').then(m => m.DataExportProcessService), () => import('./DataTransferProcessorsResource').then(m => m.DataTransferProcessorsResource), diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts index f0296277a1..70b61d8a7b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts @@ -8,7 +8,13 @@ import { selectFiles } from '@cloudbeaver/core-browser'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { createResultSetBlobValue, ResultSetDataContentAction, ResultSetEditAction, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer'; +import { + createResultSetBlobValue, + DataViewerService, + ResultSetDataContentAction, + ResultSetEditAction, + ResultSetFormatAction, +} from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService } from './DataGridContextMenuService'; @@ -17,6 +23,7 @@ export class DataGridContextMenuSaveContentService { constructor( private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly notificationService: NotificationService, + private readonly dataViewerService: DataViewerService, ) {} register(): void { @@ -38,7 +45,8 @@ export class DataGridContextMenuSaveContentService { }, isHidden: context => { const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); - return !content.isDownloadable(context.data.key); + + return !content.isDownloadable(context.data.key) || !this.dataViewerService.canExportData; }, isDisabled: context => { const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts index 0a93eeb119..03f96962e3 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts @@ -17,6 +17,10 @@ export class DataViewerService { return this.sessionPermissionsResource.has(EAdminPermission.admin) || !this.dataViewerSettingsService.disableCopyData; } + get canExportData() { + return this.sessionPermissionsResource.has(EAdminPermission.admin) || !this.dataViewerSettingsService.disableExportData; + } + constructor( private readonly dataViewerSettingsService: DataViewerSettingsService, private readonly sessionPermissionsResource: SessionPermissionsResource, diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts index e93a0de0cc..6cefb37dd4 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts @@ -77,11 +77,14 @@ const testValueNew = false; const deprecatedSettings = { 'core.app.dataViewer.disableEdit': testValueDeprecated, + 'plugin.data-viewer.disabled': testValueDeprecated, + 'plugin_data_export.disabled': testValueDeprecated, }; const newSettings = { ...deprecatedSettings, 'plugin.data-viewer.disableEdit': testValueNew, + 'plugin.data-viewer.export.disabled': testValueNew, }; async function setupSettingsService(mockConfig: any = {}) { @@ -99,6 +102,8 @@ test('New settings override deprecated settings', async () => { const settingsService = await setupSettingsService(newSettings); expect(settingsService.disableEdit).toBe(testValueNew); + expect(settingsService.disableExportData).toBe(testValueNew); + expectNoDeprecatedSettingMessage(); }); @@ -106,6 +111,8 @@ test('Deprecated settings are used if new settings are not defined', async () => const settingsService = await setupSettingsService(deprecatedSettings); expect(settingsService.disableEdit).toBe(testValueDeprecated); + expect(settingsService.disableExportData).toBe(testValueDeprecated); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts index 89881ba7e6..ecd33d456a 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts @@ -31,6 +31,7 @@ const defaultSettings = schema.object({ 'plugin.data-viewer.fetchMin': schema.coerce.number().min(FETCH_MIN).default(DEFAULT_FETCH_SIZE), 'plugin.data-viewer.fetchMax': schema.coerce.number().min(FETCH_MIN).default(FETCH_MAX), 'resultset.maxrows': schema.coerce.number().min(FETCH_MIN).max(FETCH_MAX).default(DEFAULT_FETCH_SIZE), + 'plugin.data-viewer.export.disabled': schemaExtra.stringedBoolean().default(false), }); export type DataViewerSettings = schema.infer; @@ -45,6 +46,10 @@ export class DataViewerSettingsService extends Dependency { return this.settings.getValue('plugin.data-viewer.disableCopyData'); } + get disableExportData(): boolean { + return this.settings.getValue('plugin.data-viewer.export.disabled'); + } + get maxFetchSize(): number { return this.settings.getValue('plugin.data-viewer.fetchMax'); } @@ -75,12 +80,17 @@ export class DataViewerSettingsService extends Dependency { 'plugin.data-viewer.disableCopyData': 'core.app.dataViewer.disableCopyData', 'plugin.data-viewer.fetchMin': 'core.app.dataViewer.fetchMin', 'plugin.data-viewer.fetchMax': 'core.app.dataViewer.fetchMax', + 'plugin.data-viewer.export.disabled': 'plugin.data-export.disabled', 'resultset.maxrows': 'core.app.dataViewer.fetchDefault', }), /** @deprecated Use settings instead, will be removed in 25.0.0 */ createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'resultset.maxrows': 'plugin.data-viewer.fetchDefault', }), + /** @deprecated Use settings instead, will be removed in 23.0.0 */ + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.data-viewer.export.disabled': 'plugin_data_export.disabled', + }), ); this.registerSettings(); @@ -147,6 +157,16 @@ export class DataViewerSettingsService extends Dependency { description: 'settings_data_editor_fetch_max_description', group: DATA_EDITOR_SETTINGS_GROUP, }, + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-viewer.export.disabled', + type: ESettingsValueType.Checkbox, + name: 'settings_data_editor_disable_data_export_name', + description: 'settings_data_editor_disable_data_export_description', + access: { + scope: ['server'], + }, + }, ]; if (!this.serverSettingsManagerService.providedSettings.has('resultset.maxrows')) { diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index e32b393f31..4e89471c42 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -57,11 +57,23 @@ export const ImageValuePresentation: TabContainerPanelComponent) { + if (!data.canSave) { + event.preventDefault(); + } + } + return ( - {data.src && } + {data.src && ( + + )} {isTruncatedMessageDisplay && ( {isDownloadable && ( @@ -99,14 +111,15 @@ export const ImageValuePresentation: TabContainerPanelComponent string | null; + onContextMenu?: (event: React.MouseEvent) => void; } -export const ImageRenderer = observer(function ImageRenderer({ srcGetter, className }) { +export const ImageRenderer = observer(function ImageRenderer({ srcGetter, className, onContextMenu }) { const src = srcGetter(); if (!src) { return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts index 2beb0c44ce..1990675532 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts @@ -24,6 +24,7 @@ import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import { DataViewerService } from '../../DataViewerService'; interface Props { model: IDatabaseDataModel; @@ -32,6 +33,7 @@ interface Props { export function useValuePanelImageValue({ model, resultIndex }: Props) { const notificationService = useService(NotificationService); + const dataViewerService = useService(DataViewerService); const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); @@ -88,6 +90,10 @@ export function useValuePanelImageValue({ model, resultIndex }: Props) { return this.contentAction.retrieveBlobFromCache(this.selectedCell); }, get canSave() { + if (!this.dataViewerService.canExportData) { + return false; + } + if (this.truncated && this.selectedCell) { return this.contentAction.isDownloadable(this.selectedCell); } @@ -158,6 +164,6 @@ export function useValuePanelImageValue({ model, resultIndex }: Props) { upload: action.bound, loadFullImage: action.bound, }, - { model, resultIndex, notificationService, selectAction, formatAction, contentAction, editAction }, + { model, resultIndex, notificationService, selectAction, formatAction, contentAction, editAction, dataViewerService }, ); } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx index 8ee7861d22..7cbe46a391 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -18,6 +18,7 @@ import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/R import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import { DataViewerService } from '../../DataViewerService'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { getDefaultLineWrapping } from './getDefaultLineWrapping'; import { isTextValueReadonly } from './isTextValueReadonly'; @@ -36,6 +37,7 @@ export const TextValuePresentation: TabContainerPanelComponent Date: Tue, 2 Jul 2024 19:04:42 +0300 Subject: [PATCH 04/15] CB-5138 force use default properties for default database (#2751) * CB-5138 force use default properties for default database * CB-5138 review fix --------- Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../service/security/db/CBDatabase.java | 17 ++++++++++++++++- .../service/security/db/WebDatabaseConfig.java | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index 609d1fe54e..ea1a858221 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -127,8 +127,17 @@ public void initialize() throws DBException { LoggingProgressMonitor monitor = new LoggingProgressMonitor(log); + if (isDefaultH2Configuration(databaseConfiguration)) { + //force use default values even if they are explicitly specified + databaseConfiguration.setUser(null); + databaseConfiguration.setPassword(null); + databaseConfiguration.setSchema(null); + } + String dbUser = databaseConfiguration.getUser(); String dbPassword = databaseConfiguration.getPassword(); + String schemaName = databaseConfiguration.getSchema(); + if (CommonUtils.isEmpty(dbUser) && driver.isEmbedded()) { File pwdFile = application.getDataDirectory(true).resolve(DEFAULT_DB_PWD_FILE).toFile(); if (!driver.isAnonymousAccess()) { @@ -191,7 +200,6 @@ public void initialize() throws DBException { DatabaseMetaData metaData = connection.getMetaData(); log.debug("\tConnected to " + metaData.getDatabaseProductName() + " " + metaData.getDatabaseProductVersion()); - var schemaName = databaseConfiguration.getSchema(); if (dialect instanceof SQLDialectSchemaController && CommonUtils.isNotEmpty(schemaName)) { var dialectSchemaController = (SQLDialectSchemaController) dialect; var schemaExistQuery = dialectSchemaController.getSchemaExistQuery(schemaName); @@ -578,4 +586,11 @@ public SQLDialect getDialect() { return dialect; } + public static boolean isDefaultH2Configuration(WebDatabaseConfig databaseConfiguration) { + var v1DefaultUrl = "jdbc:h2:/opt/cloudbeaver/workspace/.data/" + V1_DB_NAME; + var v2DefaultUrl = "jdbc:h2:/opt/cloudbeaver/workspace/.data/" + V2_DB_NAME; + return v1DefaultUrl.equals(databaseConfiguration.getUrl()) + || v2DefaultUrl.equals(databaseConfiguration.getUrl()); + } + } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java index da58362d03..fb7243462a 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java @@ -84,4 +84,16 @@ public boolean isBackupEnabled() { public String getSchema() { return schema; } + + void setPassword(String password) { + this.password = password; + } + + void setSchema(String schema) { + this.schema = schema; + } + + void setUser(String user) { + this.user = user; + } } From 042da7cc7c858eda2e00acbd0d1a8e6dac62026f Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Tue, 2 Jul 2024 21:00:02 +0300 Subject: [PATCH 05/15] CB-5323 less priority for .product.runtime.conf (#2754) Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../cloudbeaver/server/CBServerConfigurationController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java index 8e6a76f602..f5185fcc6e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -253,8 +253,8 @@ protected void readProductConfiguration(Map serverConfig, Gson g try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { var runtimeProductSettings = JSONUtils.parseMap(gson, reader); var productSettings = serverConfiguration.getProductSettings(); - productSettings.putAll(runtimeProductSettings); - Map flattenConfig = WebAppUtils.flattenMap(productSettings); + runtimeProductSettings.putAll(productSettings); + Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); productSettings.clear(); productSettings.putAll(flattenConfig); } catch (Exception e) { From e25960642238e3f83ee055e16c14fa260a402744 Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 2 Jul 2024 21:00:39 +0300 Subject: [PATCH 06/15] CB-4885 feat: components registry (#2745) * CB-4885 feat: components registry * CB-4885 fix: table data loading behaviour * CB-4885 chore: remove unused comments * CB-4885 fix: dispose tables correctly * CB-4885 fix: cache invalidation * CB-4885 fix: nav tree state persistence * CB-4885 fix: database disconnect detection * CB-4885 fix: auto refresh on error * CB-4885 fix: auto refresh behaviour * CB-4885 fix: auto refresh behaviour * CB-4885 fix: top menu bar styles * CB-4885 fix: auto refresh error handling * CB-4885 chore: add comment about implementation --------- Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com> --- .../src/AuthInfoService.ts | 4 + .../core-authentication/src/UsersResource.ts | 3 +- .../core-blocks/public/icons/refresh_m.svg | 4 +- .../core-blocks/public/icons/timer_m.svg | 12 +- .../src/ComponentsRegistry/CRegistry.tsx | 29 +++ .../src/ComponentsRegistry/CRegistryList.ts | 13 ++ .../ComponentsRegistry/CRegistryLoader.ts} | 7 +- .../ComponentsRegistryContext.ts | 12 + .../ComponentsTreeContext.ts | 12 + .../IComponentsTreeNode.ts} | 7 +- .../IComponentsTreeNodeValidator.ts | 13 ++ .../src/ComponentsRegistry/registry.tsx | 85 +++++++ .../src/ComponentsRegistry/useParentProps.ts | 25 ++ .../core-blocks/src/TimerIcon.module.css | 2 +- webapp/packages/core-blocks/src/TimerIcon.tsx | 2 +- webapp/packages/core-blocks/src/index.ts | 8 + .../src/ConnectionInfoResource.ts | 8 +- .../src/ConnectionsManagerService.ts | 12 +- .../src/SharedProjectsResource.ts | 2 +- .../src/ContextMenu/MenuActionElement.tsx | 1 - .../ContextMenu/MenuBar/MenuBar.module.css | 1 + .../src/ContextMenu/MenuBar/MenuBar.tsx | 142 +++++++----- .../MenuBar/MenuBarItem.module.css | 133 ++++++----- .../src/ContextMenu/MenuBar/MenuBarItem.tsx | 87 ++++--- .../ContextMenu/MenuBar/MenuBarItemLoader.ts | 11 + .../src/ContextMenu/MenuBar/MenuBarLazy.ts | 5 +- .../src/ContextMenu/MenuItemRenderer.tsx | 5 +- webapp/packages/core-ui/src/index.ts | 1 + webapp/packages/core-utils/src/MetadataMap.ts | 9 + webapp/packages/core-view/src/Menu/IMenu.ts | 5 + .../src/Menu/MenuItem/IMenuCustomItem.ts | 3 + .../src/Menu/MenuItem/IMenuSubMenuItem.ts | 6 + .../src/Menu/MenuItem/MenuCustomItem.ts | 4 + .../src/Menu/MenuItem/MenuSubMenuItem.ts | 7 + .../core-view/src/Menu/MenuService.ts | 5 +- .../packages/core-view/src/Menu/createMenu.ts | 4 +- ...AdministrationMenuBarItemStyles.module.css | 18 +- .../src/AuthenticationService.ts | 28 ++- .../src/ConnectionAuthService.ts | 23 +- .../DataGridContextMenuFilterService.ts | 12 +- .../DataGridContextMenuOrderService.ts | 6 +- .../TableColumnHeader/OrderButton.tsx | 3 +- .../src/ContainerDataSource.ts | 27 +-- ...DataViewerDataChangeConfirmationService.ts | 12 +- .../src/DataViewerTabService.ts | 45 ++-- .../Actions/DatabaseRefreshAction.ts | 111 +++++++++ .../DatabaseDataModel/DatabaseDataModel.ts | 68 +----- .../DatabaseDataModel/DatabaseDataSource.ts | 214 ++++++++++-------- .../DatabaseDataModel/IDatabaseDataModel.ts | 11 +- .../DatabaseDataModel/IDatabaseDataSource.ts | 21 +- .../{ => ResultSet}/ResultSetDataSource.ts | 24 +- .../ResultSetTableFooterMenuService.ts | 7 +- .../AutoRefresh/AutoRefreshButton.module.css | 52 ----- .../AutoRefresh/AutoRefreshButton.tsx | 98 -------- .../TableFooter/AutoRefresh/useAutoRefresh.ts | 83 ------- .../TableFooter/TableFooter.module.css | 13 -- .../TableViewer/TableFooter/TableFooter.tsx | 57 +---- .../FetchSizeAction.module.css} | 17 +- .../FetchSizeAction/FetchSizeAction.tsx | 64 ++++++ .../TableFetchSizeActionBootstrap.ts | 53 +++++ .../AutoRefreshSettingsDialog.module.css | 0 .../AutoRefreshSettingsDialog.tsx | 10 +- .../MENU_DATA_VIEWER_AUTO_REFRESH.ts | 10 + .../RefreshAction/RefreshMenuAction.tsx | 68 ++++++ .../TableRefreshActionBootstrap.ts | 184 +++++++++++++++ .../RefreshAction/getRefreshState.ts | 22 ++ .../TableFooterMenu/TableFooterMenu.tsx | 19 +- .../TableFooterMenu/TableFooterMenuService.ts | 6 +- .../src/TableViewer/TableViewer.tsx | 4 +- .../packages/plugin-data-viewer/src/index.ts | 2 +- .../plugin-data-viewer/src/manifest.ts | 3 + .../src/DdlViewer/DDLViewerTabPanel.tsx | 6 +- .../SearchResourceMenuItemComponent.tsx | 10 +- .../ElementsTreeMenuBarItem.module.css | 2 +- .../ElementsTree/useElementsTree.ts | 54 +++-- .../ObjectPropertyTableFooter.tsx | 7 +- .../Table/Table.module.css | 2 +- .../src/ObjectViewerTabService.ts | 1 - .../plugin-sql-editor/src/QueryDataSource.ts | 22 +- .../SqlEditorActionsMenuBarItem.module.css | 2 +- .../SqlResultTabs/SqlQueryResultService.ts | 10 +- .../src/SqlResultTabs/SqlQueryService.ts | 8 +- .../AppStateMenu/AppStateMenu.module.css | 2 +- .../shared/TopMenuBarItem.module.css | 12 +- .../UserProfileFormBootstrap.ts | 2 +- 85 files changed, 1411 insertions(+), 813 deletions(-) create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/CRegistry.tsx create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryList.ts rename webapp/packages/{plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.module.css => core-blocks/src/ComponentsRegistry/CRegistryLoader.ts} (57%) create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsRegistryContext.ts create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsTreeContext.ts rename webapp/packages/{plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts => core-blocks/src/ComponentsRegistry/IComponentsTreeNode.ts} (65%) create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNodeValidator.ts create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/registry.tsx create mode 100644 webapp/packages/core-blocks/src/ComponentsRegistry/useParentProps.ts create mode 100644 webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarItemLoader.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseRefreshAction.ts rename webapp/packages/plugin-data-viewer/src/{ => ResultSet}/ResultSetDataSource.ts (80%) delete mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.module.css delete mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx delete mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts rename webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/{TableFooterMenuBarItemStyles.module.css => FetchSizeAction/FetchSizeAction.module.css} (57%) create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/TableFetchSizeActionBootstrap.ts rename webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/{AutoRefresh => TableFooterMenu/RefreshAction}/AutoRefreshSettingsDialog.module.css (100%) rename webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/{AutoRefresh => TableFooterMenu/RefreshAction}/AutoRefreshSettingsDialog.tsx (89%) create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/RefreshMenuAction.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/getRefreshState.ts diff --git a/webapp/packages/core-authentication/src/AuthInfoService.ts b/webapp/packages/core-authentication/src/AuthInfoService.ts index 3dd0d707ed..e4d0d70af7 100644 --- a/webapp/packages/core-authentication/src/AuthInfoService.ts +++ b/webapp/packages/core-authentication/src/AuthInfoService.ts @@ -25,6 +25,10 @@ export class AuthInfoService { return this.userInfoResource.data; } + get isAnonymous(): boolean { + return !this.userInfoResource.data; + } + constructor( private readonly userInfoResource: UserInfoResource, private readonly authProvidersResource: AuthProvidersResource, diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 221bec3190..8155178da0 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -66,7 +66,8 @@ export class UsersResource extends CachedMapResource this.markOutdated()); this.aliases.add(UsersResourceFilterKey, key => resourceKeyList( this.entries diff --git a/webapp/packages/core-blocks/public/icons/refresh_m.svg b/webapp/packages/core-blocks/public/icons/refresh_m.svg index 2e92fd0d20..526825db88 100644 --- a/webapp/packages/core-blocks/public/icons/refresh_m.svg +++ b/webapp/packages/core-blocks/public/icons/refresh_m.svg @@ -1,8 +1,8 @@ - - + + diff --git a/webapp/packages/core-blocks/public/icons/timer_m.svg b/webapp/packages/core-blocks/public/icons/timer_m.svg index 32d1a40037..10649167d7 100644 --- a/webapp/packages/core-blocks/public/icons/timer_m.svg +++ b/webapp/packages/core-blocks/public/icons/timer_m.svg @@ -1,6 +1,8 @@ - - - - - + + + + + \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistry.tsx b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistry.tsx new file mode 100644 index 0000000000..d0db5ab6de --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistry.tsx @@ -0,0 +1,29 @@ +/* + * 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 React, { memo, useContext } from 'react'; + +import { ComponentsRegistryContext } from './ComponentsRegistryContext'; +import { CRegistryList } from './CRegistryList'; + +export interface CRegistryProps extends React.PropsWithChildren { + registry: CRegistryList; +} + +/** + * experimental, can be changed + */ +export const CRegistry = memo(function CRegistry({ registry, children }) { + const parent = useContext(ComponentsRegistryContext); + const context = new Map(parent); + + for (const [component, validators] of registry) { + context.set(component, [...(context.get(component) || []), validators]); + } + + return {children}; +}); diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryList.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryList.ts new file mode 100644 index 0000000000..02772b643e --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryList.ts @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +import { IComponentsTreeNodeValidator } from './IComponentsTreeNodeValidator'; + +export type RegistryEntry> = [T, IComponentsTreeNodeValidator[]]; +export type CRegistryList = RegistryEntry>[]; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.module.css b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryLoader.ts similarity index 57% rename from webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.module.css rename to webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryLoader.ts index 294039e523..d098f1e37c 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.module.css +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/CRegistryLoader.ts @@ -5,7 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -.wrapper { - display: flex; - height: 100%; -} +import { importLazyComponent } from '../importLazyComponent'; + +export const CRegistry = importLazyComponent(() => import('./CRegistry').then(m => m.CRegistry)); diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsRegistryContext.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsRegistryContext.ts new file mode 100644 index 0000000000..0b765ebb31 --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsRegistryContext.ts @@ -0,0 +1,12 @@ +/* + * 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 { createContext } from 'react'; + +import { IComponentsTreeNodeValidator } from './IComponentsTreeNodeValidator'; + +export const ComponentsRegistryContext = createContext, IComponentsTreeNodeValidator[][]>>(new Map()); diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsTreeContext.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsTreeContext.ts new file mode 100644 index 0000000000..d75271c41f --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/ComponentsTreeContext.ts @@ -0,0 +1,12 @@ +/* + * 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 { createContext } from 'react'; + +import { IComponentsTreeNode } from './IComponentsTreeNode'; + +export const ComponentsTreeContext = createContext>>([]); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNode.ts similarity index 65% rename from webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts rename to webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNode.ts index 2931e312bc..e33c6d98ea 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNode.ts @@ -6,7 +6,8 @@ * you may not use this file except in compliance with the License. */ -export interface IAutoRefreshSettings { - interval: number | null; - stopOnError: boolean; +export interface IComponentsTreeNode { + component: React.FC; + props: T; + replacement: React.FC | null; } diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNodeValidator.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNodeValidator.ts new file mode 100644 index 0000000000..70049ff1ce --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/IComponentsTreeNodeValidator.ts @@ -0,0 +1,13 @@ +/* + * 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 IComponentsTreeNodeValidator> { + component: T; + replacement?: T; + validator: (props: T extends React.FC ? P : unknown) => boolean; +} diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/registry.tsx b/webapp/packages/core-blocks/src/ComponentsRegistry/registry.tsx new file mode 100644 index 0000000000..2ae5090dad --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/registry.tsx @@ -0,0 +1,85 @@ +/* + * 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 { observer } from 'mobx-react-lite'; +import React, { forwardRef, useContext } from 'react'; + +import { ComponentsRegistryContext } from './ComponentsRegistryContext'; +import { ComponentsTreeContext } from './ComponentsTreeContext'; +import { IComponentsTreeNode } from './IComponentsTreeNode'; +import { IComponentsTreeNodeValidator } from './IComponentsTreeNodeValidator'; + +/** + * experimental, can be changed + */ +export function registry>(component: T): T { + const componentRef = observer( + forwardRef(function Registry(props, ref) { + const node: IComponentsTreeNode = { + component: componentRef, + props, + replacement: null, + }; + const context = [...useContext(ComponentsTreeContext), node]; + const registry = useContext(ComponentsRegistryContext); + let Component = component as any; + + const registryNodes = registry.get(componentRef); + for (const validators of registryNodes || []) { + if (validators) { + const result = getComponent(context, validators); + + if (result) { + Component = result; + break; + } + } + } + + if (Component !== component) { + node.replacement = Component; + } + + return ( + + + + ); + }), + ); + + return componentRef as unknown as T; +} + +function getComponent(context: IComponentsTreeNode[], validators: IComponentsTreeNodeValidator[]) { + let position = 0; + let node = context[position]; + let lastValidator: IComponentsTreeNodeValidator | null = null; + + for (const validator of validators) { + lastValidator = validator; + while (position < context.length) { + node = context[position++]; + if (node.component === lastValidator.component) { + if (!lastValidator.validator(node.props) || (node.replacement !== null && node.replacement === lastValidator.replacement)) { + return null; + } + break; + } + } + + if (position >= context.length) { + break; + } + } + + if (node.component === lastValidator?.component && lastValidator.replacement) { + return lastValidator.replacement; + } + + return null; +} diff --git a/webapp/packages/core-blocks/src/ComponentsRegistry/useParentProps.ts b/webapp/packages/core-blocks/src/ComponentsRegistry/useParentProps.ts new file mode 100644 index 0000000000..47e16da957 --- /dev/null +++ b/webapp/packages/core-blocks/src/ComponentsRegistry/useParentProps.ts @@ -0,0 +1,25 @@ +/* + * 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 { useContext } from 'react'; + +import { ComponentsTreeContext } from './ComponentsTreeContext'; + +/** + * experimental, can be changed + */ +export function useParentProps(component: React.FC): T | undefined { + const tree = useContext(ComponentsTreeContext); + + for (let i = tree.length - 1; i >= 0; i--) { + if (tree[i].component === component) { + return tree[i].props as T; + } + } + + return undefined; +} diff --git a/webapp/packages/core-blocks/src/TimerIcon.module.css b/webapp/packages/core-blocks/src/TimerIcon.module.css index 3a364533a0..e5299f9b3e 100644 --- a/webapp/packages/core-blocks/src/TimerIcon.module.css +++ b/webapp/packages/core-blocks/src/TimerIcon.module.css @@ -10,6 +10,7 @@ width: 16px; height: 16px; overflow: hidden; + user-select: none; & > .icon { width: 16px; @@ -45,7 +46,6 @@ opacity: 0; } } - } .interval { font-size: 8px; diff --git a/webapp/packages/core-blocks/src/TimerIcon.tsx b/webapp/packages/core-blocks/src/TimerIcon.tsx index 0de5860e34..3f0cbc8281 100644 --- a/webapp/packages/core-blocks/src/TimerIcon.tsx +++ b/webapp/packages/core-blocks/src/TimerIcon.tsx @@ -15,7 +15,7 @@ import { useS } from './useS'; interface Props { state: 'play' | 'stop'; - interval: number; + interval: React.ReactNode; } export const TimerIcon = observer>(function TimerIcon({ state, interval, ...rest }) { diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index d0f3993f09..ed29b86fba 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -16,6 +16,14 @@ export * from './CommonDialog/DialogsPortal'; export * from './ErrorDetailsDialog/ErrorDetailsDialog'; +export * from './ComponentsRegistry/CRegistryLoader'; +// we don't expect to use this component directly only as a wrapper for another component +// eslint-disable-next-line @cloudbeaver/no-sync-component-import +export * from './ComponentsRegistry/registry'; +export * from './ComponentsRegistry/CRegistryList'; +export * from './ComponentsRegistry/IComponentsTreeNodeValidator'; +export * from './ComponentsRegistry/useParentProps'; + export * from './AppRefreshButton'; export * from './ComplexLoader'; export * from './DisplayError'; diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 49c37698d0..d37c3c6e62 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -450,9 +450,7 @@ export class ConnectionInfoResource extends CachedMapResource; @@ -568,6 +566,10 @@ export class ConnectionInfoResource extends CachedMapResource connectionInfo.load(data.key)); this.onDelete.before(this.onDisconnect); + this.connectionInfo.onConnectionClose.next(this.onDisconnect, key => ({ + connections: [key], + state: 'after' as const, + })); makeObservable(this, { projectConnections: computed({ @@ -177,10 +181,6 @@ export class ConnectionsManagerService { try { for (const connection of this.projectConnections) { await this._closeConnectionAsync(connection); - this.onDisconnect.execute({ - connections: [createConnectionParam(connection)], - state: 'after', - }); } notification.close(); @@ -211,10 +211,6 @@ export class ConnectionsManagerService { await this._closeConnectionAsync(connection); notification.close(); - this.onDisconnect.execute({ - connections: [createConnectionParam(connection)], - state: 'after', - }); } catch (exception: any) { controller.reject(exception); } diff --git a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts index c4e9876637..875afc4288 100644 --- a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts +++ b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts @@ -45,7 +45,7 @@ export class SharedProjectsResource extends CachedMapResource new Map(), []); sessionPermissionsResource.require(this, EAdminPermission.admin); - this.sync(sessionPermissionsResource, () => {}); + sessionPermissionsResource.onDataOutdated.addHandler(() => this.markOutdated()); this.connect(projectInfoResource); this.onDataOutdated.addHandler(() => projectInfoResource.markOutdated()); diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx index f09ccb2518..68e6fd4212 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuActionElement.tsx @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import React from 'react'; import { Checkbox, MenuItem, MenuItemCheckbox, MenuItemElement, MenuItemRadio, Radio, useTranslate } from '@cloudbeaver/core-blocks'; import { getBindingLabel, IMenuActionItem } from '@cloudbeaver/core-view'; diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.module.css b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.module.css index ba55f561c7..5d05be7459 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.module.css +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.module.css @@ -8,6 +8,7 @@ .menuBar { height: 32px; display: flex; + flex-shrink: 0; & > .menuSeparator { height: 100%; diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx index 26388e5d80..e74ca308df 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx @@ -8,14 +8,26 @@ import { observer } from 'mobx-react-lite'; import { forwardRef, useCallback } from 'react'; -import { getComputed, MenuSeparator, MenuSeparatorStyles, s, SContext, StyleRegistry, useAutoLoad, useS } from '@cloudbeaver/core-blocks'; -import { useDataContextLink } from '@cloudbeaver/core-data-context'; +import { + getComputed, + Loader, + MenuSeparator, + MenuSeparatorStyles, + registry, + s, + SContext, + StyleRegistry, + useAutoLoad, + useS, +} from '@cloudbeaver/core-blocks'; +import { IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { DATA_CONTEXT_MENU_NESTED, DATA_CONTEXT_SUBMENU_ITEM, IMenuActionItem, IMenuData, IMenuItem, + isMenuCustomItem, MenuActionItem, MenuBaseItem, MenuSeparatorItem, @@ -28,7 +40,7 @@ import type { IMenuBarNestedMenuSettings, IMenuBarProps } from './IMenuBarProps' import style from './MenuBar.module.css'; import { MenuBarItem } from './MenuBarItem'; -const registry: StyleRegistry = [ +const styleRegistry: StyleRegistry = [ [ MenuSeparatorStyles, { @@ -49,11 +61,13 @@ export const MenuBar = observer( } return ( - +
- {items.map(item => ( - - ))} + + {items.map(item => ( + + ))} +
); @@ -78,6 +92,20 @@ const MenuBarElement = observer(function MenuBarElement({ } if (item instanceof MenuSubMenuItem) { + if (item.action) { + return ( + ( + + {children} + + )} + className={className} + /> + ); + } return ; } @@ -85,8 +113,14 @@ const MenuBarElement = observer(function MenuBarElement({ return ; } + if (isMenuCustomItem(item)) { + const CustomMenuItem = item.getComponent(); + + return ; + } + if (item instanceof MenuActionItem) { - return ; + return ; } if (item instanceof MenuBaseItem) { @@ -108,42 +142,46 @@ const MenuBarElement = observer(function MenuBarElement({ return null; }); -interface IMenuBarActionProps { +export interface IMenuBarActionProps { item: IMenuActionItem; + context: IDataContext; + submenu?: React.FC; className?: string; - onClick: () => void; } -const MenuBarAction = observer(function MenuBarAction({ item, className, onClick }) { - const actionInfo = item.action.actionInfo; - const loading = item.action.isLoading(); +export const MenuBarAction = registry( + observer(function MenuBarAction({ item, submenu, className }) { + const actionInfo = item.action.actionInfo; + const loading = item.action.isLoading(); - /** @deprecated must be refactored (#1)*/ - const displayLabel = item.action.isLabelVisible(); + /** @deprecated must be refactored (#1)*/ + const displayLabel = item.action.isLabelVisible(); - function handleClick() { - onClick(); - item.action.activate(); - } + function handleClick() { + item.events?.onSelect?.(); + item.action.activate(); + } - return ( -