From 20c4f7fb9c8f449dac3ef4eadbdf5c39ea019137 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 10 Aug 2020 23:45:40 +0300 Subject: [PATCH] feat(core): connections managment CB-215 --- .../shared/NodesManager/DBObjectService.ts | 2 +- .../src/{ => FormControls}/Checkbox.tsx | 45 ++-- .../core-blocks/src/FormControls/Combobox.tsx | 191 +++++++++++++++ .../src/FormControls/InputField.tsx | 34 +++ .../src/FormControls/InputGroup.tsx | 38 +++ .../src/{ => FormControls}/Radio.tsx | 8 +- .../src/{ => FormControls}/ShadowInput.tsx | 0 .../src/{ => FormControls}/SubmittingForm.tsx | 0 .../core-blocks/src/FormControls/Textarea.tsx | 40 +++ .../src/FormControls/baseFormControlStyles.ts | 37 +++ .../packages/core-blocks/src/InputField.tsx | 62 ----- .../src/PropertiesTable/PropertyItem.tsx | 2 +- .../packages/core-blocks/src/Table/Table.tsx | 11 +- .../src/Table/TableColumnValue.tsx | 10 +- .../core-blocks/src/Table/TableContext.tsx | 3 + .../core-blocks/src/Table/TableItem.tsx | 30 ++- .../src/Table/TableItemContext.tsx | 1 + .../core-blocks/src/Table/TableItemExpand.tsx | 67 +++++ .../packages/core-blocks/src/Table/index.ts | 1 + .../packages/core-blocks/src/Tabs/Tab/Tab.tsx | 7 +- webapp/packages/core-blocks/src/index.ts | 13 +- .../packages/core-bootstrap/src/manifest.ts | 4 +- .../Connections/ConnectionsAdministration.tsx | 6 +- .../ConnectionsAdministrationController.ts | 35 ++- .../ConnectionsTable/Connection.tsx | 37 ++- .../ConnectionsTable/ConnectionEdit.tsx | 192 +++++++++++++++ .../ConnectionEditController.ts | 230 ++++++++++++++++++ .../ConnectionForm/ConnectionForm.tsx | 143 +++++++++++ .../ConnectionForm/IFormController.ts | 26 ++ .../ConnectionForm/ParametersForm.tsx | 72 ++++++ .../ConnectionForm/formStyles.ts | 46 ++++ .../ConnectionsTable/ConnectionsTable.tsx | 9 +- .../DriverProperties/DriverProperties.tsx | 64 +++++ .../DriverPropertiesController.ts | 79 ++++++ .../src/Administration/ConnectionsResource.ts | 84 +++++-- .../core-connections/src/DBDriverResource.ts | 2 +- .../src/DriverPropertiesService.ts | 31 +++ webapp/packages/core-connections/src/index.ts | 1 + .../core-connections/src/locales/en.ts | 6 + .../core-connections/src/locales/ru.ts | 6 + webapp/packages/core-di/src/useController.ts | 26 +- .../updateConnectionConfiguration.gql | 23 ++ webapp/packages/core-sdk/src/sdk.ts | 32 +++ .../src/styles/_form-controls.scss | 5 +- .../core-theming/src/styles/_mixins.scss | 1 + .../core-theming/src/styles/_table.scss | 73 ++++-- .../core-theming/src/styles/_variables.scss | 15 ++ .../CreateUserDialog/CreateUserDialog.tsx | 2 +- .../DriverPropertiesController.ts | 12 +- .../src/CustomConnectionService.ts | 14 +- 50 files changed, 1673 insertions(+), 205 deletions(-) rename webapp/packages/core-blocks/src/{ => FormControls}/Checkbox.tsx (57%) create mode 100644 webapp/packages/core-blocks/src/FormControls/Combobox.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/InputField.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/InputGroup.tsx rename webapp/packages/core-blocks/src/{ => FormControls}/Radio.tsx (92%) rename webapp/packages/core-blocks/src/{ => FormControls}/ShadowInput.tsx (100%) rename webapp/packages/core-blocks/src/{ => FormControls}/SubmittingForm.tsx (100%) create mode 100644 webapp/packages/core-blocks/src/FormControls/Textarea.tsx create mode 100644 webapp/packages/core-blocks/src/FormControls/baseFormControlStyles.ts delete mode 100644 webapp/packages/core-blocks/src/InputField.tsx create mode 100644 webapp/packages/core-blocks/src/Table/TableItemExpand.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/formStyles.ts create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx create mode 100644 webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverPropertiesController.ts create mode 100644 webapp/packages/core-connections/src/DriverPropertiesService.ts create mode 100644 webapp/packages/core-sdk/src/queries/connections/administration/updateConnectionConfiguration.gql create mode 100644 webapp/packages/core-theming/src/styles/_variables.scss diff --git a/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts b/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts index bc0d990ca3..950d743b55 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts @@ -30,7 +30,7 @@ export class DBObjectService extends CachedMapResource { return this.data; } - this.performUpdate(key, async () => { + await this.performUpdate(key, async () => { if (this.isLoaded(key) && !this.isOutdated(key)) { return; } diff --git a/webapp/packages/core-blocks/src/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx similarity index 57% rename from webapp/packages/core-blocks/src/Checkbox.tsx rename to webapp/packages/core-blocks/src/FormControls/Checkbox.tsx index 31bf887b89..f4957ee8e2 100644 --- a/webapp/packages/core-blocks/src/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx @@ -10,43 +10,35 @@ import styled, { css, use } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; +import { baseFormControlStyles } from './baseFormControlStyles'; + const styles = css` - label { - composes: theme-typography--body1 from global; - } - field { + checkbox { + margin: 0 12px; display: flex; - flex: auto; - box-sizing: border-box; align-items: center; - padding: 12px 0; & label { - width: 230px; - text-align: right; padding: 0 12px; - line-height: 16px; - font-weight: 500; } + & input { - flex: 1; - margin: 0 12px; - height: 26px; - } - & checkbox { - display: flex; - align-items: center; + flex: auto 0 0; + margin: 0; } } `; -type Props = Omit, 'onChange'> & { +type Props = Omit, 'onChange' | 'type' | 'value'> & { + value?: string; checkboxLabel: string; mod?: 'surface'; onChange?(value: boolean): any; } export function Checkbox({ + name, + value, checkboxLabel, children, className, @@ -55,12 +47,19 @@ export function Checkbox({ ...rest }: Props) { - return styled(useStyles(styles))( + return styled(useStyles(baseFormControlStyles, styles))( - + {children} - onChange(e.target.checked)} {...rest} {...use({ mod })} /> - {checkboxLabel} + onChange(e.target.checked)} + {...rest} + {...use({ mod })} + /> + ); diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx new file mode 100644 index 0000000000..ea31478447 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -0,0 +1,191 @@ +/* + * 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 { + useLayoutEffect, useCallback, useState, useRef +} from 'react'; +import { + useMenuState, + Menu, + MenuItem, + MenuButton, +} from 'reakit/Menu'; +import styled, { css, use } from 'reshadow'; + +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { IconButton } from '../IconButton'; +import { Icon } from '../Icons/Icon'; +import { baseFormControlStyles } from './baseFormControlStyles'; + +const styles = composes( + css` + Menu { + composes: theme-text-on-surface theme-background-surface from global; + } + MenuItem, MenuButton { + composes: theme-ripple from global; + } + `, + css` + field input { + margin: 0; + } + MenuButton { + background: transparent; + outline: none; + padding: 4px; + cursor: pointer; + } + + Menu { + composes: theme-typography--caption theme-elevation-z3 from global; + display: flex; + flex-direction: column; + width: 100%; + max-height: 150px; + overflow: auto; + outline: none; + padding: 4px 0; + z-index: 999; + + & MenuItem { + background: transparent; + display: block; + padding: 4px 36px; + text-align: left; + outline: none; + color: inherit; + cursor: pointer; + } + } + Icon { + height: 16px; + display: block; + } + MenuButton Icon { + transform: rotate(90deg); + + &[|focus] { + transform: rotate(-90deg); + } + } + input-box { + flex: 1; + margin: 0 12px; + position: relative; + display: flex; + align-items: center; + } + ` +); + +type Props = Omit, 'onChange' | 'onSelect'> & { + value: string | undefined; + propertyName?: string; + items: T[]; + mod?: 'surface'; + keySelector(item: T): string; + valueSelector(item: T): string; + onChange?(value: string): any; + onSelect(value: T | null): void; + onSwitch?(state: boolean): void; +} + +export function Combobox({ + value, + propertyName, + items, + children, + className, + mod, + readOnly, + keySelector = v => v as any, + valueSelector = v => v as any, + onChange = () => {}, + onSelect, + onSwitch, + ...rest +}: Props) { + const ref = useRef(null); + const menu = useMenuState({ + placement: 'bottom-end', + currentId: null, + gutter: 4, + }); + const [searchValue, setSearchValue] = useState(''); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + onChange(value); + setSearchValue(value); + }, + [onChange] + ); + + const handleRemove = useCallback( + () => { + menu.hide(); + onSelect(null); + setSearchValue(''); + }, + [menu, onSelect] + ); + + const handleMenuSelect = useCallback( + (event: React.MouseEvent) => { + menu.hide(); + const id = event.currentTarget.id; + onSelect(items.find(item => keySelector(item) === id)!); + }, + [menu, items, onSelect] + ); + + useLayoutEffect(() => onSwitch && onSwitch(menu.visible), [onSwitch, menu.visible]); + + const selectedItem = items.find(item => keySelector(item) === value); + const filteredItems = items.filter( + item => selectedItem || !searchValue || valueSelector(item).toUpperCase().includes(searchValue.toUpperCase()) + ); + + if (ref.current === document.activeElement && !selectedItem && searchValue) { + menu.show(); + } + + return styled(useStyles(baseFormControlStyles, styles))( + + {children} + + + {(selectedItem && !readOnly) && ( + + )} + {!readOnly && ( + <> + + + {filteredItems.map(item => ( + + {valueSelector(item)} + + ))} + + + )} + + + ); +} diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx new file mode 100644 index 0000000000..615988e298 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -0,0 +1,34 @@ +/* + * 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 styled, { use } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { baseFormControlStyles } from './baseFormControlStyles'; + +type Props = Omit, 'onChange'> & { + mod?: 'surface'; + onChange?(value: string): any; +} + +export function InputField({ + children, + className, + mod, + onChange = () => {}, + ...rest +}: Props) { + + return styled(useStyles(baseFormControlStyles))( + + {children} + onChange(e.target.value)} {...rest} {...use({ mod })} /> + + ); +} diff --git a/webapp/packages/core-blocks/src/FormControls/InputGroup.tsx b/webapp/packages/core-blocks/src/FormControls/InputGroup.tsx new file mode 100644 index 0000000000..961df06584 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/InputGroup.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 styled, { use, css } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { baseFormControlStyles } from './baseFormControlStyles'; + +const styles = css` + field-label { + composes: theme-typography--body2 from global; + margin-left: 150px; + text-align: left; + font-weight: initial; + } +`; + +type Props = React.PropsWithChildren<{ + className?: string; +}> + +export function InputGroup({ + children, + className, +}: Props) { + + return styled(useStyles(baseFormControlStyles, styles))( + + {children} + + ); +} diff --git a/webapp/packages/core-blocks/src/Radio.tsx b/webapp/packages/core-blocks/src/FormControls/Radio.tsx similarity index 92% rename from webapp/packages/core-blocks/src/Radio.tsx rename to webapp/packages/core-blocks/src/FormControls/Radio.tsx index 483b1c2624..3ffd16a3ca 100644 --- a/webapp/packages/core-blocks/src/Radio.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Radio.tsx @@ -35,6 +35,8 @@ const radioStyles = composes( field { display: inline-flex; align-items: center; + font-weight: 500; + padding: 7px 12px; vertical-align: middle; } ` @@ -43,9 +45,9 @@ const radioStyles = composes( const radioMod = { primary: composes( css` - radio { - composes: theme-radio_primary from global; - } + radio { + composes: theme-radio_primary from global; + } ` ), }; diff --git a/webapp/packages/core-blocks/src/ShadowInput.tsx b/webapp/packages/core-blocks/src/FormControls/ShadowInput.tsx similarity index 100% rename from webapp/packages/core-blocks/src/ShadowInput.tsx rename to webapp/packages/core-blocks/src/FormControls/ShadowInput.tsx diff --git a/webapp/packages/core-blocks/src/SubmittingForm.tsx b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx similarity index 100% rename from webapp/packages/core-blocks/src/SubmittingForm.tsx rename to webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx diff --git a/webapp/packages/core-blocks/src/FormControls/Textarea.tsx b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx new file mode 100644 index 0000000000..514087f918 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx @@ -0,0 +1,40 @@ +/* + * 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 styled, { css, use } from 'reshadow'; + +import { useStyles } from '@cloudbeaver/core-theming'; + +import { baseFormControlStyles } from './baseFormControlStyles'; + +const styles = css` + textarea { + line-height: 19px; + } +`; + +type Props = Omit, 'onChange'> & { + mod?: 'surface'; + onChange?(value: string): any; +} + +export function Textarea({ + children, + className, + mod, + onChange = () => {}, + ...rest +}: Props) { + + return styled(useStyles(baseFormControlStyles, styles))( + + {children} + + + + + + controller.onChangeType(ConnectionType.Attributes)} + checked={controller.connectionType === ConnectionType.Attributes} + disabled={controller.isSaving} + mod={['primary']} + > + {translate('customConnection_connectionType_custom')} + + controller.onChangeType(ConnectionType.URL)} + checked={controller.connectionType === ConnectionType.URL} + disabled={controller.isSaving} + mod={['primary']} + > + {translate('customConnection_connectionType_url')} + + + {controller.connectionType === ConnectionType.Attributes ? ( + + ) : ( + + controller.onChange('url', value)} + disabled={controller.isSaving} + 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 new file mode 100644 index 0000000000..9609d90375 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts @@ -0,0 +1,26 @@ +/* + * 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 { + isNew: boolean; + connectionId: string; + drivers: DBDriver[]; + driver: DBDriver | null; + authModel: DatabaseAuthModel | null; + config: ConnectionConfig; + connectionType: ConnectionType; + isSaving: 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/ConnectionForm/ParametersForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.tsx new file mode 100644 index 0000000000..98a396fcc4 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ParametersForm.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 styled, { use } from 'reshadow'; + +import { InputField } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { formStyles } from './formStyles'; +import { IFormController } from './IFormController'; + +type ParametersFormProps = { + controller: IFormController; + embedded?: boolean; +} + +export const ParametersForm = observer(function ParametersForm({ + controller, + embedded, +}: ParametersFormProps) { + const translate = useTranslate(); + + return styled(useStyles(formStyles))( + <> + { !embedded && ( + + controller.onChange('host', value)} + disabled={controller.isSaving} + mod='surface' + > + {translate('customConnection_custom_host')} + {translate('customConnection_custom_obligatory')} + + controller.onChange('port', value)} + disabled={controller.isSaving} + {...use({ short: true })} + mod='surface' + > + {translate('customConnection_custom_port')} + + + )} + + controller.onChange('databaseName', value)} + disabled={controller.isSaving} + 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/ConnectionsTable/ConnectionForm/formStyles.ts new file mode 100644 index 0000000000..77d46a985b --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/formStyles.ts @@ -0,0 +1,46 @@ +/* + * 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 { css } from 'reshadow'; + +export const formStyles = css` + connection-form { + flex: 1; + display: flex; + flex-direction: row; + padding: 18px 36px; + } + left-box { + display: flex; + flex-direction: column; + } + right-box { + flex: 1; + display: flex; + flex-direction: column; + } + connection-type { + margin-left: 150px; + } + Radio { + composes: theme-typography--body1 from global; + } + sub-label { + composes: theme-typography--caption from global; + line-height: 14px; + } + group { + box-sizing: border-box; + display: flex; + margin: 0 12px; + } + InputField[|short] { + min-width: unset; + max-width: 250px; + } +`; 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 5fa1c8c521..554c1bccc0 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx @@ -30,12 +30,17 @@ const styles = composes( type Props = { connections: ConnectionInfo[]; selectedItems: Map; + expandedItems: Map; } -export const ConnectionsTable = observer(function ConnectionsTable({ connections, selectedItems }: Props) { +export const ConnectionsTable = observer(function ConnectionsTable({ + connections, + selectedItems, + expandedItems, +}: Props) { const translate = useTranslate(); return styled(useStyles(styles))( - +
{translate('connections_connection_name')} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx new file mode 100644 index 0000000000..e419db45c4 --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverProperties.tsx @@ -0,0 +1,64 @@ +/* + * 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 { useEffect } 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` + properties { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + } +`; + +type DriverPropertyState = { + [key: string]: string; +} + +type DriverPropertiesProps = { + driver: DBDriver; + state: DriverPropertyState; + loadProperties: boolean; +} + +export const DriverProperties = observer(function DriverProperties({ + driver, + state, + loadProperties, +}: DriverPropertiesProps) { + const controller = useController(DriverPropertiesController, driver); + + useEffect(() => { + if (loadProperties) { + controller.loadDriverProperties(); + } + }, [loadProperties]); + + return styled(useStyles(styles))( + + {controller.isLoading && } + {!controller.isLoading && ( + + )} + + ); +}); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverPropertiesController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverPropertiesController.ts new file mode 100644 index 0000000000..b9af35fabf --- /dev/null +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/DriverProperties/DriverPropertiesController.ts @@ -0,0 +1,79 @@ +/* + * 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 } from 'mobx'; + +import { IProperty } from '@cloudbeaver/core-blocks'; +import { injectable, IInitializableController } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; + +import { DBDriver } from '../../../../DBDriverResource'; +import { DriverPropertiesService } from '../../../../DriverPropertiesService'; + +export type DriverPropertyState = { + [key: string]: string; +} + +type StaticId = { + staticId: string; +} + +export type DriverPropertyInfoWithStaticId = ObjectPropertyInfo & StaticId + +@injectable() +export class DriverPropertiesController implements IInitializableController { + @observable isLoading = false; + @observable driver!: DBDriver + @observable hasDetails = false + @observable responseMessage: string | null = null + @observable driverProperties = observable([]) + + private loaded = false; + + constructor( + private driverPropertiesService: DriverPropertiesService, + private notificationService: NotificationService + ) { } + + init(driver: DBDriver) { + this.driver = driver; + } + + onAddProperty = () => { + this.driverProperties.unshift({ + id: uuid(), + key: 'property', + defaultValue: '', + }); + } + + async loadDriverProperties() { + if (this.isLoading || this.loaded) { + return; + } + this.isLoading = true; + try { + const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driver.id); + this.driverProperties = observable(driverProperties.map(property => ({ + id: property.id!, + key: property.id!, + displayName: property.displayName!, + defaultValue: property.defaultValue, + description: property.description, + validValues: property.validValues, + }))); + this.loaded = true; + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load driver properties'); + } finally { + this.isLoading = false; + } + } +} diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index b4a4dafb01..8853e141bb 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -9,42 +9,94 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, - CachedDataResource, ConnectionInfo, ConnectionConfig, + CachedMapResource, + ResourceKey, + isResourceKeyList, } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; @injectable() -export class ConnectionsResource extends CachedDataResource { +export class ConnectionsResource extends CachedMapResource { constructor( private graphQLService: GraphQLService, ) { - super([]); + super(new Map()); } - isLoaded(id?: string) { - return id - ? this.data.some(connection => connection.id === id) - : !!this.data.length; + isNew(id: string) { + return id.startsWith('new-'); } - async create(config: ConnectionConfig) { + addNew() { + const connectionInfo = { + id: `new-${uuid()}`, + name: 'New connection', + } as ConnectionInfo; + + this.data.set(connectionInfo.id, connectionInfo); + this.markUpdated(connectionInfo.id); + + return connectionInfo; + } + + async loadAll() { + await this.load('all'); + return this.data; + } + + async create(config: ConnectionConfig, id?: string) { const { connection } = await this.graphQLService.gql.createConnectionConfiguration({ config }); - this.data.push(connection as ConnectionInfo); + if (id) { + this.data.delete(id); + } + this.data.set(connection.id, connection as ConnectionInfo); - return connection as ConnectionInfo; + return this.get(connection.id)!; } - async delete(id: string) { - await this.graphQLService.gql.deleteConnectionConfiguration({ id }); - - this.data.splice(this.data.findIndex(connection => connection.id === id), 1); + async update(id: string, config: ConnectionConfig) { + await this.performUpdate(id, async () => { + await this.setActivePromise(id, this.updateConnection(id, config)); + }); + return this.get(id)!; } - protected async loader(id?: string): Promise { + async delete(key: ResourceKey) { + if (isResourceKeyList(key)) { + for (let i = 0; i < key.list.length; i++) { + this.data.delete(key.list[i]); + if (!this.isNew(key.list[i])) { + await this.graphQLService.gql.deleteConnectionConfiguration({ id: key.list[i] }); + } + } + } else { + this.data.delete(key); + if (!this.isNew(key)) { + await this.graphQLService.gql.deleteConnectionConfiguration({ id: key }); + } + } + this.markUpdated(key); + this.itemDeleteSubject.next(key); + } + + protected async loader(key: ResourceKey): Promise> { const { connections } = await this.graphQLService.gql.getConnections(); + this.data.clear(); - return connections as ConnectionInfo[]; + for (const connection of connections) { + this.set(connection.id, connection as ConnectionInfo); + } + this.markUpdated(key); + + return this.data; + } + + private async updateConnection(id: string, config: ConnectionConfig) { + const { connection } = await this.graphQLService.gql.updateConnectionConfiguration({ id, config }); + + this.set(id, connection as ConnectionInfo); } } diff --git a/webapp/packages/core-connections/src/DBDriverResource.ts b/webapp/packages/core-connections/src/DBDriverResource.ts index 32c7d67a53..3baf07217b 100644 --- a/webapp/packages/core-connections/src/DBDriverResource.ts +++ b/webapp/packages/core-connections/src/DBDriverResource.ts @@ -47,7 +47,7 @@ export class DBDriverResource extends CachedMapResource { for (const driver of driverList) { this.data.set(driver.id, driver); } - this.data.set('all', {} as any); + // this.data.set('all', {} as any); this.markUpdated(key); return this.data; } diff --git a/webapp/packages/core-connections/src/DriverPropertiesService.ts b/webapp/packages/core-connections/src/DriverPropertiesService.ts new file mode 100644 index 0000000000..e827c6a670 --- /dev/null +++ b/webapp/packages/core-connections/src/DriverPropertiesService.ts @@ -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 { injectable } from '@cloudbeaver/core-di'; +import { GraphQLService, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; + +@injectable() +export class DriverPropertiesService { + + constructor( + private graphQLService: GraphQLService, + ) { + } + + async loadDriverProperties(driverId: string): Promise { + const response = await this.graphQLService.gql.driverProperties({ + driverId, + }); + + if (response.driver.length === 0) { + throw new Error('Driver properties loading failed'); + } + + return response.driver[0].driverProperties! as ObjectPropertyInfo[]; + } +} diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index 558f3b98ee..a3a784c5e5 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -7,6 +7,7 @@ export * from './extensions/IConnectionSetter'; export * from './ConnectionsManagerService'; export * from './ConnectionInfoResource'; export * from './ConnectionAuthService'; +export * from './DriverPropertiesService'; export * from './EConnectionFeature'; export * from './ContainerResource'; export * from './ConnectionsLocaleService'; diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index 4b493955f5..d37fe37951 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -1,6 +1,12 @@ export default [ ['connections_administration_item', 'Connection Management'], + ['connections_connection_edit_save', 'Save'], + ['connections_connection_edit_add', 'Add'], + ['connections_connection_edit_cancel', 'Cancel'], + ['connections_connection_edit_authentication', 'Authentication'], ['connections_connection_name', 'Name'], + ['connections_connection_description', 'Description'], + ['connections_connection_driver', 'Driver'], ['connections_connection_host', 'Host'], ['connections_connection_port', 'Port'], ['connections_connection_in_tree', 'Show in navigation tree'], diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 67e4974bd3..2a23bc24f0 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -1,6 +1,12 @@ export default [ ['connections_administration_item', 'Управление подключениями'], + ['connections_connection_edit_save', 'Сохранить'], + ['connections_connection_edit_add', 'Добавить'], + ['connections_connection_edit_cancel', 'Отменить'], + ['connections_connection_edit_authentication', 'Авторизация'], ['connections_connection_name', 'Название'], + ['connections_connection_description', 'Описапние'], + ['connections_connection_driver', 'Driver'], ['connections_connection_host', 'Хост'], ['connections_connection_port', 'Порт'], ['connections_connection_in_tree', 'Показывать в дереве навигации'], diff --git a/webapp/packages/core-di/src/useController.ts b/webapp/packages/core-di/src/useController.ts index 805a965a33..80af4427c9 100644 --- a/webapp/packages/core-di/src/useController.ts +++ b/webapp/packages/core-di/src/useController.ts @@ -21,33 +21,29 @@ export function useController( ): T; export function useController(ctor: IServiceConstructor): T; export function useController(ctor: IServiceConstructor, ...args: any[]): T { - const app = useContext(appContext); - const isInitialized = useRef(false); - const [controller] = useState(() => { + const controllerRef = useRef(); + + useMemo(() => { + if (controllerRef.current && isDestructibleController(controllerRef.current)) { + controllerRef.current.destruct(); + } + const controller = app.resolveServiceByClass(ctor); if (isInitializableController(controller)) { controller.init(...args); } - - return controller; - }); - - useMemo(() => { - if (isInitialized.current) { - throw new Error(`${ctor.name}: Controller initialization arguments can't be updated`); - } - isInitialized.current = true; + controllerRef.current = controller; }, [...args]); useEffect(() => () => { - if (isDestructibleController(controller)) { - controller.destruct(); + if (isDestructibleController(controllerRef.current)) { + controllerRef.current.destruct(); } }, []); - return controller as T; + return controllerRef.current!; } function isDestructibleController(obj: any): obj is IDestructibleController { diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/updateConnectionConfiguration.gql b/webapp/packages/core-sdk/src/queries/connections/administration/updateConnectionConfiguration.gql new file mode 100644 index 0000000000..d56ea910d2 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/administration/updateConnectionConfiguration.gql @@ -0,0 +1,23 @@ +query updateConnectionConfiguration($id: ID!, $config: ConnectionConfig!) { + connection: updateConnectionConfiguration(id: $id, config: $config) { + id + name + description + driverId + + template + connected + readOnly + + host + port + databaseName + url + properties + + features + + authNeeded + authModel + } +} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index debb02bc9a..ee5e8431f3 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -498,6 +498,7 @@ export type ConnectionConfig = { properties?: Maybe; template?: Maybe; readOnly?: Maybe; + saveCredentials?: Maybe; authModelId?: Maybe; credentials?: Maybe; dataSourceId?: Maybe; @@ -838,6 +839,13 @@ export type GetConnectionsQueryVariables = Exact<{ [key: string]: never }>; export type GetConnectionsQuery = { connections: Array> }; +export type UpdateConnectionConfigurationQueryVariables = Exact<{ + id: Scalars['ID']; + config: ConnectionConfig; +}>; + +export type UpdateConnectionConfigurationQuery = { connection: Pick }; + export type CloseConnectionMutationVariables = Exact<{ id: Scalars['ID']; }>; @@ -1343,6 +1351,27 @@ export const GetConnectionsDocument = ` } } `; +export const UpdateConnectionConfigurationDocument = ` + query updateConnectionConfiguration($id: ID!, $config: ConnectionConfig!) { + connection: updateConnectionConfiguration(id: $id, config: $config) { + id + name + description + driverId + template + connected + readOnly + host + port + databaseName + url + properties + features + authNeeded + authModel + } +} + `; export const CloseConnectionDocument = ` mutation closeConnection($id: ID!) { connection: closeConnection(id: $id) { @@ -2021,6 +2050,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getConnections(variables?: GetConnectionsQueryVariables): Promise { return withWrapper(() => client.request(GetConnectionsDocument, variables)); }, + updateConnectionConfiguration(variables: UpdateConnectionConfigurationQueryVariables): Promise { + return withWrapper(() => client.request(UpdateConnectionConfigurationDocument, variables)); + }, closeConnection(variables: CloseConnectionMutationVariables): Promise { return withWrapper(() => client.request(CloseConnectionDocument, variables)); }, diff --git a/webapp/packages/core-theming/src/styles/_form-controls.scss b/webapp/packages/core-theming/src/styles/_form-controls.scss index d85dd88206..9c562357c4 100644 --- a/webapp/packages/core-theming/src/styles/_form-controls.scss +++ b/webapp/packages/core-theming/src/styles/_form-controls.scss @@ -19,8 +19,8 @@ @include mdc-theme-prop(border-color, background, false); &:global([type="checkbox"]) { - height: 26px; - width: 14px; + height: 16px; + width: 16px; } &:global([disabled]), @@ -36,6 +36,7 @@ label { display: block; + line-height: 15px; box-sizing: border-box; padding: 0; overflow: hidden; diff --git a/webapp/packages/core-theming/src/styles/_mixins.scss b/webapp/packages/core-theming/src/styles/_mixins.scss index 56b75e78eb..05d3f69804 100644 --- a/webapp/packages/core-theming/src/styles/_mixins.scss +++ b/webapp/packages/core-theming/src/styles/_mixins.scss @@ -11,6 +11,7 @@ @import '@material/button/mixins'; @import "@material/elevation/mixins"; @import "@material/typography/mixins"; +@import 'variables'; @import 'branding'; @import 'theme-ripple'; @import 'ripple'; diff --git a/webapp/packages/core-theming/src/styles/_table.scss b/webapp/packages/core-theming/src/styles/_table.scss index 07f926135f..ac0b485b3b 100644 --- a/webapp/packages/core-theming/src/styles/_table.scss +++ b/webapp/packages/core-theming/src/styles/_table.scss @@ -20,17 +20,21 @@ @include mdc-theme-prop(border-color, background, false); } - tbody tr:hover { - @include stripes-background($mdc-theme-secondary); - } + tbody tr { + &:focus { + @include mdc-theme-prop(border-color, background, false); + } - tbody tr:focus { - @include mdc-theme-prop(border-color, background, false); - } + &:not([use|noHover]):hover, + &:not([use|noHover])[use|selected], + &:not([use|noHover])[use|expanded] { + @include stripes-background($mdc-theme-secondary); + } - tbody tr[use|isSelected] { - @include stripes-background($mdc-theme-secondary); - // @include mdc-theme-prop(border-color, $color-positive, false); + &[use|expanded] td { + padding-top: 24px; + padding-bottom: 24px; + } } th { @@ -44,29 +48,50 @@ text-transform: uppercase; text-align: left; text-decoration: none !important; - } - th:last-child { - border-right: none; - } + &[use|min] { + width: 1%; + } - th[use|min] { - width: 1%; + &:last-child { + border-right: none; + } } td { - height: 24px; + height: 28px; padding: 0 16px; + transition: padding ease-in-out 0.24s; + + &[use|centerContent] { + align-items: center; + vertical-align: middle; + } + + &[use|flex] { + display: flex; + } + + &[use|expandArea] { + padding: 0; + } + + > input[type="checkbox"] { + display: block; + height: 16px; + width: 16px; + } } - td[use|centerContent] { - align-items: center; - vertical-align: middle; - } - - td > input[type="checkbox"] { - display: block; - height: 16px; + TableItemExpand { + cursor: pointer; + margin-left: 16px; width: 16px; + height: 100%; + padding: 0; + + &[|expanded] { + transform: rotate(180deg); + } } } diff --git a/webapp/packages/core-theming/src/styles/_variables.scss b/webapp/packages/core-theming/src/styles/_variables.scss new file mode 100644 index 0000000000..f7fcd56a05 --- /dev/null +++ b/webapp/packages/core-theming/src/styles/_variables.scss @@ -0,0 +1,15 @@ +/* + * 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. + */ + +$mdc-theme-extra-property-values: ( + color-positive: $color-positive, + color-negative: $color-negative, + input-border-color: $input-border-color, +); + +$mdc-theme-property-values: map-merge($mdc-theme-property-values, $mdc-theme-extra-property-values); \ No newline at end of file diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx index 0530549c51..f54e4e15c0 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx @@ -135,7 +135,7 @@ export const CreateUserDialog = observer(function CreateUserDialog({ {controller.roles.map((role, i) => ( handleRoleChange(role.roleId, checked)} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts index 7bf51f93aa..9a1d574942 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts @@ -9,14 +9,12 @@ import { observable } from 'mobx'; import { IProperty } from '@cloudbeaver/core-blocks'; -import { DBDriver } from '@cloudbeaver/core-connections'; +import { DBDriver, DriverPropertiesService } from '@cloudbeaver/core-connections'; import { injectable, IInitializableController } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { uuid } from '@cloudbeaver/core-utils'; -import { CustomConnectionService } from '../../../CustomConnectionService'; - export type DriverPropertyState = { [key: string]: string; } @@ -37,8 +35,10 @@ export class DriverPropertiesController implements IInitializableController { private loaded = false; - constructor(private customConnectionService: CustomConnectionService, - private notificationService: NotificationService) { } + constructor( + private driverPropertiesService: DriverPropertiesService, + private notificationService: NotificationService + ) { } init(driver: DBDriver) { this.driver = driver; @@ -58,7 +58,7 @@ export class DriverPropertiesController implements IInitializableController { } this.isLoading = true; try { - const driverProperties = await this.customConnectionService.loadDriverProperties(this.driver.id); + const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driver.id); this.driverProperties = observable(driverProperties.map(property => ({ id: property.id!, key: property.id!, diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionService.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionService.ts index f9a119f557..a8ba449118 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionService.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionService.ts @@ -8,7 +8,7 @@ import { ConnectionsManagerService, Connection } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { ConnectionConfig, GraphQLService, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { ConnectionConfig, GraphQLService } from '@cloudbeaver/core-sdk'; @injectable() export class CustomConnectionService { @@ -19,18 +19,6 @@ export class CustomConnectionService { ) { } - async loadDriverProperties(driverId: string): Promise { - const response = await this.graphQLService.gql.driverProperties({ - driverId, - }); - - if (response.driver.length === 0) { - throw new Error('Driver properties loading failed'); - } - - return response.driver[0].driverProperties! as ObjectPropertyInfo[]; - } - async createConnectionAsync(config: ConnectionConfig): Promise { const response = await this.graphQLService.gql.createConnection({ config,