From 78233d605a98f5f2e5f9933a3c53124348d8d66d Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 21 Jun 2021 14:18:42 +0300 Subject: [PATCH 01/18] feat(core-blocks): CB-1056 add option to reveal password --- .../public/icons/password_hide.svg | 3 + .../public/icons/password_show.svg | 3 + .../Checkboxes/FieldCheckboxNew.tsx | 14 +++- .../src/FormControls/InputFieldNew.tsx | 64 +++++++++++++++---- .../ChangeUserPasswordDialog.tsx | 7 +- .../plugin-authentication/src/locales/en.ts | 2 +- .../plugin-authentication/src/locales/ru.ts | 2 +- 7 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 webapp/packages/core-blocks/public/icons/password_hide.svg create mode 100644 webapp/packages/core-blocks/public/icons/password_show.svg diff --git a/webapp/packages/core-blocks/public/icons/password_hide.svg b/webapp/packages/core-blocks/public/icons/password_hide.svg new file mode 100644 index 0000000000..c2c1dd1873 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/password_hide.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/packages/core-blocks/public/icons/password_show.svg b/webapp/packages/core-blocks/public/icons/password_show.svg new file mode 100644 index 0000000000..62aa577c15 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/password_show.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx index 0687244421..ff4902f921 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import styled, { css } from 'reshadow'; +import styled, { css, use } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -29,6 +29,9 @@ const fieldCheckboxStyles = css` user-select: none; padding-left: 10px; line-height: 16px; + &[|disabled] { + cursor: auto; + } } `; @@ -46,7 +49,14 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ return styled(styles)( - {children} + + {children} + ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx index ad78555776..5fab79042f 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -7,12 +7,13 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useState } from 'react'; import styled, { use, css } from 'reshadow'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; +import { IconOrImage } from '../IconOrImage'; import { baseFormControlStylesNew } from './baseFormControlStylesNew'; import { FormContext } from './FormContext'; import { isControlPresented } from './isControlPresented'; @@ -26,6 +27,25 @@ const INPUT_FIELD_STYLES = css` field-label:not(:empty) { padding-bottom: 10px; } + input-container { + position: relative; + } + IconOrImage { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + cursor: pointer; + &[|disabled] { + cursor: auto; + opacity: 0.8; + } + } + input:not(:only-child) { + padding-right: 32px !important; + } `; type BaseProps = Omit, 'onChange' | 'name' | 'value'> & ILayoutSizeProps & { @@ -80,9 +100,17 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ onChange, ...rest }: ControlledProps | ObjectProps, ref: React.Ref) { + const [passwordRevealed, setPasswordRevealed] = useState(false); const styles = useStyles(baseFormControlStylesNew, INPUT_FIELD_STYLES, style); const context = useContext(FormContext); + const revealPassword = useCallback(() => { + if (rest.disabled) { + return; + } + setPasswordRevealed(prev => !prev); + }, [rest.disabled]); + const handleChange = useCallback((event: React.ChangeEvent) => { const value = mapValue?.(event.target.value) ?? event.target.value; @@ -112,18 +140,28 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ } return styled(styles)( - - {children}{required && ' *'} - + + {children}{required && ' *'} + + + {rest.type === 'password' && ( + + )} + {description && ( {description} diff --git a/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx b/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx index 8097f4ec51..687fc590a1 100644 --- a/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx +++ b/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx @@ -11,6 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useState, useCallback, useEffect } from 'react'; import styled, { css } from 'reshadow'; +import { UsersResource } from '@cloudbeaver/core-authentication'; import { BASE_CONTAINERS_STYLES, Container, ErrorMessage, Group, InputFieldNew, SubmittingForm, useFocus } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs'; @@ -21,7 +22,6 @@ import { GQLError, GQLErrorCatcher } from '@cloudbeaver/core-sdk'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import { ChangeUserPasswordDialogFooter } from './ChangeUserPasswordDialogFooter'; -import { UsersResource } from '@cloudbeaver/core-authentication'; const styles = composes( css` @@ -38,7 +38,6 @@ const styles = composes( } `); - interface IState { oldPassword: string; newPassword: string; @@ -55,7 +54,7 @@ export const ChangeUserPasswordDialog: DialogComponent = observer( rejectDialog, }: DialogComponentProps) { const [focusedRef] = useFocus({}); - const usersResource = useService(UsersResource) + const usersResource = useService(UsersResource); const commonDialogService = useService(CommonDialogService); const notificationService = useService(NotificationService); const style = useStyles(styles, BASE_CONTAINERS_STYLES); @@ -131,7 +130,7 @@ export const ChangeUserPasswordDialog: DialogComponent = observer( small required > - {translate('authentication_user_old_password')} + {translate('authentication_user_current_password')} Date: Mon, 21 Jun 2021 16:24:34 +0300 Subject: [PATCH 02/18] feat(core-blocks): CB-1056 style fixes --- .../packages/core-app/public/icons/icons.svg | 6 ++ .../public/icons/password_hide.svg | 3 - .../public/icons/password_show.svg | 3 - .../Checkboxes/FieldCheckboxNew.tsx | 11 ++- .../src/FormControls/InputFieldNew.tsx | 83 +++++++++---------- 5 files changed, 51 insertions(+), 55 deletions(-) delete mode 100644 webapp/packages/core-blocks/public/icons/password_hide.svg delete mode 100644 webapp/packages/core-blocks/public/icons/password_show.svg diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index c8c1bb4dd9..43c5864989 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -186,4 +186,10 @@ + + + + + + diff --git a/webapp/packages/core-blocks/public/icons/password_hide.svg b/webapp/packages/core-blocks/public/icons/password_hide.svg deleted file mode 100644 index c2c1dd1873..0000000000 --- a/webapp/packages/core-blocks/public/icons/password_hide.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/webapp/packages/core-blocks/public/icons/password_show.svg b/webapp/packages/core-blocks/public/icons/password_show.svg deleted file mode 100644 index 62aa577c15..0000000000 --- a/webapp/packages/core-blocks/public/icons/password_show.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx index ff4902f921..0c6050096c 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import styled, { css, use } from 'reshadow'; +import styled, { css } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -29,9 +29,9 @@ const fieldCheckboxStyles = css` user-select: none; padding-left: 10px; line-height: 16px; - &[|disabled] { - cursor: auto; - } + } + Checkbox[disabled] + field-label { + cursor: auto; } `; @@ -47,13 +47,12 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ } return styled(styles)( - + {children} diff --git a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx index 5fab79042f..8836eb0878 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -10,43 +10,45 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useContext, useState } from 'react'; import styled, { use, css } from 'reshadow'; -import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; +import { ComponentStyle, composes, useStyles } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; -import { IconOrImage } from '../IconOrImage'; +import { Icon } from '../Icons/Icon'; import { baseFormControlStylesNew } from './baseFormControlStylesNew'; import { FormContext } from './FormContext'; import { isControlPresented } from './isControlPresented'; -const INPUT_FIELD_STYLES = css` - field-label { - display: block; - composes: theme-typography--body1 from global; - font-weight: 500; - } - field-label:not(:empty) { - padding-bottom: 10px; - } - input-container { - position: relative; - } - IconOrImage { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - width: 24px; - height: 24px; - cursor: pointer; - &[|disabled] { - cursor: auto; - opacity: 0.8; +const INPUT_FIELD_STYLES = composes( + css` + Icon { + composes: theme-text-primary from global; } - } - input:not(:only-child) { - padding-right: 32px !important; - } -`; +`, + css` + field-label { + display: block; + composes: theme-typography--body1 from global; + font-weight: 500; + } + field-label:not(:empty) { + padding-bottom: 10px; + } + input-container { + position: relative; + } + Icon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + cursor: pointer; + } + input:not(:only-child) { + padding-right: 32px !important; + } +`); type BaseProps = Omit, 'onChange' | 'name' | 'value'> & ILayoutSizeProps & { description?: string; @@ -104,13 +106,6 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ const styles = useStyles(baseFormControlStylesNew, INPUT_FIELD_STYLES, style); const context = useContext(FormContext); - const revealPassword = useCallback(() => { - if (rest.disabled) { - return; - } - setPasswordRevealed(prev => !prev); - }, [rest.disabled]); - const handleChange = useCallback((event: React.ChangeEvent) => { const value = mapValue?.(event.target.value) ?? event.target.value; @@ -139,6 +134,8 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ value = mapState(value); } + const showRevealPasswordButton = rest.type === 'password' && !rest.disabled && !rest.readOnly; + return styled(styles)( {children}{required && ' *'} @@ -154,16 +151,16 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ {...use({ mod })} required={required} /> - {rest.type === 'password' && ( - setPasswordRevealed(!passwordRevealed)} /> )} {description && ( - + {description} )} From 4e9429702f737e5b45912fd22dc6226804e1fe68 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Mon, 21 Jun 2021 16:57:40 +0300 Subject: [PATCH 03/18] Root URI property --- .../src/io/cloudbeaver/model/WebServerConfig.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 611b731d85..cb19130906 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -64,6 +64,11 @@ public class WebServerConfig { return CommonUtils.notEmpty(application.getServerURL()); } + @Property + public String getRootURI() { + return CommonUtils.notEmpty(application.getRootURI()); + } + @Property public boolean isAnonymousAccessEnabled() { return application.getAppConfiguration().isAnonymousAccessEnabled(); From c428dd2a572f5e92891a7980f2970ef8e210fcd5 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Mon, 21 Jun 2021 16:57:54 +0300 Subject: [PATCH 04/18] Root URI property --- .../bundles/io.cloudbeaver.server/schema/service.core.graphqls | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 04f0bc0b15..7c39847187 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -92,6 +92,8 @@ type ServerConfig { serverURL: String! + rootURI: String! + anonymousAccessEnabled: Boolean authenticationEnabled: Boolean @deprecated supportsCustomConnections: Boolean From 1cafcabe3e914b1cf5a33afbe2994269590b4219 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 21 Jun 2021 17:03:45 +0300 Subject: [PATCH 05/18] fix: CB-959 extra query for configuration --- .../core-root/src/ServerConfigResource.ts | 31 ++++++++++--------- .../ServerConfigurationService.ts | 7 ++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index eb90601261..b43250cef9 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -130,33 +130,36 @@ export class ServerConfigResource extends CachedDataResource { + async save(skipConfigUpdate = false): Promise { await this.performUpdate(undefined, undefined, async () => { - await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: this.navigatorSettingsUpdate }); + if (this.isNavigatorSettingsChanged()) { + await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: this.navigatorSettingsUpdate }); - if (this.data) { - this.data.defaultNavigatorSettings = { ...this.navigatorSettingsUpdate }; - } else { + if (this.data) { + this.data.defaultNavigatorSettings = { ...this.navigatorSettingsUpdate }; + } else { + this.data = await this.loader(); + } + } + + if (this.isChanged() && !skipConfigUpdate) { + await this.graphQLService.sdk.configureServer({ + configuration: this.update, + }); this.data = await this.loader(); } - }, () => !this.isNavigatorSettingsChanged()); + }, () => !this.isNavigatorSettingsChanged() && (!this.isChanged() || skipConfigUpdate)); } - async save(onlyRestart = false): Promise { + async finishConfiguration(onlyRestart = false): Promise { await this.performUpdate(undefined, undefined, async () => { await this.graphQLService.sdk.configureServer({ - configuration: (onlyRestart && !this.isChanged()) ? {} : this.update, + configuration: !this.isChanged() && onlyRestart ? {} : this.update, }); - this.data = await this.loader(); }, () => !this.isChanged() && !onlyRestart); } - async saveAllData(): Promise { - await this.saveDefaultNavigatorSettings(); - await this.save(); - } - protected async loader(): Promise { const { serverConfig } = await this.graphQLService.sdk.serverConfig(); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 47624f3836..c006c1d419 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -196,11 +196,10 @@ export class ServerConfigurationService { } try { - await this.serverConfigResource.saveDefaultNavigatorSettings(); - if (!data.configurationWizard || data.finish) { - await this.serverConfigResource.save(); - } + await this.serverConfigResource.save(data.configurationWizard); + if (data.configurationWizard && data.finish) { + await this.serverConfigResource.finishConfiguration(); await this.sessionDataResource.refresh(); } } catch (exception) { From f111d3c0f7f35bea79601d8629957b84c5319d48 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 21 Jun 2021 18:27:29 +0300 Subject: [PATCH 06/18] chore: CB-1026 add root uri and service uri --- webapp/package.json | 1 + .../AdministrationScreenService.ts | 8 ++--- .../packages/core-app/src/useAppVersion.tsx | 4 +-- webapp/packages/core-blocks/src/Button.tsx | 4 +++ .../src/HTMLTemplates/head.html.ejs | 5 ++- webapp/packages/core-blocks/src/Icon.tsx | 5 ++- .../core-cli/configs/webpack.config.js | 3 ++ .../core-sdk/src/EnvironmentService.ts | 7 +++-- .../packages/core-utils/src/BuildVersion.ts | 15 --------- .../core-utils/src/GlobalConstants.ts | 31 +++++++++++++++++++ webapp/packages/core-utils/src/index.ts | 2 +- .../src/DataExportProcessService.ts | 4 ++- webapp/yarn.lock | 5 +++ 13 files changed, 67 insertions(+), 27 deletions(-) delete mode 100644 webapp/packages/core-utils/src/BuildVersion.ts create mode 100644 webapp/packages/core-utils/src/GlobalConstants.ts diff --git a/webapp/package.json b/webapp/package.json index 77490e938f..8a02b369e3 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -39,6 +39,7 @@ "mobx": "^6.3.0", "mobx-react-lite": "^3.2.0", "node-sass": "^5.0.0", + "path-browserify": "^1.0.1", "preact": "^10.5.13", "preact-jsx-runtime": "^1.2.0", "preact-render-to-string": "^5.1.19", diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index f08238f4eb..43dcb7eda9 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -13,7 +13,7 @@ import { IExecutor, Executor } from '@cloudbeaver/core-executor'; import { PermissionsResource, PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService, RouterState } from '@cloudbeaver/core-routing'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; -import { BuildVersion } from '@cloudbeaver/core-utils'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; @@ -76,7 +76,7 @@ export class AdministrationScreenService { this.info = { workspaceId: '', - version: BuildVersion.version || '', + version: GlobalConstants.version || '', serverVersion: '', configurationMode: false, }; @@ -154,13 +154,13 @@ export class AdministrationScreenService { this.info.workspaceId !== this.serverConfigResource.workspaceId || this.info.configurationMode !== this.isConfigurationMode || this.info.serverVersion !== this.serverConfigResource.serverVersion - || this.info.version !== BuildVersion.version + || this.info.version !== GlobalConstants.version ) { this.clearItemsState(); this.info.workspaceId = this.serverConfigResource.workspaceId; this.info.configurationMode = this.isConfigurationMode; this.info.serverVersion = this.serverConfigResource.serverVersion; - this.info.version = BuildVersion.version || ''; + this.info.version = GlobalConstants.version || ''; } if (defaultState) { diff --git a/webapp/packages/core-app/src/useAppVersion.tsx b/webapp/packages/core-app/src/useAppVersion.tsx index aa96bdce83..9edec9a5d8 100644 --- a/webapp/packages/core-app/src/useAppVersion.tsx +++ b/webapp/packages/core-app/src/useAppVersion.tsx @@ -8,7 +8,7 @@ import { useService } from '@cloudbeaver/core-di'; import { ServerService } from '@cloudbeaver/core-root'; -import { BuildVersion } from '@cloudbeaver/core-utils'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; interface IAppVersion { backendVersion: string; @@ -20,7 +20,7 @@ const VERSION_REGEX = /(\d+\.\d+\.\d+)/; export function useAppVersion(short = false): IAppVersion { const serverService = useService(ServerService); let backendVersion = serverService.config.data?.version || ''; - let frontendVersion = BuildVersion.version || ''; + let frontendVersion = GlobalConstants.version || ''; if (short) { backendVersion = VERSION_REGEX.exec(backendVersion)?.[1] ?? backendVersion; diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index c615552084..c77f3de873 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -58,6 +58,10 @@ const buttonStyles = composes( opacity: 0; } } + + &[href] { + text-decoration: none; + } } ` diff --git a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs index d751d941ce..bf06afaa32 100644 --- a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs +++ b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs @@ -6,4 +6,7 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Icon.tsx b/webapp/packages/core-blocks/src/Icon.tsx index 968297c137..29000eb187 100644 --- a/webapp/packages/core-blocks/src/Icon.tsx +++ b/webapp/packages/core-blocks/src/Icon.tsx @@ -6,12 +6,15 @@ * you may not use this file except in compliance with the License. */ +import path from 'path'; import type { SVGAttributes } from 'react'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; + export const Icon: React.FC> = function Icon(props) { const { name = '', className, ...rest } = props; const cn = `icon ${className || ''}`; - const url = name.startsWith('/') ? name : `icons/icons.svg#${name}`; + const url = name.startsWith('/') ? path.join(GlobalConstants.rootURI, name) : `icons/icons.svg#${name}`; return ( @@ -44,7 +44,7 @@ export const ServerConfigurationInfoForm: React.FC = observer(function Se state={state.serverConfig} mod='surface' required - tiny + medium > {translate('administration_configuration_wizard_configuration_server_url')} From c70e1bc09fec9a3fd4179dcc29b6bd65b614781b Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 22 Jun 2021 18:29:46 +0300 Subject: [PATCH 14/18] feat(plugin-authentication): CB-1063 settings restructure --- .../AuthenticationProviders.tsx | 67 ++++++++++++------- .../plugin-authentication/src/locales/en.ts | 2 +- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx index 7a02f2fe1f..40657eb4b8 100644 --- a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -31,8 +31,9 @@ export const AuthenticationProviders: PlaceholderComponent provider.id !== AUTH_PROVIDER_LOCAL_ID); + const externalAuthentication = providers.data.length === 1 && localProvider === undefined; const authenticationDisabled = serverConfig.enabledAuthProviders?.length === 0; useExecutor({ @@ -63,34 +64,52 @@ export const AuthenticationProviders: PlaceholderComponent {translate('administration_configuration_wizard_configuration_anonymous_access')} - {translate('administration_configuration_wizard_configuration_authentication_provider')} - {() => styled(styles)( - <> - {providers.data.map(provider => provider && ( - - {provider.label} - - ))} - + {() => localProvider && styled(styles)( + + {localProvider.label} + )} - {configurationWizard && localExists ? ( + {configurationWizard && localProvider && ( - ) : ( - )} - + {services.length > 0 ? ( + + {translate('administration_configuration_wizard_configuration_services')} + + {() => styled(styles)( + <> + {services.map(provider => ( + + {provider.label} + + ))} + + )} + + + ) : } + {!(configurationWizard && localProvider) && } ); }); diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index 629dbbfdab..09990007da 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -48,7 +48,7 @@ export default [ ['administration_configuration_wizard_configuration_anonymous_access', 'Allow anonymous access'], ['administration_configuration_wizard_configuration_anonymous_access_description', 'Allows to work with CloudBeaver without user authentication'], ['administration_configuration_wizard_configuration_authentication_group', 'Authentication settings'], - ['administration_configuration_wizard_configuration_authentication_provider', 'Authentication Source'], + ['administration_configuration_wizard_configuration_services', 'Services'], ['administration_configuration_wizard_configuration_authentication', 'Enable users authentication'], ['administration_configuration_wizard_configuration_authentication_description', 'Allows users to authenticate. Otherwise only anonymous access is enabled'], ]; From 51fda791e49bc37f36b96cf989c454c26063bf83 Mon Sep 17 00:00:00 2001 From: serge-rider Date: Tue, 22 Jun 2021 23:46:11 +0300 Subject: [PATCH 15/18] CB-1026 Filter ROOT_URI --- .../server/jetty/CBStaticServlet.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java index 4d212c4d52..b85a421854 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java @@ -1,5 +1,7 @@ package io.cloudbeaver.server.jetty; +import io.cloudbeaver.server.CBApplication; +import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.ResourceService; @@ -9,7 +11,10 @@ import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; @WebServlet(urlPatterns = "/") public class CBStaticServlet extends DefaultServlet { @@ -26,10 +31,33 @@ public class CBStaticServlet extends DefaultServlet { } private static ResourceService makeResourceService() { - ResourceService resourceService = new ResourceService(); + 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 { + if (content.getResource().getName().endsWith("index.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(); + content.getResource().writeTo(baos, 0, content.getContentLengthValue()); + String indexContents = new String(baos.toByteArray(), StandardCharsets.UTF_8); + indexContents = indexContents.replace("{ROOT_URI}", CBApplication.getInstance().getRootURI()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + putHeaders(response, content, indexBytes.length); + response.getOutputStream().write(indexBytes); + + return true; + } + } + } \ No newline at end of file From 81a6dc57927b057b3029138e4c6f163e8bc83b76 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 23 Jun 2021 10:03:14 +0300 Subject: [PATCH 16/18] fix(plugin-data-viewer): refresh button styles --- .../TableViewer/TableFooter/TableFooter.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index f1f7e0031d..fe42094709 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useRef } from 'react'; import styled, { css, use } from 'reshadow'; -import { IconButton, SubmittingForm, ToolsPanel } from '@cloudbeaver/core-blocks'; +import { IconOrImage, SubmittingForm, ToolsPanel } from '@cloudbeaver/core-blocks'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; @@ -40,7 +40,14 @@ const tableFooterStyles = composes( reload { height: 100%; display: flex; + cursor: pointer; align-items: center; + padding: 0 16px; + + & IconOrImage { + width: 24px; + height: 24px; + } } IconButton { position: relative; @@ -86,16 +93,12 @@ export const TableFooter = observer(function TableFooter({ [model] ); + const disabled = model.isLoading() || model.isDisabled(resultIndex); + return styled(useStyles(tableFooterStyles))( - - model.refresh()} - /> + model.refresh()}> + @@ -103,7 +106,7 @@ export const TableFooter = observer(function TableFooter({ ref={ref} type="number" value={model.countGain} - disabled={model.isLoading() || model.isDisabled(resultIndex)} + disabled={disabled} onBlur={handleChange} {...use({ mod: 'surface' })} /> From 8eab411a31133220eaf8c9a2a1d67501494820f4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 23 Jun 2021 11:18:31 +0300 Subject: [PATCH 17/18] chore(plugin-data-viewer): styles --- .../src/TableViewer/TableFooter/TableFooter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index fe42094709..51ed91ddf4 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -55,7 +55,6 @@ const tableFooterStyles = composes( width: 24px; display: block; } - reload, count, TableFooterMenu { margin-left: 16px; From edefe428e08586594a0135ec8651fd132eb35374 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 23 Jun 2021 14:15:12 +0300 Subject: [PATCH 18/18] feat: CB-1041 CB-1013 CB-1050 show in value panel button --- .../DataGrid/CellRenderer/CellRenderer.tsx | 5 +- .../src/DataGrid/DataGridContext.ts | 3 +- .../DataGridContextMenuFilterService.ts | 30 ++++-- .../DataGridContextMenuOrderService.ts | 2 +- .../DataGridContextMenuService.ts | 9 +- .../src/DataGrid/DataGridLoader.tsx | 14 +-- .../src/DataGrid/DataGridTable.tsx | 14 +-- .../src/DataGrid/Formatters/CellFormatter.tsx | 1 + .../src/DataGrid/Formatters/Menu/CellMenu.tsx | 15 ++- .../src/SpreadsheetBootstrap.ts | 21 ++++ .../src/SpreadsheetGrid.tsx | 8 +- .../src/locales/en.ts | 1 + .../src/locales/ru.ts | 1 + .../src/DataPresentationService.ts | 2 + .../src/TableViewer/IDataTableActions.ts | 27 +++++ .../src/TableViewer/TableGrid.tsx | 5 +- .../src/TableViewer/TableToolsPanel.tsx | 5 +- .../src/TableViewer/TableViewer.tsx | 98 +++++++++++++------ .../packages/plugin-data-viewer/src/index.ts | 1 + 19 files changed, 184 insertions(+), 78 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index 062519da18..e1511f2824 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -26,7 +26,6 @@ export const CellRenderer: React.FC> = observer(function const selectionContext = useContext(DataGridSelectionContext); const editingContext = useContext(EditingContext); const editor = dataGridContext?.model.source.getEditor(dataGridContext.resultIndex); - const dataGridApi = dataGridContext?.getDataGridApi(); const mouse = useMouse({}); const classes: string[] = []; @@ -45,6 +44,8 @@ export const CellRenderer: React.FC> = observer(function } const handleMouseDown = useCallback((event: React.MouseEvent) => { + const dataGridApi = dataGridContext?.getDataGridApi(); + if (dataGridApi) { dataGridApi.selectCell({ idx: column.idx, rowIdx }); } @@ -58,7 +59,7 @@ export const CellRenderer: React.FC> = observer(function event.shiftKey, true ); - }, [column, rowIdx, selectionContext]); + }, [column, rowIdx, dataGridContext, selectionContext]); const handleMouseUp = useCallback((event: React.MouseEvent) => { if (!dataGridContext?.isGridInFocus()) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index 3fdcf359ea..73e81c7f55 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -10,7 +10,7 @@ import { createContext } from 'react'; import type { DataGridHandle } from 'react-data-grid'; import type { IExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; +import type { IDatabaseDataModel, IDataTableActions } from '@cloudbeaver/plugin-data-viewer'; export interface IColumnResizeInfo { column: number; @@ -19,6 +19,7 @@ export interface IColumnResizeInfo { export interface IDataGridContext { model: IDatabaseDataModel; + actions: IDataTableActions; resultIndex: number; columnResize: IExecutor; isGridInFocus: () => boolean; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts index 08041291ff..0cec5d8bf5 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -73,6 +73,7 @@ export class DataGridContextMenuFilterService { context: IMenuContext, value: any | (() => any), icon: string, + isHidden?: (context: IMenuContext) => boolean, ): Array> { const { model, resultIndex, column } = context.data; const data = model.source.getAction(resultIndex, ResultSetDataAction); @@ -88,6 +89,9 @@ export class DataGridContextMenuFilterService { isDisabled(context) { return context.data.model.isLoading(); }, + isHidden(context) { + return isHidden?.(context) ?? false; + }, titleGetter() { const val = typeof value === 'function' ? value() : value; const stringifyValue = format.toDisplayString(val); @@ -115,7 +119,7 @@ export class DataGridContextMenuFilterService { return context.data.model.isDisabled(context.data.resultIndex) || context.data.model.source.results.length > 1; }, - order: 1, + order: 2, title: 'data_grid_table_filter', icon: '/icons/filter.png', isPanel: true, @@ -132,7 +136,7 @@ export class DataGridContextMenuFilterService { const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); return constraints.orderConstraints.length === 0 && constraints.filterConstraints.length === 0; }, - order: 2, + order: 3, title: 'data_grid_table_delete_filters_and_orders', icon: '/icons/erase.png', onClick: async context => { @@ -171,10 +175,19 @@ export class DataGridContextMenuFilterService { return []; } - if (this.clipboardService.state === 'prompt') { - return [{ + const valueGetter = () => this.clipboardService.clipboardValue || ''; + const items = this.getGeneralizedMenuItems( + context, + valueGetter, + '/icons/filter_clipboard.png', + () => this.clipboardService.state === 'prompt' + ); + + return [ + { id: 'permission', isPresent: () => true, + isHidden: () => this.clipboardService.state !== 'prompt', isDisabled(context) { return context.data.model.isLoading(); }, @@ -183,12 +196,9 @@ export class DataGridContextMenuFilterService { onClick: async () => { await this.clipboardService.read(); }, - }]; - } - - const valueGetter = () => this.clipboardService.clipboardValue || ''; - const items = this.getGeneralizedMenuItems(context, valueGetter, '/icons/filter_clipboard.png'); - return items; + }, + ...items, + ]; }, }), } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts index 48e19e328d..1088282fe6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts @@ -45,7 +45,7 @@ export class DataGridContextMenuOrderService { isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, - order: 0, + order: 1, title: 'data_grid_table_order', icon: 'order-arrow-unknown', isPanel: true, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts index 4991f94225..c2b19d0b61 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts @@ -9,10 +9,11 @@ import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IContextMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; +import type { IDatabaseDataModel, IDataTableActions } from '@cloudbeaver/plugin-data-viewer'; export interface IDataGridCellMenuContext { model: IDatabaseDataModel; + actions: IDataTableActions; resultIndex: number; row: number; column: number; @@ -36,6 +37,7 @@ export class DataGridContextMenuService { constructMenuWithContext( model: IDatabaseDataModel, + actions: IDataTableActions, resultIndex: number, row: number, column: number @@ -43,17 +45,18 @@ export class DataGridContextMenuService { return this.contextMenuService.createContextMenu({ menuId: this.getMenuToken(), contextType: DataGridContextMenuService.cellContext, - data: { model, resultIndex, row, column }, + data: { model, actions, resultIndex, row, column }, }, this.getMenuToken()); } openMenu( model: IDatabaseDataModel, + actions: IDataTableActions, resultIndex: number, row: number, column: number ): void { - this.onRootMenuOpen.execute({ model, resultIndex, row, column }); + this.onRootMenuOpen.execute({ model, actions, resultIndex, row, column }); } add(panelId: string, menuItem: IContextMenuItem): void { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx index bd96959ac1..8c8fddbdba 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx @@ -7,29 +7,21 @@ */ import { ComplexLoader, createComplexLoader, Loader } from '@cloudbeaver/core-blocks'; -import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; - -interface Props { - tableModel: IDatabaseDataModel; - resultIndex: number; - className?: string; -} +import type { IDataPresentationProps } from '@cloudbeaver/plugin-data-viewer'; const loader = createComplexLoader(async function loader() { const { DataGridTable } = await import('./DataGridTable'); return { DataGridTable }; }); -export const DataGridLoader: React.FC = function DataGridLoader({ - tableModel, resultIndex, className, -}) { +export const DataGridLoader: React.FC = function DataGridLoader(props) { return ( } > {({ DataGridTable }) => ( - + )} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index e661c70533..53d699e943 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -14,7 +14,7 @@ import styled from 'reshadow'; import { Executor } from '@cloudbeaver/core-executor'; import { useStyles } from '@cloudbeaver/core-theming'; -import { IDatabaseDataEditorActionsData, IDatabaseDataModel, IDatabaseResultSet, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; +import { IDatabaseDataEditorActionsData, IDatabaseDataModel, IDatabaseResultSet, IDataPresentationProps, IDataTableActions, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; import { CellPosition, EditingContext } from '../Editing/EditingContext'; import { useEditing } from '../Editing/useEditing'; @@ -30,18 +30,12 @@ import { useGridDragging } from './useGridDragging'; import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy'; import { useTableData } from './useTableData'; -interface Props { - model: IDatabaseDataModel; - resultIndex: number; - className?: string; -} - function isAtBottom(event: React.UIEvent): boolean { const target = event.target as HTMLDivElement; return target.clientHeight + target.scrollTop + 100 > target.scrollHeight; } -export const DataGridTable: React.FC = observer(function DataGridTable({ model, resultIndex, className }) { +export const DataGridTable: React.FC> = observer(function DataGridTable({ model, actions, resultIndex, className }) { const gridContainerRef = useRef(null); const dataGridRef = useRef(null); const editorRef = useRef(null); @@ -143,12 +137,13 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const gridContext = useMemo(() => ({ model, + actions, columnResize, resultIndex, isGridInFocus: () => gridContainerRef.current === document.activeElement, getEditorPortal: () => editorRef.current, getDataGridApi: () => dataGridRef.current, - }), [model, resultIndex, editorRef, dataGridRef]); + }), [model, actions, resultIndex, editorRef, dataGridRef]); return styled(styles)( @@ -157,7 +152,6 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ = observer(function CellFor {(isCellSelected || cellContext?.mouse.state.mouseEnter || menuVisible) && context && ( ; + actions: IDataTableActions; resultIndex: number; row: number; column: number; + onClick?: () => void; onStateSwitch?: (state: boolean) => void; } export const CellMenu: React.FC = observer(function TreeNodeMenu({ model, + actions, resultIndex, row, column, + onClick, onStateSwitch, }) { const dataGridContextMenuService = useService(DataGridContextMenuService); @@ -40,7 +44,7 @@ export const CellMenu: React.FC = observer(function TreeNodeMenu({ const { panel, hidden } = useMemo( () => { - const panel = dataGridContextMenuService.constructMenuWithContext(model, resultIndex, row, column); + const panel = dataGridContextMenuService.constructMenuWithContext(model, actions, resultIndex, row, column); const hidden = computed(() => !panel.menuItems.length || panel.menuItems.every(item => item.isHidden)); @@ -49,6 +53,11 @@ export const CellMenu: React.FC = observer(function TreeNodeMenu({ [column, row] ); + function handleClick() { + dataGridContextMenuService.openMenu(model, actions, resultIndex, row, column); + onClick?.(); + } + if (hidden.get()) { return null; } @@ -59,7 +68,7 @@ export const CellMenu: React.FC = observer(function TreeNodeMenu({ panel={panel} style={[cellMenuStyles]} modal - onClick={() => dataGridContextMenuService.openMenu(model, resultIndex, row, column)} + onClick={handleClick} onVisibleSwitch={onStateSwitch} > diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index 279ad2cda0..e22c682212 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -13,6 +13,7 @@ import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; +import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { SpreadsheetGrid } from './SpreadsheetGrid'; @@ -23,6 +24,7 @@ export class SpreadsheetBootstrap extends Bootstrap { private dataGridSettingsService: DataGridSettingsService, private dataGridContextMenuSortingService: DataGridContextMenuOrderService, private dataGridContextMenuFilterService: DataGridContextMenuFilterService, + private dataGridContextMenuService: DataGridContextMenuService, exceptionsCatcherService: ExceptionsCatcherService ) { super(); @@ -40,6 +42,25 @@ export class SpreadsheetBootstrap extends Bootstrap { }); this.dataGridContextMenuSortingService.register(); this.dataGridContextMenuFilterService.register(); + + this.dataGridContextMenuService.add( + this.dataGridContextMenuService.getMenuToken(), + { + id: 'view_value_panel', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden(context) { + return typeof context.data.actions.valuePresentationId === 'string'; + }, + order: 0.5, + title: 'data_grid_table_open_value_panel', + icon: '/icons/text_value_presentation.png', + onClick(context) { + context.data.actions.setValuePresentation(''); + }, + } + ); } load(): void | Promise { } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx index fb0d5c63ab..7db7d8eeeb 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx @@ -10,10 +10,6 @@ import type { IDataPresentationProps } from '@cloudbeaver/plugin-data-viewer'; import { DataGridLoader } from './DataGrid/DataGridLoader'; -export const SpreadsheetGrid: React.FC = function SpreadsheetGrid({ - model, - className, - resultIndex, -}) { - return ; +export const SpreadsheetGrid: React.FC = function SpreadsheetGrid(props) { + return ; }; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index 70c9ea54bf..852be68a18 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -1,5 +1,6 @@ export default [ ['data_grid_table_order', 'Sorting'], + ['data_grid_table_open_value_panel', 'Show in value panel'], ['data_grid_table_filter', 'Filters'], ['data_grid_table_filter_cell_value', 'Cell value'], ['data_grid_table_filter_custom_value', 'Custom'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index a9ab087b63..a5aeb23295 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -1,5 +1,6 @@ export default [ ['data_grid_table_order', 'Сортировка'], + ['data_grid_table_open_value_panel', 'Показать в панели значений'], ['data_grid_table_filter', 'Фильтры'], ['data_grid_table_filter_cell_value', 'Значение ячейки'], ['data_grid_table_filter_custom_value', 'Произвольный'], diff --git a/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts b/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts index 7c888c1f50..676eaf9f0a 100644 --- a/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataPresentationService.ts @@ -12,6 +12,7 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; import type { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult'; +import type { IDataTableActions } from './TableViewer/IDataTableActions'; export interface IDataPresentationProps< TOptions = any, @@ -19,6 +20,7 @@ export interface IDataPresentationProps< > { dataFormat: ResultDataFormat; model: IDatabaseDataModel; + actions: IDataTableActions; resultIndex: number; className?: string; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts new file mode 100644 index 0000000000..5eeb122626 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; + +export interface IDataTableActions { + presentationId: string | undefined; + valuePresentationId: string | null | undefined; + dataModel: IDatabaseDataModel | undefined; + + setPresentation: (id: string) => void; + setValuePresentation: (id: string | null) => void; +} + +export interface IDataTableActionsPrivate extends IDataTableActions { + resultIndex: number; + dataFormat: ResultDataFormat; + onPresentationChange: (id: string) => void; + onValuePresentationChange: (id: string | null) => void; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx index 292af527d3..5a0f147bc1 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx @@ -15,10 +15,12 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDataPresentationOptions } from '../DataPresentationService'; +import type { IDataTableActions } from './IDataTableActions'; import { TableStatistics } from './TableStatistics'; type TableGridProps = PropsWithChildren<{ model: IDatabaseDataModel; + actions: IDataTableActions; dataFormat: ResultDataFormat; presentation: IDataPresentationOptions; resultIndex: number; @@ -33,6 +35,7 @@ const styles = css` export const TableGrid = observer(function TableGrid({ model, + actions, dataFormat, presentation, resultIndex, @@ -58,6 +61,6 @@ export const TableGrid = observer(function TableGrid({ } return styled(styles)( - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx index 62b073681d..99dfa951bb 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx @@ -16,9 +16,11 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDataPresentationOptions } from '../DataPresentationService'; +import type { IDataTableActions } from './IDataTableActions'; type TableGridProps = PropsWithChildren<{ model: IDatabaseDataModel; + actions: IDataTableActions; dataFormat: ResultDataFormat; presentation: IDataPresentationOptions | null; resultIndex: number; @@ -33,6 +35,7 @@ const styles = css` export const TableToolsPanel = observer(function TableToolsPanel({ model, + actions, dataFormat, presentation, resultIndex, @@ -57,6 +60,6 @@ export const TableToolsPanel = observer(function TableToolsPanel({ } return styled(styles)( - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 476196342f..0d56f47eb8 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -6,16 +6,18 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, Pane, ResizerControls, Split, splitStyles, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { Loader, Pane, ResizerControls, Split, splitStyles, TextPlaceholder, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import { DataPresentationService, DataPresentationType } from '../DataPresentationService'; +import type { IDataTableActionsPrivate } from './IDataTableActions'; import { TableError } from './TableError'; import { TableFooter } from './TableFooter/TableFooter'; import { TableGrid } from './TableGrid'; @@ -117,30 +119,66 @@ export const TableViewer: React.FC = observer(function TableViewer({ const loading = dataModel?.isLoading() ?? true; const dataFormat = result?.dataFormat || ResultDataFormat.Resultset; - const handlePresentationChange = useCallback((id: string) => { - const presentation = dataPresentationService.get(id); - if (presentation) { - if ( - presentation.dataFormat !== undefined - && presentation.dataFormat !== dataModel?.source.dataFormat - ) { - dataModel?.setDataFormat(presentation.dataFormat) - .reload(); - } - onPresentationChange(id); - } - }, [onPresentationChange, dataModel]); + const dataTableActions = useObjectRef({ + presentationId, + valuePresentationId, + dataModel, + resultIndex, + dataFormat, + onPresentationChange, + onValuePresentationChange, - function handleValuePresentationChange(id: string) { - if (id === valuePresentationId) { - onValuePresentationChange(null); - return; - } - const presentation = dataPresentationService.get(id); - if (presentation) { - onValuePresentationChange(id); - } - } + setPresentation(id: string) { + const presentation = dataPresentationService.get(id); + + if (presentation) { + if ( + presentation.dataFormat !== undefined + && presentation.dataFormat !== this.dataModel?.source.dataFormat + ) { + this.dataModel?.setDataFormat(presentation.dataFormat).reload(); + } + this.onPresentationChange(id); + } + }, + + setValuePresentation(id: string | null) { + if (id === this.valuePresentationId || id === null) { + this.onValuePresentationChange(null); + return; + } + + let presentation = dataPresentationService.get(id); + + if (!presentation && this.dataModel) { + presentation = dataPresentationService.getSupported( + DataPresentationType.toolsPanel, + this.dataFormat, + undefined, + this.dataModel, + this.resultIndex + ) ?? undefined; + } + + if (presentation) { + this.onValuePresentationChange(presentation.id); + } + }, + }, { + presentationId, + valuePresentationId, + dataModel, + resultIndex, + dataFormat, + onPresentationChange, + onValuePresentationChange, + }, { + presentationId: observable, + valuePresentationId: observable, + dataFormat: observable, + resultIndex: observable, + dataModel: observable.ref, + }, ['setPresentation', 'setValuePresentation']); useEffect(() => { if (!presentationId || !dataModel) { @@ -189,9 +227,9 @@ export const TableViewer: React.FC = observer(function TableViewer({ && resultExist; return styled(styles)( - + - + = observer(function TableViewer({ supportedDataFormat={dataModel.supportedDataFormats} model={dataModel} resultIndex={resultIndex} - onPresentationChange={handlePresentationChange} + onPresentationChange={dataTableActions.setPresentation} /> @@ -207,6 +245,7 @@ export const TableViewer: React.FC = observer(function TableViewer({ = observer(function TableViewer({ {resultExist && ( = observer(function TableViewer({ supportedDataFormat={[dataFormat]} model={dataModel} resultIndex={resultIndex} - onPresentationChange={handleValuePresentationChange} + onPresentationChange={dataTableActions.setValuePresentation} /> diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 10511895f0..75da1e773b 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -37,6 +37,7 @@ export * from './DatabaseDataModel/Order'; export * from './TableViewer/TableViewerStorageService'; export * from './TableViewer/ValuePanel/DataValuePanelService'; +export * from './TableViewer/IDataTableActions'; export * from './TableViewer/TableViewer'; export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; export * from './TableViewer/DataModelWrapper';