From 21761c85de90b665fa4430ee3dbddff7206a9176 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 6 Sep 2020 23:59:14 +0300 Subject: [PATCH 01/10] refactor: connections administration redesign CB-257 CB-258 --- .../core-blocks/src/ItemList/ListItem.tsx | 18 +- .../src/ItemList/ListItemDescription.tsx | 31 ++ .../core-blocks/src/ItemList/ListItemIcon.tsx | 29 ++ .../core-blocks/src/ItemList/ListItemName.tsx | 29 ++ .../core-blocks/src/ItemList/index.ts | 3 + .../ObjectPropertyInfoForm.tsx | 17 +- .../packages/core-bootstrap/src/manifest.ts | 4 +- .../ConnectionAccess/ConnectionAccess.tsx} | 66 ++-- .../ConnectionAccess/Controller.ts | 67 ++++ .../ConnectionForm/ConnectionForm.tsx | 223 +++++++++++++ .../Connections/ConnectionForm/Controller.ts | 177 +++++++++++ .../DriverProperties/DriverProperties.tsx | 24 +- .../DriverPropertiesController.ts | 15 +- .../ConnectionForm/EConnectionType.ts | 12 + .../ConnectionForm/IConnectionFormModel.ts | 17 + .../ConnectionForm/Options/Options.tsx | 180 +++++++++++ .../Options/OptionsController.ts | 186 +++++++++++ .../Options}/ParametersForm.tsx | 33 +- .../Options}/formStyles.ts | 0 .../Connections/ConnectionsAdministration.tsx | 36 +-- .../ConnectionsAdministrationController.ts | 56 +--- .../ConnectionsAdministrationNavService.ts | 25 ++ .../ConnectionsAdministrationService.ts | 22 +- .../ConnectionsTable/Connection.tsx | 24 +- .../ConnectionsTable/ConnectionEdit.tsx | 174 +---------- .../ConnectionEditController.ts | 294 ++---------------- .../ConnectionForm/ConnectionForm.tsx | 157 ---------- .../ConnectionForm/IFormController.ts | 28 -- .../ConnectionsTable/ConnectionsTable.tsx | 7 +- .../CreateConnection/CreateConnection.tsx | 210 +++++++++++++ .../CreateConnectionController.tsx | 118 +++++++ .../CreateConnection/CustomConnection.tsx | 42 +++ .../Connections/CreateConnection/Database.tsx | 72 +++++ .../CreateConnection/DatabaseList.tsx | 53 ++++ .../Connections/CreateConnection/Driver.tsx | 41 +++ .../CreateConnection/DriverList.tsx | 38 +++ .../CreateConnection/SearchDatabase.tsx | 43 +++ .../src/Administration/ConnectionsResource.ts | 46 +-- .../DatabaseAuthDialog/DatabaseAuthDialog.tsx | 3 +- .../src/DriverSelectDialog/Driver.tsx | 12 +- .../DriverSelectDialog/DriverSelectDialog.tsx | 5 +- webapp/packages/core-connections/src/index.ts | 1 + .../core-connections/src/locales/en.ts | 2 + .../core-connections/src/locales/ru.ts | 4 + .../ConnectionForm/ConnectionForm.tsx | 3 +- .../DriverSelectorDialog/Driver.tsx | 12 +- .../src/ConnectionDialog/ConnectionDialog.tsx | 3 +- 47 files changed, 1782 insertions(+), 880 deletions(-) create mode 100644 webapp/packages/core-blocks/src/ItemList/ListItemDescription.tsx create mode 100644 webapp/packages/core-blocks/src/ItemList/ListItemIcon.tsx create mode 100644 webapp/packages/core-blocks/src/ItemList/ListItemName.tsx rename webapp/packages/core-connections/src/Administration/Connections/{ConnectionsTable/GrantedSubjects.tsx => ConnectionForm/ConnectionAccess/ConnectionAccess.tsx} (55%) create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/Controller.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Controller.ts rename webapp/packages/core-connections/src/Administration/Connections/{ConnectionsTable => ConnectionForm}/DriverProperties/DriverProperties.tsx (81%) rename webapp/packages/core-connections/src/Administration/Connections/{ConnectionsTable => ConnectionForm}/DriverProperties/DriverPropertiesController.ts (88%) create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/EConnectionType.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/IConnectionFormModel.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts rename webapp/packages/core-connections/src/Administration/Connections/{ConnectionsTable/ConnectionForm => ConnectionForm/Options}/ParametersForm.tsx (73%) rename webapp/packages/core-connections/src/Administration/Connections/{ConnectionsTable/ConnectionForm => ConnectionForm/Options}/formStyles.ts (100%) create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationNavService.ts delete mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx delete mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CustomConnection.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DatabaseList.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx diff --git a/webapp/packages/core-blocks/src/ItemList/ListItem.tsx b/webapp/packages/core-blocks/src/ItemList/ListItem.tsx index acc47a299c..a4903e9904 100644 --- a/webapp/packages/core-blocks/src/ItemList/ListItem.tsx +++ b/webapp/packages/core-blocks/src/ItemList/ListItem.tsx @@ -11,31 +11,23 @@ import styled from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; -import { StaticImage } from '../StaticImage'; import { Styles } from './styles'; -type ListItemProps = { - name?: string; - icon?: string; - description?: string; +type Props = React.PropsWithChildren<{ onClick(): void; className?: string; -} +}> export function ListItem({ - name, - icon, - description, + children, onClick, className, -}: ListItemProps) { +}: Props) { const styles = useContext(Styles); return styled(useStyles(...styles))( - - {name} - {description} + {children} ); } diff --git a/webapp/packages/core-blocks/src/ItemList/ListItemDescription.tsx b/webapp/packages/core-blocks/src/ItemList/ListItemDescription.tsx new file mode 100644 index 0000000000..9a66956836 --- /dev/null +++ b/webapp/packages/core-blocks/src/ItemList/ListItemDescription.tsx @@ -0,0 +1,31 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useContext } from 'react'; +import styled from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { Styles } from './styles'; + +type Props = React.PropsWithChildren<{ + title?: string; + className?: string; +}> + +export function ListItemDescription({ + title, + children, + className, +}: Props) { + const styles = useContext(Styles); + + return styled(useStyles(...styles))( + {children} + ); +} diff --git a/webapp/packages/core-blocks/src/ItemList/ListItemIcon.tsx b/webapp/packages/core-blocks/src/ItemList/ListItemIcon.tsx new file mode 100644 index 0000000000..45ebec2ae0 --- /dev/null +++ b/webapp/packages/core-blocks/src/ItemList/ListItemIcon.tsx @@ -0,0 +1,29 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useContext } from 'react'; +import styled from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { Styles } from './styles'; + +type Props = React.PropsWithChildren<{ + className?: string; +}> + +export function ListItemIcon({ + children, + className, +}: Props) { + const styles = useContext(Styles); + + return styled(useStyles(...styles))( + {children} + ); +} diff --git a/webapp/packages/core-blocks/src/ItemList/ListItemName.tsx b/webapp/packages/core-blocks/src/ItemList/ListItemName.tsx new file mode 100644 index 0000000000..dcea9c03c7 --- /dev/null +++ b/webapp/packages/core-blocks/src/ItemList/ListItemName.tsx @@ -0,0 +1,29 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useContext } from 'react'; +import styled from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { Styles } from './styles'; + +type Props = React.PropsWithChildren<{ + className?: string; +}> + +export function ListItemName({ + children, + className, +}: Props) { + const styles = useContext(Styles); + + return styled(useStyles(...styles))( + {children} + ); +} diff --git a/webapp/packages/core-blocks/src/ItemList/index.ts b/webapp/packages/core-blocks/src/ItemList/index.ts index ce304189a8..3b21e4bfc6 100644 --- a/webapp/packages/core-blocks/src/ItemList/index.ts +++ b/webapp/packages/core-blocks/src/ItemList/index.ts @@ -1,4 +1,7 @@ export * from './ItemList'; export * from './ItemListSearch'; export * from './ListItem'; +export * from './ListItemDescription'; +export * from './ListItemIcon'; +export * from './ListItemName'; export { ITEM_LIST_STYLES } from './styles'; diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx index a1cec901d9..fd93a59551 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx @@ -7,7 +7,6 @@ */ import { observer } from 'mobx-react'; -import { useCallback } from 'react'; import styled from 'reshadow'; import { InputField } from '@cloudbeaver/core-blocks'; @@ -19,8 +18,7 @@ import { formStyles } from './formStyles'; type Props = { properties: ObjectPropertyInfo[] | undefined; credentials: Record; - processing: boolean; - prefix?: string; + disabled?: boolean; autofillToken?: string; className?: string; } @@ -30,14 +28,10 @@ const RESERVED_KEYWORDS = ['no', 'off', 'new-password']; export const ObjectPropertyInfoForm = observer(function ObjectPropertyInfoForm({ properties, credentials, - processing, - prefix = '', + disabled, autofillToken = '', className, }: Props) { - const handleChange = useCallback((key: string, value: string) => { - credentials[key] = value; - }, [credentials]); if (!properties || properties.length === 0) { return styled(useStyles(formStyles))(
Properties empty
); @@ -49,10 +43,9 @@ export const ObjectPropertyInfoForm = observer(function ObjectPropertyInfoForm({ handleChange(property.id!, value)} - disabled={processing} + name={property.id!} + state={credentials} + disabled={disabled} autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`} mod='surface' > diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index f3d0ff4f45..5fc38ddd6a 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -39,7 +39,8 @@ import { ConnectionsAdministrationService, ConnectionsResource, ConnectionsLocaleService, - DriverPropertiesService + DriverPropertiesService, + ConnectionsAdministrationNavService } from '@cloudbeaver/core-connections'; import { PluginManifest } from '@cloudbeaver/core-di'; import { CommonDialogService, ContextMenuService, SessionExpireService } from '@cloudbeaver/core-dialogs'; @@ -124,6 +125,7 @@ export const coreManifest: PluginManifest = { NavigationTabsService, DatabaseAuthModelsResource, ConnectionAuthService, + ConnectionsAdministrationNavService, ConnectionsAdministrationService, ConnectionsResource, NavigationTreeContextMenuService, diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/GrantedSubjects.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx similarity index 55% rename from webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/GrantedSubjects.tsx rename to webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index 6cbcc776b1..c955766170 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/GrantedSubjects.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -13,12 +13,13 @@ import styled, { css } from 'reshadow'; import { Table, TableHeader, TableColumnHeader, TableBody, TableItem, TableColumnValue, TableItemSelect } from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { - AdminSubjectType, AdminConnectionGrantInfo, AdminUserInfo, AdminRoleInfo -} from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { IConnectionFormModel } from '../IConnectionFormModel'; +import { Controller } from './Controller'; + const styles = composes( css` box { @@ -39,66 +40,65 @@ const styles = composes( ); type Props = { - grantedSubjects: AdminConnectionGrantInfo[]; - users: AdminUserInfo[]; - roles: AdminRoleInfo[]; - selectedSubjects: Map; + model: IConnectionFormModel; disabled: boolean; onChange?: () => void; className?: string; } -export const GrantedSubjects = observer(function GrantedSubjects({ - grantedSubjects, - users, - roles, - selectedSubjects, +export const ConnectionAccess = observer(function ConnectionAccess({ + model, disabled, onChange, className, }: Props) { - const translate = useTranslate(); - const getSubjectPermission = useCallback((subjectId: string) => grantedSubjects - ?.find(subjectPermission => subjectPermission.subjectId === subjectId), [grantedSubjects]); + if (!model.grantedSubjects) { + return null; + } - if (users.length === 0 && roles.length) { + const controller = useController(Controller, model); + const translate = useTranslate(); + + if (controller.users.length === 0 && controller.roles.length) { return styled(useStyles(styles))(
{translate('authentication_administration_user_connections_empty')}
); } + const handleSelect = useCallback((item: string, state: boolean) => { + controller.onSelect(item, state); + if (onChange) { + onChange(); + } + }, [onChange, controller]); + return styled(useStyles(styles))( - +
{translate('connections_connection_name')} - {roles.map(role => ( + {controller.roles.map(role => ( - + {role.roleName} ))} - {users.map((user) => { - const connectionPermission = getSubjectPermission(user.userId); - const isRoleProvided = connectionPermission?.subjectType === AdminSubjectType.Role; - - return ( - - - - - {user.userId} - - - ); - })} + {controller.users.map(user => ( + + + + + {user.userId} + + + ))}
diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/Controller.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/Controller.ts new file mode 100644 index 0000000000..9129b0816c --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionAccess/Controller.ts @@ -0,0 +1,67 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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, observable } from 'mobx'; + +import { UsersResource, RolesResource } from '@cloudbeaver/core-authentication'; +import { injectable, IInitializableController } from '@cloudbeaver/core-di'; +import { AdminSubjectType } from '@cloudbeaver/core-sdk'; + +import { IConnectionFormModel } from '../IConnectionFormModel'; + +@injectable() +export class Controller +implements IInitializableController { + @observable selectedSubjects: Map = new Map(); + + @computed get users() { + return Array.from(this.usersResource.data.values()) + .filter(user => !this.usersResource.isNew(user.userId)); + } + + @computed get roles() { + return Array.from(this.rolesResource.data.values()); + } + + private model!: IConnectionFormModel + + constructor( + private usersResource: UsersResource, + private rolesResource: RolesResource, + ) { } + + init(model: IConnectionFormModel) { + this.model = model; + this.loadSubjects(); + } + + onSelect = (subjectId: string, state: boolean) => { + if (!state) { + const index = this.model.grantedSubjects!.findIndex(subject => subject.subjectId === subjectId); + if (index > -1) { + this.model.grantedSubjects!.splice(index, 1); + } + return; + } + + this.model.grantedSubjects!.push({ + connectionId: '', + subjectId, + subjectType: AdminSubjectType.User, + }); + } + + private async loadSubjects() { + await this.usersResource.loadAll(); + await this.rolesResource.loadAll(); + + for (const subject of this.model.grantedSubjects!) { + this.selectedSubjects.set(subject.subjectId, true); + } + } +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx new file mode 100644 index 0000000000..c0bfb51ed0 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx @@ -0,0 +1,223 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import styled, { css } from 'reshadow'; + +import { + TabsState, TabList, Tab, + TabTitle, Loader, TabPanel, + ErrorMessage, Button +} from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { ConnectionAccess } from './ConnectionAccess/ConnectionAccess'; +import { Controller } from './Controller'; +import { DriverProperties } from './DriverProperties/DriverProperties'; +import { IConnectionFormModel } from './IConnectionFormModel'; +import { Options } from './Options/Options'; + +const styles = composes( + css` + Tab { + composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; + } + + ErrorMessage { + composes: theme-background-secondary from global; + } + + TabList { + composes: theme-background-surface theme-text-on-surface from global; + } + + box { + composes: theme-background-secondary theme-text-on-secondary from global; + } + + content-box { + composes: theme-background-secondary theme-border-color-background from global; + } + + GrantedSubjects { + composes: theme-background-surface from global; + } + `, + css` + box { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: auto; + } + content-box { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + } + + SubmittingForm { + flex: 1; + display: flex; + flex-direction: column; + } + + TabList { + align-items: center; + box-sizing: border-box; + display: inline-flex; + width: 100%; + padding-left: 24px; + outline: none; + } + + TabPanel { + overflow: auto !important; + } + + Tab { + composes: theme-typography--body2 from global; + text-transform: uppercase; + font-weight: normal; + + &:global([aria-selected=true]) { + font-weight: normal !important; + } + + & TabTitle { + padding: 0 24px !important; + } + } + + ErrorMessage { + position: sticky; + bottom: 0; + padding: 8px 24px; + } + + fill { + flex: 1; + } + + SubmittingForm, Loader { + min-height: 320px; + max-height: 500px; + } + + Button:not(:first-child) { + margin-right: 24px; + } + ` +); + +type Props = { + model: IConnectionFormModel; + configurationWizard?: boolean; + onCancel?(): void; +} + +export const ConnectionForm = observer(function ConnectionForm({ + model, + configurationWizard, + onCancel = () => {}, +}: Props) { + const controller = useController(Controller, model, onCancel); + const translate = useTranslate(); + + return styled(useStyles(styles))( + + + + + {translate('customConnection_options')} + + + {translate('customConnection_properties')} + + + {translate('connections_connection_edit_access')} + + + + + + + {controller.isLoading + ? + : ( + <> + + + + {model.connection.driverId && ( + + {state => ( + + )} + + )} + + {(state) => { + if (state.selectedId === 'access') { + controller.loadAccessSubjects(); + } + + return ( + + ); + }} + + + ) + } + {controller.error.responseMessage && ( + + )} + + + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Controller.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Controller.ts new file mode 100644 index 0000000000..dbcd980f6d --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Controller.ts @@ -0,0 +1,177 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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, computed } from 'mobx'; + +import { + injectable, IInitializableController, IDestructibleController +} from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; +import { ConnectionConfig, GQLErrorCatcher } from '@cloudbeaver/core-sdk'; + +import { DBDriverResource } from '../../../DBDriverResource'; +import { ConnectionsResource } from '../../ConnectionsResource'; +import { EConnectionType } from './EConnectionType'; +import { IConnectionFormModel } from './IConnectionFormModel'; + +@injectable() +export class Controller +implements IInitializableController, IDestructibleController { + @observable connectionType = EConnectionType.Parameters; + @observable isLoading = false; + @observable isSaving = false; + + @computed get isDisabled() { + return this.isLoading || this.isSaving; + } + + /** It will be loaded by options controller */ + @computed get driver() { + return this.dbDriverResource.get(this.model.connection.driverId) || null; + } + + readonly error = new GQLErrorCatcher(); + + @computed private get accessLoaded() { + return !!this.model.grantedSubjects; + } + + private accessChanged = false; + private isDistructed = false; + private model!: IConnectionFormModel; + private close!: () => void; + + constructor( + private connectionsResource: ConnectionsResource, + private notificationService: NotificationService, + private commonDialogService: CommonDialogService, + private dbDriverResource: DBDriverResource, + ) { } + + init( + model: IConnectionFormModel, + close: () => void + ) { + this.model = model; + this.close = close; + } + + destruct(): void { + this.isDistructed = true; + } + + setType = (type: EConnectionType) => { + this.connectionType = type; + } + + save = async () => { + this.isSaving = true; + this.error.clear(); + try { + if (this.model.editing) { + const connection = await this.connectionsResource.update(this.model.connection.id, this.getConnectionConfig()); + await this.saveSubjectPermissions(connection.id); + + this.notificationService.logInfo({ title: `Connection ${connection.name} updated` }); + } else { + const connection = await this.connectionsResource.create(this.getConnectionConfig()); + await this.saveSubjectPermissions(connection.id); + this.close(); + this.notificationService.logInfo({ title: `Connection ${connection.name} created` }); + } + } catch (exception) { + this.showError(exception, 'Failed to create connection'); + } finally { + this.isSaving = false; + } + } + + onShowDetails = () => { + if (this.error.exception) { + this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); + } + } + + handleAccessChange = () => this.accessChanged = true; + + loadAccessSubjects = async () => { + if (this.accessLoaded || this.isLoading) { + return; + } + + this.isLoading = true; + try { + this.model.grantedSubjects = await this.connectionsResource.loadAccessSubjects(this.model.connection.id); + + } catch (exception) { + this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); + } + this.isLoading = false; + } + + private async saveSubjectPermissions(connectionId: string) { + if (!this.accessChanged || !this.model.grantedSubjects) { + return; + } + await this.connectionsResource.setAccessSubjects( + connectionId, + this.model.grantedSubjects.map(subject => subject.subjectId) + ); + this.accessChanged = false; + } + + private getConnectionConfig(): ConnectionConfig { + const config: ConnectionConfig = {}; + config.name = this.model.connection.name; + config.description = this.model.connection.description; + config.template = this.model.connection.template; + config.driverId = this.model.connection.driverId; + + if (this.connectionType === EConnectionType.Parameters) { + if (!this.driver?.embedded) { + config.host = this.model.connection.host; + config.port = this.model.connection.port; + } + config.databaseName = this.model.connection.databaseName; + } else { + config.url = this.model.connection.url; + } + if (this.model.connection.authModel) { + config.authModelId = this.model.connection.authModel; + config.saveCredentials = this.isCredentialsChanged(); + if (config.saveCredentials) { + config.credentials = this.model.credentials; + } + } + if (Object.keys(this.model.connection.properties).length > 0) { + config.properties = this.model.connection.properties; + } + + return config; + } + + private isCredentialsChanged() { + if (!this.model.connection.authProperties.length) { + return true; + } + for (const property of this.model.connection.authProperties) { + if (this.model.credentials[property.id!] !== property.value) { + return true; + } + } + return false; + } + + private showError(exception: Error, message: string) { + if (!this.error.catch(exception) || this.isDistructed) { + this.notificationService.logException(exception, message); + } + } +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx similarity index 81% rename from webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx rename to webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx index e419db45c4..9049b630e9 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -7,14 +7,13 @@ */ import { observer } from 'mobx-react'; -import { useEffect } from 'react'; +import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { Loader, PropertiesTable } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; -import { DBDriver } from '../../../../DBDriverResource'; import { DriverPropertiesController } from './DriverPropertiesController'; const styles = css` @@ -24,35 +23,34 @@ const styles = css` flex-direction: column; overflow: auto; } + center { + margin: auto; + } `; -type DriverPropertyState = { - [key: string]: string; -} - type DriverPropertiesProps = { - driver: DBDriver; - state: DriverPropertyState; + driverId: string; + state: Record; loadProperties: boolean; } export const DriverProperties = observer(function DriverProperties({ - driver, + driverId, state, loadProperties, }: DriverPropertiesProps) { - const controller = useController(DriverPropertiesController, driver); + const controller = useController(DriverPropertiesController, driverId); - useEffect(() => { + useMemo(() => { if (loadProperties) { controller.loadDriverProperties(); } - }, [loadProperties]); + }, [loadProperties, controller]); return styled(useStyles(styles))( {controller.isLoading && } - {!controller.isLoading && ( + {!controller.isLoading && controller.loaded && ( ([]) + @observable driverId!: string - private loaded = false; + loaded = false; constructor( private driverPropertiesService: DriverPropertiesService, private notificationService: NotificationService ) { } - init(driver: DBDriver) { - this.driver = driver; + init(driverId: string) { + this.driverId = driverId; } onAddProperty = () => { @@ -60,7 +55,7 @@ export class DriverPropertiesController implements IInitializableController { } this.isLoading = true; try { - const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driver.id); + const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driverId); this.driverProperties = observable(driverProperties.map(property => ({ id: property.id!, key: property.id!, diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/EConnectionType.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/EConnectionType.ts new file mode 100644 index 0000000000..105b8725f3 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/EConnectionType.ts @@ -0,0 +1,12 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 enum EConnectionType { + Parameters = 'parameters', + URL = 'url' +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/IConnectionFormModel.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/IConnectionFormModel.ts new file mode 100644 index 0000000000..765871d6b9 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/IConnectionFormModel.ts @@ -0,0 +1,17 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { ConnectionInfo, AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk'; + +export interface IConnectionFormModel { + connection: ConnectionInfo; + credentials: Record; + grantedSubjects: AdminConnectionGrantInfo[] | null; + availableDrivers: string[]; + editing?: boolean; +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx new file mode 100644 index 0000000000..ff7c20facf --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx @@ -0,0 +1,180 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import styled, { css } from 'reshadow'; + +import { + Radio, + InputField, + useFocus, + ObjectPropertyInfoForm, + Checkbox, + Textarea, + InputGroup, + RadioGroup, + TabsState, + TabPanel, + Combobox, + SubmittingForm +} from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { ConnectionInfo } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { EConnectionType } from '../EConnectionType'; +import { formStyles } from './formStyles'; +import { OptionsController } from './OptionsController'; +import { ParametersForm } from './ParametersForm'; + +type Props = { + connection: ConnectionInfo; + type: EConnectionType; + credentials: Record; + availableDrivers: string[]; + saving?: boolean; + disabled?: boolean; + editing?: boolean; + onTypeChange(type: EConnectionType): void; + onSave?(): void; +} + +const styles = css` + SubmittingForm { + display: flex; + flex-direction: column; + flex: 1; + } + box { + flex: 1; + display: flex; + flex-wrap: wrap; + } + box-element { + min-width: 450px; + } + TabPanel { + flex-direction: column; + } +`; + +export const Options = observer(function Options({ + connection, + type, + availableDrivers, + credentials, + disabled, + saving, + editing, + onTypeChange, + onSave, +}: Props) { + const controller = useController(OptionsController, connection, credentials, availableDrivers); + const translate = useTranslate(); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + return styled(useStyles(styles, formStyles))( + + + + + + + + driver.id} + valueSelector={driver => driver?.name!} + onSelect={controller.onSelectDriver} + readOnly={editing || controller.drivers.length < 2} + mod={'surface'} + > + {translate('connections_connection_driver')} + + + + + {translate('connections_connection_name')} + + + + + + + + + + + {translate('customConnection_connectionType_custom')} + + + {translate('customConnection_connectionType_url')} + + + + + + + + + + + {translate('customConnection_url_JDBC')} + + + + + {controller.authModel && ( + <> + + {translate('connections_connection_edit_authentication')} + + + + )} + + + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts new file mode 100644 index 0000000000..1629e18491 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts @@ -0,0 +1,186 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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, action, computed } from 'mobx'; + +import { injectable, IInitializableController } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ConnectionInfo } from '@cloudbeaver/core-sdk'; + +import { DatabaseAuthModelsResource } from '../../../../DatabaseAuthModelsResource'; +import { DBDriverResource } from '../../../../DBDriverResource'; + +@injectable() +export class OptionsController +implements IInitializableController { + @observable credentials!: Record; + @observable availableDrivers!: string[]; + + @computed get drivers() { + return Array.from(this.dbDriverResource.data.values()) + .filter(({ id }) => this.availableDrivers.includes(id)); + } + + @computed get driver() { + return this.dbDriverResource.get(this.connectionInfo.driverId); + } + + @computed get authModel() { + if (!this.connectionInfo?.authModel && !this.driver) { + return null; + } + return this.dbAuthModelsResource.get(this.connectionInfo?.authModel || this.driver!.defaultAuthModel) || null; + } + + @computed get authModelLoading() { + return this.dbAuthModelsResource.isLoading(); + } + + private connectionInfo!: ConnectionInfo; + private nameTemplate = /^.*?\s(|\(.*?\)\s)connection$/ + + constructor( + private notificationService: NotificationService, + private dbAuthModelsResource: DatabaseAuthModelsResource, + private dbDriverResource: DBDriverResource, + ) { } + + init(connection: ConnectionInfo, credentials: Record, availableDrivers: string[]) { + this.connectionInfo = connection; + this.credentials = credentials; + this.availableDrivers = availableDrivers; + this.loadDrivers(); + } + + onSelectDriver = (driverId: string | null, name: 'driverId', prevValue: string | null) => this.loadDriver(driverId, prevValue); + onFormChange = () => this.updateName(); + + @action + private setDefaults(prevDriverId: string | null) { + this.setDefaultParameters(prevDriverId); + this.connectionInfo.properties = {}; + this.connectionInfo.authModel = this.driver?.defaultAuthModel; + this.cleanCredentials(); + } + + private cleanCredentials() { + for (const property of Object.keys(this.credentials)) { + delete this.credentials[property]; + } + } + + private setDefaultParameters(prevDriverId?: string | null) { + const prevDriver = this.dbDriverResource.get(prevDriverId || ''); + + if (this.connectionInfo.host === prevDriver?.defaultServer) { + this.connectionInfo.host = this.driver?.defaultServer; + } + + if (this.connectionInfo.port === prevDriver?.defaultPort) { + this.connectionInfo.port = this.driver?.defaultPort; + } + + if (this.connectionInfo.databaseName === prevDriver?.defaultDatabase) { + this.connectionInfo.databaseName = this.driver?.defaultDatabase; + } + + if (this.connectionInfo.url === prevDriver?.sampleURL) { + this.connectionInfo.url = this.driver?.sampleURL; + } + + this.updateName(); + } + + private updateName() { + const databaseNames = ['New', ...this.drivers.map(driver => driver.name!)] + .filter(Boolean); + + if (!this.connectionInfo.name + || (this.nameTemplate.test(this.connectionInfo.name) + && databaseNames.some(driver => this.connectionInfo.name.startsWith(driver))) + ) { + this.connectionInfo.name = this.getNameTemplate(); + } + } + + private getNameTemplate() { + if (this.driver) { + let address = [this.connectionInfo.host, this.connectionInfo.host && this.connectionInfo.port] + .filter(Boolean) + .join(':'); + + if (address) { + address = ` (${address})`; + } + + return `${this.driver.name}${address} connection`; + } + + return 'New connection'; + } + + private async loadDrivers() { + try { + await this.dbDriverResource.loadAll(); + this.setDefaultParameters(); + + if (!this.driver || this.driver.anonymousAccess) { + return; + } + + try { + await this.dbAuthModelsResource.load( + this.connectionInfo?.authModel || this.driver.defaultAuthModel + ); + + if (this.authModel) { + for (const property of this.connectionInfo.authProperties) { + this.credentials[property.id!] = property.value; + } + } + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load driver auth model'); + } + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load drivers'); + } + } + + private async loadDriver(driverId: string | null, prev: string | null) { + if (!driverId) { + this.connectionInfo.authModel = undefined; + this.cleanCredentials(); + return; + } + + try { + await this.dbDriverResource.load(driverId); + this.setDefaults(prev); + } catch (exception) { + this.notificationService.logException(exception, `Can't load driver ${driverId}`); + } + + if (!this.driver || this.driver.anonymousAccess) { + return; + } + + try { + await this.dbAuthModelsResource.load( + this.connectionInfo?.authModel || this.driver.defaultAuthModel + ); + + if (this.authModel) { + for (const property of this.connectionInfo.authProperties) { + this.credentials[property.id!] = property.value; + } + } + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load driver auth model'); + } + } +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/ParametersForm.tsx similarity index 73% rename from webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.tsx rename to webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/ParametersForm.tsx index 562e07b503..c2a73ce15c 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/ParametersForm.tsx @@ -7,27 +7,35 @@ */ import { observer } from 'mobx-react'; -import styled, { use } from 'reshadow'; +import styled, { use, css } from 'reshadow'; import { InputField } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; +import { ConnectionInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; import { formStyles } from './formStyles'; -import { IFormController } from './IFormController'; type ParametersFormProps = { - controller: IFormController; + connection: ConnectionInfo; + disabled?: boolean; embedded?: boolean; } +const styles = css` + layout-grid-inner { + max-width: 650px; + } +`; + export const ParametersForm = observer(function ParametersForm({ - controller, + connection, embedded, + disabled, }: ParametersFormProps) { const translate = useTranslate(); - return styled(useStyles(formStyles))( + return styled(useStyles(formStyles, styles))( <> { !embedded && ( @@ -35,9 +43,8 @@ export const ParametersForm = observer(function ParametersForm({ controller.onChange('host', value)} - disabled={controller.isSaving} + state={connection} + disabled={disabled} mod='surface' > {translate('customConnection_custom_host')} @@ -48,9 +55,8 @@ export const ParametersForm = observer(function ParametersForm({ controller.onChange('port', value)} - disabled={controller.isSaving} + state={connection} + disabled={disabled} {...use({ short: true })} mod='surface' > @@ -63,9 +69,8 @@ export const ParametersForm = observer(function ParametersForm({ controller.onChange('databaseName', value)} - disabled={controller.isSaving} + state={connection} + disabled={disabled} mod='surface' > {translate('customConnection_custom_database')} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/formStyles.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/formStyles.ts similarity index 100% rename from webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/formStyles.ts rename to webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/formStyles.ts diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx index d04396816b..782baa80b4 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx @@ -10,14 +10,13 @@ import { observer } from 'mobx-react'; import styled, { css, use } from 'reshadow'; import { AdministrationTools, AdministrationItemContentProps } from '@cloudbeaver/core-administration'; -import { Loader, IconButton, Button } from '@cloudbeaver/core-blocks'; +import { Loader, IconButton } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; -import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { ConnectionsAdministrationController } from './ConnectionsAdministrationController'; import { ConnectionsTable } from './ConnectionsTable/ConnectionsTable'; -import { DatabasesSearch } from './DatabasesSearch'; +import { CreateConnection } from './CreateConnection/CreateConnection'; const styles = composes( css` @@ -64,46 +63,31 @@ const styles = composes( ); export const ConnectionsAdministration = observer(function ConnectionsAdministration({ + sub, + param, configurationWizard, }: AdministrationItemContentProps) { - const translate = useTranslate(); const controller = useController(ConnectionsAdministrationController); - if (configurationWizard && !controller.isSearching) { - controller.findDatabase(); - controller.search(); - } - return styled(useStyles(styles))( - - - - {controller.isSearching && ( - )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts index d55ee65ca5..d9e03c5135 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts @@ -14,24 +14,16 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk'; -import { DriverSelectDialog } from '../../DriverSelectDialog/DriverSelectDialog'; import { ConnectionsResource, isSearchedConnection } from '../ConnectionsResource'; +import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService'; @injectable() export class ConnectionsAdministrationController { - @observable hosts = 'localhost'; @observable isProcessing = false; - @observable isSearching = false; readonly selectedItems = observable(new Map()) readonly expandedItems = observable(new Map()) readonly error = new GQLErrorCatcher(); - @computed - get findConnections() { - return Array.from(this.connectionsResource.data.values()) - .filter(isSearchedConnection); - } - @computed get connections() { return Array.from(this.connectionsResource.data.values()) @@ -61,51 +53,13 @@ export class ConnectionsAdministrationController { private notificationService: NotificationService, private connectionsResource: ConnectionsResource, private commonDialogService: CommonDialogService, + private connectionsAdministrationNavService: ConnectionsAdministrationNavService ) { } - create = async () => { - const driverId = await this.commonDialogService.open(DriverSelectDialog, null); - if (!driverId) { - return; - } + setCreateMethod = (method: string) => this.connectionsAdministrationNavService.navToCreate(method); + cancelCreate = () => this.connectionsAdministrationNavService.navToRoot(); - const connectionInfo = this.connectionsResource.addNew(driverId); - this.expandedItems.set(connectionInfo.id, true); - } - - findDatabase = () => { - this.isSearching = !this.isSearching; - } - - search = async () => { - if (this.isProcessing || !this.hosts || !this.hosts.trim()) { - return; - } - - this.isProcessing = true; - for (const connection of this.findConnections) { - this.expandedItems.delete(connection.id); - } - - try { - const hosts = this.hosts - .trim() - .replace(/[\s,|+-]+/gm, ' ') - .split(/[\s,|+-]/); - - await this.connectionsResource.searchDatabases(hosts); - } catch (exception) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'Databases search failed'); - } - } finally { - this.isProcessing = false; - } - } - - onSearchChange = (hosts: string) => { - this.hosts = hosts; - } + create = () => this.connectionsAdministrationNavService.navToCreate('driver'); update = async () => { try { diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationNavService.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationNavService.ts new file mode 100644 index 0000000000..9a80307e9a --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationNavService.ts @@ -0,0 +1,25 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 } from '@cloudbeaver/core-administration'; +import { injectable } from '@cloudbeaver/core-di'; + +@injectable() +export class ConnectionsAdministrationNavService { + constructor( + private administrationScreenService: AdministrationScreenService, + ) { } + + navToRoot() { + this.administrationScreenService.navigateToItem('connections'); + } + + navToCreate(method: string) { + this.administrationScreenService.navigateToItemSub('connections', 'create', method); + } +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts index dc98dd51d1..2cdf222e7d 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { AdministrationItemService, AdministrationScreenService, AdministrationItemType } from '@cloudbeaver/core-administration'; +import { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -19,7 +19,6 @@ import { ConnectionsDrawerItem } from './ConnectionsDrawerItem'; export class ConnectionsAdministrationService extends Bootstrap { constructor( private administrationItemService: AdministrationItemService, - private administrationScreenService: AdministrationScreenService, private notificationService: NotificationService, private connectionsResource: ConnectionsResource, private dbDriverResource: DBDriverResource, @@ -33,8 +32,15 @@ export class ConnectionsAdministrationService extends Bootstrap { type: AdministrationItemType.Default, order: 2, configurationWizardOptions: { + defaultRoute: { sub: 'create', param: 'search-database' }, description: 'connections_administration_configuration_wizard_step_description', }, + sub: [ + { + name: 'create', + getComponent: () => ConnectionsAdministration, + }, + ], getContentComponent: () => ConnectionsAdministration, getDrawerComponent: () => ConnectionsDrawerItem, onActivate: this.loadConnections.bind(this), @@ -43,18 +49,6 @@ export class ConnectionsAdministrationService extends Bootstrap { load(): void | Promise { } - navToRoot() { - this.administrationScreenService.navigateToItem('connections'); - } - - navToCreate() { - this.administrationScreenService.navigateToItemSub('connections', 'create'); - } - - navToEdit(userId: string) { - this.administrationScreenService.navigateToItemSub('connections', 'edit', userId); - } - private async loadConnections() { try { await this.connectionsResource.loadAll(); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/Connection.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/Connection.tsx index e4ae610cab..3cba614b73 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/Connection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/Connection.tsx @@ -40,20 +40,8 @@ const styles = css` `; export const Connection = observer(function Connection({ connection }: Props) { - const translate = useTranslate(); - const connectionInfoResource = useService(ConnectionsResource); const driversResource = useService(DBDriverResource); - let drivers = [connection.driverId]; - - if (isSearchedConnection(connection)) { - drivers = connection[SEARCH_CONNECTION_SYMBOL].possibleDrivers; - } - - const icons = drivers - .map(driverId => driversResource.get(driverId)?.icon) - .filter(Boolean); - - const isNew = connectionInfoResource.isNew(connection.id); + const icon = driversResource.get(connection.driverId)?.icon; return styled(useStyles(styles))( @@ -64,18 +52,12 @@ export const Connection = observer(function Connection({ connection }: Props) { - {icons.map(icon => )} + {connection.name} {connection.host}{connection.host && connection.port && `:${connection.port}`} - - {isNew && ( - - {translate('ui_tag_new')} - - )} - + ); }); 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 ed02e1e33c..e805803c5b 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -7,107 +7,37 @@ */ import { observer } from 'mobx-react'; -import { useState, useContext, useCallback } from 'react'; -import styled, { css, use } from 'reshadow'; +import { useContext, useCallback } from 'react'; +import styled, { css } from 'reshadow'; -import { - TabsState, TabList, Tab, - TabTitle, Loader, SubmittingForm, TabPanel, - ErrorMessage, Button, TableItemContext, - TableContext -} from '@cloudbeaver/core-blocks'; -import { useService, useController } from '@cloudbeaver/core-di'; -import { useTranslate } from '@cloudbeaver/core-localization'; +import { TableContext } from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; import { useStyles, composes } from '@cloudbeaver/core-theming'; -import { ConnectionsResource } from '../../ConnectionsResource'; +import { ConnectionForm } from '../ConnectionForm/ConnectionForm'; +import { IConnectionFormModel } from '../ConnectionForm/IConnectionFormModel'; import { ConnectionEditController } from './ConnectionEditController'; -import { ConnectionForm } from './ConnectionForm/ConnectionForm'; -import { DriverProperties } from './DriverProperties/DriverProperties'; -import { GrantedSubjects } from './GrantedSubjects'; const styles = composes( css` - Tab { - composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; - } - - ErrorMessage { - composes: theme-background-secondary from global; - } - - TabList { - composes: theme-background-surface theme-text-on-surface from global; - } - box { composes: theme-background-secondary theme-text-on-secondary from global; } - - content-box { - composes: theme-background-secondary theme-border-color-background from global; - } - - GrantedSubjects { - composes: theme-background-surface from global; - } `, css` box { padding: 24px; - } - - SubmittingForm { - flex: 1; + min-height: 320px; + max-height: 500px; display: flex; flex-direction: column; } - TabList { - align-items: center; - box-sizing: border-box; - display: inline-flex; - width: 100%; - padding-left: 24px; - outline: none; - } - - TabPanel { - overflow: auto !important; - } - - Tab { - composes: theme-typography--body2 from global; - text-transform: uppercase; - font-weight: normal; - - &:global([aria-selected=true]) { - font-weight: normal !important; - } - - & TabTitle { - padding: 0 24px !important; - } - } - ErrorMessage { position: sticky; bottom: 0; padding: 8px 24px; } - - fill { - flex: 1; - } - - SubmittingForm, Loader { - min-height: 320px; - max-height: 500px; - } - - Button:not(:first-child) { - margin-right: 24px; - } ` ); @@ -120,89 +50,13 @@ export const ConnectionEdit = observer(function ConnectionEdit({ }: Props) { const tableContext = useContext(TableContext); const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]); - - const translate = useTranslate(); - const controller = useController(ConnectionEditController, item, collapse); - const connectionsResource = useService(ConnectionsResource); - const [loadProperties, setLoadProperties] = useState(false); - - const handleCancel = useCallback(() => { - collapse(); - if (controller.isNew) { - connectionsResource.delete(item); - } - }, [collapse]); + const controller = useController(ConnectionEditController, item); return styled(useStyles(styles))( - - - - - {translate('customConnection_options')} - - setLoadProperties(true)} disabled={!controller.driver} > - {translate('customConnection_properties')} - - - {translate('connections_connection_edit_access')} - - - - - - - {controller.isLoading - ? - : ( - - - - - {controller.driver && ( - - - - )} - - - - - ) - } - {controller.error.responseMessage && ( - - )} - - - + + {controller.connection && ( + + )} + ); }); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts index 8dbf704df1..715eb636b1 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts @@ -6,104 +6,63 @@ * you may not use this file except in compliance with the License. */ -import { observable, action, computed } from 'mobx'; +import { observable, computed } from 'mobx'; -import { UsersResource, RolesResource } from '@cloudbeaver/core-authentication'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { - ConnectionConfig, GQLErrorCatcher, DatabaseAuthModel, ConnectionInfo, AdminConnectionGrantInfo -} from '@cloudbeaver/core-sdk'; +import { GQLErrorCatcher, AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk'; -import { DatabaseAuthModelsResource } from '../../../DatabaseAuthModelsResource'; -import { DBDriver, DBDriverResource } from '../../../DBDriverResource'; -import { ConnectionsResource, isSearchedConnection, SEARCH_CONNECTION_SYMBOL } from '../../ConnectionsResource'; - -export enum ConnectionType { - Attributes, - URL -} +import { DBDriverResource } from '../../../DBDriverResource'; +import { ConnectionsResource } from '../../ConnectionsResource'; @injectable() export class ConnectionEditController implements IInitializableController, IDestructibleController { - @observable grantedSubjects: AdminConnectionGrantInfo[] = []; - @observable connectionType = ConnectionType.Attributes + @observable grantedSubjects: AdminConnectionGrantInfo[] | null = null; @observable isLoading = true; - @observable isSaving = false; - @observable driver: DBDriver | null = null; - @observable authModel: DatabaseAuthModel | null = null; - @observable config: ConnectionConfig = { - name: '', - driverId: '', - host: '', - port: '', - databaseName: '', - template: false, - url: '', - properties: {}, - credentials: {}, - }; - - @computed get users() { - return Array.from(this.usersResource.data.values()) - .filter(user => !this.usersResource.isNew(user.userId)); - } - - @computed get roles() { - return Array.from(this.rolesResource.data.values()); - } + @observable credentials: Record = {}; @computed get isDisabled() { - return this.isLoading || this.isSaving; + return this.isLoading; } - get isSearched() { - return this.connectionsResource.isSearched(this.connectionId); + @computed get driver() { + if (!this.connection?.driverId) { + return null; + } + return this.dbDriverResource.get(this.connection.driverId) || null; } - get isNew() { - return this.connectionsResource.isNew(this.connectionId); + @computed get connection() { + return this.connectionsResource.get(this.connectionId); } - get drivers() { - return Array.from(this.dbDriverResource.data.values()) - .filter(({ id }) => { - if (!isSearchedConnection(this.connectionInfo)) { - return true; - } - - return this.connectionInfo[SEARCH_CONNECTION_SYMBOL].possibleDrivers.includes(id); - }); + @computed get availableDrivers() { + if (!this.connection) { + return []; + } + return [this.connection.driverId]; } connectionId!: string; - readonly selectedSubjects = observable(new Map()) + readonly editing = true; // used as model IConnectionFormModel readonly error = new GQLErrorCatcher(); - private accessChanged = false; - private accessLoaded = false; private isDistructed = false; - private connectionInfo!: ConnectionInfo; - private collapse!: () => void; constructor( private connectionsResource: ConnectionsResource, private notificationService: NotificationService, private commonDialogService: CommonDialogService, - private dbAuthModelsResource: DatabaseAuthModelsResource, - private usersResource: UsersResource, - private rolesResource: RolesResource, private dbDriverResource: DBDriverResource, ) { } - init(id: string, collapse: () => void) { + init(id: string) { this.connectionId = id; - this.collapse = collapse; this.loadConnectionInfo(); } @@ -111,175 +70,12 @@ implements IInitializableController, IDestructibleController { this.isDistructed = true; } - onChangeType = (type: ConnectionType) => { - this.connectionType = type; - } - - onChange = (property: keyof ConnectionConfig, value: any) => { - this.config[property] = value; - - if (this.isNew) { - (this.connectionInfo as any)[property] = value; - } - } - - onSelectDriver = (driver: DBDriver | null) => { - this.driver = driver; - this.onChange('driverId', this.driver?.id); - if (driver) { - this.loadDriver(driver.id); - } else { - this.authModel = null; - } - } - - onSaveConnection = async () => { - this.isSaving = true; - this.error.clear(); - try { - if (this.isNew) { - const connection = await this.connectionsResource.create(this.getConnectionConfig(), this.connectionId); - await this.saveSubjectPermissions(connection.id); - this.collapse(); - this.notificationService.logInfo({ title: `Connection ${connection.name} created` }); - } else { - const connection = await this.connectionsResource.update(this.connectionId, this.getConnectionConfig()); - await this.saveSubjectPermissions(connection.id); - - this.notificationService.logInfo({ title: `Connection ${connection.name} updated` }); - } - } catch (exception) { - this.showError(exception, 'Failed to create connection'); - } finally { - this.isSaving = false; - } - } - onShowDetails = () => { if (this.error.exception) { this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); } } - handleAccessChange = () => this.accessChanged = true; - - loadAccessSubjects = async () => { - if (this.accessLoaded || this.isLoading) { - return; - } - - this.isLoading = true; - try { - await this.usersResource.loadAll(); - await this.rolesResource.loadAll(); - - this.grantedSubjects = await this.connectionsResource.loadAccessSubjects(this.connectionId); - - for (const subject of this.grantedSubjects) { - this.selectedSubjects.set(subject.subjectId, true); - } - this.accessLoaded = true; - } catch (exception) { - this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); - } - this.isLoading = false; - } - - private getGrantedSubjects() { - return Array.from(this.selectedSubjects.keys()) - .filter(connectionId => this.selectedSubjects.get(connectionId)); - } - - private async saveSubjectPermissions(connectionId: string) { - if (!this.accessChanged) { - return; - } - await this.connectionsResource.setAccessSubjects(connectionId, this.getGrantedSubjects()); - this.accessChanged = false; - } - - private getConnectionConfig(): ConnectionConfig { - const config: ConnectionConfig = {}; - config.name = this.config.name; - config.description = this.config.description; - config.template = this.config.template; - config.driverId = this.config.driverId; - - if (this.connectionType === ConnectionType.Attributes) { - if (!this.driver?.embedded) { - config.host = this.config.host; - config.port = this.config.port; - } - config.databaseName = this.config.databaseName; - } else { - config.url = this.config.url; - } - if (this.authModel) { - config.authModelId = this.config.authModelId; - config.saveCredentials = this.isCredentialsChanged(); - if (config.saveCredentials) { - config.credentials = this.config.credentials; - } - } - if (Object.keys(this.config.properties).length > 0) { - config.properties = this.config.properties; - } - - return config; - } - - @action - private setDefaults() { - if (this.connectionInfo?.url) { - this.connectionType = ConnectionType.URL; - } - - this.onChange('name', this.getNameTemplate()); - this.onChange('description', this.connectionInfo?.description || ''); - this.onChange('template', this.connectionInfo?.template); - this.onChange('driverId', this.connectionInfo?.driverId || this.driver?.id || ''); - this.onChange('host', this.connectionInfo?.host || this.driver?.defaultServer || ''); - this.onChange('port', this.connectionInfo?.port || this.driver?.defaultPort || ''); - this.onChange('databaseName', this.connectionInfo?.databaseName || this.driver?.defaultDatabase || ''); - this.onChange('url', this.connectionInfo?.url || this.driver?.sampleURL || ''); - this.onChange('properties', this.connectionInfo?.properties || {}); - this.onChange('authModelId', this.connectionInfo?.authModel || this.driver?.defaultAuthModel); - this.onChange('credentials', {}); - } - - private isCredentialsChanged() { - if (!this.connectionInfo.authProperties.length) { - return true; - } - for (const property of this.connectionInfo.authProperties) { - if (this.config.credentials[property.id!] !== property.value) { - return true; - } - } - return false; - } - - private getNameTemplate() { - if (this.connectionInfo.name) { - return this.connectionInfo.name; - } - - if (this.driver) { - const address = this.getConnectionAddress(); - return `${this.driver.name}${address ? ` (${address})` : ' connection'}`; - } - - return 'New connection'; - } - - private getConnectionAddress() { - if (!this.connectionInfo.host) { - return ''; - } - - return `${this.connectionInfo.host}${this.connectionInfo.port ? `:${this.connectionInfo.port}` : ''}`; - } - private showError(exception: Error, message: string) { if (!this.error.catch(exception) || this.isDistructed) { this.notificationService.logException(exception, message); @@ -290,58 +86,10 @@ implements IInitializableController, IDestructibleController { this.isLoading = true; try { await this.connectionsResource.load(this.connectionId); - - this.connectionInfo = this.connectionsResource.get(this.connectionId)!; - await this.loadDriver(this.connectionInfo.driverId); } catch (exception) { this.notificationService.logException(exception, `Can't load ConnectionInfo ${this.connectionId}`); } finally { this.isLoading = false; } } - - private async loadDriver(driverId: string) { - if (!driverId) { - this.isSaving = true; - try { - await this.dbDriverResource.loadAll(); - } catch (exception) { - this.notificationService.logException(exception, 'Can\'t load drivers'); - } finally { - this.isSaving = false; - } - this.setDefaults(); - return; - } - - this.isSaving = true; - try { - this.driver = await this.dbDriverResource.load(driverId); - this.setDefaults(); - } catch (exception) { - this.notificationService.logException(exception, `Can't load driver ${driverId}`); - } - - if (!this.driver || this.driver.anonymousAccess) { - this.isSaving = false; - this.authModel = null; - return; - } - - try { - this.authModel = await this.dbAuthModelsResource.load( - this.connectionInfo?.authModel || this.driver.defaultAuthModel - ); - - if (this.authModel) { - for (const property of this.connectionInfo.authProperties) { - this.config.credentials[property.id!] = property.value; - } - } - } catch (exception) { - this.notificationService.logException(exception, 'Can\'t load driver auth model'); - } finally { - this.isSaving = false; - } - } } diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx deleted file mode 100644 index b85a2b3195..0000000000 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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'; -import styled, { css } from 'reshadow'; - -import { - Radio, InputField, useFocus, ObjectPropertyInfoForm, Combobox, Checkbox, Textarea, InputGroup -} from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; -import { useStyles } from '@cloudbeaver/core-theming'; - -import { ConnectionType } from '../ConnectionEditController'; -import { formStyles } from './formStyles'; -import { IFormController } from './IFormController'; -import { ParametersForm } from './ParametersForm'; - -type ConnectionFormProps = { - controller: IFormController; -} - -const styles = css` - box { - flex: 1; - display: flex; - flex-wrap: wrap; - } - box-element { - min-width: 450px; - } -`; - -export const ConnectionForm = observer(function ConnectionForm({ - controller, -}: ConnectionFormProps) { - const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); - - return styled(useStyles(styles, formStyles))( - - - - - controller.onChange('template', value)} - disabled={!controller.isNew || controller.isDisabled} - mod='surface' - /> - - - driver.id} - valueSelector={driver => driver?.name!} - onSelect={controller.onSelectDriver} - readOnly={!controller.isSearched || controller.drivers.length < 2} - mod={'surface'} - > - {translate('connections_connection_driver')} - - - - controller.onChange('name', value)} - disabled={controller.isDisabled} - mod='surface' - > - {translate('connections_connection_name')} - - - - - - - - - controller.onChangeType(ConnectionType.Attributes)} - checked={controller.connectionType === ConnectionType.Attributes} - disabled={controller.isDisabled} - mod={['primary']} - > - {translate('customConnection_connectionType_custom')} - - controller.onChangeType(ConnectionType.URL)} - checked={controller.connectionType === ConnectionType.URL} - disabled={controller.isDisabled} - mod={['primary']} - > - {translate('customConnection_connectionType_url')} - - - {controller.connectionType === ConnectionType.Attributes ? ( - - ) : ( - - controller.onChange('url', value)} - disabled={controller.isDisabled} - autoComplete={`section-${controller.driver?.id || 'driver'} section-jdbc`} - mod='surface' - > - {translate('customConnection_url_JDBC')} - - - )} - {controller.authModel && ( - <> - - {translate('connections_connection_edit_authentication')} - - - - )} - - - - ); -}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts deleted file mode 100644 index ed0c939f80..0000000000 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { DBDriver } from '@cloudbeaver/core-connections'; -import { ConnectionConfig, DatabaseAuthModel } from '@cloudbeaver/core-sdk'; - -import { ConnectionType } from '../ConnectionEditController'; - -export interface IFormController { - isSearched: boolean; - isNew: boolean; - connectionId: string; - drivers: DBDriver[]; - driver: DBDriver | null; - authModel: DatabaseAuthModel | null; - config: ConnectionConfig; - connectionType: ConnectionType; - isSaving: boolean; - isDisabled: boolean; - onChangeType(type: ConnectionType): void; - onSelectDriver(driver: DBDriver): void; - onChange(property: T, value: ConnectionConfig[T]): void; -} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx index 9dfe816e26..927d0ea02a 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx @@ -10,13 +10,12 @@ import { observer } from 'mobx-react'; import styled, { css, use } from 'reshadow'; import { - Table, TableHeader, TableColumnHeader, TableBody, TableItemSeparator + Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ConnectionInfo } from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; -import { ConnectionSearch } from '../../ConnectionsResource'; import { Connection } from './Connection'; const styles = composes( @@ -37,14 +36,12 @@ const styles = composes( type Props = { connections: ConnectionInfo[]; - findConnections: ConnectionSearch[]; selectedItems: Map; expandedItems: Map; } export const ConnectionsTable = observer(function ConnectionsTable({ connections, - findConnections, selectedItems, expandedItems, }: Props) { @@ -62,8 +59,6 @@ export const ConnectionsTable = observer(function ConnectionsTable({ - {findConnections.map(connection => )} - {!!findConnections.length && } {connections.map(connection => )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx new file mode 100644 index 0000000000..367cc7bd2c --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -0,0 +1,210 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import { useMemo, useEffect, useCallback } from 'react'; +import styled, { css } from 'reshadow'; + +import { + TabsState, TabList, Tab, TabTitle, IconButton, Loader, StaticImage +} from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { ConnectionForm } from '../ConnectionForm/ConnectionForm'; +import { IConnectionFormModel } from '../ConnectionForm/IConnectionFormModel'; +import { CreateConnectionController } from './CreateConnectionController'; +import { CustomConnection } from './CustomConnection'; +import { SearchDatabase } from './SearchDatabase'; + +type Props = { + method: string; + configurationWizard: boolean; + onChange: (method: string) => void; + onCancel(): void; +} + +const styles = composes( + css` + title-bar { + composes: theme-border-color-background from global; + } + + Tab { + composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; + } + + TabList { + composes: theme-background-surface theme-text-on-surface from global; + } + + connection-create-footer { + composes: theme-background-secondary from global; + } + `, + css` + connection-create { + display: flex; + flex-direction: column; + height: 500px; + overflow: hidden; + } + + connection-create-footer { + padding-bottom: 48px; + flex: auto 0 0; + } + + connection-create-content { + position: relative; + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; + } + + CustomConnection, SearchDatabase { + height: 100%; + overflow: auto; + } + + Loader { + z-index: 1; + } + + title-bar { + composes: theme-typography--headline6 from global; + padding: 16px; + border-top: solid 1px; + align-items: center; + display: flex; + font-weight: 400; + flex: auto 0 0; + } + + IconButton { + color: rgba(0, 0, 0, 0.45); + } + + StaticImage { + margin-right: 16px; + } + + fill { + flex: 1; + } + + + TabList { + align-items: center; + box-sizing: border-box; + display: inline-flex; + padding-left: 24px; + outline: none; + flex: auto 0 0; + } + + TabPanel { + overflow: auto !important; + } + + Tab { + composes: theme-typography--body2 from global; + text-transform: uppercase; + font-weight: normal; + + &:global([aria-selected=true]) { + font-weight: normal !important; + } + + & TabTitle { + padding: 0 24px !important; + } + } + ` +); + +export const CreateConnection = observer(function CreateConnection({ + method, + configurationWizard, + onChange, + onCancel, +}: Props) { + const controller = useController(CreateConnectionController); + const translate = useTranslate(); + + useEffect(() => { + if (configurationWizard) { + controller.search(); + } + }, [configurationWizard]); + + const handleConnectionCancel = useCallback(() => { + if (method === 'driver') { + onCancel(); + } else { + controller.back(); + } + }, [controller, method, onCancel]); + + if (controller.connection) { + return styled(useStyles(styles))( + + + {controller.driver?.icon && } + {controller.driver?.name ?? translate('connections_administration_connection_create')} + + + + + + + + + ); + } + + return styled(useStyles(styles))( + + + + {translate('connections_administration_connection_create')} + + + + + + {translate('Driver')} + + + {translate('Search Database')} + + + + + {method === 'driver' && } + {method === 'search-database' && ( + + )} + {controller.isProcessing && } + + + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx new file mode 100644 index 0000000000..0675851835 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx @@ -0,0 +1,118 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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, computed } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; +import { GQLErrorCatcher, AdminConnectionSearchInfo, ConnectionInfo } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; + +import { DBDriverResource } from '../../../DBDriverResource'; +import { ConnectionsResource } from '../../ConnectionsResource'; +import { ConnectionsAdministrationNavService } from '../ConnectionsAdministrationNavService'; + +@injectable() +export class CreateConnectionController { + @observable hosts = 'localhost'; + @observable isProcessing = false; + @observable databases: AdminConnectionSearchInfo[]; + @observable connection: ConnectionInfo | null; + @observable availableDrivers: string[]; + @observable credentials: Record; + @observable grantedSubjects = []; + + @computed get driver() { + if (!this.connection?.driverId) { + return; + } + + return this.dbDriverResource.get(this.connection.driverId); + } + + readonly error = new GQLErrorCatcher(); + + constructor( + private notificationService: NotificationService, + private connectionsResource: ConnectionsResource, + private commonDialogService: CommonDialogService, + private dbDriverResource: DBDriverResource, + private connectionsAdministrationNavService: ConnectionsAdministrationNavService + ) { + this.credentials = {}; + this.databases = []; + this.availableDrivers = []; + this.connection = null; + } + + search = async () => { + if (this.isProcessing || !this.hosts || !this.hosts.trim()) { + return; + } + + this.isProcessing = true; + + try { + const hosts = this.hosts + .trim() + .replace(/[\s,|+-]+/gm, ' ') + .split(/[\s,|+-]/); + + this.databases = await this.connectionsResource.searchDatabases(hosts); + } catch (exception) { + if (!this.error.catch(exception)) { + this.notificationService.logException(exception, 'Databases search failed'); + } + } finally { + this.isProcessing = false; + } + } + + onSearchChange = (hosts: string) => { + this.hosts = hosts; + } + + onDriverSelect = (driverId: string) => { + this.connection = { + id: uuid(), + driverId, + template: false, + name: '', + authProperties: [], + properties: {}, + } as Partial as any; + this.availableDrivers = [driverId]; + } + + onDatabaseSelect = (database: AdminConnectionSearchInfo) => { + this.connection = { + id: uuid(), + driverId: database.defaultDriver, + template: false, + name: '', + host: database.host, + port: `${database.port}`, + authProperties: [], + properties: {}, + } as Partial as any; + this.availableDrivers = database.possibleDrivers; + } + + back = () => { + this.connection = null; + this.availableDrivers = []; + } + + showDetails = () => { + if (this.error.exception) { + this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); + } + } +} diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CustomConnection.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CustomConnection.tsx new file mode 100644 index 0000000000..5053883557 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CustomConnection.tsx @@ -0,0 +1,42 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 } from 'mobx'; +import { observer } from 'mobx-react'; +import { useMemo, useEffect } from 'react'; + +import { Loader } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { DBDriverResource } from '../../../DBDriverResource'; +import { DriverList } from './DriverList'; + +type Props = { + className?: string; + onSelect(driverId: string): void; +} + +export const CustomConnection = observer(function CustomConnection({ + className, + onSelect, +}: Props) { + const dbDriverResource = useService(DBDriverResource); + + useEffect(() => { dbDriverResource.loadAll(); }, []); + const loading = dbDriverResource.isLoading(); + const drivers = useMemo(() => computed(() => ( + Array.from(dbDriverResource.data.values()) + .sort((a, b) => dbDriverResource.compare(a, b)) + )), [dbDriverResource.data]); + + if (loading) { + return ; + } + + return ; +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx new file mode 100644 index 0000000000..a541017155 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx @@ -0,0 +1,72 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import { useCallback, useMemo } from 'react'; +import styled, { css } from 'reshadow'; + +import { + ListItem, ListItemIcon, StaticImage, ListItemName +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { DBDriverResource } from '../../../DBDriverResource'; + +const styles = composes( + css` + StaticImage { + composes: theme-background-surface theme-border-color-surface from global; + } + `, + css` + ListItemIcon { + position: relative; + min-width: 80px; + justify-content: flex-end; + } + + StaticImage { + box-sizing: border-box; + width: 32px; + border-radius: 50%; + border: solid 2px; + + &:hover { + z-index: 1; + } + &:not(:first-child) { + margin-left: -20px; + } + } + ` +); +type Props = { + database: AdminConnectionSearchInfo; + onSelect(database: AdminConnectionSearchInfo): void; +} + +export const Database = observer(function Database({ database, onSelect }: Props) { + const drivers = useService(DBDriverResource); + const select = useCallback(() => onSelect(database), [database]); + const orderedDrivers = useMemo(() => ( + database.possibleDrivers + .slice() + .sort((a, b) => (a === database.defaultDriver ? -1 : 0)) + ), [database]); + + return styled(useStyles(styles))( + + + {orderedDrivers.map(driverId => )} + + {database.host}:{database.port} + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DatabaseList.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DatabaseList.tsx new file mode 100644 index 0000000000..f360b15b72 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DatabaseList.tsx @@ -0,0 +1,53 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import styled, { css } from 'reshadow'; + +import { ItemListSearch, ItemList, SubmittingForm } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; + +import { Database } from './Database'; + +const styles = css` + SubmittingForm { + display: flex; + flex-direction: column; + } + center { + margin: auto; + } +`; + +type Props = { + databases: AdminConnectionSearchInfo[]; + hosts: string; + className?: string; + onSelect(database: AdminConnectionSearchInfo): void; + onChange(hosts: string): void; + onSearch?(): void; +} + +export const DatabaseList = observer(function DatabaseList({ + databases, hosts, className, onSelect, onChange, onSearch, +}: Props) { + const translate = useTranslate(); + + return styled(styles)( + + + + {databases.map(database => ( + + ))} + + {!databases.length &&
{translate('connections_administration_search_database_tip')}
} +
+ ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx new file mode 100644 index 0000000000..263140d0a0 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx @@ -0,0 +1,41 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import { useCallback } from 'react'; +import styled, { css } from 'reshadow'; + +import { + ListItem, ListItemIcon, StaticImage, ListItemName, ListItemDescription +} from '@cloudbeaver/core-blocks'; + +import { DBDriver } from '../../../DBDriverResource'; + +const styles = css` + StaticImage { + box-sizing: border-box; + width: 32px; + } +`; + +type Props = { + driver: DBDriver; + onSelect(driverId: string): void; +} + +export const Driver = observer(function Driver({ driver, onSelect }: Props) { + const select = useCallback(() => onSelect(driver.id), [driver]); + + return styled(styles)( + + + {driver.name} + {driver.description} + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx new file mode 100644 index 0000000000..25e455b88c --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx @@ -0,0 +1,38 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; +import { useState, useMemo } from 'react'; + +import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks'; + +import { DBDriver } from '../../../DBDriverResource'; +import { Driver } from './Driver'; + +type Props = { + drivers: DBDriver[]; + className?: string; + onSelect(driverId: string): void; +} + +export const DriverList = observer(function DriverList({ drivers, className, onSelect }: Props) { + const [search, setSearch] = useState(''); + const filteredDrivers = useMemo(() => { + if (!search) { + return drivers; + } + return drivers.filter(driver => driver.name?.toUpperCase().includes(search.toUpperCase())); + }, [search, drivers]); + + return ( + + + {filteredDrivers.map(driver => )} + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx new file mode 100644 index 0000000000..67a5b16f45 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx @@ -0,0 +1,43 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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'; + +import { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; + +import { DatabaseList } from './DatabaseList'; + +type Props = { + databases: AdminConnectionSearchInfo[]; + hosts: string; + onSelect(database: AdminConnectionSearchInfo): void; + onChange(hosts: string): void; + onSearch?(): void; + className?: string; +} + +export const SearchDatabase = observer(function SearchDatabase({ + databases, + hosts, + onChange, + onSelect, + onSearch, + className, +}: Props) { + + return ( + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index bfd3bbec9f..0b73351deb 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -31,7 +31,6 @@ export type ConnectionSearch = ConnectionNew & { [SEARCH_CONNECTION_SYMBOL]: Adm @injectable() export class ConnectionsResource extends CachedMapResource { private metadata: MetadataMap; - private searchedDatabases: string[]; constructor( private graphQLService: GraphQLService, @@ -39,7 +38,6 @@ export class ConnectionsResource extends CachedMapResource false); - this.searchedDatabases = []; } has(id: string) { @@ -82,16 +80,19 @@ export class ConnectionsResource extends CachedMapResource this.searchConnections(hosts)); + const { databases } = await this.graphQLService.gql.searchDatabases({ hosts }); + + return databases; } - async create(config: ConnectionConfig, id?: string) { + async create(config: ConnectionConfig) { const { connection } = await this.graphQLService.gql.createConnectionConfiguration({ config }); - if (id) { - this.data.delete(id); - } - this.set(connection.id, connection as ConnectionInfo); + const newConnection: ConnectionNew = { + ...connection as ConnectionInfo, + [NEW_CONNECTION_SYMBOL]: true, + }; + this.set(newConnection.id, newConnection); return this.get(connection.id)!; } @@ -119,12 +120,6 @@ export class ConnectionsResource extends CachedMapResource): Promise> { const { connections } = await this.graphQLService.gql.getConnections(); this.data.clear(); @@ -141,29 +136,6 @@ export class ConnectionsResource extends CachedMapResource, - [NEW_CONNECTION_SYMBOL]: true, - [SEARCH_CONNECTION_SYMBOL]: database, - } as ConnectionSearch; - - this.data.set(connectionInfo.id, connectionInfo); - this.markUpdated(connectionInfo.id); - this.searchedDatabases.push(connectionInfo.id); - } - } - private async getNameTemplate(connection: AdminConnectionSearchInfo) { const driver = await this.dbDriverResource.load(connection.defaultDriver); diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index 81ecb71678..1971a787c4 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -83,11 +83,10 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ : ( )} diff --git a/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx b/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx index 1d21ba354b..9ff8e33aa2 100644 --- a/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx +++ b/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx @@ -9,7 +9,9 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; -import { ListItem } from '@cloudbeaver/core-blocks'; +import { + ListItem, ListItemIcon, ListItemName, ListItemDescription, StaticImage +} from '@cloudbeaver/core-blocks'; export interface IDriver { id: string; @@ -26,5 +28,11 @@ type DriverProps = { export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) { const select = useCallback(() => onSelect(driver.id), [driver]); - return ; + return ( + + + {driver.name} + {driver.description} + + ); }); diff --git a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx b/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx index a0ae03b536..af0f0de1d4 100644 --- a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx +++ b/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx @@ -38,7 +38,10 @@ export const DriverSelectDialog = observer(function DriverSelectDialog({ useEffect(() => { dbDriverResource.loadAll(); }, []); const isLoading = dbDriverResource.isLoading(); - const drivers = useMemo(() => computed(() => Array.from(dbDriverResource.data.values())), [dbDriverResource.data]); + const drivers = useMemo(() => computed(() => ( + Array.from(dbDriverResource.data.values()) + .sort((a, b) => dbDriverResource.compare(a, b)) + )), [dbDriverResource.data]); return styled(styles)( {translate('connections_connection_edit_authentication')}
)} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx index 1d21ba354b..9ff8e33aa2 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx @@ -9,7 +9,9 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; -import { ListItem } from '@cloudbeaver/core-blocks'; +import { + ListItem, ListItemIcon, ListItemName, ListItemDescription, StaticImage +} from '@cloudbeaver/core-blocks'; export interface IDriver { id: string; @@ -26,5 +28,11 @@ type DriverProps = { export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) { const select = useCallback(() => onSelect(driver.id), [driver]); - return ; + return ( + + + {driver.name} + {driver.description} + + ); }); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx index d7de6e9722..ca55b78b64 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx @@ -83,11 +83,10 @@ export const ConnectionDialog = observer(function ConnectionDialog({ ) : ( ))} From 327379c7a2d130bec3b10196c2c730fb417c28a4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 00:05:06 +0300 Subject: [PATCH 02/10] fix(core-blocks): ItemListSearch --- webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx | 2 +- .../Administration/Connections/CreateConnection/DriverList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx b/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx index 280d0a1801..e6274d4a90 100644 --- a/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx +++ b/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx @@ -31,7 +31,7 @@ export function ItemListSearch({ const [search, setSearch] = useState(value ?? ''); const translate = useTranslate(); const searchHandler = useCallback((event: React.ChangeEvent) => { - if (value !== undefined) { + if (value === undefined) { setSearch(event.target.value); } if (onSearch) { diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx index 25e455b88c..fb9b36aca5 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx @@ -31,7 +31,7 @@ export const DriverList = observer(function DriverList({ drivers, className, onS return ( - + {filteredDrivers.map(driver => )} ); From cf80ce20c0ba797a83fbb6c50f6ee8b66c36906d Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 13:05:39 +0300 Subject: [PATCH 03/10] fix(core-connections): connection mutation while editing CB-257 --- .../ConnectionEditController.ts | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts index 715eb636b1..c3b39069b3 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts @@ -11,10 +11,8 @@ import { observable, computed } from 'mobx'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher, AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk'; +import { GQLErrorCatcher, AdminConnectionGrantInfo, ConnectionInfo } from '@cloudbeaver/core-sdk'; import { DBDriverResource } from '../../../DBDriverResource'; import { ConnectionsResource } from '../../ConnectionsResource'; @@ -25,6 +23,7 @@ implements IInitializableController, IDestructibleController { @observable grantedSubjects: AdminConnectionGrantInfo[] | null = null; @observable isLoading = true; @observable credentials: Record = {}; + @observable connection: ConnectionInfo | null = null; @computed get isDisabled() { return this.isLoading; @@ -37,10 +36,6 @@ implements IInitializableController, IDestructibleController { return this.dbDriverResource.get(this.connection.driverId) || null; } - @computed get connection() { - return this.connectionsResource.get(this.connectionId); - } - @computed get availableDrivers() { if (!this.connection) { return []; @@ -52,12 +47,10 @@ implements IInitializableController, IDestructibleController { readonly editing = true; // used as model IConnectionFormModel readonly error = new GQLErrorCatcher(); - private isDistructed = false; constructor( private connectionsResource: ConnectionsResource, private notificationService: NotificationService, - private commonDialogService: CommonDialogService, private dbDriverResource: DBDriverResource, ) { } @@ -67,25 +60,13 @@ implements IInitializableController, IDestructibleController { } destruct(): void { - this.isDistructed = true; - } - - onShowDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - } - - private showError(exception: Error, message: string) { - if (!this.error.catch(exception) || this.isDistructed) { - this.notificationService.logException(exception, message); - } } private async loadConnectionInfo() { this.isLoading = true; try { - await this.connectionsResource.load(this.connectionId); + // we create a copy to protect the current value from mutation + this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId))); } catch (exception) { this.notificationService.logException(exception, `Can't load ConnectionInfo ${this.connectionId}`); } finally { From 41f27e14ac88493feb23a778f24fbd355a291912 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 13:13:32 +0300 Subject: [PATCH 04/10] fix(core-connections): connection credentials requirement --- .../Connections/ConnectionForm/Options/Options.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx index ff7c20facf..335d234945 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx @@ -160,7 +160,7 @@ export const Options = observer(function Options({ - {controller.authModel && ( + {(controller.authModel && !controller.driver?.anonymousAccess) && ( <> {translate('connections_connection_edit_authentication')} From a397e03959fe9602e5cfd3335dfffa3719b9308f Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 13:20:38 +0300 Subject: [PATCH 05/10] fix(core-connections): database ordering --- .../Connections/CreateConnection/Database.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx index a541017155..659348b1b6 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Database.tsx @@ -58,7 +58,15 @@ export const Database = observer(function Database({ database, onSelect }: Props const orderedDrivers = useMemo(() => ( database.possibleDrivers .slice() - .sort((a, b) => (a === database.defaultDriver ? -1 : 0)) + .sort((a, b) => { + if (a === database.defaultDriver) { + return 1; + } + if (b === database.defaultDriver) { + return -1; + } + return a.localeCompare(b); + }) ), [database]); return styled(useStyles(styles))( From d22d4a9ff9c2b850421d871a9801be30aefa4b5a Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 13:24:30 +0300 Subject: [PATCH 06/10] fix: drivers icon size CB-257 --- .../Connections/CreateConnection/Driver.tsx | 1 + .../src/DriverSelectDialog/Driver.tsx | 38 ------------- .../DriverSelectDialog/DriverSelectDialog.tsx | 56 ------------------- .../src/DriverSelectDialog/DriverSelector.tsx | 37 ------------ .../DriverSelectorDialog/Driver.tsx | 11 +++- 5 files changed, 11 insertions(+), 132 deletions(-) delete mode 100644 webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx delete mode 100644 webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx delete mode 100644 webapp/packages/core-connections/src/DriverSelectDialog/DriverSelector.tsx diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx index 263140d0a0..773abdcc7e 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/Driver.tsx @@ -20,6 +20,7 @@ const styles = css` StaticImage { box-sizing: border-box; width: 32px; + max-height: 32px; } `; diff --git a/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx b/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx deleted file mode 100644 index 9ff8e33aa2..0000000000 --- a/webapp/packages/core-connections/src/DriverSelectDialog/Driver.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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'; -import { useCallback } from 'react'; - -import { - ListItem, ListItemIcon, ListItemName, ListItemDescription, StaticImage -} from '@cloudbeaver/core-blocks'; - -export interface IDriver { - id: string; - icon?: string; - name?: string; - description?: string; -} - -type DriverProps = { - driver: IDriver; - onSelect(driverId: string): void; -} - -export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) { - const select = useCallback(() => onSelect(driver.id), [driver]); - - return ( - - - {driver.name} - {driver.description} - - ); -}); diff --git a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx b/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx deleted file mode 100644 index af0f0de1d4..0000000000 --- a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelectDialog.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 } from 'mobx'; -import { observer } from 'mobx-react'; -import { useEffect, useMemo } from 'react'; -import styled, { css } from 'reshadow'; - -import { Loader } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; -import { useTranslate } from '@cloudbeaver/core-localization'; - -import { DBDriverResource } from '../DBDriverResource'; -import { DriverSelector } from './DriverSelector'; - -const styles = css` - CommonDialogWrapper { - max-height: 550px; - min-height: 550px; - } - DriverSelector { - flex: 1; - } -`; - -export const DriverSelectDialog = observer(function DriverSelectDialog({ - resolveDialog, - rejectDialog, -}: DialogComponentProps) { - const dbDriverResource = useService(DBDriverResource); - const title = useTranslate('connections_administration_new_connection'); - - useEffect(() => { dbDriverResource.loadAll(); }, []); - const isLoading = dbDriverResource.isLoading(); - const drivers = useMemo(() => computed(() => ( - Array.from(dbDriverResource.data.values()) - .sort((a, b) => dbDriverResource.compare(a, b)) - )), [dbDriverResource.data]); - - return styled(styles)( - - {isLoading && } - {!isLoading && } - - ); -}); diff --git a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelector.tsx b/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelector.tsx deleted file mode 100644 index e0a21f4a0d..0000000000 --- a/webapp/packages/core-connections/src/DriverSelectDialog/DriverSelector.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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'; -import { useState, useMemo } from 'react'; - -import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks'; - -import { Driver, IDriver } from './Driver'; - -type DriverSelectorProps = { - drivers: IDriver[]; - className?: string; - onSelect(driverId: string): void; -} - -export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) { - const [search, setSearch] = useState(''); - const filteredDrivers = useMemo(() => { - if (!search) { - return drivers; - } - return drivers.filter(driver => driver.name?.toUpperCase().includes(search.toUpperCase())); - }, [search, drivers]); - - return ( - - - {filteredDrivers.map(driver => )} - - ); -}); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx index 9ff8e33aa2..37a441fab7 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; +import styled, { css } from 'reshadow'; import { ListItem, ListItemIcon, ListItemName, ListItemDescription, StaticImage @@ -20,6 +21,14 @@ export interface IDriver { description?: string; } +const styles = css` + StaticImage { + box-sizing: border-box; + width: 24px; + max-height: 24px; + } +`; + type DriverProps = { driver: IDriver; onSelect(driverId: string): void; @@ -28,7 +37,7 @@ type DriverProps = { export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) { const select = useCallback(() => onSelect(driver.id), [driver]); - return ( + return styled(styles)( {driver.name} From 9362a4e184dea21f1257bf111ab82c9c3c8450ca Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 14:00:48 +0300 Subject: [PATCH 07/10] feat(core-connections): connection create, back button --- .../ConnectionForm/ConnectionForm.tsx | 4 +++- .../CreateConnection/CreateConnection.tsx | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx index c0bfb51ed0..5a0ccb609c 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx @@ -122,12 +122,14 @@ const styles = composes( type Props = { model: IConnectionFormModel; configurationWizard?: boolean; + onBack?(): void; onCancel?(): void; } export const ConnectionForm = observer(function ConnectionForm({ model, configurationWizard, + onBack = () => {}, onCancel = () => {}, }: Props) { const controller = useController(Controller, model, onCancel); @@ -151,7 +153,7 @@ export const ConnectionForm = observer(function ConnectionForm({ type="button" disabled={controller.isDisabled} mod={['outlined']} - onClick={onCancel} + onClick={onBack} > {translate('ui_processing_cancel')} 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 367cc7bd2c..3cd875daeb 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -11,7 +11,7 @@ import { useMemo, useEffect, useCallback } from 'react'; import styled, { css } from 'reshadow'; import { - TabsState, TabList, Tab, TabTitle, IconButton, Loader, StaticImage + TabsState, TabList, Tab, TabTitle, IconButton, Loader, StaticImage, Icon } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -80,7 +80,7 @@ const styles = composes( title-bar { composes: theme-typography--headline6 from global; - padding: 16px; + padding: 16px 24px; border-top: solid 1px; align-items: center; display: flex; @@ -93,13 +93,29 @@ const styles = composes( } StaticImage { + width: 32px; + max-height: 32px; margin-right: 16px; } fill { flex: 1; } - + + back-button { + position: relative; + box-sizing: border-box; + margin-right: 16px; + display: flex; + + & Icon { + box-sizing: border-box; + transform: rotate(90deg); + cursor: pointer; + height: 16px; + width: 16px; + } + } TabList { align-items: center; @@ -157,6 +173,7 @@ export const CreateConnection = observer(function CreateConnection({ return styled(useStyles(styles))( + {controller.driver?.icon && } {controller.driver?.name ?? translate('connections_administration_connection_create')} @@ -165,6 +182,7 @@ export const CreateConnection = observer(function CreateConnection({ From ced3e5f4d4005f39dd33b84212705ba6e622c5a0 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 14:28:48 +0300 Subject: [PATCH 08/10] fix(core-connections): editing cancelation --- .../Connections/ConnectionsTable/ConnectionEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e805803c5b..3c83bf1a29 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -55,7 +55,7 @@ export const ConnectionEdit = observer(function ConnectionEdit({ return styled(useStyles(styles))( {controller.connection && ( - + )} ); From f080e1c5351588e1fb986ea2ebc3a9d9124c7e3d Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Sep 2020 14:36:17 +0300 Subject: [PATCH 09/10] fix(core-connections): input disable state --- .../packages/core-blocks/src/ItemList/ItemListSearch.tsx | 4 +++- .../Connections/ConnectionForm/ConnectionForm.tsx | 3 ++- .../Connections/ConnectionForm/Options/Options.tsx | 9 +++++++-- .../Connections/CreateConnection/CreateConnection.tsx | 1 + .../Connections/CreateConnection/DatabaseList.tsx | 5 +++-- .../Connections/CreateConnection/SearchDatabase.tsx | 3 +++ 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx b/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx index e6274d4a90..85135a1563 100644 --- a/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx +++ b/webapp/packages/core-blocks/src/ItemList/ItemListSearch.tsx @@ -20,12 +20,13 @@ import { Styles } from './styles'; type Props = React.PropsWithChildren<{ value?: string; placeholder?: string; + disabled?: boolean; onSearch?(value: string): void; className?: string; }> export function ItemListSearch({ - value, placeholder, onSearch, className, + value, placeholder, disabled, onSearch, className, }: Props) { const styles = useContext(Styles); const [search, setSearch] = useState(value ?? ''); @@ -48,6 +49,7 @@ export function ItemListSearch({ value={value ?? search} onChange={searchHandler} autoComplete="off" + disabled={disabled} {...use({ mod: 'surface' })} /> diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx index 5a0ccb609c..be52d3b044 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/ConnectionForm.tsx @@ -177,8 +177,9 @@ export const ConnectionForm = observer(function ConnectionForm({ type={controller.connectionType} credentials={model.credentials} availableDrivers={model.availableDrivers} - onTypeChange={controller.setType} editing={model.editing} + disabled={controller.isDisabled} + onTypeChange={controller.setType} onSave={controller.save} /> diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx index 335d234945..b6f597bc06 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx @@ -89,7 +89,7 @@ export const Options = observer(function Options({ value={connection.id} state={connection} checkboxLabel={translate('connections_connection_template')} - disabled={editing} + disabled={editing || disabled} mod='surface' /> @@ -103,6 +103,7 @@ export const Options = observer(function Options({ onSelect={controller.onSelectDriver} readOnly={editing || controller.drivers.length < 2} mod={'surface'} + disabled={disabled} > {translate('connections_connection_driver')} @@ -143,7 +144,11 @@ export const Options = observer(function Options({ - + 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 3cd875daeb..fc5ec32189 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -215,6 +215,7 @@ export const CreateConnection = observer(function CreateConnection({ - + {databases.map(database => ( ))} diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx index 67a5b16f45..50b2c98676 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/SearchDatabase.tsx @@ -15,6 +15,7 @@ import { DatabaseList } from './DatabaseList'; type Props = { databases: AdminConnectionSearchInfo[]; hosts: string; + disabled?: boolean; onSelect(database: AdminConnectionSearchInfo): void; onChange(hosts: string): void; onSearch?(): void; @@ -24,6 +25,7 @@ type Props = { export const SearchDatabase = observer(function SearchDatabase({ databases, hosts, + disabled, onChange, onSelect, onSearch, @@ -34,6 +36,7 @@ export const SearchDatabase = observer(function SearchDatabase({ Date: Mon, 7 Sep 2020 14:40:41 +0300 Subject: [PATCH 10/10] fix(plugin-connection-template): selection dialog --- .../TemplateConnectionItem.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx index 2873ab99b7..f60a9196a0 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx @@ -8,8 +8,11 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; +import styled, { css } from 'reshadow'; -import { ListItem } from '@cloudbeaver/core-blocks'; +import { + ListItem, ListItemDescription, ListItemName, ListItemIcon, StaticImage +} from '@cloudbeaver/core-blocks'; import { DBDriver, Connection } from '@cloudbeaver/core-connections'; type Props = { @@ -18,6 +21,14 @@ type Props = { onSelect(connectionId: string): void; } +const styles = css` + StaticImage { + box-sizing: border-box; + width: 24px; + max-height: 24px; + } +`; + export const TemplateConnectionItem = observer(function TemplateConnectionItem({ template, dbDriver, @@ -25,5 +36,11 @@ export const TemplateConnectionItem = observer(function TemplateConnectionItem({ }: Props) { const select = useCallback(() => onSelect(template.id), [template]); - return ; + return styled(styles)( + + + {template.name} + {template.description} + + ); });