From c35f0a45b0e8269332d21bc32ec146da647ae7ff Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 24 Mar 2021 14:04:20 +0300 Subject: [PATCH 001/406] feat(core-blocks): CB-808 update input border --- .../core-blocks/src/FormControls/ComboboxNew.tsx | 4 ++-- .../core-theming/src/styles/_form-controls.scss | 8 ++++---- .../core-theming/src/styles/_theme-light.scss | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx index b6a27894f1..c97533f8d4 100644 --- a/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx @@ -45,7 +45,7 @@ const styles = composes( font-weight: 500; } input { - padding-right: 20px; + padding-right: 12px !important; } MenuButton { position: absolute; @@ -73,7 +73,7 @@ const styles = composes( & MenuItem { background: transparent; display: block; - padding: 4px 36px; + padding: 4px 12px; text-align: left; outline: none; color: inherit; diff --git a/webapp/packages/core-theming/src/styles/_form-controls.scss b/webapp/packages/core-theming/src/styles/_form-controls.scss index c4e75f0438..66add1ab59 100644 --- a/webapp/packages/core-theming/src/styles/_form-controls.scss +++ b/webapp/packages/core-theming/src/styles/_form-controls.scss @@ -58,14 +58,14 @@ input[role="new"], textarea[role="new"] { box-sizing: border-box; - padding: 7px 11px; + padding: 7px 12px; font-size: 12px; width: 100%; border: 2px solid; outline: none; border-radius: 3px; - @include mdc-theme-prop(color, on-secondary, false); + @include mdc-theme-prop(color, text-primary-on-light, false); @include mdc-theme-prop(border-color, background, false); @include mdc-theme-prop(background-color, input-background, false); @@ -81,12 +81,12 @@ &:global([readonly]) { @include mdc-theme-prop(border-color, input-border-readonly, false); @include mdc-theme-prop(background-color, input-background-readonly, false); - opacity: 0.8; + @include mdc-theme-prop(color, text-secondary-on-light, false); } &:global([disabled]) { @include mdc-theme-prop(border-color, input-border-disabled, false); @include mdc-theme-prop(background-color, input-background-disabled, false); - opacity: 0.7; + @include mdc-theme-prop(color, text-hint-on-light, false); } &:global([data-embedded]) { &:global([disabled]), diff --git a/webapp/packages/core-theming/src/styles/_theme-light.scss b/webapp/packages/core-theming/src/styles/_theme-light.scss index fcb61edd6c..127cc8938c 100644 --- a/webapp/packages/core-theming/src/styles/_theme-light.scss +++ b/webapp/packages/core-theming/src/styles/_theme-light.scss @@ -21,12 +21,12 @@ $color-negative: #e73e52; $link-color: $mdc-theme-primary; $link-color-focus: #3f96d1; -$input-border: #dedede; -$input-border-disabled: transparent; -$input-border-readonly: transparent; +$input-border: #d9d9d9; +$input-border-disabled: $input-border; +$input-border-readonly: $input-border; $input-background: $mdc-theme-surface; -$input-background-disabled: $mdc-theme-secondary; -$input-background-readonly: $mdc-theme-secondary; +$input-background-disabled: #e9e9e9; +$input-background-readonly: #e9e9e9; $mdc-theme-text-colors: ( light: ( @@ -40,7 +40,7 @@ $mdc-theme-text-colors: ( primary: rgba(black, 0.87), secondary: rgba(black, 0.45), hint: rgba(black, 0.45), - disabled: rgba(black, 0.20), + disabled: rgba(black, 0.2), icon: rgba(black, 0.38), ), ); From 2a6d533c046691d16257bcd02ee1b9a188b23bd3 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 31 Mar 2021 19:43:20 +0300 Subject: [PATCH 002/406] fix(core-app): CB-847, CB-867, CB-858 grid sorting indication, product info logo, remove sandwich from log viewer --- .../core-app/public/icons/logo-secondary.svg | 16 +++++++ .../icons/logo-secondary_theme-dark.svg | 16 +++++++ .../LogViewTab/LogEntry/LogEntry.tsx | 40 +++++++++-------- .../TableColumnHeader/TableColumnHeader.tsx | 43 +++++++++++++------ .../plugin-product/src/ProductInfoDialog.tsx | 23 +++++----- 5 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 webapp/packages/core-app/public/icons/logo-secondary.svg create mode 100644 webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg diff --git a/webapp/packages/core-app/public/icons/logo-secondary.svg b/webapp/packages/core-app/public/icons/logo-secondary.svg new file mode 100644 index 0000000000..693353bef4 --- /dev/null +++ b/webapp/packages/core-app/public/icons/logo-secondary.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg b/webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg new file mode 100644 index 0000000000..966881ade7 --- /dev/null +++ b/webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx index 94e4eb64d3..b1354eb727 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx @@ -9,9 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { Icon } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; -import { useStyles } from '@cloudbeaver/core-theming'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; import type { ILogEntry } from '../ILogEntry'; import { LogEntryController } from './LogEntryController'; @@ -26,24 +25,14 @@ const style = css` align-items: center; } message { - flex: 1; word-break: break-word; white-space: nowrap; overflow: hidden; padding-right: 16px; text-overflow: ellipsis; } - - snack { - display: flex; - cursor: pointer; - } - - snack Icon { - display: block; - width: 16px; - height: 10px; - fill: rgba(255, 255, 255, 0); + fill { + flex: 1; } tr:hover Icon { @@ -51,21 +40,34 @@ const style = css` } `; +const messageWithDetailsStyles = composes( + css` + message { + composes: theme-text-primary from global; + } + `, + css` + message { + cursor: pointer; + &:hover { + opacity: 0.8; + } + } + `); + export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) { const controller = useController(LogEntryController, item); - return styled(useStyles(style))( + return styled(useStyles(style, !!item.stackTrace && messageWithDetailsStyles))( {item.type} {item.time} - + {item.message} - - - + diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index 4299f3ea6b..edfaf42ad6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -13,6 +13,7 @@ import styled, { css, use } from 'reshadow'; import { StaticImage, Icon } from '@cloudbeaver/core-blocks'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; import type { SortMode } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; @@ -45,7 +46,7 @@ const headerStyles = css` flex-grow: 1; } - sort-icon { + sort-icons { margin-left: 4px; display: flex; padding: 2px 4px; @@ -57,25 +58,41 @@ const headerStyles = css` cursor: pointer; } - sort-icon > Icon { + sort-icons > SortIcon { width: 8px; - fill: #cbcbcb; + fill: currentColor !important; } - sort-icon > Icon:last-child { + sort-icons > SortIcon:last-child { transform: scaleY(-1); } - sort-icon > Icon[|active] { - fill: #338ECC; - } - sort-icon:hover > Icon { + sort-icons:hover > SortIcon { width: 9px; } - sort-icon[|disabled] { + sort-icons[|disabled] { opacity: 0.7; cursor: default; } `; +const activeSortIcon = composes( + css` + Icon { + composes: theme-text-primary from global; + } + ` +); + +interface ISortIconProps { + active: boolean; + className?: string; +} + +const SortIcon: React.FC = function SortIcon({ active, className }) { + return styled(useStyles(active && activeSortIcon))( + + ); +}; + function getColumn(colIdx: number, source: SqlResultSet) { return source.columns?.[colIdx]; } @@ -134,10 +151,10 @@ export const TableColumnHeader: React.FC> = observer(fu {columnName} {sortable && ( - - - - + + + + )} ); diff --git a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx index 0190a6485f..8942a81f7a 100644 --- a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx +++ b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx @@ -15,15 +15,9 @@ import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { ThemeService, useStyles } from '@cloudbeaver/core-theming'; -const productInfoDialogStyles = composes( - css` - IconOrImage { - composes: theme-background-primary from global; - } - `, - css` +const productInfoDialogStyles = css` CommonDialogWrapper { min-width: 600px; } @@ -38,20 +32,27 @@ const productInfoDialogStyles = composes( display: flex; white-space: pre-wrap; } + FormFieldDescriptionNew { + white-space: pre-wrap; + } IconOrImage { - padding: 10px; + max-width: 154px; + height: 32px; border-radius: 2px; } -`); +`; export const ProductInfoDialog: React.FC> = observer( function ProductInfoDialog(props) { const translate = useTranslate(); const styles = useStyles(BASE_CONTAINERS_STYLES, productInfoDialogStyles); const serverConfigResource = useService(ServerConfigResource); + const themeService = useService(ThemeService); + const version = useAppVersion(); const productInfo = serverConfigResource.data?.productInfo; + const logoIcon = themeService.currentThemeId === 'light' ? '/icons/logo-secondary.svg' : '/icons/logo-secondary_theme-dark.svg'; return styled(styles)( > = observer( {translate('app_product_info_placeholder')} ) : ( <> - + {productInfo.name} From 422dce28555ed450f4bcfffb1948c0434f39d882 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 1 Apr 2021 12:48:04 +0300 Subject: [PATCH 003/406] fix(core-app): CB-847, CB-867, CB-858 naming correction --- ...y_theme-dark.svg => product-logo_dark.svg} | 0 ...o-secondary.svg => product-logo_light.svg} | 0 .../LogViewTab/LogEntry/LogEntry.tsx | 34 ++++++------------- .../plugin-product/src/ProductInfoDialog.tsx | 2 +- 4 files changed, 11 insertions(+), 25 deletions(-) rename webapp/packages/core-app/public/icons/{logo-secondary_theme-dark.svg => product-logo_dark.svg} (100%) rename webapp/packages/core-app/public/icons/{logo-secondary.svg => product-logo_light.svg} (100%) diff --git a/webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg b/webapp/packages/core-app/public/icons/product-logo_dark.svg similarity index 100% rename from webapp/packages/core-app/public/icons/logo-secondary_theme-dark.svg rename to webapp/packages/core-app/public/icons/product-logo_dark.svg diff --git a/webapp/packages/core-app/public/icons/logo-secondary.svg b/webapp/packages/core-app/public/icons/product-logo_light.svg similarity index 100% rename from webapp/packages/core-app/public/icons/logo-secondary.svg rename to webapp/packages/core-app/public/icons/product-logo_light.svg diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx index b1354eb727..5c506ba491 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx @@ -7,10 +7,11 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import styled, { css, use } from 'reshadow'; +import { Link } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; -import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { useStyles } from '@cloudbeaver/core-theming'; import type { ILogEntry } from '../ILogEntry'; import { LogEntryController } from './LogEntryController'; @@ -30,9 +31,9 @@ const style = css` overflow: hidden; padding-right: 16px; text-overflow: ellipsis; - } - fill { - flex: 1; + &[|details] { + cursor: pointer; + } } tr:hover Icon { @@ -40,34 +41,19 @@ const style = css` } `; -const messageWithDetailsStyles = composes( - css` - message { - composes: theme-text-primary from global; - } - `, - css` - message { - cursor: pointer; - &:hover { - opacity: 0.8; - } - } - `); - export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) { const controller = useController(LogEntryController, item); + const isDetails = !!item.stackTrace; - return styled(useStyles(style, !!item.stackTrace && messageWithDetailsStyles))( + return styled(useStyles(style))( {item.type} {item.time} - - {item.message} + + {isDetails ? {item.message} : item.message} - diff --git a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx index 8942a81f7a..c533b81b88 100644 --- a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx +++ b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx @@ -52,7 +52,7 @@ export const ProductInfoDialog: React.FC> = observer( const version = useAppVersion(); const productInfo = serverConfigResource.data?.productInfo; - const logoIcon = themeService.currentThemeId === 'light' ? '/icons/logo-secondary.svg' : '/icons/logo-secondary_theme-dark.svg'; + const logoIcon = themeService.currentThemeId === 'light' ? '/icons/product-logo_light.svg' : '/icons/product-logo_dark.svg'; return styled(styles)( Date: Thu, 1 Apr 2021 16:53:44 +0300 Subject: [PATCH 004/406] fix(core-app): CB-911 add catch error block for delete and refresh node operations --- .../NavigationTreeContextMenuService.ts | 8 ++++++-- .../NodesManager/ConnectionDialogsService.ts | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index c76e3a614c..a4101e7d7d 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -177,9 +177,13 @@ export class NavigationTreeContextMenuService extends Bootstrap { }, order: Number.MAX_SAFE_INTEGER, title: 'app_navigationTree_refreshNode', - onClick: context => { + onClick: async context => { const node = context.data; - this.navNodeManagerService.refreshTree(node.id); + try { + await this.navNodeManagerService.refreshTree(node.id); + } catch (exception) { + this.notificationService.logException(exception, 'Failed to refresh node'); + } }, } ); diff --git a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts index b80fa2390b..f2943953f6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts @@ -9,6 +9,7 @@ import { ConnectionInfoResource, ConnectionsManagerService, EConnectionFeature } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; import { NavigationTreeContextMenuService } from '../../NavigationTree/NavigationTreeContextMenuService'; import { EMainMenu, MainMenuService } from '../../TopNavBar/MainMenu/MainMenuService'; @@ -22,7 +23,8 @@ export class ConnectionDialogsService extends Bootstrap { private mainMenuService: MainMenuService, private contextMenuService: ContextMenuService, private connectionsManagerService: ConnectionsManagerService, - private connectionInfoResource: ConnectionInfoResource + private connectionInfoResource: ConnectionInfoResource, + private notificationService: NotificationService, ) { super(); } @@ -76,15 +78,19 @@ export class ConnectionDialogsService extends Bootstrap { || !connection?.features.includes(EConnectionFeature.manageable); }, title: 'ui_delete', - onClick: (context: IMenuContext) => { + onClick: async (context: IMenuContext) => { const node = context.data; - this.connectionsManagerService.deleteConnection( - NodeManagerUtils.connectionNodeIdToConnectionId(node.id) - ); + try { + await this.connectionsManagerService.deleteConnection( + NodeManagerUtils.connectionNodeIdToConnectionId(node.id) + ); + } catch (exception) { + this.notificationService.logException(exception, 'Failed to delete connection'); + } }, } ); } - load(): void {} + load(): void { } } From 1cd93bee58ae0139c0acdafde769ba35dcd984f2 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 1 Apr 2021 18:00:49 +0300 Subject: [PATCH 005/406] fix(core-app): CB-898 children limit for all nodes in app --- webapp/packages/core-app/src/CoreSettingsService.ts | 3 ++- .../src/NavigationTree/NavigationTreeService.ts | 9 ++------- .../src/shared/NodesManager/NavTreeResource.ts | 13 ++++++++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/webapp/packages/core-app/src/CoreSettingsService.ts b/webapp/packages/core-app/src/CoreSettingsService.ts index a6abacd327..6e3c90a1f0 100644 --- a/webapp/packages/core-app/src/CoreSettingsService.ts +++ b/webapp/packages/core-app/src/CoreSettingsService.ts @@ -14,7 +14,8 @@ const defaultSettings = { 'app.logViewer.maxLogRecords': 1000, 'app.logViewer.logBatchSize': 2000, 'app.logViewer.maxFailedRequests': 3, - 'app.navigationTree.childrenLimit': 500, + // temporary limit for all nodes children in app + 'app.childrenLimit': 500, }; export type CoreSettings = typeof defaultSettings; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index f4eaffc6bb..97722ab392 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable } from 'mobx'; +import { action, makeObservable } from 'mobx'; import { ConnectionAuthService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; @@ -39,10 +39,6 @@ export class NavigationTreeService { readonly navigationTreeMetadata: MetadataMap; readonly nodeSelectionTask: IExecutor; - get childrenLimit(): number { - return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); - } - constructor( private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, @@ -53,7 +49,6 @@ export class NavigationTreeService { private navNodeInfoResource: NavNodeInfoResource ) { makeObservable(this, { - childrenLimit: computed, unselectAll: action, }); @@ -80,7 +75,7 @@ export class NavigationTreeService { const children = this.navTreeResource.get(id); if (children) { - return children.slice(0, this.childrenLimit); + return children.slice(0, this.navTreeResource.childrenLimit); } return children; diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 5681024ec9..ad1115a6bb 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, makeObservable } from 'mobx'; +import { action, computed, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { @@ -22,6 +22,7 @@ import { } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; +import { CoreSettingsService } from '../../CoreSettingsService'; import { NavNodeInfoResource } from './NavNodeInfoResource'; // TODO: so much dirty @@ -39,13 +40,19 @@ interface INodeMetadata extends ICachedMapResourceMetadata { export class NavTreeResource extends CachedMapResource { protected metadata: MetadataMap; + get childrenLimit(): number { + return this.coreSettingsService.settings.getValue('app.childrenLimit'); + } + constructor( private graphQLService: GraphQLService, - private navNodeInfoResource: NavNodeInfoResource + private navNodeInfoResource: NavNodeInfoResource, + private coreSettingsService: CoreSettingsService, ) { super(); makeObservable(this, { + childrenLimit: computed, setDetails: action, }); @@ -242,6 +249,6 @@ export class NavTreeResource extends CachedMapResource { withDetails: metadata.withDetails, }); - return { navNodeChildren, navNodeInfo, parentPath }; + return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath }; } } From a8efae88a8020a98cfd70a48d8ffc8d2cef79773 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 2 Apr 2021 12:01:24 +0300 Subject: [PATCH 006/406] fix(core-app): CB-898 remove unnecessary code --- webapp/packages/core-app/src/CoreSettingsService.ts | 2 +- .../core-app/src/NavigationTree/NavigationTreeService.ts | 8 +------- .../core-app/src/shared/NodesManager/NavTreeResource.ts | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/webapp/packages/core-app/src/CoreSettingsService.ts b/webapp/packages/core-app/src/CoreSettingsService.ts index 6e3c90a1f0..4d5917a883 100644 --- a/webapp/packages/core-app/src/CoreSettingsService.ts +++ b/webapp/packages/core-app/src/CoreSettingsService.ts @@ -15,7 +15,7 @@ const defaultSettings = { 'app.logViewer.logBatchSize': 2000, 'app.logViewer.maxFailedRequests': 3, // temporary limit for all nodes children in app - 'app.childrenLimit': 500, + 'app.navigationTree.childrenLimit': 500, }; export type CoreSettings = typeof defaultSettings; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 97722ab392..96cdfb80c5 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -72,13 +72,7 @@ export class NavigationTreeService { } getChildren(id: string): string[] | undefined { - const children = this.navTreeResource.get(id); - - if (children) { - return children.slice(0, this.navTreeResource.childrenLimit); - } - - return children; + return this.navTreeResource.get(id); } async navToNode(id: string, parentId: string): Promise { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index ad1115a6bb..fe7d329cf0 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -41,7 +41,7 @@ export class NavTreeResource extends CachedMapResource { protected metadata: MetadataMap; get childrenLimit(): number { - return this.coreSettingsService.settings.getValue('app.childrenLimit'); + return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); } constructor( From 511bfcf7748b1b2f65393dcd58911926218cbdd2 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 2 Apr 2021 12:12:56 +0300 Subject: [PATCH 007/406] fix(core-app): CB-867 code optimization --- .../ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx index 5c506ba491..bc9e6c9c1e 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; +import styled, { css } from 'reshadow'; import { Link } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; @@ -31,9 +31,9 @@ const style = css` overflow: hidden; padding-right: 16px; text-overflow: ellipsis; - &[|details] { - cursor: pointer; - } + } + Link:hover { + cursor: pointer; } tr:hover Icon { @@ -43,7 +43,6 @@ const style = css` export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) { const controller = useController(LogEntryController, item); - const isDetails = !!item.stackTrace; return styled(useStyles(style))( @@ -51,8 +50,8 @@ export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) { {item.time} - - {isDetails ? {item.message} : item.message} + + {item.stackTrace ? {item.message} : item.message} From 9f04fa163bdf210e42bc6bf61a4a1ba603225f1c Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 2 Apr 2021 13:25:28 +0300 Subject: [PATCH 008/406] fix(plugin-gis-viewer): CB-904 add support for gis geometry collection --- .../configs/webpack.product.config.js | 2 +- .../packages/plugin-gis-viewer/package.json | 7 ++- .../src/GISValuePresentation.tsx | 11 ++-- .../src/terraformer-wkt.d.ts | 15 ----- webapp/yarn.lock | 55 ++++++++++++++++--- 5 files changed, 58 insertions(+), 32 deletions(-) delete mode 100644 webapp/packages/plugin-gis-viewer/src/terraformer-wkt.d.ts diff --git a/webapp/packages/core-cli/configs/webpack.product.config.js b/webapp/packages/core-cli/configs/webpack.product.config.js index 05f2621a36..62d61f4a8d 100644 --- a/webapp/packages/core-cli/configs/webpack.product.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.config.js @@ -31,7 +31,7 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), { cacheGroups: { vendor: { // TODO: we need another way to detect libraries to exclude - test: /[\\/]node_modules[\\/](?!(leaflet|react-leaflet|@terraformer|react-data-grid))(.[a-zA-Z0-9.\-_]+)[\\/]/, + test: /[\\/]node_modules[\\/](?!(leaflet|react-leaflet|wellknown|react-data-grid))(.[a-zA-Z0-9.\-_]+)[\\/]/, name: 'vendors', chunks: 'all', }, diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json index 86c811d289..7e3b684632 100644 --- a/webapp/packages/plugin-gis-viewer/package.json +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -38,11 +38,12 @@ "rxjs": "^6.x.x" }, "dependencies": { - "@terraformer/wkt": "^2.0.7", "leaflet": "^1.7.1", - "react-leaflet": "^3.1.0" + "react-leaflet": "^3.1.0", + "wellknown": "^0.5.0" }, "devDependencies": { - "@types/react-leaflet": "^2.8.1" + "@types/react-leaflet": "^2.8.1", + "@types/wellknown": "^0.5.1" } } diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx index bd81676338..396fdc104c 100644 --- a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx @@ -6,16 +6,13 @@ * you may not use this file except in compliance with the License. */ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import { observer } from 'mobx-react-lite'; import { useCallback, useMemo } from 'react'; +import wellknown from 'wellknown'; import { TextPlaceholder } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { IDatabaseResultSet, ResultSetSelectAction, IResultSetElementKey, IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; -import { wktToGeoJSON } from '@terraformer/wkt'; import { IGeoJSONFeature, IAssociatedValue, LeafletMap } from './LeafletMap'; import { ResultSetGISAction } from './ResultSetGISAction'; @@ -53,7 +50,11 @@ export const GISValuePresentation: React.FC = observer(function GISValueP } try { - const parsedCellValue = wktToGeoJSON(cellValue.mapText || cellValue.text); + const parsedCellValue = wellknown.parse(cellValue.mapText || cellValue.text); + if (!parsedCellValue) { + continue; + } + result.push({ type: 'Feature', geometry: parsedCellValue, properties: { associatedCell: cell, srid: cellValue.srid } }); } catch (exception) { console.error(`Failed to parse "${cellValue.mapText || cellValue.text}" value.`); diff --git a/webapp/packages/plugin-gis-viewer/src/terraformer-wkt.d.ts b/webapp/packages/plugin-gis-viewer/src/terraformer-wkt.d.ts deleted file mode 100644 index 6aee3c7dfd..0000000000 --- a/webapp/packages/plugin-gis-viewer/src/terraformer-wkt.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ - -declare module '@terraformer/wkt' { - import * as terraformer from '@terraformer/wkt'; - - export function wktToGeoJSON(wkt: string): GeoJSON.GeometryObject; - export function GeoJSONToWkt(geoJSON: GeoJSON.GeometryObject): string; - -} diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1f2c321629..2261a998c1 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2712,11 +2712,6 @@ dependencies: defer-to-connect "^1.0.1" -"@terraformer/wkt@^2.0.7": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@terraformer/wkt/-/wkt-2.0.7.tgz#970e7f72f361194fee4daca7738b6f15421d29d3" - integrity sha512-AOQHnQ5eqs5xWtIiX+ys/Xg80aYouTtBHiQDxRWFUhlmOBzrg7TfjrpC9gNPx8NP7XvuTzSwkARwCWCjOtDiWA== - "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2912,6 +2907,11 @@ dependencies: "@types/node" "*" +"@types/wellknown@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@types/wellknown/-/wellknown-0.5.1.tgz#4fdf9a9a21e78b640e7c60b7b6857165153d6a9d" + integrity sha512-Q654r/2cjmh4XdFcZbRUKrY9Z3KmJJhXGQ4snwjWJCNrDNsNCJRaKQZRd/KPJaNGC0gZUxcmAu7184JpDX2Sjw== + "@typescript-eslint/eslint-plugin@^4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz#5f580ea520fa46442deb82c038460c3dd3524bb6" @@ -4553,6 +4553,15 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" +concat-stream@~1.5.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + integrity sha1-cIl4Yk2FavQaWnQd790mHadSwmY= + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + config-chain@^1.1.11: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -7492,7 +7501,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8970,7 +8979,7 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -10922,6 +10931,11 @@ pretty-format@^3.8.0: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U= +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -11320,6 +11334,18 @@ read@1, read@~1.0.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readdir-scoped-modules@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -12559,6 +12585,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -13130,7 +13161,7 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray@^0.0.6: +typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= @@ -13636,6 +13667,14 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +wellknown@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101" + integrity sha1-Ca6YcfqCbPCm7BU37wDDedeNcQE= + dependencies: + concat-stream "~1.5.0" + minimist "~1.2.0" + whatwg-fetch@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" From 4cda0825ac80181f83d3f8549aa454e03ad6e1fd Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 2 Apr 2021 14:13:36 +0300 Subject: [PATCH 009/406] fix: CB-792 CB-730 CB-906 CB-620 CB-912 CB-914 auth --- .../AdministrationToolsStyles.ts | 1 + .../AdministrationItemService.ts | 10 +++ .../AdministrationItem/IAdministrationItem.ts | 1 + .../SessionExpiredDialogService.ts | 10 +-- .../core-authentication/src/AppAuthService.ts | 3 +- .../src/Snackbars/ActionSnackbar.tsx | 4 +- .../core-blocks/src/useFormValidator.ts | 3 +- .../core-events/src/EventsSettingsService.ts | 2 +- .../core-events/src/NotificationService.ts | 35 ++++++--- .../core-root/src/ServerConfigResource.ts | 45 ++++++++--- .../core-root/src/SessionExpireService.ts | 8 +- .../core-sdk/src/Resource/CachedResource.ts | 20 +++++ .../src/queries/session/serverConfig.gql | 1 - webapp/packages/core-sdk/src/sdk.ts | 3 +- webapp/packages/core-utils/src/index.ts | 1 + .../packages/core-utils/src/isArraysEqual.ts | 15 ++++ ...onfigurationWizardPagesBootstrapService.ts | 4 +- .../Form/ServerConfigurationInfoForm.tsx | 1 + .../ServerConfigurationNavigatorViewForm.tsx | 4 +- .../ServerConfigurationPage.tsx | 64 +++++++++++----- .../ServerConfigurationPageController.ts | 69 ----------------- .../ServerConfigurationService.ts | 76 ++++++++++++++----- .../plugin-administration/src/locales/en.ts | 3 + .../plugin-administration/src/locales/ru.ts | 12 ++- .../AuthenticationProviders.tsx | 32 ++------ ...verConfigurationAuthenticationBootstrap.ts | 3 +- .../src/AuthenticationService.ts | 5 +- .../src/PluginBootstrap.ts | 6 +- 28 files changed, 257 insertions(+), 184 deletions(-) create mode 100644 webapp/packages/core-utils/src/isArraysEqual.ts delete mode 100644 webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts diff --git a/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationToolsStyles.ts b/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationToolsStyles.ts index 47a968df1b..9d8524d7e4 100644 --- a/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationToolsStyles.ts +++ b/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationToolsStyles.ts @@ -21,6 +21,7 @@ export const ADMINISTRATION_TOOLS_STYLES = composes( AdministrationTools { padding: 0 16px; display: flex; + flex-shrink: 0; align-items: center; border-bottom: solid 1px; height: 48px; diff --git a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts index ef775be0eb..826519cfce 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts @@ -199,6 +199,16 @@ export class AdministrationItemService { } private activateHandler: IExecutorHandler = async ({ screen, configurationWizard, outside }) => { + let lastItem = 0; + while (true) { + const items = this.getActiveItems(configurationWizard); + if (lastItem === items.length) { + break; + } + await items[lastItem]?.onLoad?.(configurationWizard, outside); + lastItem++; + } + if (configurationWizard) { let item = 0; while (true) { diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 61b370d814..2e27c0b476 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -91,6 +91,7 @@ export interface IAdministrationItemOptions { replace?: IAdministrationItemReplaceOptions; getDrawerComponent: () => AdministrationItemDrawerComponent; getContentComponent: () => AdministrationItemContentComponent; + onLoad?: AdministrationItemEvent; onActivate?: AdministrationItemEvent; onDeActivate?: AdministrationItemEvent; canActivate?: AdministrationItemCanActivateEvent; diff --git a/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialogService.ts b/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialogService.ts index 24fc4e4add..1f2e12950e 100644 --- a/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialogService.ts +++ b/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialogService.ts @@ -8,7 +8,7 @@ import { ActionSnackbar } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; import { SessionExpireService } from '@cloudbeaver/core-root'; @@ -25,15 +25,15 @@ export class SessionExpiredDialogService extends Bootstrap { } register(): void { - this.sessionExpireService.onSessionExpire.subscribe(this.handleSessionExpired.bind(this)); + this.sessionExpireService.onSessionExpire.addHandler(this.handleSessionExpired.bind(this)); } load(): void | Promise { } private async handleSessionExpired(): Promise { - try { - await this.commonDialogService.open(SessionExpiredDialog, null); - } finally { + const state = await this.commonDialogService.open(SessionExpiredDialog, null); + + if (state === DialogueStateResult.Rejected) { this.notificationService.customNotification(() => ActionSnackbar, { actionText: 'app_root_session_expired_reload', onAction: () => location.reload(), diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index a4c40b30f1..d02dd28044 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -42,8 +42,7 @@ export class AppAuthService extends Bootstrap { const user = await this.userInfoResource.load(); - return !!config.authenticationEnabled - && !config.configurationMode + return !config.configurationMode && !config.anonymousAccessEnabled && user === null; } diff --git a/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx index ec3fb2b233..f32c441818 100644 --- a/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx +++ b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx @@ -18,12 +18,12 @@ import { SnackbarFooter } from './SnackbarMarkups/SnackbarFooter'; import { SnackbarStatus } from './SnackbarMarkups/SnackbarStatus'; import { SnackbarWrapper } from './SnackbarMarkups/SnackbarWrapper'; -interface Props extends INotificationExtraProps { +export interface ActionSnackbarProps extends INotificationExtraProps { onAction: () => void; actionText: string; } -export const ActionSnackbar: NotificationComponent = observer(function ActionSnackbar({ +export const ActionSnackbar: NotificationComponent = observer(function ActionSnackbar({ notification, onAction, actionText, }) { const translate = useTranslate(); diff --git a/webapp/packages/core-blocks/src/useFormValidator.ts b/webapp/packages/core-blocks/src/useFormValidator.ts index 46952aa43d..6398d05539 100644 --- a/webapp/packages/core-blocks/src/useFormValidator.ts +++ b/webapp/packages/core-blocks/src/useFormValidator.ts @@ -18,7 +18,8 @@ export function useFormValidator( ): void { useExecutor({ executor: validationTask, - handlers: [function validate() { + postHandlers: [function validate() { + ref.current?.focus(); ref.current?.checkValidity(); ref.current?.reportValidity(); diff --git a/webapp/packages/core-events/src/EventsSettingsService.ts b/webapp/packages/core-events/src/EventsSettingsService.ts index 439342bd2d..455bbe5f53 100644 --- a/webapp/packages/core-events/src/EventsSettingsService.ts +++ b/webapp/packages/core-events/src/EventsSettingsService.ts @@ -11,7 +11,7 @@ import { PluginManagerService } from '@cloudbeaver/core-plugin'; const defaultSettings = { notificationsPool: 5, - maxPersistentAllow: 1, + maxPersistentAllow: 5, }; diff --git a/webapp/packages/core-events/src/NotificationService.ts b/webapp/packages/core-events/src/NotificationService.ts index e8a99c8790..061c1fd05e 100644 --- a/webapp/packages/core-events/src/NotificationService.ts +++ b/webapp/packages/core-events/src/NotificationService.ts @@ -9,6 +9,7 @@ import { observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { getErrorDetails, GQLError } from '@cloudbeaver/core-sdk'; import { OrderedMap } from '@cloudbeaver/core-utils'; @@ -29,8 +30,9 @@ export const DELAY_DELETING = 1000; export class NotificationService { // todo change to common new Map() - readonly notificationList = new OrderedMap>(({ id }) => id); - private notificationNextId = 0; + readonly notificationList: OrderedMap>; + readonly closeTask: IExecutor; + private notificationNextId: number; get visibleNotifications(): Array> { return this.notificationList.values.filter(notification => !notification.isSilent); @@ -38,7 +40,11 @@ export class NotificationService { constructor( private settings: EventsSettingsService - ) {} + ) { + this.notificationList = new OrderedMap>(({ id }) => id); + this.closeTask = new Executor(); + this.notificationNextId = 0; + } notify = INotificationExtraProps>( options: INotificationOptions, type: ENotificationType @@ -64,7 +70,10 @@ export class NotificationService { state: observable({ deleteDelay: 0 }), timestamp: options.timestamp || Date.now(), type, - close: delayDeleting => this.close(id, delayDeleting), + close: delayDeleting => { + this.close(id, delayDeleting); + options.onClose?.(delayDeleting); + }, showDetails: this.showDetails.bind(this, id), }; @@ -89,8 +98,8 @@ export class NotificationService { component: () => NotificationComponent, props?: TProps extends any ? TProps : never, // some magic options?: INotificationOptions & { type?: ENotificationType } - ): void { - this.notify({ + ): INotification { + return this.notify({ title: '', ...options, customComponent: component, @@ -117,16 +126,16 @@ export class NotificationService { return { controller: processController, notification }; } - logInfo(notification: INotificationOptions): void { - this.notify(notification, ENotificationType.Info); + logInfo(notification: INotificationOptions): INotification { + return this.notify(notification, ENotificationType.Info); } - logSuccess(notification: INotificationOptions): void { - this.notify(notification, ENotificationType.Success); + logSuccess(notification: INotificationOptions): INotification { + return this.notify(notification, ENotificationType.Success); } - logError(notification: INotificationOptions): void { - this.notify(notification, ENotificationType.Error); + logError(notification: INotificationOptions): INotification { + return this.notify(notification, ENotificationType.Error); } logException(exception: Error | GQLError, title?: string, message?: string, silent?: boolean): void { @@ -154,11 +163,13 @@ export class NotificationService { notification.state.deleteDelay = DELAY_DELETING; setTimeout(() => { this.notificationList.remove(id); + this.closeTask.execute(id); }, DELAY_DELETING); } return; } this.notificationList.remove(id); + this.closeTask.execute(id); } showDetails(id: number): void { diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index ea133fb7b7..eca1623c0b 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -10,12 +10,14 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, CachedDataResource, ServerConfig, ServerConfigInput, NavigatorSettingsInput } from '@cloudbeaver/core-sdk'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings'; @injectable() export class ServerConfigResource extends CachedDataResource { update: ServerConfigInput; + navigatorSettingsUpdate: NavigatorSettingsInput; constructor( private graphQLService: GraphQLService @@ -23,9 +25,19 @@ export class ServerConfigResource extends CachedDataResource { + setNavigatorSettingsUpdate(update: NavigatorSettingsInput): void { + this.navigatorSettingsUpdate = update; + } + + async saveDefaultNavigatorSettings(): Promise { await this.performUpdate(undefined, undefined, async () => { - await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings }); + await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: this.navigatorSettingsUpdate }); if (this.data) { - this.data.defaultNavigatorSettings = { ...settings }; + this.data.defaultNavigatorSettings = { ...this.navigatorSettingsUpdate }; } else { this.data = await this.loader(); } - }, () => !this.isNavigatorSettingsChanged(settings)); + }, () => !this.isNavigatorSettingsChanged()); } async save(onlyRestart = false): Promise { await this.performUpdate(undefined, undefined, async () => { await this.graphQLService.sdk.configureServer({ - configuration: onlyRestart ? {} : this.update, + configuration: (onlyRestart && !this.isChanged()) ? {} : 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(); @@ -128,6 +148,8 @@ export class ServerConfigResource extends CachedDataResource>(null, (a, b) => this.includes(a.param, b.param)); + + // const logName = (action: string) => () => console.log(this.constructor.name + ': ' + action); + // const logInterrupted = (action: string): IExecutorHandler => (data, contexts) => { + // if (ExecutorInterrupter.isInterrupted(contexts)) { + // console.log(this.constructor.name + ': ' + action + 'interrupted'); + // } + // }; + + // this.beforeLoad + // .addHandler(logName('beforeLoad')) + // .addPostHandler(logInterrupted('beforeLoad')); + // this.onDataOutdated + // .addHandler(logName('onDataOutdated')) + // .addPostHandler(logInterrupted('onDataOutdated')); + // this.onDataUpdate + // .addHandler(logName('onDataUpdate')) + // .addPostHandler(logInterrupted('onDataUpdate')); + // this.onDataError + // .addHandler(logName('onDataError')) + // .addPostHandler(logInterrupted('onDataError')); } sync( diff --git a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql index 048783dab7..79657fe94d 100644 --- a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql +++ b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql @@ -10,7 +10,6 @@ query serverConfig { sessionExpireTime anonymousAccessEnabled - authenticationEnabled adminCredentialsSaveEnabled publicCredentialsSaveEnabled diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 0e42946493..ecf6fd9d09 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -1571,7 +1571,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>; export interface ServerConfigQuery { serverConfig: ( - Pick + Pick & { supportedLanguages: Array>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick } ); } @@ -2506,7 +2506,6 @@ export const ServerConfigDocument = ` supportsWorkspaces sessionExpireTime anonymousAccessEnabled - authenticationEnabled adminCredentialsSaveEnabled publicCredentialsSaveEnabled licenseRequired diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 41bc0758dc..e82ecdcad8 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -5,6 +5,7 @@ export * from './OrderedMap'; export * from './parseJSONFlat'; export * from './uuid_'; export * from './generateFileName'; +export * from './isArraysEqual'; export * from './md5'; export * from './TextTools'; export * from './copyToClipboard'; diff --git a/webapp/packages/core-utils/src/isArraysEqual.ts b/webapp/packages/core-utils/src/isArraysEqual.ts new file mode 100644 index 0000000000..16a3f1ad52 --- /dev/null +++ b/webapp/packages/core-utils/src/isArraysEqual.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export function isArraysEqual(first: T[], second: T[]): boolean { + if (first.length !== second.length) { + return false; + } + + return !first.some(region => !second.includes(region)); +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts index 4c83787b98..9aeef61533 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts @@ -56,7 +56,9 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap { ), }, order: 4, - onActivate: this.serverConfigurationService.loadConfig.bind(this.serverConfigurationService), + onActivate: () => this.serverConfigurationService.loadConfig(), + onDeActivate: () => this.serverConfigurationService.deactivate(), + onLoad: this.serverConfigurationService.loadConfig.bind(this.serverConfigurationService, false), getContentComponent: () => ServerConfigurationPage, getDrawerComponent: () => ServerConfigurationDrawerItem, }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index ce90213f68..0c54cf22d6 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx @@ -43,6 +43,7 @@ export const ServerConfigurationInfoForm: React.FC = observer(function Se name="sessionExpireTime" state={state.serverConfig} mod='surface' + min={1} mapState={v => (v ?? 1800000) / 1000 / 60} mapValue={v => (v ?? 30) * 1000 * 60} required diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx index f24e95fca7..8a1c79e3f2 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx @@ -30,9 +30,9 @@ export const ServerConfigurationNavigatorViewForm: React.FC = observer(fu const onNavigatorViewChangeHandler = useCallback((value: boolean) => { if (value) { - configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple }; + Object.assign(configs.navigatorConfig, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple); } else { - configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced }; + Object.assign(configs.navigatorConfig, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced); } }, [configs]); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx index 9a966da4df..6da79805ed 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx @@ -9,9 +9,10 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { AdministrationItemContentComponent, AdministrationTools, ADMINISTRATION_TOOLS_STYLES } from '@cloudbeaver/core-administration'; +import { AdministrationItemContentComponent, AdministrationTools, ADMINISTRATION_TOOLS_STYLES, ConfigurationWizardService } from '@cloudbeaver/core-administration'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupItem, GroupTitle, IconButton, Loader, Placeholder, SubmittingForm, useFocus, useFormValidator } from '@cloudbeaver/core-blocks'; -import { useController, useService } from '@cloudbeaver/core-di'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -20,7 +21,6 @@ import { ServerConfigurationConfigurationForm } from './Form/ServerConfiguration import { ServerConfigurationInfoForm } from './Form/ServerConfigurationInfoForm'; import { ServerConfigurationNavigatorViewForm } from './Form/ServerConfigurationNavigatorViewForm'; import { ServerConfigurationSecurityForm } from './Form/ServerConfigurationSecurityForm'; -import { ServerConfigurationPageController } from './ServerConfigurationPageController'; import { ServerConfigurationService } from './ServerConfigurationService'; const styles = css` @@ -43,23 +43,53 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser const translate = useTranslate(); const style = useStyles(styles, ADMINISTRATION_TOOLS_STYLES, BASE_CONTAINERS_STYLES); const [focusedRef] = useFocus({ focusFirstChild: true }); - const serverConfigResource = useService(ServerConfigResource); const service = useService(ServerConfigurationService); - const controller = useController(ServerConfigurationPageController); - const changed = serverConfigResource.isChanged() - || serverConfigResource.isNavigatorSettingsChanged(service.state.navigatorConfig); + const serverConfigResource = useService(ServerConfigResource); + const commonDialogService = useService(CommonDialogService); + const configurationWizardService = useService(ConfigurationWizardService); + const changed = serverConfigResource.isChanged() || serverConfigResource.isNavigatorSettingsChanged(); useFormValidator(service.validationTask, focusedRef); + function handleChange() { + service.changed(); + + if (!service.state.serverConfig.adminCredentialsSaveEnabled) { + service.state.serverConfig.publicCredentialsSaveEnabled = false; + } + } + + function reset() { + service.loadConfig(true); + } + + async function save() { + if (configurationWizard) { + await configurationWizardService.next(); + } else { + if (serverConfigResource.isChanged()) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'administration_server_configuration_save_confirmation_title', + message: 'administration_server_configuration_save_confirmation_message', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + } + await service.saveConfiguration(true); + } + } + return styled(style)( - - {controller.editing && ( + + {!configurationWizard && ( - - + + )} - {!controller.editing && ( + {configurationWizard && (

{translate('administration_configuration_wizard_configuration_title')}

@@ -73,18 +103,18 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser {() => styled(style)( - + {translate('administration_configuration_wizard_configuration_plugins')} - - + + - + )} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts deleted file mode 100644 index f491f1e154..0000000000 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { AdministrationScreenService, ConfigurationWizardService } from '@cloudbeaver/core-administration'; -import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; - -import type { IServerConfigurationPageState } from './IServerConfigurationPageState'; -import { ServerConfigurationService } from './ServerConfigurationService'; - -@injectable() -export class ServerConfigurationPageController { - get state(): IServerConfigurationPageState { - return this.serverConfigurationService.state; - } - - get editing(): boolean { - return !this.administrationScreenService.isConfigurationMode; - } - - constructor( - private readonly configurationWizardService: ConfigurationWizardService, - private readonly serverConfigurationService: ServerConfigurationService, - private readonly commonDialogService: CommonDialogService, - private readonly administrationScreenService: AdministrationScreenService - ) { - } - - reset = (): void => { - this.serverConfigurationService.loadConfig(); - }; - - change = (): void => { - this.serverConfigurationService.changed(); - - if (!this.state.serverConfig.adminCredentialsSaveEnabled) { - this.state.serverConfig.publicCredentialsSaveEnabled = false; - } - }; - - save = async (): Promise => { - if (this.administrationScreenService.isConfigurationMode) { - await this.finishConfiguration(); - } else { - await this.updateConfiguration(); - } - }; - - private async updateConfiguration(): Promise { - const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'administration_server_configuration_save_confirmation_title', - message: 'administration_server_configuration_save_confirmation_message', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - await this.serverConfigurationService.saveConfiguration(true); - } - - private async finishConfiguration(): Promise { - await this.configurationWizardService.next(); - } -} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 6de9745314..61898a9bde 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -9,13 +9,12 @@ import { observable, makeObservable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { ActionSnackbarProps, PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { INotification, NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutorHandler, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; import type { IServerConfigurationPageState } from './IServerConfigurationPageState'; @@ -32,7 +31,7 @@ export interface IServerConfigSaveData { export interface ILoadConfigData { state: IServerConfigurationPageState; - reload: boolean; + reset: boolean; } @injectable() @@ -48,11 +47,12 @@ export class ServerConfigurationService { readonly pluginsContainer: PlaceholderContainer; private done: boolean; + private stateLinked: boolean; + private unSaveNotification: INotification | null; constructor( private readonly administrationScreenService: AdministrationScreenService, private readonly serverConfigResource: ServerConfigResource, - private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService, ) { makeObservable(this, { @@ -61,9 +61,11 @@ export class ServerConfigurationService { done: observable, }); + this.stateLinked = false; this.done = false; this.loading = true; this.state = serverConfigStateContext(); + this.unSaveNotification = null; this.loadConfigTask = new Executor(); this.prepareConfigTask = new Executor(); this.saveTask = new Executor(); @@ -75,7 +77,10 @@ export class ServerConfigurationService { .next(this.validationTask, () => this.getSaveData(false)) .addHandler(() => { this.loading = true; }) .addHandler(this.loadServerConfig) - .addPostHandler(() => { this.loading = false; }); + .addPostHandler(() => { + this.loading = false; + this.showUnsavedNotification(); + }); this.saveTask .before(this.validationTask) @@ -85,28 +90,37 @@ export class ServerConfigurationService { this.validationTask .addHandler(this.validateForm) .addPostHandler(this.ensureValidation); + + this.serverConfigResource.onDataUpdate.addPostHandler(this.showUnsavedNotification.bind(this)); } changed(): void { this.done = false; } - async loadConfig(): Promise { + deactivate(): void { + this.showUnsavedNotification(); + } + + async loadConfig(reset = false): Promise { try { - let reload = false; - this.state = this.administrationScreenService.getItemState( - 'server-configuration', - () => { - reload = true; - return this.state; - }, - !this.administrationScreenService.isConfigurationMode - ); - this.serverConfigResource.setDataUpdate(this.state.serverConfig); + if (!this.stateLinked) { + this.state = this.administrationScreenService.getItemState( + 'server-configuration', + () => { + reset = true; + return this.state; + } + ); + this.serverConfigResource.setDataUpdate(this.state.serverConfig); + this.serverConfigResource.setNavigatorSettingsUpdate(this.state.navigatorConfig); + + this.stateLinked = true; + } await this.loadConfigTask.execute({ state: this.state, - reload, + reset, }); } catch (exception) { this.notificationService.logException(exception, 'Can\'t load server configuration'); @@ -126,7 +140,7 @@ export class ServerConfigurationService { } private loadServerConfig: IExecutorHandler = async (data, contexts) => { - if (!data.reload) { + if (!data.reset) { return; } @@ -144,7 +158,7 @@ export class ServerConfigurationService { data.state.serverConfig.publicCredentialsSaveEnabled = config.publicCredentialsSaveEnabled; data.state.serverConfig.customConnectionsEnabled = config.supportsCustomConnections; - data.state.navigatorConfig = { ...config.defaultNavigatorSettings }; + Object.assign(data.state.navigatorConfig, config.defaultNavigatorSettings); } catch (exception) { ExecutorInterrupter.interrupt(contexts); this.notificationService.logException(exception, 'Can\'t load server configuration'); @@ -167,7 +181,7 @@ export class ServerConfigurationService { } try { - await this.serverConfigResource.setDefaultNavigatorSettings(data.state.navigatorConfig); + await this.serverConfigResource.saveDefaultNavigatorSettings(); if (!data.configurationWizard || data.finish) { await this.serverConfigResource.save(); } @@ -206,6 +220,26 @@ export class ServerConfigurationService { } return true; } + + private showUnsavedNotification() { + if ( + !this.serverConfigResource.isChanged() + && !this.serverConfigResource.isNavigatorSettingsChanged() + ) { + this.unSaveNotification?.close(true); + return; + } + + if (this.unSaveNotification || this.administrationScreenService.isConfigurationMode) { + return; + } + + this.unSaveNotification = this.notificationService.logInfo({ + title: 'administration_configuration_wizard_configuration_server_info_unsaved', + persistent: true, + onClose: () => { this.unSaveNotification = null; }, + }); + } } export interface IValidationStatusContext { diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index 65eac90e24..786fff653c 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -1,11 +1,13 @@ export default [ ['administration_server_configuration_save_confirmation_title', 'Server settings update'], ['administration_server_configuration_save_confirmation_message', 'You are about to change critical settings, the application will be restarted.'], + ['administration_configuration_wizard_welcome', 'Welcome'], ['administration_configuration_wizard_welcome_step_description', 'Welcome to CloudBeaver'], ['administration_configuration_wizard_welcome_title', 'Welcome to CloudBeaver, cloud database management system!'], ['administration_configuration_wizard_welcome_message', 'The easy configuration wizard will guide you through several simple steps to set up the CloudBeaver server. You will need to set server information and administrator credentials. You can also add first database connections here.'], ['administration_configuration_wizard_welcome_note', 'Note: you will be able to change these configuration parameters later on the administration panel.'], + ['administration_configuration_wizard_configuration', 'Server configuration'], ['administration_configuration_wizard_configuration_step_description', 'Main server configuration'], ['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'], @@ -13,6 +15,7 @@ export default [ ['administration_configuration_wizard_configuration_server_info', 'Server Information'], ['administration_configuration_wizard_configuration_server_name', 'Server Name'], + ['administration_configuration_wizard_configuration_server_info_unsaved', 'You have unsaved changes'], ['administration_configuration_wizard_configuration_server_session_lifetime', 'Session lifetime'], ['administration_configuration_wizard_configuration_server_session_lifetime_description', 'Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires'], diff --git a/webapp/packages/plugin-administration/src/locales/ru.ts b/webapp/packages/plugin-administration/src/locales/ru.ts index e996b1e63b..b96c71e0e8 100644 --- a/webapp/packages/plugin-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-administration/src/locales/ru.ts @@ -1,16 +1,24 @@ export default [ + ['administration_server_configuration_save_confirmation_title', 'Изменение настроек сервера'], + ['administration_server_configuration_save_confirmation_message', 'Вы собираетесь изменить критичные настройки, приложение будет перезагружено.'], + ['administration_configuration_wizard_configuration', 'Настройки сервера'], + + ['administration_configuration_wizard_configuration_server_info', 'Информация о сервере'], + ['administration_configuration_wizard_configuration_server_name', 'Назвение сервера'], + ['administration_configuration_wizard_configuration_server_info_unsaved', 'Есть несохранённые изменения'], ['administration_configuration_wizard_configuration_server_session_lifetime', 'Время сессии'], ['administration_configuration_wizard_configuration_server_session_lifetime_description', 'В случае бездействия пользователя на протяжении указанного количества минут, его сессия будет закрыта.'], + + ['administration_configuration_wizard_configuration_plugins', 'Настройки'], ['administration_configuration_wizard_configuration_custom_connections', 'Разрешить пользовательские подключения'], ['administration_configuration_wizard_configuration_custom_connections_description', 'Позволяет пользователям создавать собственные подключения. Иначе, любые подключения могут быть добавлены только со страницы администрирования'], ['administration_configuration_wizard_configuration_navigation_tree_view', 'Упрощенная версия дерева навигации'], ['administration_configuration_wizard_configuration_navigation_tree_view_description', 'Все новые подключения, созданные пользователем, будут иметь только базовую информацию в дереве навигации'], + ['administration_configuration_wizard_configuration_security', 'Безопасность'], ['administration_configuration_wizard_configuration_security_admin_credentials', 'Позволить сохранять приватные данные'], ['administration_configuration_wizard_configuration_security_public_credentials', 'Позволить сохранять приватные данные для пользователей'], ['administration_configuration_wizard_configuration_security_admin_credentials_description', 'Позволяет сохранять приватные данные, такие как пароли и SSH ключи'], ['administration_configuration_wizard_configuration_security_public_credentials_description', 'Пользователи будут иметь возможность сохранять приватные данные, такие как пароли и SSH ключи'], - ['administration_server_configuration_save_confirmation_title', 'Изменение настроек сервера'], - ['administration_server_configuration_save_confirmation_message', 'Вы собираетесь изменить критичные настройки, приложение будет перезагружено.'], ]; diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx index 11a8d790f0..7a02f2fe1f 100644 --- a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -33,26 +33,21 @@ export const AuthenticationProviders: PlaceholderComponent 1; + const authenticationDisabled = serverConfig.enabledAuthProviders?.length === 0; useExecutor({ executor: formContext.changeExecutor, handlers: [function switchControls() { - if (externalAuthentication) { - serverConfig.enabledAuthProviders = [...providers.resource.keys]; - serverConfig.authenticationEnabled = true; - } - if (serverConfig.enabledAuthProviders?.length === 0) { - serverConfig.authenticationEnabled = false; - } - - if (!serverConfig.authenticationEnabled) { serverConfig.anonymousAccessEnabled = true; } }], }); + if (externalAuthentication) { + return null; + } + return styled(styles)( @@ -62,27 +57,16 @@ export const AuthenticationProviders: PlaceholderComponent {translate('administration_configuration_wizard_configuration_anonymous_access')} - - {translate('administration_configuration_wizard_configuration_authentication')} - + {translate('administration_configuration_wizard_configuration_authentication_provider')} - {() => providersSelectable && styled(styles)( + {() => styled(styles)( <> - {translate('administration_configuration_wizard_configuration_authentication_provider')} {providers.data.map(provider => provider && ( = async (data, contexts) => { - if (!data.reload) { + if (!data.reset) { return; } @@ -55,7 +55,6 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { } data.state.serverConfig.anonymousAccessEnabled = config.anonymousAccessEnabled; - data.state.serverConfig.authenticationEnabled = config.authenticationEnabled; data.state.serverConfig.enabledAuthProviders = [...config.enabledAuthProviders]; } catch (exception) { ExecutorInterrupter.interrupt(contexts); diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 234609abcb..9566586bc6 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -11,7 +11,7 @@ import { AppScreenService } from '@cloudbeaver/core-app'; import { AppAuthService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { IExecutorHandler } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; import { SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; @@ -75,6 +75,9 @@ export class AuthenticationService extends Bootstrap { } register(): void { + this.sessionDataResource.beforeLoad.addHandler( + ExecutorInterrupter.interrupter(() => this.appAuthService.isAuthNeeded()) + ); this.sessionDataResource.beforeLoad.addPostHandler(() => { this.requireAuthentication(); }); this.appScreenService.activation.addHandler(() => this.requireAuthentication()); diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index cd5b45f8e3..11dd05a769 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -10,7 +10,7 @@ import { AdministrationTopAppBarService } from '@cloudbeaver/core-administration import { SettingsMenuService, TopNavService } from '@cloudbeaver/core-app'; import { AuthInfoService } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; -import { ServerService } from '@cloudbeaver/core-root'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { ServerConfigurationService } from '@cloudbeaver/plugin-administration'; import { AuthenticationProviders } from './Administration/ServerConfiguration/AuthenticationProviders'; @@ -21,7 +21,7 @@ import { UserInfo } from './UserInfo'; @injectable() export class PluginBootstrap extends Bootstrap { constructor( - private serverService: ServerService, + private serverConfigResource: ServerConfigResource, private authDialogService: AuthDialogService, private authenticationService: AuthenticationService, private authInfoService: AuthInfoService, @@ -39,7 +39,7 @@ export class PluginBootstrap extends Bootstrap { { id: 'login', order: 0, - isHidden: () => !this.serverService.config.data?.authenticationEnabled || !!this.authInfoService.userInfo, + isHidden: () => this.serverConfigResource.enabledAuthProviders.length === 0 || !!this.authInfoService.userInfo, title: 'authentication_login', onClick: () => this.authDialogService.showLoginForm(), } From 27937a2afb741a6e26a631d6b13d44cfa060f25d Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Fri, 2 Apr 2021 17:09:42 +0300 Subject: [PATCH 010/406] JSON serialization enhancement --- .../src/io/cloudbeaver/server/CBApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index ce951b5766..442b47d84e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -619,7 +619,7 @@ public class CBApplication extends BaseApplicationImpl { json.endObject(); } if (appConfig.getEnabledAuthProviders() != null) { - JSONUtils.serializeStringList(json, "enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders())); + JSONUtils.serializeStringList(json, "enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders()), true); } if (!CommonUtils.isEmpty(appConfig.getPlugins())) { From 7afbc8ec028722b3f01d0f696754566fd3925b6e Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 5 Apr 2021 14:05:33 +0300 Subject: [PATCH 011/406] fix(plugin-data-spreadsheet-new): CB-743 don't copy null values to clipboard --- .../DataGrid/CellRenderer/CellRenderer.tsx | 8 +++- .../src/DataGrid/DataGridTable.tsx | 3 +- .../src/DataGrid/useGridSelectedCellsCopy.ts | 40 +++++++++++-------- .../src/styles/themes/_base-react-grid.scss | 8 +++- 4 files changed, 38 insertions(+), 21 deletions(-) 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 5778bceec0..1c3a726840 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 @@ -30,7 +30,13 @@ export const CellRenderer: React.FC> = observer(function const { rowIdx, column } = props; if (selectionContext?.isSelected(rowIdx, column.idx)) { - classes.push('rdg-cell-custom-selected'); + const columnIndex = tableDataContext?.getDataColumnIndexFromKey(column.key) ?? null; + + if (columnIndex !== null && editor?.getCell(rowIdx, columnIndex) === null) { + classes.push('rdg-cell-custom-null-selected'); + } else { + classes.push('rdg-cell-custom-selected'); + } } if (editingContext?.isEditing({ idx: column.idx, rowIdx })) { 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 95488c2ed6..5b74accbd6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -51,7 +51,6 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const [columnResize] = useState(() => new Executor()); const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); - const modelResultData = model.getResult(resultIndex); const tableData = useTableData(model, resultIndex); @@ -78,7 +77,7 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ }, }); - const { onKeydownHandler } = useGridSelectedCellsCopy(modelResultData, gridSelectionContext); + const { onKeydownHandler } = useGridSelectedCellsCopy(model, resultIndex, gridSelectionContext); const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({ onDragStart: startPosition => { dataGridRef.current?.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 4314ed4c6d..217007e541 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -9,9 +9,8 @@ import { useCallback } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; -import type { SqlResultSet } from '@cloudbeaver/core-sdk'; import { copyToClipboard } from '@cloudbeaver/core-utils'; -import type { IDatabaseDataResult } from '@cloudbeaver/plugin-data-viewer'; +import { IDatabaseDataModel, IDatabaseResultSet, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer'; import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; @@ -19,11 +18,12 @@ const EVENT_KEY_CODE = { C: 'KeyC', }; -function copyGridSelectedDataToClipboard(modelData: SqlResultSet, selectedCells: Map) { - if (!modelData.rows) { - return; - } - +function copyGridSelectedDataToClipboard( + model: IDatabaseDataModel, + resultIndex: number, + selectedCells: Map +) { + const format = model.source.getAction(resultIndex, ResultSetFormatAction); const orderedSelectedCells: Map = new Map([...selectedCells].sort((a, b) => a[0] - b[0])); const selectedColumns: Set = new Set(); @@ -34,7 +34,7 @@ function copyGridSelectedDataToClipboard(modelData: SqlResultSet, selectedCells: } } - const columns = [...selectedColumns].sort(); + const columns = [...selectedColumns].sort((a, b) => a - b); let data = ''; for (const [rowIdx, colIndexes] of orderedSelectedCells) { @@ -44,8 +44,12 @@ function copyGridSelectedDataToClipboard(modelData: SqlResultSet, selectedCells: } if (colIndexes.includes(column)) { - const value = modelData.rows?.[rowIdx][column]; - data += value; + const cell = model.source.getEditor(resultIndex).getCell(rowIdx, column); + const cellValue = format.get(cell); + if (cellValue === null) { + continue; + } + data += cellValue; } } data += '\r\n'; @@ -58,17 +62,19 @@ function copyGridSelectedDataToClipboard(modelData: SqlResultSet, selectedCells: type IKeyboardEvent = React.KeyboardEvent & KeyboardEvent; export function useGridSelectedCellsCopy( - modelResultData: IDatabaseDataResult | null, + model: IDatabaseDataModel, + resultIndex: number, selectionContext: IDataGridSelectionContext ) { - const props = useObjectRef({ modelResultData, selectionContext }); - const onKeydownHandler = useCallback((event: IKeyboardEvent) => { - if (!props.modelResultData) { - return; - } + const props = useObjectRef({ model, resultIndex, selectionContext }); + const onKeydownHandler = useCallback((event: IKeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) { - copyGridSelectedDataToClipboard(props.modelResultData.data, props.selectionContext.selectedCells); + copyGridSelectedDataToClipboard( + props.model, + props.resultIndex, + props.selectionContext.selectedCells + ); } }, []); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss index 52afbbd717..56491db925 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss @@ -16,9 +16,14 @@ $edited-color: #ff99006b; .rdg-cell-custom-selected:not(.rdg-cell-custom-editing) { background-color: rgba(0, 145, 234, 0.3); } + .rdg-cell-custom-null-selected:not(.rdg-cell-custom-editing) { + background-color: rgba(0, 145, 234, 0.1); + } + .rdg-cell-selected { box-shadow: inset 0 0 0 1px #0091ea; } + .rdg-cell-custom-editing { box-shadow: none; } @@ -56,7 +61,8 @@ $edited-color: #ff99006b; @include mdc-theme-prop(background-color, surface, false); } - .rdg-cell-custom-selected { + .rdg-cell-custom-selected, + .rdg-cell-custom-null-selected { background-color: rgba(150, 150, 150, 0.3); box-shadow: none; } From 8e43e6c95edafacf622679a6ba2981b82997b53e Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Mon, 5 Apr 2021 15:11:23 +0300 Subject: [PATCH 012/406] Auth provider identifying parameters --- .../io.cloudbeaver.service.auth/schema/service.auth.graphqls | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 68f4637954..ec4466c455 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -13,6 +13,9 @@ type AuthCredentialInfo { admin: Boolean # This field must be shown in login form user: Boolean + # This field can be used as user identifier + + identifying: Boolean possibleValues: [String] encryption: AuthCredentialEncryption } From db9b8a7156c8e4eef8fd68df0123333683ad6a8c Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 5 Apr 2021 16:55:06 +0300 Subject: [PATCH 013/406] fix(plugin-data-spreadsheet-new): CB-743, CB-851, CB-902, CB-903 run mouse up callback only if grid in focus --- .../CreateConnection/CreateConnection.tsx | 4 ---- .../src/ConnectionForm/SSH/SSH.tsx | 1 + .../src/DataGrid/CellRenderer/CellRenderer.tsx | 6 +++++- .../src/DataGrid/DataGridContext.ts | 1 + .../src/DataGrid/DataGridTable.tsx | 3 +++ .../src/GISViewerBootstrap.ts | 18 +++++++----------- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx index 9d58d7d300..eb7244077a 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -79,10 +79,6 @@ const styles = composes( flex: auto 0 0; } - IconButton { - color: rgba(0, 0, 0, 0.45); - } - StaticImage { width: 32px; max-height: 32px; diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx index c739855979..938ea96501 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx @@ -167,6 +167,7 @@ export const SSH: TabContainerPanelComponent = observer type='button' mod={['outlined']} disabled={disabled || !enabled || !passwordFilled} + loading={loading} onClick={testConnection} > {translate('connections_network_handler_test')} 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 5778bceec0..a41ceb31e3 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 @@ -58,6 +58,10 @@ export const CellRenderer: React.FC> = observer(function }, [column, rowIdx, selectionContext]); const handleMouseUp = useCallback((event: React.MouseEvent) => { + if (!dataGridContext?.isGridInFocus()) { + return; + } + selectionContext?.select( { colIdx: column.idx, @@ -67,7 +71,7 @@ export const CellRenderer: React.FC> = observer(function event.shiftKey, false ); - }, [column, rowIdx, selectionContext]); + }, [column, rowIdx, selectionContext, dataGridContext]); const handleDoubleClick = useCallback(() => { if (!column.editable) { 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 53017b49a8..3fdcf359ea 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -21,6 +21,7 @@ export interface IDataGridContext { model: IDatabaseDataModel; resultIndex: number; columnResize: IExecutor; + isGridInFocus: () => boolean; getEditorPortal: () => HTMLDivElement | null; getDataGridApi: () => DataGridHandle | null; } 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 95488c2ed6..f7bbbe9193 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -45,6 +45,7 @@ function isAtBottom(event: React.UIEvent): boolean { } export const DataGridTable: React.FC = observer(function DataGridTable({ model, resultIndex, className }) { + const gridContainerRef = useRef(null); const dataGridRef = useRef(null); const editorRef = useRef(null); const styles = useStyles(reactGridStyles, baseStyles); @@ -149,6 +150,7 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ model, columnResize, resultIndex, + isGridInFocus: () => gridContainerRef.current === document.activeElement, getEditorPortal: () => editorRef.current, getDataGridApi: () => dataGridRef.current, }), [model, resultIndex, editorRef, dataGridRef]); @@ -160,6 +162,7 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ 0) { - const firstSelectedCell = selectedCells[0]; - - return !gis.isGISFormat(firstSelectedCell); - } - - return true; }, }); } From 6aeb8b4d563082376f27c648390adf2bcee146d5 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 5 Apr 2021 17:00:48 +0300 Subject: [PATCH 014/406] fix(plugin-data-spreadsheet-new): CB-743 remove unnecessary css class --- .../src/DataGrid/CellRenderer/CellRenderer.tsx | 8 +------- .../src/styles/themes/_base-react-grid.scss | 6 +----- 2 files changed, 2 insertions(+), 12 deletions(-) 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 1c3a726840..5778bceec0 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 @@ -30,13 +30,7 @@ export const CellRenderer: React.FC> = observer(function const { rowIdx, column } = props; if (selectionContext?.isSelected(rowIdx, column.idx)) { - const columnIndex = tableDataContext?.getDataColumnIndexFromKey(column.key) ?? null; - - if (columnIndex !== null && editor?.getCell(rowIdx, columnIndex) === null) { - classes.push('rdg-cell-custom-null-selected'); - } else { - classes.push('rdg-cell-custom-selected'); - } + classes.push('rdg-cell-custom-selected'); } if (editingContext?.isEditing({ idx: column.idx, rowIdx })) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss index 56491db925..5ffd6cf884 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss @@ -16,9 +16,6 @@ $edited-color: #ff99006b; .rdg-cell-custom-selected:not(.rdg-cell-custom-editing) { background-color: rgba(0, 145, 234, 0.3); } - .rdg-cell-custom-null-selected:not(.rdg-cell-custom-editing) { - background-color: rgba(0, 145, 234, 0.1); - } .rdg-cell-selected { box-shadow: inset 0 0 0 1px #0091ea; @@ -61,8 +58,7 @@ $edited-color: #ff99006b; @include mdc-theme-prop(background-color, surface, false); } - .rdg-cell-custom-selected, - .rdg-cell-custom-null-selected { + .rdg-cell-custom-selected { background-color: rgba(150, 150, 150, 0.3); box-shadow: none; } From 9d476c2775ad3ee0e89851c58062c49661267c56 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 5 Apr 2021 17:15:25 +0300 Subject: [PATCH 015/406] fix(build): graphql-codegen build in nested workspace --- webapp/package.json | 10 +++++++--- webapp/packages/core-sdk/package.json | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index c0379fba06..3f3fb6413d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -6,7 +6,10 @@ "packages": [ "packages/*" ], - "nohoist": ["**/core-cli", "**/core-cli/**"] + "nohoist": [ + "**/core-cli", + "**/core-cli/**" + ] }, "scripts": { "prebuild": "npm run clean-dist", @@ -37,15 +40,16 @@ "mobx-react-lite": "^3.1.6", "node-sass": "^5.0.0", "preact": "^10.5.10", + "preact-jsx-runtime": "^1.2.0", "preact-render-to-string": "^5.1.12", "preact-ssr-prepass": "^1.1.3", - "preact-jsx-runtime": "^1.2.0", "react": "^16.13.1", "react-dom": "^16.13.1", "reakit": "^1.3.4", "reflect-metadata": "^0.1.13", "reshadow": "^0.0.1-alpha.78", + "rimraf": "^3.0.2", "typescript": "^4.2.3" }, "dependencies": {} -} \ No newline at end of file +} diff --git a/webapp/packages/core-sdk/package.json b/webapp/packages/core-sdk/package.json index 849eddf43b..c44f69b3a3 100644 --- a/webapp/packages/core-sdk/package.json +++ b/webapp/packages/core-sdk/package.json @@ -11,6 +11,7 @@ "prebuild": "rimraf lib/*", "build": "webpack --config ../../configs/webpack.plugin.config.js", "postbuild": "tsc", + "pregql:gen": "rimraf node_modules", "gql:gen": "graphql-codegen", "gql:gen:dev": "graphql-codegen --watch", "lint": "eslint ./src/ --ext .ts,.tsx", From 562a5abd342fe7c864b624c9dc794f7958e227ba Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 5 Apr 2021 17:28:44 +0300 Subject: [PATCH 016/406] fix(plugin-data-spreadsheet-new): CB-743 moved getEditor() from for loop --- .../src/DataGrid/useGridSelectedCellsCopy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 217007e541..93dc194b28 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -24,6 +24,7 @@ function copyGridSelectedDataToClipboard( selectedCells: Map ) { const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const editor = model.source.getEditor(resultIndex); const orderedSelectedCells: Map = new Map([...selectedCells].sort((a, b) => a[0] - b[0])); const selectedColumns: Set = new Set(); @@ -44,7 +45,7 @@ function copyGridSelectedDataToClipboard( } if (colIndexes.includes(column)) { - const cell = model.source.getEditor(resultIndex).getCell(rowIdx, column); + const cell = editor.getCell(rowIdx, column); const cellValue = format.get(cell); if (cellValue === null) { continue; From b5da67c9e17b78f0561a5a1c2557142233910aaa Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 5 Apr 2021 17:45:46 +0300 Subject: [PATCH 017/406] chore(core-sdk): CB-792 update sdk --- .../core-sdk/src/queries/authentication/getAuthProviders.gql | 1 + webapp/packages/core-sdk/src/sdk.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql b/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql index ae72d185b2..79906897e7 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/getAuthProviders.gql @@ -24,6 +24,7 @@ query getAuthProviders { description admin user + identifying possibleValues encryption } diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index ecf6fd9d09..0c19ae4511 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -912,6 +912,7 @@ export interface AuthCredentialInfo { description?: Maybe; admin?: Maybe; user?: Maybe; + identifying?: Maybe; possibleValues?: Maybe>>; encryption?: Maybe; } @@ -997,7 +998,7 @@ export type GetAuthProvidersQueryVariables = Exact<{ [key: string]: never }>; export interface GetAuthProvidersQuery { providers: Array<( Pick - & { credentialParameters: Array> } + & { credentialParameters: Array> } )>; } @@ -1866,6 +1867,7 @@ export const GetAuthProvidersDocument = ` description admin user + identifying possibleValues encryption } From 4a86e51f98528b83330513ccbc1da57ddea84b7f Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 5 Apr 2021 19:36:14 +0300 Subject: [PATCH 018/406] fix(plugin-authentication): CB-924 ignore enabled auth provider on administration page --- .../AdministrationScreenService.ts | 8 ++++++-- .../src/UserInfoResource.ts | 19 +++++++++---------- .../src/Dialog/AuthDialog.tsx | 11 +++++++++-- .../src/Dialog/AuthDialogController.ts | 19 ++++++++++++++----- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 20eae196eb..6461c64cf3 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -249,7 +249,9 @@ export class AdministrationScreenService { return false; } - if (!(await this.isAccessProvided(state))) { + const accessProvided = await this.isAccessProvided(state); + + if (!accessProvided) { this.screenService.navigateToRoot(); return false; } @@ -269,7 +271,9 @@ export class AdministrationScreenService { await this.ensurePermissions.execute(); - if (!(await this.permissionsService.hasAsync(EAdminPermission.admin))) { + const administrator = await this.permissionsService.hasAsync(EAdminPermission.admin); + + if (!administrator) { return false; } diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 34bba9c15b..6abddcdc63 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -46,17 +46,16 @@ export class UserInfoResource extends CachedDataResource } async login(provider: string, credentials: Record, link?: boolean): Promise { + const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + + const { authToken } = await this.graphQLService.sdk.authLogin({ + provider, + credentials: processedCredentials, + linkUser: link, + customIncludeOriginDetails: true, + }); + await this.performUpdate(undefined, undefined, async () => { - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); - - // TODO: will be replaced with another function - const { authToken } = await this.graphQLService.sdk.authLogin({ - provider, - credentials: processedCredentials, - linkUser: link, - customIncludeOriginDetails: true, - }); - if (this.data === null || link) { this.data = await this.loader(); } else { diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 72f309eb4d..17484b13ac 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; +import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle, Loader @@ -91,16 +92,22 @@ interface IAuthPayload { } export const AuthDialog: DialogComponent = observer(function AuthDialog({ - payload: { provider, link }, + payload: { + provider, + link = false, + }, options, rejectDialog, }) { + const administrationScreenService = useService(AdministrationScreenService); const userInfo = useService(UserInfoResource); - const controller = useController(AuthDialogController, link || false, rejectDialog); + const controller = useController(AuthDialogController, link, rejectDialog); const translate = useTranslate(); if (provider) { controller.selectProvider(provider); + } else { + controller.admin = administrationScreenService.activeScreen !== null; } const showTabs = !provider && controller.providers.length > 1; diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts index eb38e982b1..17a45c1feb 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts @@ -26,14 +26,19 @@ export class AuthDialogController implements IInitializableController, IDestruct } get providers(): AuthProvider[] { - return this.authProvidersResource - .getEnabledProviders() - .sort(this.compareProviders); + let providers = this.authProvidersResource.values; + + if (!this.admin) { + providers = this.authProvidersResource.getEnabledProviders(); + } + + return providers.sort(this.compareProviders); } readonly error = new GQLErrorCatcher(); private isDistructed = false; private link!: boolean; + admin: boolean; private close!: () => void; constructor( @@ -46,11 +51,14 @@ export class AuthDialogController implements IInitializableController, IDestruct provider: observable, isAuthenticating: observable, credentials: observable, + admin: observable, providers: computed, }); + + this.admin = false; } - init(link: boolean, onClose: () => void) { + init(link: boolean, onClose: () => void): void { this.link = link; this.close = onClose; this.loadProviders(); @@ -95,8 +103,9 @@ export class AuthDialogController implements IInitializableController, IDestruct private async loadProviders() { try { await this.authProvidersResource.loadAll(); + if (this.providers.length > 0) { - this.provider = this.providers[0]; + this.provider = this.providers.find(provider => provider.defaultProvider) ?? this.providers[0]; } } catch (exception) { this.notificationService.logException(exception, 'Can\'t load auth providers'); From 748496accf6430f3bc64f57c82f4d000ea8b8e30 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Mon, 5 Apr 2021 19:50:05 +0300 Subject: [PATCH 019/406] CB-924 Allow admin to use local auth provider always --- .../service/auth/impl/WebServiceAuthImpl.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 025ac157ff..f4d6db88ed 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -17,6 +17,7 @@ package io.cloudbeaver.service.auth.impl; import io.cloudbeaver.*; +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebAuthProviderInfo; @@ -74,7 +75,10 @@ public class WebServiceAuthImpl implements DBWServiceAuth { } } else { if (!providerEnabled) { - throw new DBWebException("Authentication provider '" + providerId + "' is disabled"); + // Admin can use local provider anytime + if (!isAdminAuthTry(providerId, authParameters)) { + throw new DBWebException("Authentication provider '" + providerId + "' is disabled"); + } } } WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(providerId); @@ -205,6 +209,23 @@ public class WebServiceAuthImpl implements DBWServiceAuth { } } + private boolean isAdminAuthTry(@NotNull String providerId, @NotNull Map authParameters) { + boolean isAdmin = false; + if (LocalAuthProvider.PROVIDER_ID.equals(providerId)) { + Object userId = authParameters.get(LocalAuthProvider.CRED_USER); + if (userId != null) { + try { + isAdmin = CBPlatform.getInstance().getApplication().getSecurityController() + .getUserPermissions(CommonUtils.toString(userId)) + .contains(DBWConstants.PERMISSION_ADMIN); + } catch (DBCException e) { + log.error(e); + } + } + } + return isAdmin; + } + @Override public void authLogout(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException { if (webSession.getUser() == null) { From 20a3bec878bb5c44999332c3e9e0350c609b4da4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 5 Apr 2021 20:35:11 +0300 Subject: [PATCH 020/406] fix(core-app): CB-620 logviewer exception behavior --- .../ToolsPanel/LogViewTab/LogViewerService.ts | 36 +++++++++++-------- .../core-root/src/SessionExpireService.ts | 6 ++-- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts index 4f5ae809af..2599789f42 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerService.ts @@ -10,7 +10,7 @@ import { action, observable, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { PermissionsService, EPermission, PermissionsResource } from '@cloudbeaver/core-root'; +import { PermissionsService, EPermission, PermissionsResource, SessionExpireService } from '@cloudbeaver/core-root'; import { GraphQLService } from '@cloudbeaver/core-sdk'; import { uuid } from '@cloudbeaver/core-utils'; @@ -26,7 +26,7 @@ export class LogViewerService { } private log: ILogEntry[] = []; - private interval: any = null; + private timeoutTaskId: any = null; private failedRequestsCount = 0; private maxFailedRequests = 0; @@ -36,6 +36,7 @@ export class LogViewerService { private notificationService: NotificationService, private permissionsService: PermissionsService, private permissionsResource: PermissionsResource, + private sessionExpireService: SessionExpireService ) { makeObservable(this, { _isActive: observable, @@ -66,19 +67,18 @@ export class LogViewerService { if (!this.isLogViewerAvailable()) { throw new Error('Access denied'); } + this.failedRequestsCount = 0; this._isActive = true; - await this.updateLog(); const refreshInterval = this.coreSettingsService.settings.getValue('app.logViewer.refreshTimeout'); this.maxFailedRequests = this.coreSettingsService.settings.getValue('app.logViewer.maxFailedRequests'); - this.interval = setInterval(() => { - this.updateLog(); - }, refreshInterval); + + this.runInterval(refreshInterval); } stopLog(): void { - if (this.interval) { - clearInterval(this.interval); - this.interval = null; + if (this.timeoutTaskId) { + clearTimeout(this.timeoutTaskId); + this.timeoutTaskId = null; } this._isActive = false; } @@ -108,16 +108,24 @@ export class LogViewerService { } } - private async updateLog() { - if (!this.isLogViewerAvailable()) { - return; - } + private runInterval(refreshInterval: number) { + this.timeoutTaskId = setTimeout(async () => { + await this.updateLog(); + if (this._isActive) { + this.runInterval(refreshInterval); + } + }, refreshInterval); + } + + private async updateLog() { try { const newEntries = await this.loadLog(); this.addNewEntries(newEntries); } catch (e) { - this.notificationService.logException(e, 'Failed to load log'); + if (this.failedRequestsCount === 0 && !this.sessionExpireService.sessionExpired) { + this.notificationService.logException(e, 'Failed to load log'); + } this.failedRequestsCount++; if (this.failedRequestsCount === this.maxFailedRequests) { diff --git a/webapp/packages/core-root/src/SessionExpireService.ts b/webapp/packages/core-root/src/SessionExpireService.ts index ff1a9acf8e..e7a9bbdf01 100644 --- a/webapp/packages/core-root/src/SessionExpireService.ts +++ b/webapp/packages/core-root/src/SessionExpireService.ts @@ -16,7 +16,7 @@ import { SessionError } from './SessionError'; @injectable() export class SessionExpireService extends Bootstrap { - private isNotifiedAboutExpiredSession = false; + sessionExpired = false; onSessionExpire: IExecutor; constructor( @@ -38,10 +38,10 @@ export class SessionExpireService extends Bootstrap { } catch (exception) { if (exception instanceof GQLError && exception.errorCode === EServerErrorCode.sessionExpired - && !this.isNotifiedAboutExpiredSession) { + && !this.sessionExpired) { const e = new SessionError('Session expired'); this.graphQLService.blockRequests(e); - this.isNotifiedAboutExpiredSession = true; + this.sessionExpired = true; await this.onSessionExpire.execute(); } throw exception; From 544698810af2f1f498f6aadfdf24208430ca1222 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 01:43:11 +0300 Subject: [PATCH 021/406] fix: CB-754 query execution status --- .../src/DataExportMenuService.ts | 25 +++++----- .../src/ContainerDataSource.ts | 22 ++++---- .../src/DataViewerTableService.ts | 14 +++--- .../DatabaseDataModel/DatabaseDataModel.ts | 4 ++ .../DatabaseDataModel/DatabaseDataSource.ts | 18 +++++-- .../DatabaseDataModel/IDatabaseDataModel.ts | 1 + .../DatabaseDataModel/IDatabaseDataOptions.ts | 15 ++++++ .../DatabaseDataModel/IDatabaseDataResult.ts | 1 + .../DatabaseDataModel/IDatabaseDataSource.ts | 8 +-- .../IDatabaseExecutionContext.ts} | 2 +- .../TableViewer/TableFooter/TableFooter.tsx | 8 +-- .../TableFooterMenu/TableFooterMenu.tsx | 4 +- .../TableFooterMenu/TableFooterMenuService.ts | 8 +-- .../src/TableViewer/TableGrid.tsx | 10 ++-- .../TableViewer/TableHeader/TableHeader.tsx | 8 +-- .../TableHeader/TableHeaderService.ts | 5 +- .../TableHeader/TableWhereFilter.tsx | 16 +++--- .../TablePresentationBar/PresentationTab.tsx | 6 ++- .../TablePresentationBar.tsx | 1 + .../src/TableViewer/TableStatistics.tsx | 50 +++++++++++++++++++ .../src/TableViewer/TableToolsPanel.tsx | 4 +- .../src/TableViewer/TableViewer.tsx | 2 +- .../TableViewer/TableViewerStorageService.ts | 4 +- .../packages/plugin-data-viewer/src/index.ts | 6 ++- .../plugin-data-viewer/src/locales/en.ts | 3 ++ .../plugin-data-viewer/src/useDataModel.ts | 18 +++++++ .../src/ISqlEditorTabState.ts | 6 +-- .../plugin-sql-editor/src/QueryDataSource.ts | 41 ++++++++------- .../plugin-sql-editor/src/SqlEditorService.ts | 8 +-- .../SqlResultTabs/SQLQueryExecutionProcess.ts | 17 ++++--- .../src/SqlResultTabs/SqlResultTabsService.ts | 3 +- .../packages/plugin-sql-editor/src/index.ts | 1 + 32 files changed, 229 insertions(+), 110 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts rename webapp/packages/plugin-data-viewer/src/{IExecutionContext.ts => DatabaseDataModel/IDatabaseExecutionContext.ts} (87%) create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/useDataModel.ts diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 2f022c2bfa..eeaef96db6 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -6,14 +6,11 @@ * you may not use this file except in compliance with the License. */ -import { - NavigationTreeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature -} from '@cloudbeaver/core-app'; +import { NavigationTreeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature } from '@cloudbeaver/core-app'; import { injectable } from '@cloudbeaver/core-di'; -import { - IMenuContext, CommonDialogService, ContextMenuService -} from '@cloudbeaver/core-dialogs'; -import { TableFooterMenuService, ITableFooterMenuContext } from '@cloudbeaver/plugin-data-viewer'; +import { IMenuContext, CommonDialogService, ContextMenuService } from '@cloudbeaver/core-dialogs'; +import { TableFooterMenuService, ITableFooterMenuContext, IDatabaseDataSource, IDataContainerOptions } from '@cloudbeaver/plugin-data-viewer'; +import type { IDataQueryOptions } from '@cloudbeaver/plugin-sql-editor'; import { DataExportDialog } from './Dialog/DataExportDialog'; @@ -32,7 +29,9 @@ export class DataExportMenuService { return context.contextType === TableFooterMenuService.nodeContextType; }, isDisabled(context) { - return context.data.model.isLoading() || !context.data.model.getResult(context.data.resultIndex); + return context.data.model.isLoading() + || context.data.model.isDisabled(context.data.resultIndex) + || !context.data.model.getResult(context.data.resultIndex); }, order: 5, title: 'data_transfer_dialog_export', @@ -69,16 +68,18 @@ export class DataExportMenuService { throw new Error('Result must be provided'); } - if (!context.data.model.source.options) { + const source = context.data.model.source as IDatabaseDataSource; + + if (!source.options) { throw new Error('Source options must be provided'); } this.commonDialogService.open(DataExportDialog, { - connectionId: context.data.model.source.options.connectionId, + connectionId: source.options.connectionId, contextId: context.data.model.source.executionContext?.contextId, - containerNodePath: context.data.model.source.options.containerNodePath, + containerNodePath: source.options.containerNodePath, resultId: result.id, - sourceName: context.data.model.source.options.sourceName, + sourceName: source.options.query, }); } } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 3c14184c6f..4f79ce9251 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -9,21 +9,18 @@ import { observable, makeObservable } from 'mobx'; import type { NotificationService } from '@cloudbeaver/core-events'; -import type { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; +import type { GraphQLService } from '@cloudbeaver/core-sdk'; import { EDeferredState } from '@cloudbeaver/core-utils'; import { DatabaseDataEditor } from './DatabaseDataModel/DatabaseDataEditor'; import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; +import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions'; +import type { IDatabaseExecutionContext } from './DatabaseDataModel/IDatabaseExecutionContext'; import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet'; import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess'; -import type { IExecutionContext } from './IExecutionContext'; -export interface IDataContainerOptions { +export interface IDataContainerOptions extends IDatabaseDataOptions { containerNodePath: string; - sourceName?: string; // TODO: should be refactored, used only in QueryDataSource - connectionId: string; - whereFilter: string; - constraints: SqlDataFilterConstraint[]; } export class ContainerDataSource extends DatabaseDataSource { @@ -48,6 +45,10 @@ export class ContainerDataSource extends DatabaseDataSource(result => ({ id: result.resultSet?.id || '0', dataFormat: result.dataFormat!, + updateRowCount: result.updateRowCount || 0, loadedFully: (result.resultSet?.rows?.length || 0) < limit, data: result.resultSet, })); @@ -142,6 +145,7 @@ export class ContainerDataSource extends DatabaseDataSource result.id === update.resultId)!; @@ -175,7 +179,7 @@ export class ContainerDataSource extends DatabaseDataSource { + private async ensureContextCreated(): Promise { if (!this.executionContext) { if (!this.options) { throw new Error('Options must be provided'); @@ -189,7 +193,7 @@ export class ContainerDataSource extends DatabaseDataSource { + ): Promise { const response = await this.graphQLService.sdk.sqlContextCreate({ connectionId, defaultCatalog, diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index 1dc637043d..833d8870b3 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -12,29 +12,31 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { GraphQLService } from '@cloudbeaver/core-sdk'; import { ContainerDataSource } from './ContainerDataSource'; +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataSource'; -import type { DataModelWrapper } from './TableViewer/DataModelWrapper'; +import { DataModelWrapper } from './TableViewer/DataModelWrapper'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; @injectable() export class DataViewerTableService { - constructor(private tableViewerStorageService: TableViewerStorageService, + constructor( + private tableViewerStorageService: TableViewerStorageService, private connectionInfoResource: ConnectionInfoResource, private graphQLService: GraphQLService, - private notificationService: NotificationService) { - } + private notificationService: NotificationService + ) { } has(tableId: string): boolean { return this.tableViewerStorageService.has(tableId); } - get(modelId: string): DataModelWrapper | undefined { + get(modelId: string): IDatabaseDataModel | undefined { return this.tableViewerStorageService.get(modelId); } async removeTableModel(tableId: string): Promise { const model = this.tableViewerStorageService.get(tableId); - if (model) { + if (model instanceof DataModelWrapper) { await model.dispose(); } this.tableViewerStorageService.remove(tableId); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts index f54a214107..b8856697ad 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts @@ -43,6 +43,10 @@ implements IDatabaseDataModel { return this.source.isLoading(); } + isDisabled(resultIndex: number): boolean { + return this.source.isDisabled(resultIndex); + } + isReadonly(): boolean { return this.source.isReadonly(); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 29b8885cca..fa57248f6e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -10,13 +10,13 @@ import { observable, makeObservable } from 'mobx'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IExecutionContext } from '../IExecutionContext'; import { DatabaseDataActions } from './DatabaseDataActions'; import type { IDatabaseDataAction, IDatabaseDataActionClass } from './IDatabaseDataAction'; import type { IDatabaseDataActions } from './IDatabaseDataActions'; import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export abstract class DatabaseDataSource implements IDatabaseDataSource { @@ -31,14 +31,15 @@ implements IDatabaseDataSource { options: TOptions | null; requestInfo: IRequestInfo; error: Error | null; - executionContext: IExecutionContext | null; + executionContext: IDatabaseExecutionContext | null; abstract get canCancel(): boolean; + protected disabled: boolean; private activeRequest: Promise | null; private activeSave: Promise | null; constructor() { - makeObservable, 'activeRequest' | 'activeSave'>(this, { + makeObservable, 'activeRequest' | 'activeSave' | 'disabled'>(this, { access: observable, dataFormat: observable, supportedDataFormats: observable, @@ -50,6 +51,7 @@ implements IDatabaseDataSource { requestInfo: observable, error: observable, executionContext: observable, + disabled: observable, activeRequest: observable, activeSave: observable, }); @@ -61,6 +63,7 @@ implements IDatabaseDataSource { this.offset = 0; this.count = 0; this.options = null; + this.disabled = false; this.activeRequest = null; this.activeSave = null; this.executionContext = null; @@ -69,6 +72,7 @@ implements IDatabaseDataSource { this.requestInfo = { requestDuration: 0, requestMessage: '', + source: null, }; this.error = null; } @@ -117,13 +121,17 @@ implements IDatabaseDataSource { } isReadonly(): boolean { - return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1; + return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1 || this.disabled; } isLoading(): boolean { return !!this.activeRequest || !!this.activeSave; } + isDisabled(resultIndex: number): boolean { + return !!this.activeRequest || !!this.activeSave || this.disabled; + } + setEditor(editor: IDatabaseDataEditor): this { this.editor = editor; return this; @@ -156,7 +164,7 @@ implements IDatabaseDataSource { return this; } - setExecutionContext(context: IExecutionContext | null): this { + setExecutionContext(context: IDatabaseExecutionContext | null): this { this.executionContext = context; return this; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts index 9c0c1c5c6b..cb82adcf9d 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -19,6 +19,7 @@ export interface IDatabaseDataModel boolean; + isDisabled: (resultIndex: number) => boolean; isLoading: () => boolean; isDataAvailable: (offset: number, count: number) => boolean; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts new file mode 100644 index 0000000000..a10e122467 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts @@ -0,0 +1,15 @@ +/* + * 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 { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +export interface IDatabaseDataOptions { + connectionId: string; + whereFilter: string; + constraints: SqlDataFilterConstraint[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts index 49869ca758..6c9f249f76 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts @@ -12,5 +12,6 @@ export interface IDatabaseDataResult { id: string; dataFormat: ResultDataFormat; loadedFully: boolean; + updateRowCount: number; data: any; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 017bdb7d61..c7050eaa65 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -8,15 +8,16 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IExecutionContext } from '../IExecutionContext'; import type { IDatabaseDataAction, IDatabaseDataActionClass } from './IDatabaseDataAction'; import type { IDatabaseDataActions } from './IDatabaseDataActions'; import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; +import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export interface IRequestInfo { readonly requestDuration: number; readonly requestMessage: string; + readonly source: string | null; } export enum DatabaseDataAccessMode { @@ -36,11 +37,12 @@ export interface IDatabaseDataSource boolean; isLoading: () => boolean; + isDisabled: (resultIndex: number) => boolean; hasResult: (resultIndex: number) => boolean; @@ -60,7 +62,7 @@ export interface IDatabaseDataSource this; setDataFormat: (dataFormat: ResultDataFormat) => this; setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this; - setExecutionContext: (context: IExecutionContext | null) => this; + setExecutionContext: (context: IDatabaseExecutionContext | null) => this; requestData: () => Promise | void; saveData: () => Promise | void; diff --git a/webapp/packages/plugin-data-viewer/src/IExecutionContext.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseExecutionContext.ts similarity index 87% rename from webapp/packages/plugin-data-viewer/src/IExecutionContext.ts rename to webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseExecutionContext.ts index 8c2a83a9a7..0e994952bb 100644 --- a/webapp/packages/plugin-data-viewer/src/IExecutionContext.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseExecutionContext.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -export interface IExecutionContext { +export interface IDatabaseExecutionContext { connectionId: string; contextId: string; objectCatalogId?: string; 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 5c26e31114..07522a1ae0 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -13,7 +13,7 @@ import styled, { css, use } from 'reshadow'; import { IconButton, SubmittingForm } from '@cloudbeaver/core-blocks'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import type { DataModelWrapper } from '../DataModelWrapper'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu'; const tableFooterStyles = composes( @@ -65,7 +65,7 @@ const tableFooterStyles = composes( interface TableFooterProps { resultIndex: number; - model: DataModelWrapper; + model: IDatabaseDataModel; } export const TableFooter = observer(function TableFooter({ @@ -95,7 +95,7 @@ export const TableFooter = observer(function TableFooter({ type="button" name='reload' viewBox="0 0 16 16" - disabled={model.isLoading()} + disabled={model.isLoading() || model.isDisabled(resultIndex)} onClick={() => model.refresh()} /> @@ -105,7 +105,7 @@ export const TableFooter = observer(function TableFooter({ ref={ref} type="number" value={model.countGain} - disabled={model.isLoading()} + disabled={model.isLoading() || model.isDisabled(resultIndex)} onBlur={handleChange} {...use({ mod: 'surface' })} /> diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx index 7e32c93a1f..022b9dcd90 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -11,7 +11,7 @@ import styled, { css } from 'reshadow'; import { useService } from '@cloudbeaver/core-di'; -import type { DataModelWrapper } from '../../DataModelWrapper'; +import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; import { TableFooterMenuItem } from './TableFooterMenuItem'; import { TableFooterMenuService } from './TableFooterMenuService'; @@ -28,7 +28,7 @@ const styles = css` interface TableFooterMenuProps { resultIndex: number; - model: DataModelWrapper; + model: IDatabaseDataModel; className?: string; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index 2bf3db1df2..dabac58950 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -13,11 +13,11 @@ import { import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { DetailsError } from '@cloudbeaver/core-sdk'; -import type { DataModelWrapper } from '../../DataModelWrapper'; +import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; import { ErrorDialog } from '../../ErrorDialog'; export interface ITableFooterMenuContext { - model: DataModelWrapper; + model: IDatabaseDataModel; resultIndex: number; } @@ -75,7 +75,7 @@ export class TableFooterMenuService { }); } - constructMenuWithContext(model: DataModelWrapper, resultIndex: number): IMenuItem[] { + constructMenuWithContext(model: IDatabaseDataModel, resultIndex: number): IMenuItem[] { const context: IMenuContext = { menuId: this.tableFooterMenuToken, contextId: model.id, @@ -89,7 +89,7 @@ export class TableFooterMenuService { this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); } - private async saveData(model: DataModelWrapper) { + private async saveData(model: IDatabaseDataModel) { while (true) { try { await model.source.saveData(); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx index e71c7e0d4e..292af527d3 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx @@ -11,14 +11,14 @@ import type { PropsWithChildren } from 'react'; import styled, { css } from 'reshadow'; import { TextPlaceholder } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDataPresentationOptions } from '../DataPresentationService'; -import type { DataModelWrapper } from './DataModelWrapper'; +import { TableStatistics } from './TableStatistics'; type TableGridProps = PropsWithChildren<{ - model: DataModelWrapper; // TODO: change to IDatabaseDataModel + model: IDatabaseDataModel; dataFormat: ResultDataFormat; presentation: IDataPresentationOptions; resultIndex: number; @@ -37,8 +37,6 @@ export const TableGrid = observer(function TableGrid({ presentation, resultIndex, }: TableGridProps) { - const translate = useTranslate(); - if ( (presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat) || !model.source.hasResult(resultIndex) @@ -56,7 +54,7 @@ export const TableGrid = observer(function TableGrid({ const Presentation = presentation.getPresentationComponent(); if (result?.loadedFully && !result.data) { - return {translate('data_viewer_nodata_message')}; + return ; } return styled(styles)( diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx index c9a3301961..96c23e2dd3 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx @@ -12,7 +12,7 @@ import styled, { css } from 'reshadow'; import { Placeholder } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import type { DataModelWrapper } from '../DataModelWrapper'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import { TableHeaderService } from './TableHeaderService'; const styles = css` @@ -25,17 +25,19 @@ const styles = css` `; interface Props { - model: DataModelWrapper; + model: IDatabaseDataModel; + resultIndex: number; } export const TableHeader = observer(function TableHeader({ model, + resultIndex, }: Props) { const service = useService(TableHeaderService); return styled(styles)( - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index 498b05d48e..c076c11ccc 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -9,11 +9,12 @@ import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; -import type { DataModelWrapper } from '../DataModelWrapper'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import { TableWhereFilter } from './TableWhereFilter'; export interface ITableHeaderPlaceholderProps { - model: DataModelWrapper; + model: IDatabaseDataModel; + resultIndex: number; } @injectable() diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index 258c61309b..93b331934f 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -11,7 +11,7 @@ import { useCallback, useState } from 'react'; import styled, { css } from 'reshadow'; import { InlineEditor } from '@cloudbeaver/core-app'; -import type { PlaceholderComponent } from '@cloudbeaver/core-blocks'; +import { PlaceholderComponent, useObjectRef } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -33,20 +33,24 @@ const styles = composes( export const TableWhereFilter: PlaceholderComponent = observer(function TableWhereFilter({ model, + resultIndex, }) { const translate = useTranslate(); const [filterValue, setValue] = useState(() => model.source.options?.whereFilter || ''); + const props = useObjectRef({ model, resultIndex, filterValue }); const handleApply = useCallback(() => { - if (model.isLoading()) { + const { model, resultIndex, filterValue } = props; + if (model.isLoading() || model.isDisabled(resultIndex)) { return; } model.source.options!.whereFilter = filterValue; model.refresh(); - }, [model, filterValue]); + }, []); const resetFilter = useCallback(() => { - if (model.isLoading()) { + const { model, resultIndex, filterValue } = props; + if (model.isLoading() || model.isDisabled(resultIndex)) { return; } const applyNeeded = model.source.options?.whereFilter === filterValue; @@ -57,7 +61,7 @@ export const TableWhereFilter: PlaceholderComponent 1} + disabled={model.isLoading() || model.source.results.length > 1 || model.isDisabled(resultIndex)} simple onSave={handleApply} onUndo={resetFilter} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx index 8f9a2d062b..d2056da9f0 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx @@ -20,6 +20,7 @@ import type { IDataPresentationOptions } from '../../DataPresentationService'; interface Props { model: IDatabaseDataModel; + resultIndex: number; presentation: IDataPresentationOptions; className?: string; style?: ComponentStyle; @@ -27,6 +28,7 @@ interface Props { export const PresentationTab = observer(function PresentationTab({ model, + resultIndex, presentation, className, style, @@ -44,7 +46,7 @@ export const PresentationTab = observer(function PresentationTab({ style={verticalRotatedTabStyles} model={model} presentation={presentation} - disabled={model.isLoading()} + disabled={model.isLoading() || model.isDisabled(resultIndex)} /> ); } @@ -53,7 +55,7 @@ export const PresentationTab = observer(function PresentationTab({ {presentation.icon && } {presentation.title && {translate(presentation.title)}} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx index 8690c7e9cc..a619551bbc 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx @@ -93,6 +93,7 @@ export const TablePresentationBar = observer(function TablePresentationBar({ key={presentation.id} presentation={presentation} model={model} + resultIndex={resultIndex} style={styles} /> ))} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx new file mode 100644 index 0000000000..9646a89ca6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx @@ -0,0 +1,50 @@ +/* + * 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 { observer } from 'mobx-react-lite'; +import type { PropsWithChildren } from 'react'; +import styled, { css } from 'reshadow'; + +import { useTranslate } from '@cloudbeaver/core-localization'; + +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; + +type TableGridProps = PropsWithChildren<{ + model: IDatabaseDataModel; + resultIndex: number; +}>; + +const styles = css` + statistics { + composes: theme-typography--caption from global; + flex: 1; + overflow: auto; + box-sizing: border-box; + white-space: pre-wrap; + padding: 16px; + } +`; + +export const TableStatistics = observer(function TableStatistics({ + model, + resultIndex, +}: TableGridProps) { + const translate = useTranslate(); + const source = model.source; + const result = model.getResult(resultIndex); + + return styled(styles)( + + {translate('data_viewer_statistics_status')} {source.requestInfo.requestMessage}
+ {translate('data_viewer_statistics_duration')} {source.requestInfo.requestDuration} ms
+ {translate('data_viewer_statistics_updated_rows')} {result?.updateRowCount || 0}
+
+
{source.requestInfo.source}
+
+ ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx index cfcb31f8c1..62b073681d 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx @@ -14,11 +14,11 @@ import { TextPlaceholder } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDataPresentationOptions } from '../DataPresentationService'; -import type { DataModelWrapper } from './DataModelWrapper'; type TableGridProps = PropsWithChildren<{ - model: DataModelWrapper; // TODO: change to IDatabaseDataModel + model: IDatabaseDataModel; dataFormat: ResultDataFormat; presentation: IDataPresentationOptions | null; resultIndex: number; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 3fc790f5ba..96ae937989 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -200,7 +200,7 @@ export const TableViewer = observer(function TableViewer({ return styled(styles)( - + >(tableId: string): T | undefined { + return this.tableModelMap.get(tableId) as any; } /** diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index c63d044d45..399636ba27 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -18,8 +18,10 @@ export * from './DatabaseDataModel/IDatabaseDataAction'; export * from './DatabaseDataModel/IDatabaseDataActions'; export * from './DatabaseDataModel/IDatabaseDataEditor'; export * from './DatabaseDataModel/IDatabaseDataModel'; +export * from './DatabaseDataModel/IDatabaseDataOptions'; export * from './DatabaseDataModel/IDatabaseDataResult'; export * from './DatabaseDataModel/IDatabaseDataSource'; +export * from './DatabaseDataModel/IDatabaseExecutionContext'; export * from './DatabaseDataModel/IDatabaseResultSet'; // All Services and Components that is provided by this plugin should be exported here @@ -35,6 +37,6 @@ export * from './TableViewer/TableDataModel/TableColumn'; export * from './TableViewer/TableDataModel/TableRow'; export * from './TableViewer/TableDataModel/EditedRow'; -export * from './IExecutionContext'; - +export * from './ContainerDataSource'; export * from './DataPresentationService'; +export * from './useDataModel'; diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 1a5e2cdb8a..23a3890406 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -1,6 +1,9 @@ export default [ ['table_header_sql_expression', 'Enter a SQL expression to filter results'], ['data_viewer_nodata_message', 'No data to show'], + ['data_viewer_statistics_status', 'Status:'], + ['data_viewer_statistics_duration', 'Duration:'], + ['data_viewer_statistics_updated_rows', 'Updated Rows:'], ['data_viewer_presentation_value_title', 'Value'], ['data_viewer_presentation_value_text_title', 'Text'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/useDataModel.ts b/webapp/packages/plugin-data-viewer/src/useDataModel.ts new file mode 100644 index 0000000000..6579d1bd7b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/useDataModel.ts @@ -0,0 +1,18 @@ +/* + * 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 { useService } from '@cloudbeaver/core-di'; + +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; + +export function useDataModel>(modelId: string): T | undefined { + const dataViewerTableService = useService(TableViewerStorageService); + + return dataViewerTableService.get(modelId); +} diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index 6beb9e382e..7ffd0f1c33 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ -import type { IExecutionContext } from '@cloudbeaver/plugin-data-viewer'; +import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer'; -export interface ISqlQueryParams extends IExecutionContext { +export interface ISqlQueryParams extends IDatabaseExecutionContext { query: string; } @@ -35,7 +35,7 @@ export interface IQueryTabGroup { order: number; } -export interface ISqlEditorTabState extends Partial { +export interface ISqlEditorTabState extends Partial { order: number; query: string; currentResultTabId?: string; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 8f5eb0f484..0a75170c98 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -9,27 +9,19 @@ import { observable, makeObservable } from 'mobx'; import type { NotificationService } from '@cloudbeaver/core-events'; -import type { GraphQLService, SqlDataFilterConstraint, SqlExecuteInfo } from '@cloudbeaver/core-sdk'; +import type { GraphQLService, SqlExecuteInfo } from '@cloudbeaver/core-sdk'; import { EDeferredState } from '@cloudbeaver/core-utils'; -import { - DatabaseDataEditor, - DatabaseDataSource, IDatabaseResultSet -} from '@cloudbeaver/plugin-data-viewer'; +import { DatabaseDataEditor, DatabaseDataSource, IDatabaseDataOptions, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; -import type { IQueryTabGroup } from './ISqlEditorTabState'; import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess'; import type { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService'; -export interface IDataContainerOptions { +export interface IDataQueryOptions extends IDatabaseDataOptions { tabId: string; - sourceName: string; - connectionId: string; - whereFilter: string; - constraints: SqlDataFilterConstraint[]; - group: IQueryTabGroup; + query: string; } -export class QueryDataSource extends DatabaseDataSource { +export class QueryDataSource extends DatabaseDataSource { get canCancel(): boolean { return this.queryExecutionProcess?.getState() === EDeferredState.PENDING; } @@ -51,6 +43,10 @@ export class QueryDataSource extends DatabaseDataSource { - const params = this.options?.group.sqlQueryParams; - if (!params) { - throw new Error('sqlQueryParams must be provided'); + if (!this.options || !this.executionContext) { + return prevResults; } const changes = this.editor?.getChanges(true); @@ -75,8 +70,8 @@ export class QueryDataSource extends DatabaseDataSource ({ data: diff.source, @@ -92,6 +87,7 @@ export class QueryDataSource extends DatabaseDataSource result.id === update.resultId)!; @@ -115,7 +111,7 @@ export class QueryDataSource extends DatabaseDataSource(result => ({ id: result.resultSet?.id || '0', dataFormat: result.dataFormat!, + updateRowCount: result.updateRowCount || 0, loadedFully: (result.resultSet?.rows?.length || 0) < limit, // allays returns false // || !result.resultSet?.hasMoreData, @@ -143,7 +141,7 @@ export class QueryDataSource extends DatabaseDataSource { - if (!this.options) { + if (!this.options || !this.executionContext) { return prevResults; } const limit = this.count; @@ -155,7 +153,8 @@ export class QueryDataSource extends DatabaseDataSource { + async initEditorConnection(state: ISqlEditorTabState): Promise { if (!state.connectionId) { console.error('executeEditorQuery connectionId is not provided'); return; @@ -61,7 +61,7 @@ export class SqlEditorService { connectionId?: string, catalogId?: string, schemaId?: string - ): Promise { + ): Promise { const connection = await this.connectionsManagerService.requireConnection(connectionId); if (!connection) { return null; @@ -106,7 +106,7 @@ export class SqlEditorService { connectionId: string, defaultCatalog?: string, defaultSchema?: string - ): Promise { + ): Promise { const response = await this.gql.sdk.sqlContextCreate({ connectionId, defaultCatalog, diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLQueryExecutionProcess.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLQueryExecutionProcess.ts index 87e402683b..1638784cf9 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLQueryExecutionProcess.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SQLQueryExecutionProcess.ts @@ -13,8 +13,7 @@ import { import { CancellablePromise, cancellableTimeout, Deferred, EDeferredState } from '@cloudbeaver/core-utils'; - -import type { ISqlQueryParams } from '../ISqlEditorTabState'; +import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer'; const DELAY_BETWEEN_TRIES = 1000; @@ -31,13 +30,14 @@ export class SQLQueryExecutionProcess extends Deferred { } async start( - sqlQueryParams: ISqlQueryParams, + query: string, + context: IDatabaseExecutionContext, filter: SqlDataFilter, dataFormat: ResultDataFormat ): Promise { // start async task try { - const taskInfo = await this.executeQueryAsync(sqlQueryParams, filter, dataFormat); + const taskInfo = await this.executeQueryAsync(query, context, filter, dataFormat); await this.applyResult(taskInfo); this.taskId = taskInfo.id; if (this.getState() === EDeferredState.CANCELLING) { @@ -106,14 +106,15 @@ export class SQLQueryExecutionProcess extends Deferred { } private async executeQueryAsync( - sqlQueryParams: ISqlQueryParams, + query: string, + context: IDatabaseExecutionContext, filter: SqlDataFilter, dataFormat: ResultDataFormat ): Promise { const { taskInfo } = await this.graphQLService.sdk.asyncSqlExecuteQuery({ - connectionId: sqlQueryParams.connectionId, - contextId: sqlQueryParams.contextId, - query: sqlQueryParams.query, + connectionId: context.connectionId, + contextId: context.contextId, + query, filter, dataFormat, }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index f09e77580c..3b8f06c896 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -95,9 +95,8 @@ export class SqlResultTabsService { source.setOptions({ tabId: tabId, - group: tabGroup, + query: sqlQueryParams.query, connectionId: sqlQueryParams.connectionId, - sourceName: sqlQueryParams.query, constraints: [], whereFilter: '', }) diff --git a/webapp/packages/plugin-sql-editor/src/index.ts b/webapp/packages/plugin-sql-editor/src/index.ts index d1aac9a17b..735b6e0d12 100644 --- a/webapp/packages/plugin-sql-editor/src/index.ts +++ b/webapp/packages/plugin-sql-editor/src/index.ts @@ -3,4 +3,5 @@ import { sqlEditorPluginManifest } from './manifest'; export default sqlEditorPluginManifest; export * from './SqlEditor/CodeEditor/CodeEditor'; +export * from './QueryDataSource'; export * from './SqlDialectInfoService'; From 64e77eade693c1fa9089186c311ef93d97fac046 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Tue, 6 Apr 2021 11:00:19 +0300 Subject: [PATCH 022/406] CB-909 Template connection delete fix.Deprecated code removed. --- .../service/core/DBWServiceCore.java | 7 --- .../service/core/WebServiceBindingCore.java | 1 - .../service/core/impl/WebServiceCore.java | 51 ++----------------- 3 files changed, 3 insertions(+), 56 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index a355201a6e..8c2151b542 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -89,13 +89,6 @@ public interface DBWServiceCore extends DBWService { @WebAction WebConnectionInfo getConnectionState(WebSession webSession, String connectionId) throws DBWebException; - // Replaced with initConnection - @Deprecated - @WebAction - WebConnectionInfo openConnection( - @NotNull WebSession webSession, - @NotNull WebConnectionConfig connectionConfig) throws DBWebException; - @WebAction WebConnectionInfo initConnection( @NotNull WebSession webSession, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 479ec91de5..8f384b4ff9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -85,7 +85,6 @@ public class WebServiceBindingCore extends WebServiceBindingBase .dataFetcher("refreshSessionConnections", env -> getService(env).refreshSessionConnections(GraphQLEndpoint.getServletRequest(env))) .dataFetcher("changeSessionLanguage", env -> getService(env).changeSessionLanguage(getWebSession(env), env.getArgument("locale"))) - .dataFetcher("openConnection", env -> getService(env).openConnection(getWebSession(env), getConnectionConfig(env))) .dataFetcher("createConnection", env -> getService(env).createConnection(getWebSession(env), getConnectionConfig(env))) .dataFetcher("updateConnection", env -> getService(env).updateConnection(getWebSession(env), getConnectionConfig(env))) .dataFetcher("deleteConnection", env -> getService(env).deleteConnection(getWebSession(env), env.getArgument("id"))) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 6ee0eb2a53..fc5313a55f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -33,8 +33,6 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.navigator.DBNDataSource; import org.jkiss.dbeaver.model.navigator.DBNModel; @@ -200,51 +198,6 @@ public class WebServiceCore implements DBWServiceCore { return webSession.getWebConnectionInfo(connectionId); } - @Deprecated - @Override - public WebConnectionInfo openConnection(@NotNull WebSession webSession, @NotNull WebConnectionConfig config) throws DBWebException { - String templateId = config.getTemplateId(); - if (CommonUtils.isEmpty(templateId)) { - throw new DBWebException("Only preconfigured data sources are supported yet"); - } - DBPDataSourceRegistry templateRegistry = WebServiceUtils.getGlobalDataSourceRegistry(); - DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(templateId); - if (dataSourceTemplate == null) { - throw new DBWebException("Datasource '" + templateId + "' not found"); - } - - DBPDataSourceRegistry sessionRegistry = webSession.getSingletonProject().getDataSourceRegistry(); - DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(dataSourceTemplate); - newDataSource.setSavePassword(true); - ((DataSourceDescriptor)newDataSource).setTemporary(true); - - DBPConnectionConfiguration cfg = newDataSource.getConnectionConfiguration(); - if (AuthModelDatabaseNative.ID.equals(config.getAuthModelId())) { - cfg.setUserName(JSONUtils.getString(config.getCredentials(), "userName")); - cfg.setUserPassword(JSONUtils.getString(config.getCredentials(), "userPassword")); - } else { - cfg.setUserName(config.getUserName()); - cfg.setUserPassword(config.getUserPassword()); - } - if (!CommonUtils.isEmpty(config.getName())) { - newDataSource.setName(config.getName()); - } - if (!CommonUtils.isEmpty(config.getDescription())) { - newDataSource.setDescription(config.getDescription()); - } - try { - newDataSource.connect(webSession.getProgressMonitor(), true, false); - } catch (DBException e) { - throw new DBWebException("Error connecting to database", e); - } - sessionRegistry.addDataSource(newDataSource); - - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); - webSession.addConnection(connectionInfo); - - return connectionInfo; - } - @Override public WebConnectionInfo initConnection( @NotNull WebSession webSession, @@ -345,7 +298,8 @@ public class WebServiceCore implements DBWServiceCore { @Override public boolean deleteConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException { - if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { + WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(connectionId); + if (!connectionInfo.isTemplate() && !CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { throw new DBWebException("Connection delete is restricted by server configuration"); } closeAndDeleteConnection(webSession, connectionId, true); @@ -368,6 +322,7 @@ public class WebServiceCore implements DBWServiceCore { DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(dataSourceTemplate); ((DataSourceDescriptor) newDataSource).setNavigatorSettings(CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); + ((DataSourceDescriptor) newDataSource).setTemplate(true); if (!CommonUtils.isEmpty(connectionName)) { newDataSource.setName(connectionName); } From ee8681ab261afd289f0515c729a99c29eb9f83c1 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Tue, 6 Apr 2021 11:26:09 +0300 Subject: [PATCH 023/406] CB-919 Do not logout during server re-configuration --- .../service/admin/impl/WebServiceAdmin.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 8e1b10f27c..dad6f58c45 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -396,6 +396,8 @@ public class WebServiceAdmin implements DBWServiceAdmin { } } + boolean configurationMode = CBApplication.getInstance().isConfigurationMode(); + CBApplication.getInstance().finishConfiguration( config.getServerName(), adminName, @@ -405,7 +407,13 @@ public class WebServiceAdmin implements DBWServiceAdmin { appConfig); // Refresh active session - webSession.forceUserRefresh(null); + if (configurationMode) { + // In config mode we always refresh because admin user doesn't exist yet + webSession.forceUserRefresh(null); + } else { + // Just reload session state + webSession.forceUserRefresh(webSession.getUser()); + } } catch (Throwable e) { throw new DBWebException("Error configuring server", e); } From 01b419dc9a9a215edcc271591ca2f082d00bbfd2 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 12:59:28 +0300 Subject: [PATCH 024/406] fix(plugin-authentication): CB-924 auth dialog mode switch --- .../src/Dialog/AuthDialog.tsx | 2 +- .../src/Dialog/AuthDialogController.ts | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 17484b13ac..d31342e414 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -107,7 +107,7 @@ export const AuthDialog: DialogComponent = observer(function if (provider) { controller.selectProvider(provider); } else { - controller.admin = administrationScreenService.activeScreen !== null; + controller.setAdminMode(administrationScreenService.activeScreen !== null); } const showTabs = !provider && controller.providers.length > 1; diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts index 17a45c1feb..a79973c0e8 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts @@ -38,7 +38,7 @@ export class AuthDialogController implements IInitializableController, IDestruct readonly error = new GQLErrorCatcher(); private isDistructed = false; private link!: boolean; - admin: boolean; + private admin: boolean; private close!: () => void; constructor( @@ -47,7 +47,7 @@ export class AuthDialogController implements IInitializableController, IDestruct private authInfoService: AuthInfoService, private commonDialogService: CommonDialogService ) { - makeObservable(this, { + makeObservable(this, { provider: observable, isAuthenticating: observable, credentials: observable, @@ -64,6 +64,14 @@ export class AuthDialogController implements IInitializableController, IDestruct this.loadProviders(); } + setAdminMode(mode: boolean): void { + if (this.admin !== mode) { + this.admin = mode; + + this.selectFirstAvailable(); + } + } + destruct(): void { this.isDistructed = true; } @@ -104,14 +112,18 @@ export class AuthDialogController implements IInitializableController, IDestruct try { await this.authProvidersResource.loadAll(); - if (this.providers.length > 0) { - this.provider = this.providers.find(provider => provider.defaultProvider) ?? this.providers[0]; - } + this.selectFirstAvailable(); } catch (exception) { this.notificationService.logException(exception, 'Can\'t load auth providers'); } } + private selectFirstAvailable(): void { + if (this.providers.length > 0) { + this.provider = this.providers.find(provider => provider.defaultProvider) ?? this.providers[0]; + } + } + private compareProviders = (providerA: AuthProvider, providerB: AuthProvider): number => { if (providerA.defaultProvider === providerB.defaultProvider) { return providerA.label.localeCompare(providerB.label); From 57a2ccccae3ab6d7bd2b55f8cb6ce596cd46ea72 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Tue, 6 Apr 2021 17:19:31 +0300 Subject: [PATCH 025/406] fix(plugin-data-spreadsheet-new): CB-743 rewrite copy logic --- .../src/DataGrid/DataGridTable.tsx | 2 +- .../src/DataGrid/useGridSelectedCellsCopy.ts | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) 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 5b74accbd6..65f5f7ab62 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -77,7 +77,7 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ }, }); - const { onKeydownHandler } = useGridSelectedCellsCopy(model, resultIndex, gridSelectionContext); + const { onKeydownHandler } = useGridSelectedCellsCopy(model, resultIndex, tableData, gridSelectionContext); const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({ onDragStart: startPosition => { dataGridRef.current?.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 93dc194b28..02cb63fefa 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -13,6 +13,7 @@ import { copyToClipboard } from '@cloudbeaver/core-utils'; import { IDatabaseDataModel, IDatabaseResultSet, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer'; import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; +import type { ITableData } from './TableDataContext'; const EVENT_KEY_CODE = { C: 'KeyC', @@ -21,42 +22,42 @@ const EVENT_KEY_CODE = { function copyGridSelectedDataToClipboard( model: IDatabaseDataModel, resultIndex: number, + tableData: ITableData, selectedCells: Map ) { const format = model.source.getAction(resultIndex, ResultSetFormatAction); const editor = model.source.getEditor(resultIndex); + const orderedSelectedCells: Map = new Map([...selectedCells].sort((a, b) => a[0] - b[0])); const selectedColumns: Set = new Set(); - for (const colIndexes of orderedSelectedCells.values()) { for (const colIdx of colIndexes) { selectedColumns.add(colIdx); } } - const columns = [...selectedColumns].sort((a, b) => a - b); - - let data = ''; + const rowsValues: string[] = []; for (const [rowIdx, colIndexes] of orderedSelectedCells) { - for (const column of columns) { - if (column !== columns[0]) { - data += '\t'; + const rowCellsValues: string[] = []; + for (const column of tableData.columns) { + const columnIdx = tableData.getDataColumnIndexFromKey(column.key); + if (columnIdx === null || !selectedColumns.has(columnIdx)) { + continue; } - if (colIndexes.includes(column)) { - const cell = editor.getCell(rowIdx, column); + if (colIndexes.includes(columnIdx)) { + const cell = editor.getCell(rowIdx, columnIdx); const cellValue = format.get(cell); - if (cellValue === null) { - continue; - } - data += cellValue; + rowCellsValues.push(cellValue ?? ''); + } else { + rowCellsValues.push(''); } } - data += '\r\n'; + rowsValues.push(rowCellsValues.join('\t')); } - copyToClipboard(data); + copyToClipboard(rowsValues.join('\r\n')); } // needed for event.code @@ -65,15 +66,17 @@ type IKeyboardEvent = React.KeyboardEvent & KeyboardEvent; export function useGridSelectedCellsCopy( model: IDatabaseDataModel, resultIndex: number, + tableData: ITableData, selectionContext: IDataGridSelectionContext ) { - const props = useObjectRef({ model, resultIndex, selectionContext }); + const props = useObjectRef({ model, resultIndex, tableData, selectionContext }); const onKeydownHandler = useCallback((event: IKeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) { copyGridSelectedDataToClipboard( props.model, props.resultIndex, + props.tableData, props.selectionContext.selectedCells ); } From d76d7a40863b5e055c12aef4b496ad288ac68dca Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 18:35:05 +0300 Subject: [PATCH 026/406] fix: CB-930 server config update unlink --- .../AdministrationItemService.ts | 31 +++++++++++++------ .../AdministrationItem/IAdministrationItem.ts | 6 +++- .../AdministrationScreenService.ts | 7 +++-- .../core-root/src/ServerConfigResource.ts | 18 +++++++++++ .../ServerConfigurationService.ts | 12 +++++++ .../src/Dialog/AuthDialogController.ts | 12 ++++--- 6 files changed, 70 insertions(+), 16 deletions(-) diff --git a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts index 826519cfce..ec0c2cb3ec 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts @@ -23,6 +23,7 @@ interface IActivationData { screen: IAdministrationItemRoute; configurationWizard: boolean; outside: boolean; + outsideAdminPage: boolean; } @injectable() @@ -161,17 +162,19 @@ export class AdministrationItemService { async activate( screen: IAdministrationItemRoute, configurationWizard: boolean, - outside: boolean + outside: boolean, + outsideAdminPage: boolean ): Promise { - await this.activationTask.execute({ screen, configurationWizard, outside }); + await this.activationTask.execute({ screen, configurationWizard, outside, outsideAdminPage }); } async deActivate( screen: IAdministrationItemRoute, configurationWizard: boolean, - outside: boolean + outside: boolean, + outsideAdminPage: boolean ): Promise { - await this.deActivationTask.execute({ screen, configurationWizard, outside }); + await this.deActivationTask.execute({ screen, configurationWizard, outside, outsideAdminPage }); } async canActivate( @@ -198,14 +201,19 @@ export class AdministrationItemService { return true; } - private activateHandler: IExecutorHandler = async ({ screen, configurationWizard, outside }) => { + private activateHandler: IExecutorHandler = async ({ + screen, + configurationWizard, + outside, + outsideAdminPage, + }) => { let lastItem = 0; while (true) { const items = this.getActiveItems(configurationWizard); if (lastItem === items.length) { break; } - await items[lastItem]?.onLoad?.(configurationWizard, outside); + await items[lastItem]?.onLoad?.(configurationWizard, outside, outsideAdminPage); lastItem++; } @@ -226,20 +234,25 @@ export class AdministrationItemService { return; } - await item.onActivate?.(configurationWizard, outside); + await item.onActivate?.(configurationWizard, outside, outsideAdminPage); if (screen.sub) { await this.getItemSub(item, screen.sub)?.onActivate?.(screen.param, configurationWizard, outside); } }; - private deActivateHandler: IExecutorHandler = async ({ screen, configurationWizard, outside }) => { + private deActivateHandler: IExecutorHandler = async ({ + screen, + configurationWizard, + outside, + outsideAdminPage, + }) => { const item = this.getItem(screen.item, configurationWizard); if (!item) { return; } - await item.onDeActivate?.(configurationWizard, outside); + await item.onDeActivate?.(configurationWizard, outside, outsideAdminPage); if (screen.sub) { await this.getItemSub(item, screen.sub)?.onDeActivate?.(screen.param, configurationWizard, outside); diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 2e27c0b476..f1abbe3614 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -44,7 +44,11 @@ export type AdministrationItemSubContentProps = AdministrationItemContentProps & }; export type AdministrationItemSubContentComponent = React.FunctionComponent; -export type AdministrationItemEvent = (configurationWizard: boolean, outside: boolean) => Promise | void; +export type AdministrationItemEvent = ( + configurationWizard: boolean, + outside: boolean, + outsideAdminPage: boolean +) => Promise | void; export type AdministrationItemCanActivateEvent = ( configurationWizard: boolean, administration: boolean diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 6461c64cf3..f08238f4eb 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -194,11 +194,13 @@ export class AdministrationScreenService { const toScreen = this.getScreen(nextState); const screen = this.getScreen(state); + if (screen) { await this.administrationItemService.deActivate( screen, this.isConfigurationMode, - screen.item !== toScreen?.item + screen.item !== toScreen?.item, + toScreen === null ); } @@ -239,7 +241,8 @@ export class AdministrationScreenService { await this.administrationItemService.activate( screen, this.isConfigurationMode, - screen.item !== fromScreen?.item + screen.item !== fromScreen?.item, + fromScreen === null ); } } diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index eca1623c0b..eb90601261 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -112,6 +112,24 @@ export class ServerConfigResource extends CachedDataResource { await this.performUpdate(undefined, undefined, async () => { await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: this.navigatorSettingsUpdate }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 61898a9bde..7ff9587051 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -92,6 +92,8 @@ export class ServerConfigurationService { .addPostHandler(this.ensureValidation); this.serverConfigResource.onDataUpdate.addPostHandler(this.showUnsavedNotification.bind(this)); + + this.administrationScreenService.activationEvent.addHandler(this.unlinkState.bind(this)); } changed(): void { @@ -240,6 +242,16 @@ export class ServerConfigurationService { onClose: () => { this.unSaveNotification = null; }, }); } + + private unlinkState(state: boolean): void { + if (state) { + return; + } + + this.serverConfigResource.unlinkUpdate(); + this.stateLinked = false; + console.log('unlinked'); + } } export interface IValidationStatusContext { diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts index a79973c0e8..9bf0fe5d7d 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogController.ts @@ -8,7 +8,7 @@ import { observable, computed, makeObservable } from 'mobx'; -import { AuthInfoService, AuthProvidersResource, AuthProvider } from '@cloudbeaver/core-authentication'; +import { AuthInfoService, AuthProvidersResource, AuthProvider, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -26,10 +26,14 @@ export class AuthDialogController implements IInitializableController, IDestruct } get providers(): AuthProvider[] { - let providers = this.authProvidersResource.values; + const providers = this.authProvidersResource.getEnabledProviders(); - if (!this.admin) { - providers = this.authProvidersResource.getEnabledProviders(); + if (this.admin && !this.authProvidersResource.isEnabled(AUTH_PROVIDER_LOCAL_ID)) { + const local = this.authProvidersResource.get(AUTH_PROVIDER_LOCAL_ID); + + if (local) { + providers.push(local); + } } return providers.sort(this.compareProviders); From fdc99d4b17085c32e45d1b91c51bcc59955b1a68 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 19:18:18 +0300 Subject: [PATCH 027/406] chore(plugin-administration): CB-930 remove console log --- .../ServerConfiguration/ServerConfigurationService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 7ff9587051..663dfeff6a 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -250,7 +250,6 @@ export class ServerConfigurationService { this.serverConfigResource.unlinkUpdate(); this.stateLinked = false; - console.log('unlinked'); } } From 3e2bcafcee2d136e38185fcb1c384fdc4843bd43 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 19:33:59 +0300 Subject: [PATCH 028/406] fix: CB-884 handle network state change --- .../packages/core-bootstrap/src/manifest.ts | 2 + webapp/packages/core-root/src/NetworkError.ts | 15 ++++ .../core-root/src/NetworkStateService.ts | 78 +++++++++++++++++++ webapp/packages/core-root/src/index.ts | 2 + .../core-sdk/src/CustomGraphQLClient.ts | 13 +++- .../packages/core-sdk/src/GraphQLService.ts | 4 + 6 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 webapp/packages/core-root/src/NetworkError.ts create mode 100644 webapp/packages/core-root/src/NetworkStateService.ts diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 56933cf17e..0d540b85e8 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -78,6 +78,7 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { PluginManagerService } from '@cloudbeaver/core-plugin'; import { ProductManagerService, ProductSettingsService } from '@cloudbeaver/core-product'; import { + NetworkStateService, SessionService, ServerService, PermissionsService, @@ -104,6 +105,7 @@ export const coreManifest: PluginManifest = { providers: [ RouterService, // important, should be first because the router starts in load phase first after all plugins register phase + NetworkStateService, AdministrationLocaleService, AdministrationTopAppBarService, AdministrationScreenService, diff --git a/webapp/packages/core-root/src/NetworkError.ts b/webapp/packages/core-root/src/NetworkError.ts new file mode 100644 index 0000000000..f9ba8896be --- /dev/null +++ b/webapp/packages/core-root/src/NetworkError.ts @@ -0,0 +1,15 @@ + +/* + * 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. + */ + +export class NetworkError extends Error { + constructor(message: string) { + super(message); + this.name = 'NetworkError'; + } +} diff --git a/webapp/packages/core-root/src/NetworkStateService.ts b/webapp/packages/core-root/src/NetworkStateService.ts new file mode 100644 index 0000000000..337c85986f --- /dev/null +++ b/webapp/packages/core-root/src/NetworkStateService.ts @@ -0,0 +1,78 @@ +/* + * 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 { makeObservable, observable } from 'mobx'; + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +import { NetworkError } from './NetworkError'; + +@injectable() +export class NetworkStateService extends Bootstrap { + get state(): boolean { + return this.networkState; + } + + readonly networkStateExecutor: IExecutor; + private networkState: boolean; + + constructor( + private graphQLService: GraphQLService + ) { + super(); + makeObservable(this, { + networkState: observable, + }); + + this.networkState = true; + this.networkStateExecutor = new Executor(); + } + + register(): void { + this.networkState = window.navigator.onLine; + + window.addEventListener('online', () => this.setState(true)); + window.addEventListener('offline', () => this.setState(false)); + + this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); + } + + load(): void {} + + private setState(state: boolean) { + if (this.networkState === state) { + return; + } + + if (state) { + if (this.graphQLService.client.blockReason instanceof NetworkError) { + this.graphQLService.enableRequests(); + } + } else { + this.graphQLService.blockRequests(new NetworkError('Network connection was lost')); + } + this.networkState = state; + this.networkStateExecutor.execute(this.networkState); + } + + private async sessionExpiredInterceptor(request: Promise): Promise { + try { + return await request; + } catch (exception) { + if ( + exception instanceof TypeError + && exception.message === 'Failed to fetch' + ) { + throw new NetworkError('Error while processing request'); + } + throw exception; + } + } +} diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 54c1c80c4f..84e2b35431 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -1,4 +1,6 @@ export * from './ConnectionNavigatorViewSettings'; +export * from './NetworkError'; +export * from './NetworkStateService'; export * from './PermissionsResource'; export * from './PermissionsService'; export * from './ServerConfigResource'; diff --git a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts index 65faa8ca28..5390073ebf 100644 --- a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts +++ b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts @@ -13,9 +13,13 @@ import { GQLError } from './GQLError'; import type { IResponseInterceptor } from './IResponseInterceptor'; export class CustomGraphQLClient extends GraphQLClient { + get blockReason(): Error | string | null { + return this.requestsBlockedReason; + } + private interceptors: IResponseInterceptor[] = []; private isRequestsBlocked = false; - private requestsBlockedReason?: Error | string; + private requestsBlockedReason: Error | string | null = null; registerInterceptor(interceptor: IResponseInterceptor): void { this.interceptors.push(interceptor); @@ -28,6 +32,11 @@ export class CustomGraphQLClient extends GraphQLClient { ); } + enableRequests(): void { + this.isRequestsBlocked = false; + this.requestsBlockedReason = null; + } + blockRequests(reason: Error | string): void { this.isRequestsBlocked = true; this.requestsBlockedReason = reason; @@ -38,7 +47,7 @@ export class CustomGraphQLClient extends GraphQLClient { if (this.requestsBlockedReason instanceof Error) { throw this.requestsBlockedReason; } else { - throw new Error(this.requestsBlockedReason); + throw new Error(this.requestsBlockedReason ?? undefined); } } } diff --git a/webapp/packages/core-sdk/src/GraphQLService.ts b/webapp/packages/core-sdk/src/GraphQLService.ts index 1ae8df23ff..60f5413c6e 100644 --- a/webapp/packages/core-sdk/src/GraphQLService.ts +++ b/webapp/packages/core-sdk/src/GraphQLService.ts @@ -29,6 +29,10 @@ export class GraphQLService { this.client.registerInterceptor(interceptor); } + enableRequests(): void { + this.client.enableRequests(); + } + blockRequests(reason: Error | string): void { this.client.blockRequests(reason); } From 7b5f60ac2251ea1a1c20b13f697348aa4ef5699e Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 20:03:33 +0300 Subject: [PATCH 029/406] fix: CB-608 infinite loaders --- .../core-blocks/src/Loader/Loader.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/webapp/packages/core-blocks/src/Loader/Loader.tsx b/webapp/packages/core-blocks/src/Loader/Loader.tsx index 9f50b8883a..a712eb9ecd 100644 --- a/webapp/packages/core-blocks/src/Loader/Loader.tsx +++ b/webapp/packages/core-blocks/src/Loader/Loader.tsx @@ -72,9 +72,13 @@ export const Loader: React.FC = observer(function Loader({ if ('loading' in element) { loading = element.loading; loaded = !loading; - } else if ('isLoading' in element) { - loaded = element.isLoaded(); - loading = element.isLoading() || !loaded; + } else { + if ('isLoaded' in element) { + loaded = element.isLoaded(); + } + if ('isLoading' in element) { + loading = element.isLoading(); + } } } } @@ -95,8 +99,14 @@ export const Loader: React.FC = observer(function Loader({ return () => clearTimeout(id); }, [loading]); - if (children && loaded) { - return children(); + if (children) { + if (loaded) { + return children(); + } + + if (!loading) { + return null; + } } if ((!isVisible && overlay) || !loading) { From 3b7247aa48d9c1024750812236db63d9b6517faf Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 6 Apr 2021 21:15:51 +0300 Subject: [PATCH 030/406] fix: CB-870 options panel close behavior --- .../src/ConnectionForm/useConnectionData.ts | 31 +++++++++++-------- .../src/ConnectionInfoResource.ts | 1 + .../core-routing/src/RouterService.ts | 25 +++++++++++++++ .../Screens/AppScreen/NavigationService.ts | 6 +++- .../PublicConnectionFormService.ts | 7 ++++- 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts b/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts index 385414a473..80e1282078 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts @@ -6,16 +6,17 @@ * you may not use this file except in compliance with the License. */ -import { useRef } from 'react'; - +import { useObjectRef } from '@cloudbeaver/core-blocks'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; +import type { DatabaseConnection } from '../Administration/ConnectionsResource'; import type { IConnectionFormData } from './ConnectionFormService'; interface IState { config: ConnectionConfig | undefined; connectionId: string | undefined; driverId: string | undefined; + info: DatabaseConnection | undefined; } export function useConnectionData( @@ -25,25 +26,29 @@ export function useConnectionData( const connectionId = data.info?.id || data.config.connectionId; const driverId = data.info?.driverId || data.config.driverId; - const lastDataRef = useRef({ + const lastDataRef = useObjectRef({ connectionId: undefined, driverId: undefined, config: undefined, - }); + info: undefined, + }, {}); if ( - lastDataRef.current.connectionId !== connectionId - || lastDataRef.current.driverId !== driverId - || lastDataRef.current.config !== data.config + lastDataRef.connectionId !== connectionId + || lastDataRef.driverId !== driverId + || lastDataRef.config !== data.config + || lastDataRef.info !== data.info ) { fill( data, - lastDataRef.current.connectionId !== null - || lastDataRef.current.driverId !== null - || lastDataRef.current.config !== data.config + lastDataRef.connectionId !== null + || lastDataRef.driverId !== null + || lastDataRef.config !== data.config + || lastDataRef.info !== data.info ); - lastDataRef.current.connectionId = connectionId; - lastDataRef.current.driverId = driverId; - lastDataRef.current.config = data.config; + lastDataRef.connectionId = connectionId; + lastDataRef.driverId = driverId; + lastDataRef.config = data.config; + lastDataRef.info = data.info; } } diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 8c2e79b403..8431379458 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -95,6 +95,7 @@ export class ConnectionInfoResource extends CachedMapResource; readonly router: Router; private currentState: RouterState; @@ -45,6 +54,7 @@ export class RouterService extends Bootstrap { currentParams: observable, }); + this.transitionTask = new Executor(); this.router = createRouter(); this.currentState = this.router.getState(); @@ -70,6 +80,21 @@ export class RouterService extends Bootstrap { })); this.router.subscribe(this.onRouteChange.bind(this)); + this.router.useMiddleware(() => async ( + fromState, + toState, + done + ) => { + const contexts = await this.transitionTask.execute({ fromState, toState, done }); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject(); + } + + return Promise.resolve(); + } + ); } private onRouteChange(state: SubscribeState) { diff --git a/webapp/packages/core-ui/src/Screens/AppScreen/NavigationService.ts b/webapp/packages/core-ui/src/Screens/AppScreen/NavigationService.ts index c2b02d6371..85ab2a8c2c 100644 --- a/webapp/packages/core-ui/src/Screens/AppScreen/NavigationService.ts +++ b/webapp/packages/core-ui/src/Screens/AppScreen/NavigationService.ts @@ -8,12 +8,16 @@ import { injectable } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { RouterService } from '@cloudbeaver/core-routing'; @injectable() export class NavigationService { readonly navigationTask: IExecutor; - constructor() { + constructor( + routerService: RouterService + ) { this.navigationTask = new Executor(); + routerService.transitionTask.before(this.navigationTask); } } diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index a588140b74..532135d17a 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -12,6 +12,7 @@ import { ConnectionInfoResource, IConnectionFormDataOptions, IConnectionFormOpti import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { ConnectionConfig, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; @@ -27,7 +28,8 @@ export class PublicConnectionFormService { constructor( private readonly commonDialogService: CommonDialogService, private readonly optionsPanelService: OptionsPanelService, - private readonly connectionInfoResource: ConnectionInfoResource + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly sessionDataResource: SessionDataResource ) { makeObservable(this, { dataOptions: observable, @@ -43,6 +45,9 @@ export class PublicConnectionFormService { this.dataOptions = null; this.optionsPanelService.closeTask.addHandler(this.closeHandler); this.connectionInfoResource.onItemDelete.addHandler(this.closeDeleted); + this.sessionDataResource.onDataOutdated.addHandler(() => { + this.close(true); + }); } change(config: ConnectionConfig, availableDrivers?: string[]): void { From b982d43729a39a980b41c2aa6ff0439486f48ee8 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 7 Apr 2021 10:17:49 +0300 Subject: [PATCH 031/406] chore(plugin-administration): CB-794 unsaved settings notification behavior --- ...onfigurationWizardPagesBootstrapService.ts | 4 +-- .../ServerConfigurationService.ts | 33 ++++++++++++------- .../plugin-administration/src/locales/en.ts | 4 ++- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts index 9aeef61533..f0a2c1934b 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts @@ -39,7 +39,7 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap { getDrawerComponent: () => WelcomeDrawerItem, }); this.administrationItemService.create({ - name: 'configuration', + name: this.serverConfigurationService.routeName, type: AdministrationItemType.Default, configurationWizardOptions: { description: 'administration_configuration_wizard_configuration_step_description', @@ -57,7 +57,7 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap { }, order: 4, onActivate: () => this.serverConfigurationService.loadConfig(), - onDeActivate: () => this.serverConfigurationService.deactivate(), + onDeActivate: this.serverConfigurationService.deactivate.bind(this.serverConfigurationService), onLoad: this.serverConfigurationService.loadConfig.bind(this.serverConfigurationService, false), getContentComponent: () => ServerConfigurationPage, getDrawerComponent: () => ServerConfigurationDrawerItem, diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 663dfeff6a..dc54f66e61 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -9,10 +9,10 @@ import { observable, makeObservable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ActionSnackbarProps, PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { ActionSnackbar, ActionSnackbarProps, PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { INotification, NotificationService } from '@cloudbeaver/core-events'; +import { ENotificationType, INotification, NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutorHandler, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ServerConfigResource } from '@cloudbeaver/core-root'; @@ -39,6 +39,7 @@ export class ServerConfigurationService { state: IServerConfigurationPageState; loading: boolean; + readonly routeName: string; readonly loadConfigTask: IExecutor; readonly prepareConfigTask: IExecutor; readonly saveTask: IExecutor; @@ -61,6 +62,7 @@ export class ServerConfigurationService { done: observable, }); + this.routeName = 'configuration'; this.stateLinked = false; this.done = false; this.loading = true; @@ -79,7 +81,7 @@ export class ServerConfigurationService { .addHandler(this.loadServerConfig) .addPostHandler(() => { this.loading = false; - this.showUnsavedNotification(); + this.showUnsavedNotification(false); }); this.saveTask @@ -91,17 +93,21 @@ export class ServerConfigurationService { .addHandler(this.validateForm) .addPostHandler(this.ensureValidation); - this.serverConfigResource.onDataUpdate.addPostHandler(this.showUnsavedNotification.bind(this)); + this.serverConfigResource.onDataUpdate.addPostHandler(this.showUnsavedNotification.bind(this, false)); this.administrationScreenService.activationEvent.addHandler(this.unlinkState.bind(this)); } changed(): void { this.done = false; + + this.showUnsavedNotification(true); } - deactivate(): void { - this.showUnsavedNotification(); + deactivate(configurationWizard: boolean, outside: boolean, outsideAdminPage: boolean): void { + if (!outsideAdminPage) { + this.showUnsavedNotification(false); + } } async loadConfig(reset = false): Promise { @@ -223,7 +229,7 @@ export class ServerConfigurationService { return true; } - private showUnsavedNotification() { + private showUnsavedNotification(close: boolean) { if ( !this.serverConfigResource.isChanged() && !this.serverConfigResource.isNavigatorSettingsChanged() @@ -232,13 +238,17 @@ export class ServerConfigurationService { return; } - if (this.unSaveNotification || this.administrationScreenService.isConfigurationMode) { + if (close || this.unSaveNotification || this.administrationScreenService.isConfigurationMode) { return; } - this.unSaveNotification = this.notificationService.logInfo({ - title: 'administration_configuration_wizard_configuration_server_info_unsaved', - persistent: true, + this.unSaveNotification = this.notificationService.customNotification(() => ActionSnackbar, { + actionText: 'administration_configuration_wizard_configuration_server_info_unsaved_navigate', + onAction: () => this.administrationScreenService.navigateToItem(this.routeName), + }, { + title: 'administration_configuration_wizard_configuration_server_info_unsaved_title', + message: 'administration_configuration_wizard_configuration_server_info_unsaved_message', + type: ENotificationType.Info, onClose: () => { this.unSaveNotification = null; }, }); } @@ -248,6 +258,7 @@ export class ServerConfigurationService { return; } + this.unSaveNotification?.close(true); this.serverConfigResource.unlinkUpdate(); this.stateLinked = false; } diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index 786fff653c..89eca1736d 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -15,7 +15,9 @@ export default [ ['administration_configuration_wizard_configuration_server_info', 'Server Information'], ['administration_configuration_wizard_configuration_server_name', 'Server Name'], - ['administration_configuration_wizard_configuration_server_info_unsaved', 'You have unsaved changes'], + ['administration_configuration_wizard_configuration_server_info_unsaved_title', 'Unsaved settings'], + ['administration_configuration_wizard_configuration_server_info_unsaved_message', 'Settings can be saved on Server configuration page'], + ['administration_configuration_wizard_configuration_server_info_unsaved_navigate', 'Open'], ['administration_configuration_wizard_configuration_server_session_lifetime', 'Session lifetime'], ['administration_configuration_wizard_configuration_server_session_lifetime_description', 'Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires'], From 305174c44b9028991bc1a7b77c281d60e282beab Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 7 Apr 2021 14:42:34 +0300 Subject: [PATCH 032/406] fix: CB-608 unnecessary connection info update --- .../core-app/src/NavigationTree/NavigationTree.tsx | 4 +++- .../src/shared/NodesManager/NavNodeManagerService.ts | 2 ++ .../core-app/src/shared/TabEntity/TabViewModel.ts | 6 ++++-- webapp/packages/core-di/src/entities/Entity.ts | 6 ++++-- .../src/ObjectPropertiesPage/FolderContext.ts | 2 +- .../ObjectPropertiesPage/ObjectFoldersController.ts | 6 +++--- .../plugin-object-viewer/src/ObjectViewerPanel.tsx | 10 +++------- .../plugin-object-viewer/src/ObjectViewerTabService.ts | 2 +- 8 files changed, 21 insertions(+), 17 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index d2658b9c8d..c725b84230 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -9,12 +9,13 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { useFocus } from '@cloudbeaver/core-blocks'; +import { useFocus, useMapResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { usePermission, EPermission } from '@cloudbeaver/core-root'; import { useActiveView } from '@cloudbeaver/core-view'; import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; import { ElementsTree } from './ElementsTree'; import { NavigationTreeService } from './NavigationTreeService'; import { useNavigationTree } from './useNavigationTree'; @@ -59,6 +60,7 @@ export const NavigationTree = observer(function NavigationTree() { const [ref] = useFocus({ onFocus, onBlur }); const isEnabled = usePermission(EPermission.public); const { isSelected, handleOpen, handleSelect } = useNavigationTree(); + useMapResource(NavTreeResource, ROOT_NODE_PATH); if (!isEnabled) { return null; diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 558c9ee759..56c45a7603 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -341,6 +341,7 @@ export class NavNodeManagerService extends Bootstrap { this.navTree.delete(ROOT_NODE_PATH); const enabled = await this.isNavTreeEnabled(); if (enabled) { + // this.navTree.markOutdated(ROOT_NODE_PATH); await this.navTree.refresh(ROOT_NODE_PATH); } } @@ -367,6 +368,7 @@ export class NavNodeManagerService extends Bootstrap { return; } + // this.navTree.markOutdated(ROOT_NODE_PATH); await this.navTree.refresh(ROOT_NODE_PATH); } diff --git a/webapp/packages/core-app/src/shared/TabEntity/TabViewModel.ts b/webapp/packages/core-app/src/shared/TabEntity/TabViewModel.ts index e51e89b3cb..df24819e8f 100644 --- a/webapp/packages/core-app/src/shared/TabEntity/TabViewModel.ts +++ b/webapp/packages/core-app/src/shared/TabEntity/TabViewModel.ts @@ -31,9 +31,11 @@ export class TabViewModel implements ITab { onClose = this.tabModel.onClose ? () => this._onClose() : undefined; onActivate = () => this.tabContainer.activateTab(this.tabId); - constructor(@inject(TabToken) private tabModel: ITab, + constructor( + @inject(TabToken) private tabModel: ITab, private entity: Entity, - @inject(TabContainerToken) private tabContainer: ITabContainerEntity) { + @inject(TabContainerToken) private tabContainer: ITabContainerEntity + ) { this.tabId = this.entity.id; this.panel = () => TabContext(tabContainer.getTabServiceInjector(this.tabId), this.tabModel.panel); } diff --git a/webapp/packages/core-di/src/entities/Entity.ts b/webapp/packages/core-di/src/entities/Entity.ts index 95a22c2bf8..bbb3cc8f6e 100644 --- a/webapp/packages/core-di/src/entities/Entity.ts +++ b/webapp/packages/core-di/src/entities/Entity.ts @@ -37,7 +37,8 @@ export class Entity { addChild(entity: Entity): void { if (this.children.has(entity.id)) { - throw new Error(`Entity (${this.id}) already contains child entity (${entity.id})`); + this.removeChild(entity.id); + // throw new Error(`Entity (${this.id}) already contains child entity (${entity.id})`); } this.children.set(entity.id, entity); entity.bindWithParent(this); @@ -45,7 +46,8 @@ export class Entity { removeChild(id: string): void { if (!this.children.has(id)) { - throw new Error(`Child entity (${id}) not found in entity (${this.id})`); + return; + // throw new Error(`Child entity (${id}) not found in entity (${this.id})`); } const entity = this.children.get(id); if (entity) { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderContext.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderContext.ts index 28063b6820..c6c54bbf49 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderContext.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderContext.ts @@ -7,6 +7,6 @@ */ import { createContext } from 'react'; -import { TabStateReturn } from 'reakit'; +import type { TabStateReturn } from 'reakit'; export const FolderContext = createContext(undefined); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFoldersController.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFoldersController.ts index 58f71062ca..77944e083b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFoldersController.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFoldersController.ts @@ -26,7 +26,7 @@ export class ObjectFoldersController implements IInitializableController, IDestr private navigationTabsService: NavigationTabsService ) { } - getTabContainer() { + getTabContainer(): ObjectFoldersTabContainer { return this.tabContainer; } @@ -43,7 +43,7 @@ export class ObjectFoldersController implements IInitializableController, IDestr }); } - activateTab(folderId: string | null) { + private activateTab(folderId: string | null): void { try { this.tabContainer.activateTab(folderId); } catch { @@ -52,7 +52,7 @@ export class ObjectFoldersController implements IInitializableController, IDestr } } - activateFirstTab() { + private activateFirstTab() { const firstTab = this.tabContainer.tabs[0]; this.tabContainer.activateTab(firstTab?.tabId || null); } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx index 6f81eff74a..927a1c1f97 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx @@ -10,10 +10,8 @@ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { useChildren, TabHandlerPanelProps, NavigationTabsService } from '@cloudbeaver/core-app'; -import { - Loader, TabsBox, TabPanel, TextPlaceholder, Button -} from '@cloudbeaver/core-blocks'; +import { useChildren, TabHandlerPanelProps } from '@cloudbeaver/core-app'; +import { Loader, TabsBox, TabPanel, TextPlaceholder, Button } from '@cloudbeaver/core-blocks'; import { useConnectionInfo } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -46,7 +44,6 @@ export const ObjectViewerPanel = observer(function ObjectViewerPanel({ }: TabHandlerPanelProps) { const translate = useTranslate(); const connection = useConnectionInfo(tab.handlerState.connectionId || ''); - const navigation = useService(NavigationTabsService); const style = useStyles(styles); const { children, isOutdated, isLoading, isLoaded, @@ -56,8 +53,7 @@ export const ObjectViewerPanel = observer(function ObjectViewerPanel({ const handleConnect = useCallback(async () => { await connection.connect(); - navigation.selectTab(tab.id); - }, [navigation, connection, tab]); + }, [connection]); if (connection.connectionInfo) { if (connection.isLoading()) { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index 10e1c46c8c..9491435a3a 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -96,7 +96,7 @@ export class ObjectViewerTabService { connectionId: nodeInfo.connection?.id, objectId: nodeInfo.nodeId, parentId: nodeInfo.parentId, - parents: await nodeInfo.getParents(), + parents: nodeInfo.getParents(), folderId: nodeInfo.folderId, pageId: '', pagesState: {}, From c57a23f57028a8875e3cc00b6fb0febc4095d0be Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Wed, 7 Apr 2021 15:15:19 +0300 Subject: [PATCH 033/406] 21.0.3 config --- .../META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml | 2 +- server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.product.ce/pom.xml | 2 +- .../META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml | 2 +- server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.server/pom.xml | 2 +- .../bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.admin/pom.xml | 2 +- .../bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.auth/pom.xml | 2 +- .../io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.data.transfer/pom.xml | 2 +- .../io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF | 4 ++-- server/bundles/io.cloudbeaver.service.metadata/pom.xml | 2 +- server/features/io.cloudbeaver.ce.drivers.feature/feature.xml | 2 +- server/features/io.cloudbeaver.ce.drivers.feature/pom.xml | 2 +- server/features/io.cloudbeaver.product.ce.feature/feature.xml | 2 +- server/features/io.cloudbeaver.product.ce.feature/pom.xml | 2 +- server/features/io.cloudbeaver.sample.feature/feature.xml | 2 +- server/features/io.cloudbeaver.sample.feature/pom.xml | 2 +- server/features/io.cloudbeaver.server.feature/feature.xml | 2 +- server/features/io.cloudbeaver.server.feature/pom.xml | 2 +- server/pom.xml | 2 +- .../product/provided-sample/DBeaverWebProvidedSample.product | 2 +- server/product/provided-sample/pom.xml | 2 +- server/product/web-server/CloudbeaverServer.product | 2 +- server/product/web-server/pom.xml | 2 +- 29 files changed, 37 insertions(+), 37 deletions(-) diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF index 47a1d03d0c..ae8478b94e 100644 --- a/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.connect.provider.sample;singleton:=true -Bundle-Version: 1.0.25.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.26.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml index 419737adee..750bb169c7 100644 --- a/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.connect.provider.sample - 1.0.25-SNAPSHOT + 1.0.26-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF index 329fbe68cc..2872e6fd7d 100644 --- a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 20.0.6.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 21.0.0.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.product.ce/pom.xml b/server/bundles/io.cloudbeaver.product.ce/pom.xml index 1056772825..b7867978cb 100644 --- a/server/bundles/io.cloudbeaver.product.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.product.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF index 75f0fdde64..b90c263cae 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Base JDBC drivers Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true -Bundle-Version: 1.0.22.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.23.qualifier +Bundle-Release-Date: 20210419 Bundle-Vendor: DBeaver Corp Bundle-ActivationPolicy: lazy Automatic-Module-Name: io.cloudbeaver.resources.drivers.base diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 45c0e036fa..a47c56c982 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.resources.drivers.base - 1.0.22-SNAPSHOT + 1.0.23-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index b8dd6b2d82..dcf110dda4 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 20.0.6.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 21.0.0.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.WebPlatformActivator diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index 2717504305..fc81934652 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF index 463323d472..6aa3841139 100644 --- a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.22.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.admin/pom.xml b/server/bundles/io.cloudbeaver.service.admin/pom.xml index 66fc3d2657..a6bbff47bc 100644 --- a/server/bundles/io.cloudbeaver.service.admin/pom.xml +++ b/server/bundles/io.cloudbeaver.service.admin/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.admin - 1.0.21-SNAPSHOT + 1.0.22-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF index fbd4ce883f..580b281ca0 100644 --- a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.22.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.auth/pom.xml b/server/bundles/io.cloudbeaver.service.auth/pom.xml index ab13094b68..c2b9778d46 100644 --- a/server/bundles/io.cloudbeaver.service.auth/pom.xml +++ b/server/bundles/io.cloudbeaver.service.auth/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.auth - 1.0.21-SNAPSHOT + 1.0.22-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF index 376532c1e3..ca50d688ab 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.22.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.23.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml index 6f07da1396..a78c8ccf18 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml +++ b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.data.transfer - 1.0.22-SNAPSHOT + 1.0.23-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF index 30df09fadf..a71edc0c46 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.25.qualifier -Bundle-Release-Date: 20210405 +Bundle-Version: 1.0.26.qualifier +Bundle-Release-Date: 20210419 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml index 871278454d..d9f7d1639c 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/pom.xml +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.metadata - 1.0.25-SNAPSHOT + 1.0.26-SNAPSHOT eclipse-plugin diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml index 64b45ecf35..72f9d136da 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml index 4a4b5c8b6a..61242f9766 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.ce.drivers.feature - 1.0.45-SNAPSHOT + 1.0.46-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.product.ce.feature/feature.xml b/server/features/io.cloudbeaver.product.ce.feature/feature.xml index 353c2768c9..2fc41ae55e 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/feature.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.product.ce.feature/pom.xml b/server/features/io.cloudbeaver.product.ce.feature/pom.xml index 46d95e5385..da6e7bdd43 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/pom.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce.feature - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.sample.feature/feature.xml b/server/features/io.cloudbeaver.sample.feature/feature.xml index c6efa3b5c0..2a15c5338a 100644 --- a/server/features/io.cloudbeaver.sample.feature/feature.xml +++ b/server/features/io.cloudbeaver.sample.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.sample.feature/pom.xml b/server/features/io.cloudbeaver.sample.feature/pom.xml index 9b21108671..3ec93f4a2d 100644 --- a/server/features/io.cloudbeaver.sample.feature/pom.xml +++ b/server/features/io.cloudbeaver.sample.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.sample.feature - 1.0.25-SNAPSHOT + 1.0.26-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index b0eee41f4b..d17be34d5b 100644 --- a/server/features/io.cloudbeaver.server.feature/feature.xml +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index 1987f11cf7..ceccd853c4 100644 --- a/server/features/io.cloudbeaver.server.feature/pom.xml +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.server.feature - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index 262b9f85c7..653f8e1d7e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,7 +19,7 @@ CloudBeaver CE - 20.0.6 + 21.0.0 diff --git a/server/product/provided-sample/DBeaverWebProvidedSample.product b/server/product/provided-sample/DBeaverWebProvidedSample.product index 7173fe62a7..e63b6f553e 100644 --- a/server/product/provided-sample/DBeaverWebProvidedSample.product +++ b/server/product/provided-sample/DBeaverWebProvidedSample.product @@ -1,7 +1,7 @@ - + diff --git a/server/product/provided-sample/pom.xml b/server/product/provided-sample/pom.xml index 946083d69e..39eb9a1f8b 100644 --- a/server/product/provided-sample/pom.xml +++ b/server/product/provided-sample/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT provided-sample eclipse-repository Cloudbeaver Provided Sample Product diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 0c1fb043e8..0fa419608c 100644 --- a/server/product/web-server/CloudbeaverServer.product +++ b/server/product/web-server/CloudbeaverServer.product @@ -2,7 +2,7 @@ diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index a54e046171..03b413e722 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 20.0.6-SNAPSHOT + 21.0.0-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product From 089f0edf2722c9454273823d3854a53c6caac787 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Wed, 7 Apr 2021 15:34:35 +0300 Subject: [PATCH 034/406] CB-909 Connection delete - behavior change --- .../src/io/cloudbeaver/service/core/impl/WebServiceCore.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index fc5313a55f..2f3dc35178 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -299,8 +299,8 @@ public class WebServiceCore implements DBWServiceCore { @Override public boolean deleteConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException { WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(connectionId); - if (!connectionInfo.isTemplate() && !CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { - throw new DBWebException("Connection delete is restricted by server configuration"); + if (connectionInfo.getDataSourceContainer().getProject() != webSession.getSingletonProject()) { + throw new DBWebException("Global connection '" + connectionInfo.getName() + "' configuration cannot be deleted"); } closeAndDeleteConnection(webSession, connectionId, true); return true; @@ -322,7 +322,6 @@ public class WebServiceCore implements DBWServiceCore { DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(dataSourceTemplate); ((DataSourceDescriptor) newDataSource).setNavigatorSettings(CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); - ((DataSourceDescriptor) newDataSource).setTemplate(true); if (!CommonUtils.isEmpty(connectionName)) { newDataSource.setName(connectionName); } From 1c3999c3441d2ca6f41731fc09006e74211f7f10 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 7 Apr 2021 16:25:27 +0300 Subject: [PATCH 035/406] fix(core-app): CB-854 permission check --- webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index c725b84230..b422e48aff 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -60,7 +60,7 @@ export const NavigationTree = observer(function NavigationTree() { const [ref] = useFocus({ onFocus, onBlur }); const isEnabled = usePermission(EPermission.public); const { isSelected, handleOpen, handleSelect } = useNavigationTree(); - useMapResource(NavTreeResource, ROOT_NODE_PATH); + useMapResource(NavTreeResource, isEnabled ? ROOT_NODE_PATH : null); if (!isEnabled) { return null; From a627ce7f569f7b5bf20cd0fc1e5d6e2f1d7765d9 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Wed, 7 Apr 2021 17:20:08 +0300 Subject: [PATCH 036/406] Admin user API (multiple providers and origins) --- .../AbstractExternalAuthProvider.java | 1 + .../schema/service.admin.graphqls | 4 +- .../service/admin/AdminUserInfo.java | 38 +++++++++++++------ .../schema/service.auth.graphqls | 2 +- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java index f6ab04e602..606357882e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java @@ -26,6 +26,7 @@ import org.jkiss.utils.CommonUtils; */ public abstract class AbstractExternalAuthProvider implements DBWAuthProviderExternal { + @Deprecated protected void setUserOrigin(WebUser user, String type, String subType) { user.setMetaParameter(META_AUTH_PROVIDER, type); if (!CommonUtils.isEmpty(subType)) { diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index c3c02bbe76..567261bcbf 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -26,7 +26,9 @@ type AdminUserInfo { grantedRoles: [ID!]! grantedConnections: [AdminConnectionGrantInfo!]! - origin: ObjectOrigin! + origins: [ObjectOrigin!]! + + linkedAuthProviders: [String!]! } type AdminRoleInfo { diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java index e874084dbd..6655554333 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java @@ -16,21 +16,22 @@ */ package io.cloudbeaver.service.admin; -import io.cloudbeaver.DBWAuthProviderExternal; import io.cloudbeaver.DBWConnectionGrant; -import io.cloudbeaver.auth.provider.local.LocalAuthProvider; +import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebRole; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.model.user.WebUserOriginInfo; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; +import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.utils.CommonUtils; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -42,6 +43,8 @@ public class AdminUserInfo { private final WebSession session; private final WebUser user; + private String[] userLinkedProviders; + public AdminUserInfo(WebSession session, WebUser user) { this.session = session; @@ -78,16 +81,29 @@ public class AdminUserInfo { } @Property - public WebUserOriginInfo getOrigin() { - String providerId = user.getMetaParameter(DBWAuthProviderExternal.META_AUTH_PROVIDER); - if (CommonUtils.isEmpty(providerId)) { - providerId = LocalAuthProvider.PROVIDER_ID; + public WebUserOriginInfo[] getOrigins() throws DBWebException { + List result = new ArrayList<>(); + for (String provider : getUserLinkedProviders()) { + WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(provider); + if (authProvider == null) { + log.error("Auth provider '" + provider + "' not found"); + } else { + result.add(new WebUserOriginInfo(session, user, authProvider)); + } } - WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(providerId); - if (authProvider == null) { - log.error("Auth provider '" + providerId + "' not found"); + return result.toArray(new WebUserOriginInfo[0]); + } + + public String[] getUserLinkedProviders() throws DBWebException { + if (userLinkedProviders != null) { + return userLinkedProviders; } - return new WebUserOriginInfo(session, user, authProvider); + try { + userLinkedProviders = CBApplication.getInstance().getSecurityController().getUserLinkedProviders(user.getUserId()); + } catch (DBCException e) { + throw new DBWebException("Error reading user linked providers", e); + } + return userLinkedProviders; } } diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index ec4466c455..500393eb80 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -55,7 +55,7 @@ type UserAuthToken { type UserInfo { # User unique identifier - userId: String! + userId: ID! # Human readable display name. It is taken from the first auth provider which was used for user login. displayName: String From b91d42546feddafcd7ae45c2eeb2a3f034b1812c Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Wed, 7 Apr 2021 17:25:35 +0300 Subject: [PATCH 037/406] CB-573 Reset password on credentials save disable --- .../src/io/cloudbeaver/WebServiceUtils.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java index 63477280af..c3daef132b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -236,7 +236,12 @@ public class WebServiceUtils { } } - public static void saveAuthProperties(DBPDataSourceContainer dataSourceContainer, DBPConnectionConfiguration configuration, Map authProperties, boolean saveCredentials) { + public static void saveAuthProperties( + @NotNull DBPDataSourceContainer dataSourceContainer, + @NotNull DBPConnectionConfiguration configuration, + @Nullable Map authProperties, + boolean saveCredentials) + { dataSourceContainer.setSavePassword(saveCredentials); if (!saveCredentials) { // Reset credentials @@ -247,6 +252,9 @@ public class WebServiceUtils { return; } } + if (!saveCredentials) { + configuration.setUserPassword(null); + } { // Read save credentials DBAAuthCredentials credentials = configuration.getAuthModel().loadCredentials(dataSourceContainer, configuration); From a985b5f519b0e6a5eb204345df20e26628b14cce Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 7 Apr 2021 17:31:07 +0300 Subject: [PATCH 038/406] feat: CB-854 connection tabs design --- .../packages/core-blocks/src/Tabs/TabList.tsx | 2 +- .../ConnectionsTable/ConnectionEdit.tsx | 2 +- .../CreateConnection/CreateConnection.tsx | 1 + .../src/ConnectionForm/ConnectionForm.tsx | 32 +++++++++++++------ .../PublicConnectionForm.tsx | 7 ++-- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/webapp/packages/core-blocks/src/Tabs/TabList.tsx b/webapp/packages/core-blocks/src/Tabs/TabList.tsx index 1105fc7dd4..decf1f5c41 100644 --- a/webapp/packages/core-blocks/src/Tabs/TabList.tsx +++ b/webapp/packages/core-blocks/src/Tabs/TabList.tsx @@ -46,7 +46,7 @@ export const TabList: React.FC = observer(function TabList({ component={tabInfo.tab?.()} {...state.props} style={style} - disabled={tabInfo.isDisabled?.(tabInfo.key, state.props)} + disabled={props.disabled || tabInfo.isDisabled?.(tabInfo.key, state.props)} onOpen={tabInfo.onOpen} onClose={tabInfo.onClose} /> diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx index 6e5633df10..2effeefec7 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -33,7 +33,7 @@ const styles = composes( padding: 24px 0; display: flex; flex-direction: column; - height: 630px; + height: 640px; } ` ); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx index eb7244077a..1a1486761b 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -55,6 +55,7 @@ const styles = composes( position: relative; display: flex; flex-direction: column; + padding-top: 16px; flex: 1; overflow: auto; } diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx index 186852b28a..acf88a0c25 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx @@ -12,7 +12,7 @@ import styled, { css } from 'reshadow'; import { TabsState, TabList, - Button, BORDER_TAB_STYLES, TabPanelList, Placeholder, useObjectRef + Button, UNDERLINE_TAB_STYLES, TabPanelList, Placeholder, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -24,12 +24,8 @@ import { useConnectionFormState } from './useConnectionFormState'; const styles = composes( css` - Tab { - composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; - } - TabList { - composes: theme-background-surface theme-text-on-surface from global; + composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; } box { @@ -42,7 +38,23 @@ const styles = composes( `, css` TabList { + position: relative; flex-shrink: 0; + align-items: center; + + &:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; + } + } + Tab { + height: 46px!important; + text-transform: uppercase; + font-weight: 500 !important; } box { display: flex; @@ -74,6 +86,7 @@ interface Props { options: IConnectionFormOptions; onCancel?: () => void; onSave?: (config: ConnectionConfig) => void; + className?: string; } export const ConnectionForm = observer(function ConnectionForm({ @@ -81,9 +94,10 @@ export const ConnectionForm = observer(function ConnectionForm({ options, onCancel = () => {}, onSave = () => {}, + className, }: Props) { const props = useObjectRef({ onSave }); - const style = [styles, BORDER_TAB_STYLES]; + const style = [styles, UNDERLINE_TAB_STYLES]; const translate = useTranslate(); const service = useService(ConnectionFormService); const formState = useConnectionFormState(data, options); @@ -108,8 +122,8 @@ export const ConnectionForm = observer(function ConnectionForm({ form={formState} options={options} > - - + + diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts index 60af206679..e591c8f95f 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts @@ -14,49 +14,47 @@ import type { CachedMapResource, ConnectionConfig, GetConnectionsQueryVariables import type { MetadataMap } from '@cloudbeaver/core-utils'; import type { DatabaseConnection } from '../Administration/ConnectionsResource'; +import type { IConnectionFormStateContext } from './connectionFormStateContext'; -export interface IConnectionForm { - originLocal: boolean; // connection specific, maybe should be in another place - disabled: boolean; - loading: boolean; - readonly: boolean; -} - -export interface IConnectionFormData { - config: ConnectionConfig; - availableDrivers?: string[]; - - info: DatabaseConnection | undefined; - resource?: CachedMapResource; - partsState: MetadataMap; -} - -export interface IConnectionFormOptions { - mode: 'edit' | 'create'; - type: 'admin' | 'public'; -} +export type ConnectionFormMode = 'edit' | 'create'; +export type ConnectionFormType = 'admin' | 'public'; export interface IConnectionFormProps { - data: IConnectionFormData; - options: IConnectionFormOptions; - form: IConnectionForm; + state: IConnectionFormState; } -export interface IConnectionFormSubmitData extends IConnectionFormProps { +export interface IConnectionFormSubmitData { submitType: 'submit' | 'test'; + state: IConnectionFormState; } export interface IConnectionFormState { - form: IConnectionForm; - submittingHandlers: IExecutorHandlersCollection; - save: () => Promise; - test: () => Promise; -} + mode: ConnectionFormMode; + type: ConnectionFormType; -export interface IConnectionFormTabProps { - data: IConnectionFormData; - options: IConnectionFormOptions; - form: IConnectionFormState; + config: ConnectionConfig; + + partsState: MetadataMap; + + disabled: boolean; + loading: boolean; + + readonly availableDrivers: string[]; + readonly resource: CachedMapResource; + readonly info: DatabaseConnection | undefined; + readonly readonly: boolean; + readonly submittingHandlers: IExecutorHandlersCollection; + + readonly setPartsState: (state: MetadataMap) => this; + readonly setOptions: ( + mode: ConnectionFormMode, + type: ConnectionFormType + ) => this; + readonly setConfig: (config: ConnectionConfig) => this; + readonly setAvailableDrivers: (drivers: string[]) => this; + readonly save: () => Promise; + readonly test: () => Promise; + readonly checkFormState: () => Promise; } export interface IConnectionFormValidation { @@ -76,11 +74,12 @@ export interface IConnectionFormStatus { @injectable() export class ConnectionFormService { - readonly tabsContainer: TabsContainer; + readonly tabsContainer: TabsContainer; readonly actionsContainer: PlaceholderContainer; readonly prepareConfigTask: IExecutor; readonly formValidationTask: IExecutor; readonly formSubmittingTask: IExecutor; + readonly formStateTask: IExecutor; constructor( private readonly notificationService: NotificationService @@ -90,17 +89,19 @@ export class ConnectionFormService { this.prepareConfigTask = new Executor(); this.formSubmittingTask = new Executor(); this.formValidationTask = new Executor(); + this.formStateTask = new Executor(); this.formSubmittingTask .before(this.formValidationTask) .before(this.prepareConfigTask); + this.formStateTask + .before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); + this.formSubmittingTask.addPostHandler(this.showStatusMessage); this.formValidationTask.addPostHandler(this.ensureValidation); } - connectionConfigContext = (): ConnectionConfig => ({}); - connectionValidationContext = (): IConnectionFormValidation => ({ valid: true, messages: [], @@ -159,7 +160,9 @@ export class ConnectionFormService { if (validation.messages.length > 0) { this.notificationService.notify({ - title: data.options.mode === 'edit' ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', + title: data.state.mode === 'edit' + ? 'connections_administration_connection_save_error' + : 'connections_administration_connection_create_error', message: validation.messages.join('\n'), }, validation.valid ? ENotificationType.Info : ENotificationType.Error); } diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts new file mode 100644 index 0000000000..06d247aef3 --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts @@ -0,0 +1,168 @@ +/* + * 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 { computed, makeObservable, observable } from 'mobx'; + +import { ExecutorHandlersCollection, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import type { CachedMapResource, ConnectionConfig, GetConnectionsQueryVariables } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { DatabaseConnection } from '../Administration/ConnectionsResource'; +import { EConnectionFeature } from '../EConnectionFeature'; +import type { IConnectionFormState, IConnectionFormSubmitData, ConnectionFormMode, ConnectionFormType, ConnectionFormService } from './ConnectionFormService'; +import { connectionFormStateContext, IConnectionFormStateContext } from './connectionFormStateContext'; + +export class ConnectionFormState implements IConnectionFormState { + mode: ConnectionFormMode; + type: ConnectionFormType; + + config: ConnectionConfig; + + partsState: MetadataMap; + + disabled: boolean; + loading: boolean; + + get availableDrivers(): string[] { + if (this._availableDrivers.length === 0 && this.config.driverId) { + return [this.config.driverId]; + } + + return this._availableDrivers; + } + + get info(): DatabaseConnection | undefined { + if (!this.config.connectionId) { + return undefined; + } + + return this.resource.get(this.config.connectionId); + } + + get readonly(): boolean { + if (this.type === 'admin' || this.mode === 'create') { + return false; + } + + if (this.info?.features && !this.info.features.includes(EConnectionFeature.manageable)) { + return true; + } + + return false; + } + + readonly resource: CachedMapResource; + readonly service: ConnectionFormService; + readonly submittingHandlers: IExecutorHandlersCollection; + + private _availableDrivers: string[]; + + constructor( + service: ConnectionFormService, + resource: CachedMapResource + ) { + makeObservable(this, { + mode: observable, + type: observable, + config: observable, + availableDrivers: computed, + _availableDrivers: observable, + info: computed, + disabled: observable, + loading: observable, + readonly: computed, + }); + + this.resource = resource; + this.config = {}; + this._availableDrivers = []; + this.partsState = new MetadataMap(); + this.service = service; + this.submittingHandlers = new ExecutorHandlersCollection(); + this.disabled = false; + this.loading = false; + this.mode = 'create'; + this.type = 'public'; + + this.submittingHandlers + .addHandler(() => { + this.loading = true; + this.disabled = true; + }) + .addPostHandler(() => { + this.loading = false; + this.disabled = false; + }); + + this.test = this.test.bind(this); + this.save = this.save.bind(this); + this.checkFormState = this.checkFormState.bind(this); + } + + reset(): void { + this.partsState.clear(); + } + + setPartsState(state: MetadataMap): this { + this.partsState = state; + return this; + } + + setOptions( + mode: ConnectionFormMode, + type: ConnectionFormType + ): this { + this.mode = mode; + this.type = type; + return this; + } + + setConfig(config: ConnectionConfig): this { + this.config = config; + this.reset(); + return this; + } + + setAvailableDrivers(drivers: string[]): this { + this._availableDrivers = drivers; + this.reset(); + return this; + } + + async save(): Promise { + await this.service.formSubmittingTask.executeScope( + { + state: this, + submitType: 'submit', + }, + this.submittingHandlers + ); + } + + async test(): Promise { + await this.service.formSubmittingTask.executeScope( + { + state: this, + submitType: 'test', + }, + this.submittingHandlers + ); + } + + async checkFormState(): Promise { + if (this.mode === 'create') { + const context = connectionFormStateContext(); + context.markEdited(); + return context; + } + + const contexts = await this.service.formStateTask.execute(this); + + return contexts.getContext(connectionFormStateContext); + } +} diff --git a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts index 682a2f9225..498e71de44 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts @@ -8,8 +8,11 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { isPropertiesEqual } from '@cloudbeaver/core-utils'; -import { IConnectionFormSubmitData, ConnectionFormService } from '../ConnectionFormService'; +import { connectionConfigContext } from '../connectionConfigContext'; +import { IConnectionFormSubmitData, ConnectionFormService, IConnectionFormState } from '../ConnectionFormService'; +import { connectionFormStateContext } from '../connectionFormStateContext'; import { DriverProperties } from './DriverProperties'; @injectable() @@ -27,8 +30,8 @@ export class ConnectionDriverPropertiesTabService extends Bootstrap { order: 2, panel: () => DriverProperties, isDisabled: (tabId, props) => { - if (props?.data.config.driverId) { - return !props?.data.config.driverId; + if (props?.state.config.driverId) { + return !props?.state.config.driverId; } return true; }, @@ -36,18 +39,33 @@ export class ConnectionDriverPropertiesTabService extends Bootstrap { this.connectionFormService.prepareConfigTask .addHandler(this.prepareConfig.bind(this)); + + this.connectionFormService.formStateTask + .addHandler(this.formState.bind(this)); } load(): void { } - private async prepareConfig( + private prepareConfig( { - data, + state, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { - const config = contexts.getContext(this.connectionFormService.connectionConfigContext); + const config = contexts.getContext(connectionConfigContext); - config.properties = data.config.properties; + config.properties = state.config.properties; + } + + private formState( + data: IConnectionFormState, + contexts: IExecutionContextProvider + ) { + const config = contexts.getContext(connectionConfigContext); + if (!isPropertiesEqual(config.properties, data.info?.properties)) { + const stateContext = contexts.getContext(connectionFormStateContext); + + stateContext.markEdited(); + } } } diff --git a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx index 471d918f5b..3ad84d24d5 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -16,7 +16,7 @@ import { useStyles } from '@cloudbeaver/core-theming'; import { uuid } from '@cloudbeaver/core-utils'; import { DBDriverResource } from '../../DBDriverResource'; -import type { IConnectionFormTabProps } from '../ConnectionFormService'; +import type { IConnectionFormProps } from '../ConnectionFormService'; import { useConnectionData } from '../useConnectionData'; const styles = css` @@ -31,24 +31,23 @@ const styles = css` } `; -export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ +export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ tabId, - data, - form, + state: formState, }) { const style = useStyles(styles); const { selected } = useTab(tabId); - useConnectionData(data, data => { - if (!data.config.properties) { - data.config.properties = {}; + useConnectionData(formState, state => { + if (!state.config.properties) { + state.config.properties = {}; } - if (!data.info) { + if (!state.info) { return; } - data.config.properties = { ...data.info.properties }; + state.config.properties = { ...state.info.properties }; }); const [state] = useState(() => { @@ -69,16 +68,16 @@ export const DriverProperties: TabContainerPanelComponent { - for (const key of Object.keys(data.config.properties)) { + for (const key of Object.keys(formState.config.properties)) { if (driver.driverProperties?.some(property => property.id === key) || state.propertiesList.some(property => property.key === key)) { continue; } - state.add(key, data.config.properties[key]); + state.add(key, formState.config.properties[key]); } }, } @@ -106,8 +105,8 @@ export const DriverProperties: TabContainerPanelComponent ( )} diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index e26a9f3fbd..1e85ee3f06 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -9,14 +9,17 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { isPropertiesEqual } from '@cloudbeaver/core-utils'; -import { ConnectionsResource } from '../../Administration/ConnectionsResource'; +import { ConnectionsResource, DatabaseConnection } from '../../Administration/ConnectionsResource'; import { ConnectionInfoResource } from '../../ConnectionInfoResource'; import { DatabaseAuthModelsResource } from '../../DatabaseAuthModelsResource'; import { DBDriverResource } from '../../DBDriverResource'; import { getUniqueConnectionName } from '../../getUniqueConnectionName'; import { isJDBCConnection } from '../../isJDBCConnection'; -import { IConnectionFormSubmitData, ConnectionFormService } from '../ConnectionFormService'; +import { connectionConfigContext } from '../connectionConfigContext'; +import { IConnectionFormSubmitData, ConnectionFormService, IConnectionFormState } from '../ConnectionFormService'; +import { connectionFormStateContext } from '../connectionFormStateContext'; import { Options } from './Options'; @injectable() @@ -47,29 +50,27 @@ export class ConnectionOptionsTabService extends Bootstrap { this.connectionFormService.formSubmittingTask .addHandler(this.save.bind(this)); + + this.connectionFormService.formStateTask + .addHandler(this.formState.bind(this)); } load(): void { } private async save( { - form, - data, - options, + state, submitType, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { const status = contexts.getContext(this.connectionFormService.connectionStatusContext); - const config = contexts.getContext(this.connectionFormService.connectionConfigContext); - - form.disabled = true; - form.loading = true; + const config = contexts.getContext(connectionConfigContext); try { - if (options.type === 'admin') { + if (state.type === 'admin') { if (submitType === 'submit') { - if (options.mode === 'edit') { + if (state.mode === 'edit') { const connection = await this.connectionsResource.update(config.connectionId!, config); status.info('Connection updated'); status.info(connection.name); @@ -88,7 +89,7 @@ export class ConnectionOptionsTabService extends Bootstrap { } } else { if (submitType === 'submit') { - if (options.mode === 'edit') { + if (state.mode === 'edit') { const connection = await this.connectionInfoResource.update(config); status.info('Connection updated'); status.info(connection.name); @@ -112,44 +113,39 @@ export class ConnectionOptionsTabService extends Bootstrap { } else { status.error('connections_connection_test_fail', exception); } - } finally { - form.disabled = false; - form.loading = false; } } private validate( { - data, - options, + state, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - if (!data.config.name?.length) { + if (!state.config.name?.length) { validation.error("Field 'name' can't be empty"); } } private async prepareConfig( { - data, - options, + state, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { - const config = contexts.getContext(this.connectionFormService.connectionConfigContext); - const driver = await this.dbDriverResource.load(data.config.driverId!); + const config = contexts.getContext(connectionConfigContext); + const driver = await this.dbDriverResource.load(state.config.driverId!, ['includeProviderProperties']); - if (options.mode === 'edit') { - config.connectionId = data.config.connectionId; + if (state.mode === 'edit') { + config.connectionId = state.config.connectionId; } - config.name = data.config.name?.trim(); + config.name = state.config.name?.trim(); - if (config.name && options.mode === 'create') { - if (options.type === 'admin') { + if (config.name && state.mode === 'create') { + if (state.type === 'admin') { await this.connectionsResource.loadAll(); const connectionNames = this.connectionsResource.values.map(connection => connection.name); config.name = getUniqueConnectionName(config.name, connectionNames); @@ -159,39 +155,33 @@ export class ConnectionOptionsTabService extends Bootstrap { } } - config.description = data.config.description; - config.template = data.config.template; - config.driverId = data.config.driverId; + config.description = state.config.description; + config.template = state.config.template; + config.driverId = state.config.driverId; - if (isJDBCConnection(driver, data.info)) { - config.url = data.config.url; + if (isJDBCConnection(driver, state.info)) { + config.url = state.config.url; } else { if (!driver.embedded) { - config.host = data.config.host; - config.port = data.config.port; + config.host = state.config.host; + config.port = state.config.port; } - config.databaseName = data.config.databaseName; + config.databaseName = state.config.databaseName; } - if (data.config.authModelId || driver.defaultAuthModel) { - config.authModelId = data.config.authModelId || driver.defaultAuthModel; - config.saveCredentials = data.config.saveCredentials; + if (state.config.authModelId || driver.defaultAuthModel) { + config.authModelId = state.config.authModelId || driver.defaultAuthModel; + config.saveCredentials = state.config.saveCredentials; - const authModel = await this.databaseAuthModelsResource.load(config.authModelId); + const properties = await this.getConnectionAuthModelProperties(config.authModelId, state.info); - let properties = authModel?.properties; - - if (data.info && data.info.authProperties.length > 0) { - properties = data.info.authProperties; - } - - if (this.isCredentialsChanged(properties, data.config.credentials)) { - config.credentials = data.config.credentials; + if (this.isCredentialsChanged(properties, state.config.credentials)) { + config.credentials = state.config.credentials; } } if (driver.providerProperties.length > 0) { - const providerProperties: Record = { ...data.config.providerProperties }; + const providerProperties: Record = { ...state.config.providerProperties }; for (const providerProperty of driver.providerProperties) { if (providerProperty.defaultValue === null @@ -208,6 +198,38 @@ export class ConnectionOptionsTabService extends Bootstrap { } } + private async formState( + data: IConnectionFormState, + contexts: IExecutionContextProvider + ) { + if (!data.info) { + return; + } + + const config = contexts.getContext(connectionConfigContext); + const stateContext = contexts.getContext(connectionFormStateContext); + + if ( + config.name !== data.info.name + || config.description !== data.info.description + || config.template !== data.info.template + || config.driverId !== data.info.driverId + || (config.url !== undefined && config.url !== data.info.url) + || (config.host !== undefined && config.host !== data.info.host) + || (config.port !== undefined && config.port !== data.info.port) + || (config.databaseName !== undefined && config.databaseName !== data.info.databaseName) + || config.credentials !== undefined + || (config.authModelId !== undefined && config.authModelId !== data.info.authModel) + || (config.saveCredentials !== undefined && config.saveCredentials !== data.info.saveCredentials) + || ( + config.providerProperties !== undefined + && !isPropertiesEqual(config.providerProperties, data.info.providerProperties) + ) + ) { + stateContext.markEdited(); + } + } + private isCredentialsChanged( authProperties: ObjectPropertyInfo[], credentials: Record @@ -225,4 +247,19 @@ export class ConnectionOptionsTabService extends Bootstrap { } return false; } + + private async getConnectionAuthModelProperties( + authModelId: string, + connectionInfo?: DatabaseConnection + ): Promise { + const authModel = await this.databaseAuthModelsResource.load(authModelId); + + let properties = authModel?.properties; + + if (connectionInfo && connectionInfo.authProperties.length > 0) { + properties = connectionInfo.authProperties; + } + + return properties; + } } diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx index b0dc766be4..6cd6f0f1e8 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx @@ -32,10 +32,11 @@ import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; +import { isLocalConnection } from '../../Administration/ConnectionsResource'; import { DatabaseAuthModelsResource } from '../../DatabaseAuthModelsResource'; import { DBDriverResource } from '../../DBDriverResource'; import { isJDBCConnection } from '../../isJDBCConnection'; -import { IConnectionFormTabProps, ConnectionFormService } from '../ConnectionFormService'; +import { IConnectionFormProps, ConnectionFormService } from '../ConnectionFormService'; import { useConnectionData } from '../useConnectionData'; import { ParametersFormNew } from './ParametersFormNew'; import { useOptions } from './useOptions'; @@ -47,18 +48,25 @@ const styles = css` } `; -export const Options: TabContainerPanelComponent = observer(function Options(props) { +export const Options: TabContainerPanelComponent = observer(function Options(props) { const { - data, - form, - options, + state, } = props; const service = useService(ConnectionFormService); const formRef = useRef(null); const translate = useTranslate(); + const { + info, + config, + availableDrivers, + submittingHandlers, + save, + readonly, + disabled, + } = state; - useFormValidator(form.submittingHandlers.for(service.formValidationTask), formRef); - useConnectionData(data, action((data, update) => { + useFormValidator(submittingHandlers.for(service.formValidationTask), formRef); + useConnectionData(state, action((data, update) => { if (!data.config.credentials || update) { data.config.credentials = {}; data.config.saveCredentials = false; @@ -68,10 +76,6 @@ export const Options: TabContainerPanelComponent = obse data.config.providerProperties = {}; } - if ((!data.availableDrivers || data.availableDrivers.length === 0) && data.config.driverId) { - data.availableDrivers = [data.config.driverId]; - } - if (!data.info) { return; } @@ -83,10 +87,6 @@ export const Options: TabContainerPanelComponent = obse data.config.template = data.info.template; data.config.driverId = data.info.driverId; - if (!data.availableDrivers || data.availableDrivers.length === 0) { - data.availableDrivers = [data.info.driverId]; - } - data.config.host = data.info.host; data.config.port = data.info.port; data.config.databaseName = data.info.databaseName; @@ -107,18 +107,13 @@ export const Options: TabContainerPanelComponent = obse data.config.providerProperties = { ...data.info.providerProperties }; } })); - const optionsHook = useOptions({ data, form: form.form, options }); + const optionsHook = useOptions(props.state); const { credentialsSavingEnabled } = useAdministrationSettings(); const driver = useMapResource( DBDriverResource, - { key: data.config.driverId || null, includes: ['includeProviderProperties'] }, + { key: config.driverId || null, includes: ['includeProviderProperties'] }, { - onLoad: async () => { - if (!data.config.driverId && data.info) { - data.info.authModel = undefined; - } - }, onData: (data, resource, prevData) => optionsHook.setDefaults(data, prevData), } ); @@ -131,21 +126,22 @@ export const Options: TabContainerPanelComponent = obse const { data: authModel } = useMapResource( DatabaseAuthModelsResource, - data.info?.authModel || driver.data?.defaultAuthModel || null, + info?.authModel || driver.data?.defaultAuthModel || null, { onData: data => optionsHook.setAuthModel(data), } ); - const JDBC = isJDBCConnection(driver.data, data.info); - const admin = options.type === 'admin'; - const edit = options.mode === 'edit'; + const JDBC = isJDBCConnection(driver.data, info); + const admin = state.type === 'admin'; + const edit = state.mode === 'edit'; + const originLocal = !info || isLocalConnection(info); - const drivers = driver.resource.values.filter(({ id }) => data.availableDrivers?.includes(id)); + const drivers = driver.resource.values.filter(({ id }) => availableDrivers?.includes(id)); let properties = authModel?.properties; - if (data.info && data.info.authProperties.length > 0) { - properties = data.info.authProperties; + if (info && info.authProperties.length > 0) { + properties = info.authProperties; } // TODO we need to get these values other way @@ -153,19 +149,19 @@ export const Options: TabContainerPanelComponent = obse const booleanProviderProperties = driver.data?.providerProperties?.slice().filter(property => property.dataType === 'Boolean'); return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( - + driver.id} valueSelector={driver => driver?.name ?? ''} - readOnly={form.form.readonly || edit || drivers.length < 2} - disabled={form.form.disabled} + readOnly={readonly || edit || drivers.length < 2} + disabled={disabled} tiny fill > @@ -175,9 +171,9 @@ export const Options: TabContainerPanelComponent = obse type="text" name="name" minLength={1} - state={data.config} - disabled={form.form.disabled} - readOnly={form.form.readonly} + state={config} + disabled={disabled} + readOnly={readonly} mod='surface' required tiny @@ -190,30 +186,30 @@ export const Options: TabContainerPanelComponent = obse {translate('customConnection_url_JDBC')} ) : ( )} - {admin && form.form.originLocal && ( + {admin && originLocal && ( {translate('connections_connection_template')} @@ -222,9 +218,9 @@ export const Options: TabContainerPanelComponent = obse {translate('connections_connection_description')} @@ -238,9 +234,9 @@ export const Options: TabContainerPanelComponent = obse @@ -248,9 +244,9 @@ export const Options: TabContainerPanelComponent = obse {credentialsSavingEnabled && ( {translate('connections_connection_edit_save_credentials')} )} @@ -263,9 +259,9 @@ export const Options: TabContainerPanelComponent = obse @@ -274,9 +270,9 @@ export const Options: TabContainerPanelComponent = obse diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/useOptions.ts b/webapp/packages/core-connections/src/ConnectionForm/Options/useOptions.ts index fd7f870918..d38746b3a1 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/useOptions.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/useOptions.ts @@ -13,36 +13,37 @@ import type { DatabaseAuthModel } from '@cloudbeaver/core-sdk'; import type { DBDriver } from '../../DBDriverResource'; import { isJDBCConnection } from '../../isJDBCConnection'; -import type { IConnectionFormProps } from '../ConnectionFormService'; +import type { IConnectionFormState } from '../ConnectionFormService'; interface IRefObject { prevName: string | null; - props: IConnectionFormProps; + state: IConnectionFormState; } const MAX_HOST_LENGTH = 20; -export function useOptions(props: IConnectionFormProps) { +export function useOptions(state: IConnectionFormState) { const refObject = useObjectRef({ prevName: null, - props, + state, }, { - props, + state, }); return useObjectRef({ updateNameTemplate(driver: DBDriver | undefined) { const { prevName, - props: { - data: { config, info }, - options, + state: { + config, + info, + mode, }, } = refObject; const isAutoFill = config.name === prevName || prevName === null; - if (options.mode === 'edit' || !isAutoFill) { + if (mode === 'edit' || !isAutoFill) { return; } @@ -69,8 +70,9 @@ export function useOptions(props: IConnectionFormProps) { }, setDefaults(driver: DBDriver | undefined, prevDriver: DBDriver | undefined) { const { - props: { - data: { config, info }, + state: { + config, + info, }, } = refObject; diff --git a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts index 62bda4734a..9bd05a5280 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts @@ -27,7 +27,7 @@ export class ConnectionOriginInfoTabService extends Bootstrap { order: 3, tab: () => OriginInfoTab, panel: () => OriginInfo, - isHidden: (tabId, props) => props?.data.info ? isLocalConnection(props.data.info) : true, + isHidden: (tabId, props) => props?.state.info ? isLocalConnection(props.state.info) : true, }); } diff --git a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx index 53736936bf..7821a01a39 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx @@ -18,7 +18,7 @@ import { useStyles } from '@cloudbeaver/core-theming'; import { AuthenticationProvider } from '@cloudbeaver/core-ui'; import { css } from '@reshadow/react'; -import type { IConnectionFormTabProps } from '../ConnectionFormService'; +import type { IConnectionFormProps } from '../ConnectionFormService'; const style = css` Loader { @@ -26,9 +26,12 @@ const style = css` } `; -export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ +export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ tabId, - data, + state: { + info, + resource, + }, }) { const tab = useTab(tabId); const translate = useTranslate(); @@ -36,11 +39,11 @@ export const OriginInfo: TabContainerPanelComponent = o const state = useTabState>(() => ({})); const styles = useStyles(style, BASE_CONTAINERS_STYLES); - const connection = useMapResource(data.resource!, { - key: tab.selected ? data.info!.id : null, + const connection = useMapResource(resource!, { + key: tab.selected ? info!.id : null, includes: ['includeOrigin', 'customIncludeOriginDetails'], }, { - isActive: () => !data.info?.origin || userInfoService.hasOrigin(data.info.origin), + isActive: () => !info?.origin || userInfoService.hasOrigin(info.origin), onData: (connection, res, prev) => { if (!connection.origin.details) { return; @@ -76,12 +79,12 @@ export const OriginInfo: TabContainerPanelComponent = o ); } - const authorized = !data.info?.origin || userInfoService.hasOrigin(data.info.origin); + const authorized = !info?.origin || userInfoService.hasOrigin(info.origin); - if (!authorized && data.info?.origin) { + if (!authorized && info?.origin) { return styled(styles)( - + ); } diff --git a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx index c99c9653ff..302691f268 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx @@ -13,16 +13,16 @@ import { TabTitle, Tab, TabContainerTabComponent } from '@cloudbeaver/core-block import { Translate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; -import type { IConnectionFormTabProps } from '../ConnectionFormService'; +import type { IConnectionFormProps } from '../ConnectionFormService'; -export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ - data, +export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ + state: { info }, style, ...rest }) { return styled(useStyles(style))( - + ); }); diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index fced3f5b18..c567721ebf 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -13,7 +13,9 @@ import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; import type { DatabaseConnection } from '../../Administration/ConnectionsResource'; import { DBDriverResource } from '../../DBDriverResource'; import { SSH_TUNNEL_ID } from '../../NetworkHandlerResource'; -import { IConnectionFormSubmitData, ConnectionFormService } from '../ConnectionFormService'; +import { connectionConfigContext } from '../connectionConfigContext'; +import { IConnectionFormSubmitData, ConnectionFormService, IConnectionFormState } from '../ConnectionFormService'; +import { connectionFormStateContext } from '../connectionFormStateContext'; import { SSH } from './SSH'; import { SSHTab } from './SSHTab'; @@ -34,8 +36,8 @@ export class ConnectionSSHTabService extends Bootstrap { tab: () => SSHTab, panel: () => SSH, isHidden: (tabId, props) => { - if (props?.data.config.driverId) { - const driver = this.dbDriverResource.get(props.data.config.driverId); + if (props?.state.config.driverId) { + const driver = this.dbDriverResource.get(props?.state.config.driverId); return !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID); } @@ -48,25 +50,30 @@ export class ConnectionSSHTabService extends Bootstrap { this.connectionFormService.formValidationTask .addHandler(this.validate.bind(this)); + + this.connectionFormService.formStateTask + .addHandler(this.formState.bind(this)); } load(): void { } private validate( { - data, - options, + state: { + config, + info, + }, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - if (!data.config.networkHandlersConfig) { + if (!config.networkHandlersConfig) { return; } - for (const handler of data.config.networkHandlersConfig) { - if (handler.enabled && handler.savePassword && this.isChanged(handler, data.info)) { + for (const handler of config.networkHandlersConfig) { + if (handler.enabled && handler.savePassword && this.isChanged(handler, info)) { if (!handler.userName?.length) { validation.error("Field SSH 'User' can't be empty"); } @@ -87,21 +94,20 @@ export class ConnectionSSHTabService extends Bootstrap { private async prepareConfig( { - data, - options, + state, }: IConnectionFormSubmitData, contexts: IExecutionContextProvider ) { - const config = contexts.getContext(this.connectionFormService.connectionConfigContext); + const config = contexts.getContext(connectionConfigContext); - if (!data.config.networkHandlersConfig || data.config.networkHandlersConfig.length === 0) { + if (!state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0) { return; } const configs: NetworkHandlerConfigInput[] = []; - for (const handler of data.config.networkHandlersConfig) { - if (this.isChanged(handler, data.info)) { + for (const handler of state.config.networkHandlersConfig) { + if (this.isChanged(handler, state.info)) { configs.push(handler); } } @@ -111,11 +117,27 @@ export class ConnectionSSHTabService extends Bootstrap { } } + private formState( + data: IConnectionFormState, + contexts: IExecutionContextProvider + ) { + const config = contexts.getContext(connectionConfigContext); + if (config.networkHandlersConfig !== undefined) { + const stateContext = contexts.getContext(connectionFormStateContext); + + stateContext.markEdited(); + } + } + private isChanged(handler: NetworkHandlerConfigInput, info?: DatabaseConnection) { const initialConfig = info?.networkHandlersConfig.find(h => h.id === handler.id); + if (!initialConfig && !handler.enabled) { + return false; + } const port = Number(initialConfig?.properties?.port); const formPort = Number(handler.properties?.port); + if (handler.enabled !== initialConfig?.enabled || handler.savePassword !== initialConfig?.savePassword || handler.userName !== initialConfig?.userName diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx index 938ea96501..96625d5c42 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSH.tsx @@ -18,7 +18,7 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '../../NetworkHandlerResource'; -import type { IConnectionFormTabProps } from '../ConnectionFormService'; +import type { IConnectionFormProps } from '../ConnectionFormService'; import { useConnectionData } from '../useConnectionData'; const SSH_STYLES = css` @@ -30,15 +30,21 @@ const SSH_STYLES = css` } `; -export const SSH: TabContainerPanelComponent = observer(function SSH({ - data, - form, +export const SSH: TabContainerPanelComponent = observer(function SSH({ + state: formState, }) { + const { + info, + config, + save, + readonly, + disabled: formDisabled, + } = formState; const [loading, setLoading] = useState(false); - const initialConfig = data.info?.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); + const initialConfig = info?.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); const { credentialsSavingEnabled } = useAdministrationSettings(); - useConnectionData(data, data => { + useConnectionData(formState, data => { if (!data.config.networkHandlersConfig) { data.config.networkHandlersConfig = []; } @@ -61,7 +67,7 @@ export const SSH: TabContainerPanelComponent = observer } }); - const state = data.config.networkHandlersConfig!.find(state => state.id === SSH_TUNNEL_ID)!; + const state = config.networkHandlersConfig!.find(state => state.id === SSH_TUNNEL_ID)!; const resource = useMapResource(NetworkHandlerResource, SSH_TUNNEL_ID, { onData: handler => { @@ -83,7 +89,7 @@ export const SSH: TabContainerPanelComponent = observer const styles = useStyles(SSH_STYLES, BASE_CONTAINERS_STYLES); const translate = useTranslate(); - const disabled = form.form.disabled || loading; + const disabled = formDisabled || loading; const enabled = state.enabled || false; const passwordFilled = (initialConfig?.password === null && state.password !== '') || (state.password?.length || 0) > 0; let passwordHint = ''; @@ -93,14 +99,14 @@ export const SSH: TabContainerPanelComponent = observer } return styled(styles)( - + {translate('connections_network_handler_ssh_tunnel_enable')} @@ -110,7 +116,7 @@ export const SSH: TabContainerPanelComponent = observer name="host" state={state.properties} disabled={disabled || !enabled} - readOnly={form.form.readonly} + readOnly={readonly} mod='surface' small > @@ -121,7 +127,7 @@ export const SSH: TabContainerPanelComponent = observer name="port" state={state.properties} disabled={disabled || !enabled} - readOnly={form.form.readonly} + readOnly={readonly} mod='surface' tiny > @@ -134,7 +140,7 @@ export const SSH: TabContainerPanelComponent = observer name="userName" state={state} disabled={disabled || !enabled} - readOnly={form.form.readonly} + readOnly={readonly} mod='surface' tiny > @@ -146,7 +152,7 @@ export const SSH: TabContainerPanelComponent = observer placeholder={passwordHint} state={state} disabled={disabled || !enabled} - readOnly={form.form.readonly} + readOnly={readonly} mod='surface' tiny > @@ -158,7 +164,7 @@ export const SSH: TabContainerPanelComponent = observer name="savePassword" value={SSH_TUNNEL_ID + ' savePassword'} state={state} - disabled={disabled || !enabled || form.form.readonly} + disabled={disabled || !enabled || readonly} >{translate('connections_connection_edit_save_credentials')} )} diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSHTab.tsx b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSHTab.tsx index 4820a74702..63ae966e5a 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/SSHTab.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/SSHTab.tsx @@ -14,9 +14,9 @@ import { Translate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '../../NetworkHandlerResource'; -import type { IConnectionFormTabProps } from '../ConnectionFormService'; +import type { IConnectionFormProps } from '../ConnectionFormService'; -export const SSHTab: TabContainerTabComponent = observer(function SSHTab({ +export const SSHTab: TabContainerTabComponent = observer(function SSHTab({ style, ...rest }) { diff --git a/webapp/packages/core-connections/src/ConnectionForm/connectionConfigContext.ts b/webapp/packages/core-connections/src/ConnectionForm/connectionConfigContext.ts new file mode 100644 index 0000000000..6b1b425a15 --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionForm/connectionConfigContext.ts @@ -0,0 +1,13 @@ +/* + * 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 { ConnectionConfig } from '@cloudbeaver/core-sdk'; + +export function connectionConfigContext(): ConnectionConfig { + return {}; +} diff --git a/webapp/packages/core-connections/src/ConnectionForm/connectionFormStateContext.ts b/webapp/packages/core-connections/src/ConnectionForm/connectionFormStateContext.ts new file mode 100644 index 0000000000..0d219326b4 --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionForm/connectionFormStateContext.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export interface IConnectionFormStateContext { + edited: boolean; + + markEdited: () => void; +} + +export function connectionFormStateContext(): IConnectionFormStateContext { + return { + edited: false, + markEdited() { + this.edited = true; + }, + }; +} diff --git a/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts b/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts index 80e1282078..c5412086db 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/useConnectionData.ts @@ -10,7 +10,7 @@ import { useObjectRef } from '@cloudbeaver/core-blocks'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; import type { DatabaseConnection } from '../Administration/ConnectionsResource'; -import type { IConnectionFormData } from './ConnectionFormService'; +import type { IConnectionFormState } from './ConnectionFormService'; interface IState { config: ConnectionConfig | undefined; @@ -20,12 +20,9 @@ interface IState { } export function useConnectionData( - data: IConnectionFormData, - fill: (data: IConnectionFormData, update: boolean) => void + state: IConnectionFormState, + fill: (state: IConnectionFormState, update: boolean) => void ): void { - const connectionId = data.info?.id || data.config.connectionId; - const driverId = data.info?.driverId || data.config.driverId; - const lastDataRef = useObjectRef({ connectionId: undefined, driverId: undefined, @@ -34,21 +31,21 @@ export function useConnectionData( }, {}); if ( - lastDataRef.connectionId !== connectionId - || lastDataRef.driverId !== driverId - || lastDataRef.config !== data.config - || lastDataRef.info !== data.info + lastDataRef.connectionId !== state.config.connectionId + || lastDataRef.driverId !== state.config.driverId + || lastDataRef.config !== state.config + || lastDataRef.info !== state.info ) { fill( - data, + state, lastDataRef.connectionId !== null || lastDataRef.driverId !== null - || lastDataRef.config !== data.config - || lastDataRef.info !== data.info + || lastDataRef.config !== state.config + || lastDataRef.info !== state.info ); - lastDataRef.connectionId = connectionId; - lastDataRef.driverId = driverId; - lastDataRef.config = data.config; - lastDataRef.info = data.info; + lastDataRef.connectionId = state.config.connectionId; + lastDataRef.driverId = state.config.driverId; + lastDataRef.config = state.config; + lastDataRef.info = state.info; } } diff --git a/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormData.ts b/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormData.ts deleted file mode 100644 index 07d7efef77..0000000000 --- a/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormData.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { observable } from 'mobx'; - -import { useObjectRef } from '@cloudbeaver/core-blocks'; -import type { CachedMapResource, ConnectionConfig, GetConnectionsQueryVariables } from '@cloudbeaver/core-sdk'; -import { MetadataMap } from '@cloudbeaver/core-utils'; - -import type { DatabaseConnection } from '../Administration/ConnectionsResource'; -import type { IConnectionFormData } from './ConnectionFormService'; - -export interface IConnectionFormDataOptions { - config: ConnectionConfig; - availableDrivers?: string[]; - - resource?: CachedMapResource; - partsState?: MetadataMap; -} - -export function useConnectionFormData({ - config, - availableDrivers = [], - resource, - partsState, -}: IConnectionFormDataOptions): IConnectionFormData { - if (availableDrivers.length > 0) { - availableDrivers = [...availableDrivers]; - } else if (config.driverId) { - availableDrivers = [config.driverId]; - } - - const data = useObjectRef({ - config, - availableDrivers: observable(availableDrivers), - resource, - partsState: partsState ?? new MetadataMap(), - get info() { - if (!this.config.connectionId) { - return undefined; - } - - return this.resource?.get(this.config.connectionId); - }, - }, { - config, - resource, - }, { - config: observable, - }); - - if (partsState) { - data.partsState = partsState; - } - - for (const driver of availableDrivers) { - if (!data.availableDrivers.includes(driver)) { - data.availableDrivers.push(driver); - } - } - - for (const driver of [...data.availableDrivers]) { - if (!availableDrivers.includes(driver)) { - data.availableDrivers.splice(data.availableDrivers.indexOf(driver), 1); - } - } - - return data; -} diff --git a/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormState.ts b/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormState.ts index aa3f606f42..96af8e7dfa 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormState.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/useConnectionFormState.ts @@ -6,67 +6,28 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; import { useState } from 'react'; -import { useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import type { CachedMapResource, GetConnectionsQueryVariables } from '@cloudbeaver/core-sdk'; -import { isLocalConnection } from '../Administration/ConnectionsResource'; -import { EConnectionFeature } from '../EConnectionFeature'; -import { IConnectionForm, IConnectionFormData, IConnectionFormOptions, IConnectionFormState, IConnectionFormSubmitData, ConnectionFormService } from './ConnectionFormService'; +import type { DatabaseConnection } from '../Administration/ConnectionsResource'; +import { IConnectionFormState, ConnectionFormService } from './ConnectionFormService'; +import { ConnectionFormState } from './ConnectionFormState'; export function useConnectionFormState( - data: IConnectionFormData, - options: IConnectionFormOptions, - defaultState?: IConnectionForm + resource: CachedMapResource, + configure?: (state: IConnectionFormState) => any ): IConnectionFormState { - const props = useObjectRef({ data, options }); const service = useService(ConnectionFormService); - const [submittingHandlers] = useState(() => new ExecutorHandlersCollection()); - const [form] = useState(() => observable({ - disabled: false, - loading: false, - get readonly() { - if (options.type === 'admin' || options.mode === 'create') { - return false; - } - - if (props.data.info?.features && !props.data.info.features.includes(EConnectionFeature.manageable)) { - return true; - } - - return false; - }, - get originLocal() { - return !props.data.info || isLocalConnection(props.data.info); - }, - ...defaultState, - })); - - return useObjectRef({ - form, - submittingHandlers, - async save() { - await service.formSubmittingTask.executeScope( - { - ...props, - form, - submitType: 'submit', - }, - submittingHandlers - ); - }, - async test() { - await service.formSubmittingTask.executeScope( - { - ...props, - form, - submitType: 'test', - }, - submittingHandlers - ); - }, + const [state] = useState(() => { + const state = new ConnectionFormState( + service, + resource, + ); + configure?.(state); + return state; }); + + return state; } diff --git a/webapp/packages/core-connections/src/DBDriverResource.ts b/webapp/packages/core-connections/src/DBDriverResource.ts index 564e85258c..123a5151e6 100644 --- a/webapp/packages/core-connections/src/DBDriverResource.ts +++ b/webapp/packages/core-connections/src/DBDriverResource.ts @@ -62,6 +62,7 @@ export class DBDriverResource extends CachedMapResource(first: T, second: T): boolean { + if ( + first === null + || second === null + || typeof first !== 'object' + || typeof second !== 'object' + ) { + return false; + } + + const firstProperties = Object.entries(first); + const secondProperties = Object.entries(second); + + if (firstProperties.length !== secondProperties.length) { + return false; + } + + for (const [key, value] of firstProperties) { + if (second[key as keyof T] !== value) { + return false; + } + } + + return true; +} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts b/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts index 120469ebe4..96326c6d07 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts @@ -34,7 +34,7 @@ export class CustomConnectionController implements IInitializableController { }); } - init(onClose: () => void) { + init(onClose: () => void): void { this.loadDBDrivers(); this.onClose = onClose; } @@ -42,7 +42,7 @@ export class CustomConnectionController implements IInitializableController { onDriverSelect = async (driverId: string) => { const state = await this.publicConnectionFormService.open( { driverId }, - this.dbDriverResource.values.map(driver => driver.id) + this.dbDriverResource.keys ); if (state) { diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx index 0671211f90..71be28b1df 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx @@ -12,7 +12,7 @@ import styled from 'reshadow'; import { css } from 'reshadow'; import { Loader, useMapResource } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, ConnectionForm, useConnectionFormData, IConnectionFormDataOptions, IConnectionFormOptions } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, ConnectionForm, IConnectionFormState } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; @@ -28,31 +28,26 @@ const styles = css` `; interface Props { - options: IConnectionFormOptions; - dataOptions: IConnectionFormDataOptions; + state: IConnectionFormState; onCancel: () => void; onSave: (config: ConnectionConfig) => void; } const PublicConnectionFormRenderer: React.FC = observer(function PublicConnectionForm({ - options, - dataOptions, + state, onCancel, onSave, }) { const connection = useMapResource(ConnectionInfoResource, { - key: dataOptions.config.connectionId || null, + key: state.config.connectionId || null, includes: ['includeOrigin', 'customIncludeNetworkHandlerCredentials', 'includeAuthProperties', 'customIncludeNetworkHandlerCredentials'], }); - const data = useConnectionFormData(dataOptions); - return styled(styles)( {() => styled(styles)( @@ -68,11 +63,10 @@ export const PublicConnectionForm: React.FC = observer(function PublicConnection const save = useCallback(() => service.close(true), []); return styled(styles)( - - {() => ( + + {() => service.formState && ( diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index 532135d17a..2e4058def4 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -8,7 +8,8 @@ import { action, makeObservable, observable } from 'mobx'; -import { ConnectionInfoResource, IConnectionFormDataOptions, IConnectionFormOptions } from '@cloudbeaver/core-connections'; +import { ConnectionFormService, ConnectionInfoResource, IConnectionFormState } from '@cloudbeaver/core-connections'; +import { ConnectionFormState } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; @@ -22,27 +23,22 @@ const formGetter = () => PublicConnectionForm; @injectable() export class PublicConnectionFormService { - options: IConnectionFormOptions; - dataOptions: IConnectionFormDataOptions | null; + formState: IConnectionFormState | null; constructor( private readonly commonDialogService: CommonDialogService, private readonly optionsPanelService: OptionsPanelService, + private readonly connectionFormService: ConnectionFormService, private readonly connectionInfoResource: ConnectionInfoResource, private readonly sessionDataResource: SessionDataResource ) { makeObservable(this, { - dataOptions: observable, - options: observable, + formState: observable.shallow, + change: action, open: action, close: action, }); - - this.options = { - mode: 'create', - type: 'public', - }; - this.dataOptions = null; + this.formState = null; this.optionsPanelService.closeTask.addHandler(this.closeHandler); this.connectionInfoResource.onItemDelete.addHandler(this.closeDeleted); this.sessionDataResource.onDataOutdated.addHandler(() => { @@ -51,12 +47,17 @@ export class PublicConnectionFormService { } change(config: ConnectionConfig, availableDrivers?: string[]): void { - this.dataOptions = { - config: { ...config }, - availableDrivers: availableDrivers, - resource: this.connectionInfoResource, - }; - this.options.mode = config.connectionId ? 'edit' : 'create'; + if (!this.formState) { + this.formState = new ConnectionFormState( + this.connectionFormService, + this.connectionInfoResource + ); + } + + this.formState + .setOptions(config.connectionId ? 'edit' : 'create', 'public') + .setConfig(config) + .setAvailableDrivers(availableDrivers || []); } async open(config: ConnectionConfig, availableDrivers?: string[]): Promise { @@ -71,38 +72,44 @@ export class PublicConnectionFormService { async close(saved?: boolean): Promise { if (saved) { - this.dataOptions = null; + this.formState = null; } const state = await this.optionsPanelService.close(); if (state) { - this.dataOptions = null; + this.formState = null; } } private closeDeleted: IExecutorHandler> = async (data, contexts) => { - if (!this.dataOptions) { + if (!this.formState) { return; } - if (ResourceKeyUtils.includes(data, this.dataOptions.config.connectionId)) { + if (ResourceKeyUtils.includes(data, this.formState.config.connectionId)) { this.close(); } }; private closeHandler: IExecutorHandler = async (data, contexts) => { if ( - !this.dataOptions + !this.formState || this.optionsPanelService.panelComponent !== formGetter || ( - this.dataOptions.config.connectionId - && !this.connectionInfoResource.has(this.dataOptions.config.connectionId) + this.formState.config.connectionId + && !this.connectionInfoResource.has(this.formState.config.connectionId) ) ) { return; } + const state = await this.formState.checkFormState(); + + if (!state.edited) { + return; + } + const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'connections_public_connection_edit_cancel_title', message: 'connections_public_connection_edit_cancel_message', From 5d9294409a24e0805bd3ec101da770f53be41ffe Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 13 Apr 2021 21:42:45 +0300 Subject: [PATCH 057/406] fix: CB-936 allow reload after an error in data request --- webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts | 2 +- webapp/packages/plugin-sql-editor/src/QueryDataSource.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 4f79ce9251..46ca3f080b 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -46,7 +46,7 @@ export class ContainerDataSource extends DatabaseDataSource Date: Tue, 13 Apr 2021 22:03:47 +0300 Subject: [PATCH 058/406] fix(plugin-data-viewer): CB-936 data request error close behavior --- .../src/TableViewer/TableViewer.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 96ae937989..aab3ddf99d 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -6,11 +6,12 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { PropsWithChildren, useCallback } from 'react'; import styled, { css, use } from 'reshadow'; -import { Button, Loader, Pane, ResizerControls, Split, splitStyles, TextPlaceholder, useErrorDetails, useStateDelay } from '@cloudbeaver/core-blocks'; +import { Button, Loader, Pane, ResizerControls, Split, splitStyles, TextPlaceholder, useErrorDetails, useObjectRef, useStateDelay } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; @@ -118,6 +119,12 @@ type TableViewerProps = PropsWithChildren<{ onValuePresentationChange: (id: string | null) => void; }>; +interface ErrorInfo { + error: Error | null; + display: boolean; + hide: () => void; +} + export const TableViewer = observer(function TableViewer({ tableId, resultIndex = 0, @@ -133,8 +140,17 @@ export const TableViewer = observer(function TableViewer({ const dataModel = tableViewerStorageService.get(tableId); const result = dataModel?.getResult(resultIndex); const translate = useTranslate(); + const errorInfo = useObjectRef({ + error: null, + display: false, + hide() { + this.display = false; + }, + }, {}, { + display: observable, + }); const error = useErrorDetails(dataModel?.source.error || null); - const animated = useStateDelay(!!error.details, 1); + const animated = useStateDelay(errorInfo.display, 1); const handlePresentationChange = useCallback((id: string) => { const presentation = dataPresentationService.get(id); @@ -198,6 +214,11 @@ export const TableViewer = observer(function TableViewer({ && resultExist; const loading = dataModel.isLoading(); + if (errorInfo.error !== dataModel?.source.error) { + errorInfo.error = dataModel?.source.error || null; + errorInfo.display = !!dataModel?.source.error; + } + return styled(styles)( @@ -237,10 +258,10 @@ export const TableViewer = observer(function TableViewer({ -