mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #153 from dbeaver/refactor-connections-administration
refactor: connections administration redesign
This commit is contained in:
@@ -20,18 +20,19 @@ import { Styles } from './styles';
|
||||
type Props = React.PropsWithChildren<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
onSearch?(value: string): void;
|
||||
className?: string;
|
||||
}>
|
||||
|
||||
export function ItemListSearch({
|
||||
value, placeholder, onSearch, className,
|
||||
value, placeholder, disabled, onSearch, className,
|
||||
}: Props) {
|
||||
const styles = useContext(Styles);
|
||||
const [search, setSearch] = useState(value ?? '');
|
||||
const translate = useTranslate();
|
||||
const searchHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (value !== undefined) {
|
||||
if (value === undefined) {
|
||||
setSearch(event.target.value);
|
||||
}
|
||||
if (onSearch) {
|
||||
@@ -48,6 +49,7 @@ export function ItemListSearch({
|
||||
value={value ?? search}
|
||||
onChange={searchHandler}
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
{...use({ mod: 'surface' })}
|
||||
/>
|
||||
</list-search>
|
||||
|
||||
@@ -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))(
|
||||
<list-item as="div" onClick={onClick} className={className}>
|
||||
<list-item-icon as="div"><StaticImage icon={icon}/></list-item-icon>
|
||||
<list-item-name as="div">{name}</list-item-name>
|
||||
<list-item-description as="div" title={description}>{description}</list-item-description>
|
||||
{children}
|
||||
</list-item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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))(
|
||||
<list-item-description as="div" title={title} className={className}>{children}</list-item-description>
|
||||
);
|
||||
}
|
||||
@@ -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))(
|
||||
<list-item-icon as="div" className={className}>{children}</list-item-icon>
|
||||
);
|
||||
}
|
||||
@@ -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))(
|
||||
<list-item-name as="div" className={className}>{children}</list-item-name>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string, string | number>;
|
||||
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))(<center as="div">Properties empty</center>);
|
||||
@@ -49,10 +43,9 @@ export const ObjectPropertyInfoForm = observer(function ObjectPropertyInfoForm({
|
||||
<group as="div" key={property.id}>
|
||||
<InputField
|
||||
type={property.features.includes('password') ? 'password' : 'text'}
|
||||
name={`${prefix}_${property.id}`}
|
||||
value={credentials[property.id!]}
|
||||
onChange={value => handleChange(property.id!, value)}
|
||||
disabled={processing}
|
||||
name={property.id!}
|
||||
state={credentials}
|
||||
disabled={disabled}
|
||||
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
|
||||
mod='surface'
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
+33
-33
@@ -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<string, boolean>;
|
||||
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))(
|
||||
<center as='div'>{translate('authentication_administration_user_connections_empty')}</center>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSelect = useCallback((item: string, state: boolean) => {
|
||||
controller.onSelect(item, state);
|
||||
if (onChange) {
|
||||
onChange();
|
||||
}
|
||||
}, [onChange, controller]);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<box as='div'>
|
||||
<Table selectedItems={selectedSubjects} onSelect={onChange} className={className}>
|
||||
<Table selectedItems={controller.selectedSubjects} onSelect={handleSelect} className={className}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader min/>
|
||||
<TableColumnHeader>{translate('connections_connection_name')}</TableColumnHeader>
|
||||
<TableColumnHeader></TableColumnHeader>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{roles.map(role => (
|
||||
{controller.roles.map(role => (
|
||||
<TableItem key={role.roleId} item={role.roleId} selectDisabled={disabled}>
|
||||
<TableColumnValue centerContent flex>
|
||||
<TableItemSelect disabled={disabled} checked={disabled}/>
|
||||
<TableItemSelect disabled={disabled}/>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue>{role.roleName}</TableColumnValue>
|
||||
<TableColumnValue></TableColumnValue>
|
||||
</TableItem>
|
||||
))}
|
||||
{users.map((user) => {
|
||||
const connectionPermission = getSubjectPermission(user.userId);
|
||||
const isRoleProvided = connectionPermission?.subjectType === AdminSubjectType.Role;
|
||||
|
||||
return (
|
||||
<TableItem key={user.userId} item={user.userId} selectDisabled={disabled || isRoleProvided}>
|
||||
<TableColumnValue centerContent flex>
|
||||
<TableItemSelect disabled={disabled || isRoleProvided} checked={disabled || isRoleProvided}/>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue>{user.userId}</TableColumnValue>
|
||||
<TableColumnValue></TableColumnValue>
|
||||
</TableItem>
|
||||
);
|
||||
})}
|
||||
{controller.users.map(user => (
|
||||
<TableItem key={user.userId} item={user.userId} selectDisabled={disabled}>
|
||||
<TableColumnValue centerContent flex>
|
||||
<TableItemSelect disabled={disabled}/>
|
||||
</TableColumnValue>
|
||||
<TableColumnValue>{user.userId}</TableColumnValue>
|
||||
<TableColumnValue></TableColumnValue>
|
||||
</TableItem>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</box>
|
||||
+67
@@ -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<string, boolean> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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;
|
||||
onBack?(): void;
|
||||
onCancel?(): void;
|
||||
}
|
||||
|
||||
export const ConnectionForm = observer(function ConnectionForm({
|
||||
model,
|
||||
configurationWizard,
|
||||
onBack = () => {},
|
||||
onCancel = () => {},
|
||||
}: Props) {
|
||||
const controller = useController(Controller, model, onCancel);
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<TabsState selectedId='options'>
|
||||
<box as='div'>
|
||||
<TabList>
|
||||
<Tab tabId='options' >
|
||||
<TabTitle>{translate('customConnection_options')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='driver_properties' disabled={!controller.driver}>
|
||||
<TabTitle>{translate('customConnection_properties')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='access' disabled={!controller.driver || configurationWizard}>
|
||||
<TabTitle>{translate('connections_connection_edit_access')}</TabTitle>
|
||||
</Tab>
|
||||
<fill as="div" />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isDisabled}
|
||||
mod={['outlined']}
|
||||
onClick={onBack}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isDisabled}
|
||||
mod={['unelevated']}
|
||||
onClick={controller.save}
|
||||
>
|
||||
{translate(!model.editing ? 'ui_processing_create' : 'ui_processing_save')}
|
||||
</Button>
|
||||
</TabList>
|
||||
<content-box as="div">
|
||||
{controller.isLoading
|
||||
? <Loader />
|
||||
: (
|
||||
<>
|
||||
<TabPanel tabId='options'>
|
||||
<Options
|
||||
connection={model.connection}
|
||||
type={controller.connectionType}
|
||||
credentials={model.credentials}
|
||||
availableDrivers={model.availableDrivers}
|
||||
editing={model.editing}
|
||||
disabled={controller.isDisabled}
|
||||
onTypeChange={controller.setType}
|
||||
onSave={controller.save}
|
||||
/>
|
||||
</TabPanel>
|
||||
{model.connection.driverId && (
|
||||
<TabPanel tabId='driver_properties'>
|
||||
{state => (
|
||||
<DriverProperties
|
||||
driverId={model.connection.driverId}
|
||||
state={model.connection.properties}
|
||||
loadProperties={state.selectedId === 'driver_properties'}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel tabId='access'>
|
||||
{(state) => {
|
||||
if (state.selectedId === 'access') {
|
||||
controller.loadAccessSubjects();
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectionAccess
|
||||
model={model}
|
||||
disabled={controller.isDisabled}
|
||||
onChange={controller.handleAccessChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</TabPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{controller.error.responseMessage && (
|
||||
<ErrorMessage
|
||||
text={controller.error.responseMessage}
|
||||
hasDetails={controller.error.hasDetails}
|
||||
onShowDetails={controller.onShowDetails}
|
||||
/>
|
||||
)}
|
||||
</content-box>
|
||||
</box>
|
||||
</TabsState>
|
||||
);
|
||||
});
|
||||
+177
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-13
@@ -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<string, string>;
|
||||
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))(
|
||||
<properties as="div">
|
||||
{controller.isLoading && <Loader />}
|
||||
{!controller.isLoading && (
|
||||
{!controller.isLoading && controller.loaded && (
|
||||
<PropertiesTable
|
||||
properties={controller.driverProperties}
|
||||
propertiesState={state}
|
||||
+5
-10
@@ -14,13 +14,8 @@ 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;
|
||||
}
|
||||
@@ -30,20 +25,20 @@ 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<IProperty>([])
|
||||
@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!,
|
||||
+12
@@ -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'
|
||||
}
|
||||
+17
@@ -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<string, string | number>;
|
||||
grantedSubjects: AdminConnectionGrantInfo[] | null;
|
||||
availableDrivers: string[];
|
||||
editing?: boolean;
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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<string, string | number>;
|
||||
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<HTMLFormElement>({ focusFirstChild: true });
|
||||
|
||||
return styled(useStyles(styles, formStyles))(
|
||||
<SubmittingForm ref={focusedRef} onChange={controller.onFormChange} onSubmit={onSave}>
|
||||
<box as="div">
|
||||
<box-element as='div'>
|
||||
<group as="div">
|
||||
<Checkbox
|
||||
name="template"
|
||||
value={connection.id}
|
||||
state={connection}
|
||||
checkboxLabel={translate('connections_connection_template')}
|
||||
disabled={editing || disabled}
|
||||
mod='surface'
|
||||
/>
|
||||
</group>
|
||||
<group as="div">
|
||||
<Combobox
|
||||
name='driverId'
|
||||
state={connection}
|
||||
items={controller.drivers}
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver?.name!}
|
||||
onSelect={controller.onSelectDriver}
|
||||
readOnly={editing || controller.drivers.length < 2}
|
||||
mod={'surface'}
|
||||
disabled={disabled}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
</Combobox>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type="text"
|
||||
name="name"
|
||||
state={connection}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<Textarea
|
||||
name="description"
|
||||
rows={3}
|
||||
state={connection}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('connections_connection_description')}
|
||||
</Textarea>
|
||||
</group>
|
||||
</box-element>
|
||||
<box-element as='div'>
|
||||
<connection-type as="div">
|
||||
<RadioGroup name='type' value={type} onChange={onTypeChange}>
|
||||
<Radio value={EConnectionType.Parameters} disabled={disabled} mod={['primary']}>
|
||||
{translate('customConnection_connectionType_custom')}
|
||||
</Radio>
|
||||
<Radio value={EConnectionType.URL} disabled={disabled} mod={['primary']}>
|
||||
{translate('customConnection_connectionType_url')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</connection-type>
|
||||
<TabsState currentTabId={type}>
|
||||
<TabPanel tabId={EConnectionType.Parameters}>
|
||||
<ParametersForm
|
||||
connection={connection}
|
||||
embedded={controller.driver?.embedded}
|
||||
disabled={disabled || saving}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel tabId={EConnectionType.URL}>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type="text"
|
||||
name="url"
|
||||
state={connection}
|
||||
disabled={disabled}
|
||||
autoComplete={`section-${controller.driver?.id || 'driver'} section-jdbc`}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('customConnection_url_JDBC')}
|
||||
</InputField>
|
||||
</group>
|
||||
</TabPanel>
|
||||
</TabsState>
|
||||
{(controller.authModel && !controller.driver?.anonymousAccess) && (
|
||||
<>
|
||||
<group as="div">
|
||||
<InputGroup>{translate('connections_connection_edit_authentication')}</InputGroup>
|
||||
</group>
|
||||
<ObjectPropertyInfoForm
|
||||
autofillToken='new-password'
|
||||
properties={controller.authModel.properties}
|
||||
credentials={credentials}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</box-element>
|
||||
</box>
|
||||
</SubmittingForm>
|
||||
);
|
||||
});
|
||||
+186
@@ -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<string, number | string>;
|
||||
@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<string, number | string>, 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-14
@@ -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 && (
|
||||
<layout-grid-inner as="div">
|
||||
@@ -35,9 +43,8 @@ export const ParametersForm = observer(function ParametersForm({
|
||||
<InputField
|
||||
type="text"
|
||||
name="host"
|
||||
value={controller.config.host}
|
||||
onChange={value => 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({
|
||||
<InputField
|
||||
type="number"
|
||||
name="port"
|
||||
value={controller.config.port}
|
||||
onChange={value => 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({
|
||||
<InputField
|
||||
type="text"
|
||||
name="databaseName"
|
||||
value={controller.config.databaseName}
|
||||
onChange={value => controller.onChange('databaseName', value)}
|
||||
disabled={controller.isSaving}
|
||||
state={connection}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('customConnection_custom_database')}
|
||||
+10
-26
@@ -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))(
|
||||
<layout-grid as="div">
|
||||
<layout-grid-inner as="div">
|
||||
<layout-grid-cell as='div' {...use({ span: 12 })}>
|
||||
<AdministrationTools>
|
||||
<actions as='div'>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isLoading}
|
||||
mod={['outlined']}
|
||||
onClick={controller.findDatabase}
|
||||
>
|
||||
{translate('connections_connection_edit_search')}
|
||||
</Button>
|
||||
</actions>
|
||||
<IconButton name="add" viewBox="0 0 28 28" onClick={controller.create} />
|
||||
<IconButton name="trash" viewBox="0 0 28 28" onClick={controller.delete} />
|
||||
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
|
||||
</AdministrationTools>
|
||||
{controller.isSearching && (
|
||||
<DatabasesSearch
|
||||
hosts={controller.hosts}
|
||||
onChange={controller.onSearchChange}
|
||||
onSearch={controller.search}
|
||||
disabled={controller.isLoading}
|
||||
{sub && (
|
||||
<CreateConnection
|
||||
method={param || 'driver'}
|
||||
configurationWizard={configurationWizard}
|
||||
onChange={controller.setCreateMethod}
|
||||
onCancel={controller.cancelCreate}
|
||||
/>
|
||||
)}
|
||||
<ConnectionsTable
|
||||
connections={controller.connections}
|
||||
findConnections={controller.findConnections}
|
||||
selectedItems={controller.selectedItems}
|
||||
expandedItems={controller.expandedItems}
|
||||
/>
|
||||
|
||||
+5
-51
@@ -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<string, boolean>(new Map())
|
||||
readonly expandedItems = observable<string, boolean>(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 {
|
||||
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
+8
-14
@@ -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<void> { }
|
||||
|
||||
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();
|
||||
|
||||
+3
-21
@@ -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))(
|
||||
<TableItem item={connection.id} expandElement={ConnectionEdit}>
|
||||
@@ -64,18 +52,12 @@ export const Connection = observer(function Connection({ connection }: Props) {
|
||||
<TableItemExpand />
|
||||
</TableColumnValue>
|
||||
<TableColumnValue centerContent flex expand>
|
||||
{icons.map(icon => <StaticImage key={icon} icon={icon} />)}
|
||||
<StaticImage icon={icon} />
|
||||
</TableColumnValue>
|
||||
<TableColumnValue expand>{connection.name}</TableColumnValue>
|
||||
<TableColumnValue>{connection.host}{connection.host && connection.port && `:${connection.port}`}</TableColumnValue>
|
||||
<TableColumnValue><input type="checkbox" checked={connection.template} disabled/></TableColumnValue>
|
||||
<TableColumnValue align='right'>
|
||||
{isNew && (
|
||||
<tag as='div' {...use({ mod: 'positive' })}>
|
||||
{translate('ui_tag_new')}
|
||||
</tag>
|
||||
)}
|
||||
</TableColumnValue>
|
||||
<TableColumnValue/>
|
||||
</TableItem>
|
||||
);
|
||||
});
|
||||
|
||||
+14
-160
@@ -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))(
|
||||
<TabsState selectedId='options'>
|
||||
<box as='div'>
|
||||
<TabList>
|
||||
<Tab tabId='options' >
|
||||
<TabTitle>{translate('customConnection_options')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='driver_properties' onOpen={() => setLoadProperties(true)} disabled={!controller.driver} >
|
||||
<TabTitle>{translate('customConnection_properties')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='access' onOpen={controller.loadAccessSubjects} disabled={!controller.driver} >
|
||||
<TabTitle>{translate('connections_connection_edit_access')}</TabTitle>
|
||||
</Tab>
|
||||
<fill as="div" />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isDisabled}
|
||||
mod={['outlined']}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isDisabled}
|
||||
mod={['unelevated']}
|
||||
onClick={controller.onSaveConnection}
|
||||
>
|
||||
{translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')}
|
||||
</Button>
|
||||
</TabList>
|
||||
<content-box as="div">
|
||||
{controller.isLoading
|
||||
? <Loader />
|
||||
: (
|
||||
<SubmittingForm onSubmit={controller.onSaveConnection} name='connection_edit'>
|
||||
<TabPanel tabId='options'>
|
||||
<ConnectionForm controller={controller} />
|
||||
</TabPanel>
|
||||
{controller.driver && (
|
||||
<TabPanel tabId='driver_properties'>
|
||||
<DriverProperties
|
||||
driver={controller.driver}
|
||||
state={controller.config.properties!}
|
||||
loadProperties={loadProperties}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel tabId='access'>
|
||||
<GrantedSubjects
|
||||
grantedSubjects={controller.grantedSubjects}
|
||||
users={controller.users}
|
||||
roles={controller.roles}
|
||||
selectedSubjects={controller.selectedSubjects}
|
||||
disabled={controller.isLoading || controller.isSaving}
|
||||
onChange={controller.handleAccessChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
</SubmittingForm>
|
||||
)
|
||||
}
|
||||
{controller.error.responseMessage && (
|
||||
<ErrorMessage
|
||||
text={controller.error.responseMessage}
|
||||
hasDetails={controller.error.hasDetails}
|
||||
onShowDetails={controller.onShowDetails}
|
||||
/>
|
||||
)}
|
||||
</content-box>
|
||||
</box>
|
||||
</TabsState>
|
||||
<box as='div'>
|
||||
{controller.connection && (
|
||||
<ConnectionForm model={controller as IConnectionFormModel} onBack={collapse} onCancel={collapse}/>
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
});
|
||||
|
||||
+22
-293
@@ -6,342 +6,71 @@
|
||||
* 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, ConnectionInfo } 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<string, string> = {};
|
||||
@observable connection: ConnectionInfo | null = null;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
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<string, boolean>(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();
|
||||
}
|
||||
|
||||
destruct(): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConnectionInfo() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
await this.connectionsResource.load(this.connectionId);
|
||||
|
||||
this.connectionInfo = this.connectionsResource.get(this.connectionId)!;
|
||||
await this.loadDriver(this.connectionInfo.driverId);
|
||||
// we create a copy to protect the current value from mutation
|
||||
this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId)));
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, `Can't load ConnectionInfo ${this.connectionId}`);
|
||||
} finally {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-157
@@ -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<HTMLDivElement>({ focusFirstChild: true });
|
||||
|
||||
return styled(useStyles(styles, formStyles))(
|
||||
<connection-form as='div' ref={focusedRef}>
|
||||
<box as="div">
|
||||
<box-element as='div'>
|
||||
<group as="div">
|
||||
<Checkbox
|
||||
name="template"
|
||||
value={controller.connectionId}
|
||||
checkboxLabel={translate('connections_connection_template')}
|
||||
checked={controller.config.template}
|
||||
onChange={value => controller.onChange('template', value)}
|
||||
disabled={!controller.isNew || controller.isDisabled}
|
||||
mod='surface'
|
||||
/>
|
||||
</group>
|
||||
<group as="div">
|
||||
<Combobox
|
||||
value={controller.driver?.id}
|
||||
items={controller.drivers}
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver?.name!}
|
||||
onSelect={controller.onSelectDriver}
|
||||
readOnly={!controller.isSearched || controller.drivers.length < 2}
|
||||
mod={'surface'}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
</Combobox>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type="text"
|
||||
name="name"
|
||||
value={controller.config.name}
|
||||
onChange={value => controller.onChange('name', value)}
|
||||
disabled={controller.isDisabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<Textarea
|
||||
name="description"
|
||||
rows={3}
|
||||
value={controller.config.description}
|
||||
onChange={value => controller.onChange('description', value)}
|
||||
disabled={controller.isDisabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('connections_connection_description')}
|
||||
</Textarea>
|
||||
</group>
|
||||
</box-element>
|
||||
<box-element as='div'>
|
||||
<connection-type as="div">
|
||||
<Radio
|
||||
name="type"
|
||||
id={`${controller.connectionId}custom`}
|
||||
value={'custom'}
|
||||
onClick={() => controller.onChangeType(ConnectionType.Attributes)}
|
||||
checked={controller.connectionType === ConnectionType.Attributes}
|
||||
disabled={controller.isDisabled}
|
||||
mod={['primary']}
|
||||
>
|
||||
{translate('customConnection_connectionType_custom')}
|
||||
</Radio>
|
||||
<Radio
|
||||
name="type"
|
||||
id={`${controller.connectionId}url`}
|
||||
value={'url'}
|
||||
onClick={() => controller.onChangeType(ConnectionType.URL)}
|
||||
checked={controller.connectionType === ConnectionType.URL}
|
||||
disabled={controller.isDisabled}
|
||||
mod={['primary']}
|
||||
>
|
||||
{translate('customConnection_connectionType_url')}
|
||||
</Radio>
|
||||
</connection-type>
|
||||
{controller.connectionType === ConnectionType.Attributes ? (
|
||||
<ParametersForm controller={controller} embedded={controller.driver?.embedded} />
|
||||
) : (
|
||||
<group as="div">
|
||||
<InputField
|
||||
type="text"
|
||||
name="url"
|
||||
value={controller.config.url}
|
||||
onChange={value => controller.onChange('url', value)}
|
||||
disabled={controller.isDisabled}
|
||||
autoComplete={`section-${controller.driver?.id || 'driver'} section-jdbc`}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('customConnection_url_JDBC')}
|
||||
</InputField>
|
||||
</group>
|
||||
)}
|
||||
{controller.authModel && (
|
||||
<>
|
||||
<group as="div">
|
||||
<InputGroup>{translate('connections_connection_edit_authentication')}</InputGroup>
|
||||
</group>
|
||||
<ObjectPropertyInfoForm
|
||||
prefix={`auth_${controller.driver?.id || 'driver'}`}
|
||||
autofillToken={'off'}
|
||||
properties={controller.authModel.properties}
|
||||
credentials={controller.config.credentials}
|
||||
processing={controller.isDisabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</box-element>
|
||||
</box>
|
||||
</connection-form>
|
||||
);
|
||||
});
|
||||
-28
@@ -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<T extends keyof ConnectionConfig>(property: T, value: ConnectionConfig[T]): void;
|
||||
}
|
||||
+1
-6
@@ -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<string, boolean>;
|
||||
expandedItems: Map<string, boolean>;
|
||||
}
|
||||
|
||||
export const ConnectionsTable = observer(function ConnectionsTable({
|
||||
connections,
|
||||
findConnections,
|
||||
selectedItems,
|
||||
expandedItems,
|
||||
}: Props) {
|
||||
@@ -62,8 +59,6 @@ export const ConnectionsTable = observer(function ConnectionsTable({
|
||||
<TableColumnHeader></TableColumnHeader>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{findConnections.map(connection => <Connection key={connection.id} connection={connection}/>)}
|
||||
{!!findConnections.length && <TableItemSeparator key='search' colSpan={7}></TableItemSeparator>}
|
||||
{connections.map(connection => <Connection key={connection.id} connection={connection}/>)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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, Icon
|
||||
} 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 24px;
|
||||
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 {
|
||||
width: 32px;
|
||||
max-height: 32px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
fill {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
back-button {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
|
||||
& Icon {
|
||||
box-sizing: border-box;
|
||||
transform: rotate(90deg);
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
TabList {
|
||||
align-items: center;
|
||||
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))(
|
||||
<connection-create as='div'>
|
||||
<title-bar as='div'>
|
||||
<back-button as='div'><Icon name="angle" viewBox="0 0 15 8" onClick={controller.back}/></back-button>
|
||||
{controller.driver?.icon && <StaticImage icon={controller.driver.icon} />}
|
||||
{controller.driver?.name ?? translate('connections_administration_connection_create')}
|
||||
<fill as="div" />
|
||||
<IconButton name="cross" viewBox="0 0 16 16" onClick={onCancel} />
|
||||
</title-bar>
|
||||
<connection-create-content as='div'>
|
||||
<ConnectionForm
|
||||
model={controller as IConnectionFormModel}
|
||||
onBack={controller.back}
|
||||
onCancel={handleConnectionCancel}
|
||||
configurationWizard={configurationWizard}
|
||||
/>
|
||||
</connection-create-content>
|
||||
<connection-create-footer as='div'/>
|
||||
</connection-create>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<connection-create as='div'>
|
||||
<TabsState currentTabId={method} onChange={onChange}>
|
||||
<title-bar as='div'>
|
||||
{translate('connections_administration_connection_create')}
|
||||
<fill as="div" />
|
||||
<IconButton name="cross" viewBox="0 0 16 16" onClick={onCancel} />
|
||||
</title-bar>
|
||||
<TabList>
|
||||
<Tab tabId='driver'>
|
||||
<TabTitle>{translate('Driver')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='search-database'>
|
||||
<TabTitle>{translate('Search Database')}</TabTitle>
|
||||
</Tab>
|
||||
</TabList>
|
||||
</TabsState>
|
||||
<connection-create-content as='div'>
|
||||
{method === 'driver' && <CustomConnection onSelect={controller.onDriverSelect}/>}
|
||||
{method === 'search-database' && (
|
||||
<SearchDatabase
|
||||
databases={controller.databases}
|
||||
hosts={controller.hosts}
|
||||
disabled={controller.isProcessing}
|
||||
onSelect={controller.onDatabaseSelect}
|
||||
onSearch={controller.search}
|
||||
onChange={controller.onSearchChange}
|
||||
/>
|
||||
)}
|
||||
{controller.isProcessing && <Loader overlay/>}
|
||||
</connection-create-content>
|
||||
<connection-create-footer as='div'/>
|
||||
</connection-create>
|
||||
);
|
||||
});
|
||||
+118
@@ -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<string, string | number>;
|
||||
@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<ConnectionInfo> 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<ConnectionInfo> as any;
|
||||
this.availableDrivers = database.possibleDrivers;
|
||||
}
|
||||
|
||||
back = () => {
|
||||
this.connection = null;
|
||||
this.availableDrivers = [];
|
||||
}
|
||||
|
||||
showDetails = () => {
|
||||
if (this.error.exception) {
|
||||
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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 <Loader className={className}/>;
|
||||
}
|
||||
|
||||
return <DriverList drivers={drivers.get()} onSelect={onSelect} className={className}/>;
|
||||
});
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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) => {
|
||||
if (a === database.defaultDriver) {
|
||||
return 1;
|
||||
}
|
||||
if (b === database.defaultDriver) {
|
||||
return -1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
), [database]);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<ListItem onClick={select}>
|
||||
<ListItemIcon>
|
||||
{orderedDrivers.map(driverId => <StaticImage key={driverId} icon={drivers.get(driverId)?.icon}/>)}
|
||||
</ListItemIcon>
|
||||
<ListItemName>{database.host}:{database.port}</ListItemName>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onSelect(database: AdminConnectionSearchInfo): void;
|
||||
onChange(hosts: string): void;
|
||||
onSearch?(): void;
|
||||
}
|
||||
|
||||
export const DatabaseList = observer(function DatabaseList({
|
||||
databases, hosts, disabled, className, onSelect, onChange, onSearch,
|
||||
}: Props) {
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(styles)(
|
||||
<SubmittingForm onSubmit={onSearch} className={className}>
|
||||
<ItemList>
|
||||
<ItemListSearch value={hosts} placeholder={translate('connections_administration_search_database_tip')} onSearch={onChange} disabled={disabled}/>
|
||||
{databases.map(database => (
|
||||
<Database key={database.host + database.port} database={database} onSelect={onSelect}/>
|
||||
))}
|
||||
</ItemList>
|
||||
{!databases.length && <center as='div'>{translate('connections_administration_search_database_tip')}</center>}
|
||||
</SubmittingForm>
|
||||
);
|
||||
});
|
||||
+42
@@ -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 { 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;
|
||||
max-height: 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)(
|
||||
<ListItem onClick={select}>
|
||||
<ListItemIcon><StaticImage icon={driver.icon}/></ListItemIcon>
|
||||
<ListItemName>{driver.name}</ListItemName>
|
||||
<ListItemDescription title={driver.description}>{driver.description}</ListItemDescription>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
+6
-5
@@ -11,15 +11,16 @@ import { useState, useMemo } from 'react';
|
||||
|
||||
import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { Driver, IDriver } from './Driver';
|
||||
import { DBDriver } from '../../../DBDriverResource';
|
||||
import { Driver } from './Driver';
|
||||
|
||||
type DriverSelectorProps = {
|
||||
drivers: IDriver[];
|
||||
type Props = {
|
||||
drivers: DBDriver[];
|
||||
className?: string;
|
||||
onSelect(driverId: string): void;
|
||||
}
|
||||
|
||||
export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) {
|
||||
export const DriverList = observer(function DriverList({ drivers, className, onSelect }: Props) {
|
||||
const [search, setSearch] = useState('');
|
||||
const filteredDrivers = useMemo(() => {
|
||||
if (!search) {
|
||||
@@ -30,7 +31,7 @@ export const DriverSelector = observer(function DriverSelector({ drivers, classN
|
||||
|
||||
return (
|
||||
<ItemList className={className}>
|
||||
<ItemListSearch onSearch={setSearch} />
|
||||
<ItemListSearch value={search} onSearch={setSearch} />
|
||||
{filteredDrivers.map(driver => <Driver key={driver.id} driver={driver} onSelect={onSelect}/>)}
|
||||
</ItemList>
|
||||
);
|
||||
+46
@@ -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 { observer } from 'mobx-react';
|
||||
|
||||
import { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DatabaseList } from './DatabaseList';
|
||||
|
||||
type Props = {
|
||||
databases: AdminConnectionSearchInfo[];
|
||||
hosts: string;
|
||||
disabled?: boolean;
|
||||
onSelect(database: AdminConnectionSearchInfo): void;
|
||||
onChange(hosts: string): void;
|
||||
onSearch?(): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SearchDatabase = observer(function SearchDatabase({
|
||||
databases,
|
||||
hosts,
|
||||
disabled,
|
||||
onChange,
|
||||
onSelect,
|
||||
onSearch,
|
||||
className,
|
||||
}: Props) {
|
||||
|
||||
return (
|
||||
<DatabaseList
|
||||
databases={databases}
|
||||
hosts={hosts}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
onChange={onChange}
|
||||
onSearch={onSearch}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -31,7 +31,6 @@ export type ConnectionSearch = ConnectionNew & { [SEARCH_CONNECTION_SYMBOL]: Adm
|
||||
@injectable()
|
||||
export class ConnectionsResource extends CachedMapResource<string, ConnectionInfo> {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
private searchedDatabases: string[];
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
@@ -39,7 +38,6 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
) {
|
||||
super(new Map());
|
||||
this.metadata = new MetadataMap(() => false);
|
||||
this.searchedDatabases = [];
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
@@ -82,16 +80,19 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
}
|
||||
|
||||
async searchDatabases(hosts: string[]) {
|
||||
await this.performUpdate('search', () => 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<string, ConnectionInf
|
||||
await this.graphQLService.gql.setConnectionAccess({ connectionId, subjects });
|
||||
}
|
||||
|
||||
cleanSearchDatabases() {
|
||||
for (const id of this.searchedDatabases) {
|
||||
this.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loader(key: ResourceKey<string>): Promise<Map<string, ConnectionInfo>> {
|
||||
const { connections } = await this.graphQLService.gql.getConnections();
|
||||
this.data.clear();
|
||||
@@ -141,29 +136,6 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
return this.data;
|
||||
}
|
||||
|
||||
private async searchConnections(hosts: string[]) {
|
||||
const { databases } = await this.graphQLService.gql.searchDatabases({ hosts });
|
||||
|
||||
this.cleanSearchDatabases();
|
||||
|
||||
for (const database of databases) {
|
||||
const connectionInfo = {
|
||||
id: uuid(),
|
||||
driverId: database.defaultDriver,
|
||||
name: await this.getNameTemplate(database),
|
||||
host: database.host,
|
||||
port: `${database.port}`,
|
||||
authProperties: [] as Array<ObjectPropertyInfo>,
|
||||
[NEW_CONNECTION_SYMBOL]: true,
|
||||
[SEARCH_CONNECTION_SYMBOL]: database,
|
||||
} as ConnectionSearch;
|
||||
|
||||
this.data.set(connectionInfo.id, connectionInfo);
|
||||
this.markUpdated(connectionInfo.id);
|
||||
this.searchedDatabases.push(connectionInfo.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async getNameTemplate(connection: AdminConnectionSearchInfo) {
|
||||
const driver = await this.dbDriverResource.load(connection.defaultDriver);
|
||||
|
||||
|
||||
@@ -83,11 +83,10 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({
|
||||
: (
|
||||
<SubmittingForm onSubmit={controller.login} ref={focusedRef}>
|
||||
<ObjectPropertyInfoForm
|
||||
prefix={`auth_${connection.connectionInfo?.id || ''} `}
|
||||
autofillToken={`section-${connection.connectionInfo?.id || ''} section-auth`}
|
||||
properties={connection.connectionInfo?.authProperties}
|
||||
credentials={controller.credentials}
|
||||
processing={controller.isAuthenticating}
|
||||
disabled={controller.isAuthenticating}
|
||||
/>
|
||||
</SubmittingForm>
|
||||
)}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ListItem } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export interface IDriver {
|
||||
id: string;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type DriverProps = {
|
||||
driver: IDriver;
|
||||
onSelect(driverId: string): void;
|
||||
}
|
||||
|
||||
export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) {
|
||||
const select = useCallback(() => onSelect(driver.id), [driver]);
|
||||
|
||||
return <ListItem icon={driver.icon} name={driver.name} description={driver.description} onClick={select}/>;
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
|
||||
import { DBDriverResource } from '../DBDriverResource';
|
||||
import { DriverSelector } from './DriverSelector';
|
||||
|
||||
const styles = css`
|
||||
CommonDialogWrapper {
|
||||
max-height: 550px;
|
||||
min-height: 550px;
|
||||
}
|
||||
DriverSelector {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DriverSelectDialog = observer(function DriverSelectDialog({
|
||||
resolveDialog,
|
||||
rejectDialog,
|
||||
}: DialogComponentProps<null, string>) {
|
||||
const dbDriverResource = useService(DBDriverResource);
|
||||
const title = useTranslate('connections_administration_new_connection');
|
||||
|
||||
useEffect(() => { dbDriverResource.loadAll(); }, []);
|
||||
const isLoading = dbDriverResource.isLoading();
|
||||
const drivers = useMemo(() => computed(() => Array.from(dbDriverResource.data.values())), [dbDriverResource.data]);
|
||||
|
||||
return styled(styles)(
|
||||
<CommonDialogWrapper
|
||||
title={title}
|
||||
noBodyPadding
|
||||
onReject={rejectDialog}
|
||||
>
|
||||
{isLoading && <Loader />}
|
||||
{!isLoading && <DriverSelector drivers={drivers.get()} onSelect={resolveDialog}/>}
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Administration/Connections/ConnectionsAdministration';
|
||||
export * from './Administration/Connections/ConnectionsAdministrationNavService';
|
||||
export * from './Administration/Connections/ConnectionsAdministrationService';
|
||||
export * from './Administration/ConnectionsResource';
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Connection Management'],
|
||||
['connections_administration_connection_create', 'Connection Create'],
|
||||
['connections_administration_search_database_tip', 'Type your hosts here, e.g. \'localhost host1.myhost.com 192.168.0.1\' and press Enter'],
|
||||
['connections_administration_new_connection', 'New connection'],
|
||||
['connections_administration_configuration_wizard_step_title', 'Database connections'],
|
||||
['connections_administration_configuration_wizard_step_description', 'Database connections'],
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Управление подключениями'],
|
||||
['connections_administration_connection_create', 'Создание подключения'],
|
||||
['connections_administration_search_database_tip', 'Укажите ваши хосты, например \'localhost host1.myhost.com 192.168.0.1\' и нажмите Enter'],
|
||||
['connections_administration_new_connection', 'Создание подключения'],
|
||||
['connections_administration_configuration_wizard_step_title', 'Подключения к базам'],
|
||||
['connections_administration_configuration_wizard_step_description', 'Подключения к базам'],
|
||||
['connections_connection_edit_authentication', 'Авторизация'],
|
||||
['connections_connection_edit_access', 'Доступ'],
|
||||
['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'],
|
||||
|
||||
+1
-2
@@ -81,11 +81,10 @@ export const ConnectionForm = observer(function ConnectionForm({
|
||||
<InputGroup>{translate('connections_connection_edit_authentication')}</InputGroup>
|
||||
</group>
|
||||
<ObjectPropertyInfoForm
|
||||
prefix={`auth_${driver?.id || ''}`}
|
||||
autofillToken={`section-${driver?.id || ''} section-auth`}
|
||||
properties={controller.authModel.properties}
|
||||
credentials={controller.config.credentials}
|
||||
processing={controller.isConnecting}
|
||||
disabled={controller.isConnecting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
+19
-2
@@ -8,8 +8,11 @@
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { ListItem } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
ListItem, ListItemIcon, ListItemName, ListItemDescription, StaticImage
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
|
||||
export interface IDriver {
|
||||
id: string;
|
||||
@@ -18,6 +21,14 @@ export interface IDriver {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
StaticImage {
|
||||
box-sizing: border-box;
|
||||
width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
type DriverProps = {
|
||||
driver: IDriver;
|
||||
onSelect(driverId: string): void;
|
||||
@@ -26,5 +37,11 @@ type DriverProps = {
|
||||
export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) {
|
||||
const select = useCallback(() => onSelect(driver.id), [driver]);
|
||||
|
||||
return <ListItem icon={driver.icon} name={driver.name} description={driver.description} onClick={select}/>;
|
||||
return styled(styles)(
|
||||
<ListItem onClick={select}>
|
||||
<ListItemIcon><StaticImage icon={driver.icon}/></ListItemIcon>
|
||||
<ListItemName>{driver.name}</ListItemName>
|
||||
<ListItemDescription title={driver.description}>{driver.description}</ListItemDescription>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
+1
-2
@@ -83,11 +83,10 @@ export const ConnectionDialog = observer(function ConnectionDialog({
|
||||
) : (
|
||||
<SubmittingForm onSubmit={controller.onConnect} ref={focusedRef}>
|
||||
<ObjectPropertyInfoForm
|
||||
prefix={`auth_${controller.template?.id || ''}`}
|
||||
autofillToken={`section-${controller.template?.id || ''} section-auth`}
|
||||
properties={controller.authModel.properties}
|
||||
credentials={controller.credentials}
|
||||
processing={controller.isConnecting}
|
||||
disabled={controller.isConnecting}
|
||||
/>
|
||||
</SubmittingForm>
|
||||
))}
|
||||
|
||||
+19
-2
@@ -8,8 +8,11 @@
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { ListItem } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
ListItem, ListItemDescription, ListItemName, ListItemIcon, StaticImage
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { DBDriver, Connection } from '@cloudbeaver/core-connections';
|
||||
|
||||
type Props = {
|
||||
@@ -18,6 +21,14 @@ type Props = {
|
||||
onSelect(connectionId: string): void;
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
StaticImage {
|
||||
box-sizing: border-box;
|
||||
width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TemplateConnectionItem = observer(function TemplateConnectionItem({
|
||||
template,
|
||||
dbDriver,
|
||||
@@ -25,5 +36,11 @@ export const TemplateConnectionItem = observer(function TemplateConnectionItem({
|
||||
}: Props) {
|
||||
const select = useCallback(() => onSelect(template.id), [template]);
|
||||
|
||||
return <ListItem icon={dbDriver?.icon} name={template.name} description={template.description} onClick={select}/>;
|
||||
return styled(styles)(
|
||||
<ListItem onClick={select}>
|
||||
<ListItemIcon><StaticImage icon={dbDriver?.icon}/></ListItemIcon>
|
||||
<ListItemName>{template.name}</ListItemName>
|
||||
<ListItemDescription title={template.description}>{template.description}</ListItemDescription>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user