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
>
)}
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({
) : (
))}