feat(core): connections managment

CB-215
This commit is contained in:
Wroud
2020-08-10 23:45:40 +03:00
parent fe393a71b1
commit 20c4f7fb9c
50 changed files with 1673 additions and 205 deletions
@@ -30,7 +30,7 @@ export class DBObjectService extends CachedMapResource<string, DBObject> {
return this.data;
}
this.performUpdate(key, async () => {
await this.performUpdate(key, async () => {
if (this.isLoaded(key) && !this.isOutdated(key)) {
return;
}
@@ -10,43 +10,35 @@ import styled, { css, use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { baseFormControlStyles } from './baseFormControlStyles';
const styles = css`
label {
composes: theme-typography--body1 from global;
}
field {
checkbox {
margin: 0 12px;
display: flex;
flex: auto;
box-sizing: border-box;
align-items: center;
padding: 12px 0;
& label {
width: 230px;
text-align: right;
padding: 0 12px;
line-height: 16px;
font-weight: 500;
}
& input {
flex: 1;
margin: 0 12px;
height: 26px;
}
& checkbox {
display: flex;
align-items: center;
flex: auto 0 0;
margin: 0;
}
}
`;
type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type' | 'value'> & {
value?: string;
checkboxLabel: string;
mod?: 'surface';
onChange?(value: boolean): any;
}
export function Checkbox({
name,
value,
checkboxLabel,
children,
className,
@@ -55,12 +47,19 @@ export function Checkbox({
...rest
}: Props) {
return styled(useStyles(styles))(
return styled(useStyles(baseFormControlStyles, styles))(
<field as="div" className={className}>
<label>{children}</label>
<field-label as="div">{children}</field-label>
<checkbox as='div'>
<input onChange={e => onChange(e.target.checked)} {...rest} {...use({ mod })} />
<checkbox-label as='div'>{checkboxLabel}</checkbox-label>
<input
name={name}
id={value || name}
type='checkbox'
onChange={e => onChange(e.target.checked)}
{...rest}
{...use({ mod })}
/>
<label htmlFor={value || name}>{checkboxLabel}</label>
</checkbox>
</field>
);
@@ -0,0 +1,191 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import {
useLayoutEffect, useCallback, useState, useRef
} from 'react';
import {
useMenuState,
Menu,
MenuItem,
MenuButton,
} from 'reakit/Menu';
import styled, { css, use } from 'reshadow';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { IconButton } from '../IconButton';
import { Icon } from '../Icons/Icon';
import { baseFormControlStyles } from './baseFormControlStyles';
const styles = composes(
css`
Menu {
composes: theme-text-on-surface theme-background-surface from global;
}
MenuItem, MenuButton {
composes: theme-ripple from global;
}
`,
css`
field input {
margin: 0;
}
MenuButton {
background: transparent;
outline: none;
padding: 4px;
cursor: pointer;
}
Menu {
composes: theme-typography--caption theme-elevation-z3 from global;
display: flex;
flex-direction: column;
width: 100%;
max-height: 150px;
overflow: auto;
outline: none;
padding: 4px 0;
z-index: 999;
& MenuItem {
background: transparent;
display: block;
padding: 4px 36px;
text-align: left;
outline: none;
color: inherit;
cursor: pointer;
}
}
Icon {
height: 16px;
display: block;
}
MenuButton Icon {
transform: rotate(90deg);
&[|focus] {
transform: rotate(-90deg);
}
}
input-box {
flex: 1;
margin: 0 12px;
position: relative;
display: flex;
align-items: center;
}
`
);
type Props<T> = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSelect'> & {
value: string | undefined;
propertyName?: string;
items: T[];
mod?: 'surface';
keySelector(item: T): string;
valueSelector(item: T): string;
onChange?(value: string): any;
onSelect(value: T | null): void;
onSwitch?(state: boolean): void;
}
export function Combobox<T>({
value,
propertyName,
items,
children,
className,
mod,
readOnly,
keySelector = v => v as any,
valueSelector = v => v as any,
onChange = () => {},
onSelect,
onSwitch,
...rest
}: Props<T>) {
const ref = useRef<HTMLInputElement>(null);
const menu = useMenuState({
placement: 'bottom-end',
currentId: null,
gutter: 4,
});
const [searchValue, setSearchValue] = useState('');
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onChange(value);
setSearchValue(value);
},
[onChange]
);
const handleRemove = useCallback(
() => {
menu.hide();
onSelect(null);
setSearchValue('');
},
[menu, onSelect]
);
const handleMenuSelect = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
menu.hide();
const id = event.currentTarget.id;
onSelect(items.find(item => keySelector(item) === id)!);
},
[menu, items, onSelect]
);
useLayoutEffect(() => onSwitch && onSwitch(menu.visible), [onSwitch, menu.visible]);
const selectedItem = items.find(item => keySelector(item) === value);
const filteredItems = items.filter(
item => selectedItem || !searchValue || valueSelector(item).toUpperCase().includes(searchValue.toUpperCase())
);
if (ref.current === document.activeElement && !selectedItem && searchValue) {
menu.show();
}
return styled(useStyles(baseFormControlStyles, styles))(
<field as="div" className={className}>
<field-label as='label'>{children}</field-label>
<input-box as="div">
<input
ref={ref}
onChange={handleChange}
value={selectedItem ? valueSelector(selectedItem) : searchValue}
readOnly={!!selectedItem || readOnly}
{...use({ mod })}
{...rest}
/>
{(selectedItem && !readOnly) && (
<IconButton type="button" name="reject" viewBox="0 0 11 11" onClick={handleRemove} />
)}
{!readOnly && (
<>
<MenuButton {...menu}><Icon name="arrow" viewBox="0 0 16 16" {...use({ focus: menu.visible })} /></MenuButton>
<Menu {...menu} aria-label={propertyName} unstable_initialFocusRef={ref}>
{filteredItems.map(item => (
<MenuItem key={keySelector(item)} id={keySelector(item)} type='button' {...menu} onClick={handleMenuSelect}>
{valueSelector(item)}
</MenuItem>
))}
</Menu>
</>
)}
</input-box>
</field>
);
}
@@ -0,0 +1,34 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { baseFormControlStyles } from './baseFormControlStyles';
type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
mod?: 'surface';
onChange?(value: string): any;
}
export function InputField({
children,
className,
mod,
onChange = () => {},
...rest
}: Props) {
return styled(useStyles(baseFormControlStyles))(
<field as="div" className={className}>
<field-label as='label'>{children}</field-label>
<input onChange={e => onChange(e.target.value)} {...rest} {...use({ mod })} />
</field>
);
}
@@ -0,0 +1,38 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { use, css } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { baseFormControlStyles } from './baseFormControlStyles';
const styles = css`
field-label {
composes: theme-typography--body2 from global;
margin-left: 150px;
text-align: left;
font-weight: initial;
}
`;
type Props = React.PropsWithChildren<{
className?: string;
}>
export function InputGroup({
children,
className,
}: Props) {
return styled(useStyles(baseFormControlStyles, styles))(
<field as="div" className={className}>
<field-label as='label'>{children}</field-label>
</field>
);
}
@@ -35,6 +35,8 @@ const radioStyles = composes(
field {
display: inline-flex;
align-items: center;
font-weight: 500;
padding: 7px 12px;
vertical-align: middle;
}
`
@@ -43,9 +45,9 @@ const radioStyles = composes(
const radioMod = {
primary: composes(
css`
radio {
composes: theme-radio_primary from global;
}
radio {
composes: theme-radio_primary from global;
}
`
),
};
@@ -0,0 +1,40 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css, use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { baseFormControlStyles } from './baseFormControlStyles';
const styles = css`
textarea {
line-height: 19px;
}
`;
type Props = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> & {
mod?: 'surface';
onChange?(value: string): any;
}
export function Textarea({
children,
className,
mod,
onChange = () => {},
...rest
}: Props) {
return styled(useStyles(baseFormControlStyles, styles))(
<field as="div" className={className}>
<field-label as='label'>{children}</field-label>
<textarea onChange={e => onChange(e.target.value)} {...rest} {...use({ mod })} />
</field>
);
}
@@ -0,0 +1,37 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { css } from 'reshadow';
export const baseFormControlStyles = css`
field {
display: flex;
flex: auto;
box-sizing: border-box;
align-items: center;
padding: 12px 0;
min-height: 54px;
& input,
& textarea {
flex: 1;
margin: 0 12px;
min-height: 26px;
}
}
field-label {
composes: theme-typography--body1 from global;
box-sizing: border-box;
width: 150px;
text-align: right;
padding: 0 12px;
line-height: 16px;
font-weight: 500;
}
`;
@@ -1,62 +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 styled, { css, use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
const styles = css`
label {
composes: theme-typography--body1 from global;
}
field {
display: flex;
flex: auto;
box-sizing: border-box;
align-items: center;
padding: 12px 0;
& label {
width: 150px;
text-align: right;
padding: 0 12px;
line-height: 16px;
font-weight: 500;
&:empty {
display: none;
}
}
& input {
flex: 1;
margin: 0 12px;
height: 26px;
}
}
`;
type InputFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
mod?: 'surface';
onChange?(value: string): any;
}
export function InputField({
children,
className,
mod,
onChange = () => {},
...rest
}: InputFieldProps) {
return styled(useStyles(styles))(
<field as="div" className={className}>
<label>{children}</label>
<input onChange={e => onChange(e.target.value)} {...rest} {...use({ mod })} />
</field>
);
}
@@ -14,8 +14,8 @@ import styled, { css, use } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ShadowInput } from '../FormControls/ShadowInput';
import { Icon } from '../Icons';
import { ShadowInput } from '../ShadowInput';
import { IProperty } from './IProperty';
import { PropertyValueSelector } from './PropertyValueSelector';
@@ -16,18 +16,27 @@ import { TableContext, ITableContext } from './TableContext';
type Props = React.PropsWithChildren<{
selectedItems?: Map<any, boolean>;
expandedItems?: Map<any, boolean>;
className?: string;
}>
export function Table({ selectedItems, children, className }: Props) {
export function Table({
selectedItems, expandedItems, children, className,
}: Props) {
const [selected] = useState<Map<any, boolean>>(() => selectedItems || observable(new Map()));
const [expanded] = useState<Map<any, boolean>>(() => expandedItems || observable(new Map()));
const setItemSelect = useCallback((item: any, state: boolean) => selected.set(item, state), []);
const setItemExpand = useCallback((item: any, state: boolean) => expanded.set(item, state), []);
const clearSelection = useCallback(() => selected.clear(), []);
const collapse = useCallback(() => expanded.clear(), []);
const [context] = useState<ITableContext>(() => ({
selectedItems: selected,
expandedItems: expanded,
setItemExpand,
setItemSelect,
clearSelection,
collapse,
}));
return styled(useStyles())(
@@ -14,10 +14,16 @@ import { useStyles } from '@cloudbeaver/core-theming';
type Props = React.PropsWithChildren<{
className?: string;
centerContent?: boolean;
flex?: boolean;
}>
export const TableColumnValue = observer(function TableColumnValue({ children, centerContent, className }: Props) {
export const TableColumnValue = observer(function TableColumnValue({
children,
centerContent,
flex,
className,
}: Props) {
return styled(useStyles())(
<td className={className} {...use({ centerContent })}>{children}</td>
<td className={className} {...use({ centerContent, flex })}>{children}</td>
);
});
@@ -10,8 +10,11 @@ import { createContext } from 'react';
export interface ITableContext {
selectedItems: Map<any, boolean>;
expandedItems: Map<any, boolean>;
setItemSelect: (item: any, state: boolean) => void;
setItemExpand: (item: any, state: boolean) => void;
clearSelection: () => void;
collapse: () => void;
}
export const TableContext = createContext<ITableContext | undefined>(undefined);
@@ -7,7 +7,9 @@
*/
import { observer } from 'mobx-react';
import { useContext, useCallback, useMemo } from 'react';
import {
useContext, useCallback, useMemo, Children
} from 'react';
import styled, { use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -15,15 +17,20 @@ import { useStyles } from '@cloudbeaver/core-theming';
import { TableContext } from './TableContext';
import { TableItemContext, ITableItemContext } from './TableItemContext';
type ExpandProps = {
item: any;
}
type Props = React.PropsWithChildren<{
item: any;
expandElement?: React.FunctionComponent<ExpandProps>;
className?: string;
onClick?: () => void;
onDoubleClick?: () => void;
}>
export const TableItem = observer(function TableItem({
item, children, className, onClick, onDoubleClick,
item, expandElement, children, className, onClick, onDoubleClick,
}: Props) {
const context = useContext(TableContext);
if (!context) {
@@ -33,6 +40,7 @@ export const TableItem = observer(function TableItem({
const itemContext = useMemo<ITableItemContext>(() => ({
item,
isSelected: () => !!context.selectedItems.get(item),
isExpanded: () => !!context.expandedItems.get(item),
}), [item]);
const handleClick = useCallback(
@@ -56,13 +64,27 @@ export const TableItem = observer(function TableItem({
}
}, [onDoubleClick]);
const isSelected = context.selectedItems.get(item);
const isSelected = itemContext.isSelected();
const isExpanded = itemContext.isExpanded();
const ExpandElement = expandElement;
return styled(useStyles())(
<TableItemContext.Provider value={itemContext}>
<tr onClick={handleClick} onDoubleClick={handleDoubleClick} className={className} {...use({ isSelected })}>
<tr
onClick={handleClick}
onDoubleClick={handleDoubleClick}
className={className}
{...use({ selected: isSelected, expanded: isExpanded })}
>
{children}
</tr>
{isExpanded && (
<tr {...use({ noHover: true })}>
<td colSpan={Children.count(children)} {...use({ expandArea: true })}>
{ExpandElement && <ExpandElement item={item} />}
</td>
</tr>
)}
</TableItemContext.Provider>
);
});
@@ -11,6 +11,7 @@ import { createContext } from 'react';
export interface ITableItemContext {
item: any;
isSelected(): boolean;
isExpanded(): boolean;
}
export const TableItemContext = createContext<ITableItemContext | undefined>(undefined);
@@ -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 { observer } from 'mobx-react';
import { useContext, useCallback } from 'react';
import styled, { css, use } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import { Icon } from '../Icons/Icon';
import { TableContext } from './TableContext';
import { TableItemContext } from './TableItemContext';
type Props = {
onExpand?: (item: any, state: boolean) => void;
className?: string;
disabled?: boolean;
}
const styles = css`
Icon {
margin-left: 16px;
width: 16px;
height: 100%;
padding: 0 16px;
&[|expanded] {
transform: rotate(180deg);
}
}
`;
export const TableItemExpand = observer(function TableItemExpand({
onExpand,
className,
disabled,
}: Props) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
if (!context) {
return null;
}
const handleClick = useCallback((event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
event.stopPropagation();
if (disabled) {
return;
}
const state = !context.isExpanded();
tableContext?.setItemExpand(context.item, state);
if (onExpand) {
onExpand(context.item, state);
}
}, [tableContext, context, onExpand, disabled]);
return styled(useStyles(styles))(
<Icon name="angle" viewBox="0 0 15 8" {...use({ expanded: context.isExpanded() })} onClick={handleClick} className={className}/>
);
});
@@ -6,4 +6,5 @@ export * from './TableContext';
export * from './TableHeader';
export * from './TableItem';
export * from './TableItemContext';
export * from './TableItemExpand';
export * from './TableItemSelect';
@@ -17,6 +17,7 @@ import { TabsContext } from '../TabsContext';
type TabProps = PropsWithChildren<{
tabId: string;
disabled?: boolean;
className?: string;
onOpen?: (tabId: string) => void;
onClose?: (tabId: string) => void;
@@ -24,10 +25,11 @@ type TabProps = PropsWithChildren<{
export function Tab({
tabId,
onOpen,
onClose,
disabled,
className,
children,
onOpen,
onClose,
}: TabProps) {
const state = useContext(TabsContext);
if (!state) {
@@ -55,6 +57,7 @@ export function Tab({
id={tabId}
className={className}
onClick={handleOpen}
disabled={disabled}
>
<tab-container as='div'>
{children}
+8 -5
View File
@@ -13,15 +13,18 @@ export * from './Split';
export * from './Table';
export * from './Tabs';
export * from './Button';
export * from './Checkbox';
export * from './FormControls/Checkbox';
export * from './FormControls/Combobox';
export * from './FormControls/InputField';
export * from './FormControls/InputGroup';
export * from './FormControls/Radio';
export * from './FormControls/ShadowInput';
export * from './FormControls/SubmittingForm';
export * from './FormControls/Textarea';
export * from './ErrorMessage';
export * from './IconButton';
export * from './IconOrImage';
export * from './InputField';
export * from './Radio';
export * from './ShadowInput';
export * from './StaticImage';
export * from './SubmittingForm';
export * from './TextPlaceholder';
export * from './useClipboard';
export * from './useFocus';
@@ -30,7 +30,8 @@ import {
ConnectionAuthService,
ConnectionsAdministrationService,
ConnectionsResource,
ConnectionsLocaleService
ConnectionsLocaleService,
DriverPropertiesService
} from '@cloudbeaver/core-connections';
import { PluginManifest } from '@cloudbeaver/core-di';
import { CommonDialogService, ContextMenuService, SessionExpireService } from '@cloudbeaver/core-dialogs';
@@ -85,6 +86,7 @@ export const coreManifest: PluginManifest = {
ConnectionInfoResource,
ContainerResource,
DBDriverResource,
DriverPropertiesService,
ConnectionsManagerService,
RouterService,
ScreenService,
@@ -57,7 +57,11 @@ export const ConnectionsAdministration = observer(function ConnectionsAdministra
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
</AdministrationTools>
<content as='div'>
<ConnectionsTable connections={controller.connections} selectedItems={controller.selectedItems} />
<ConnectionsTable
connections={controller.connections}
selectedItems={controller.selectedItems}
expandedItems={controller.expandedItems}
/>
{controller.isLoading && <Loader overlay/>}
</content>
</>
@@ -12,7 +12,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher } from '@cloudbeaver/core-sdk';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
import { ConnectionsResource } from '../ConnectionsResource';
@@ -20,9 +20,19 @@ import { ConnectionsResource } from '../ConnectionsResource';
export class ConnectionsAdministrationController {
@observable isDeleting = false;
readonly selectedItems = observable<string, boolean>(new Map())
readonly expandedItems = observable<string, boolean>(new Map())
readonly error = new GQLErrorCatcher();
get connections() {
return this.connectionsResource.data;
return Array.from(this.connectionsResource.data.values())
.sort((a, b) => {
if (this.connectionsResource.isNew(a.id) === this.connectionsResource.isNew(b.id)) {
return 0;
}
if (this.connectionsResource.isNew(a.id)) {
return -1;
}
return 1;
});
}
get isLoading() {
return this.connectionsResource.isLoading();
@@ -34,14 +44,17 @@ export class ConnectionsAdministrationController {
private commonDialogService: CommonDialogService,
) { }
create = () => { }
create = () => {
const connectionInfo = this.connectionsResource.addNew();
this.expandedItems.set(connectionInfo.id, true);
}
update = async () => {
try {
await this.connectionsResource.refresh(undefined);
await this.connectionsResource.refresh('all');
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Users update failed');
this.notificationService.logException(exception, 'Connections update failed');
}
}
}
@@ -57,28 +70,26 @@ export class ConnectionsAdministrationController {
const deletionList = Array
.from(this.selectedItems)
.filter(([_, value]) => value)
.map(([userId]) => userId);
.map(([connectionId]) => connectionId);
if (deletionList.length === 0) {
return;
}
const confirmed = await this.commonDialogService.open(ConfirmationDialog, {
title: 'authentication_administration_confirm_user_deletion',
message: `Would you like to delete users: ${deletionList.join(', ')}`,
message: `Would you like to delete connections: ${deletionList.join(', ')}`,
});
if (!confirmed) {
return;
}
for (const userId of deletionList) {
await this.connectionsResource.delete(userId);
}
await this.connectionsResource.delete(resourceKeyList(deletionList));
this.selectedItems.clear();
await this.connectionsResource.refresh(undefined);
await this.connectionsResource.refresh('all');
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'User delete failed');
this.notificationService.logException(exception, 'Connections delete failed');
}
} finally {
this.isDeleting = false;
@@ -7,26 +7,49 @@
*/
import { observer } from 'mobx-react';
import styled from 'reshadow';
import styled, { use, css } from 'reshadow';
import {
TableItem, TableColumnValue, TableItemSelect
TableItem, TableColumnValue, TableItemSelect, TableItemExpand
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ConnectionInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { ConnectionsResource } from '../../ConnectionsResource';
import { ConnectionEdit } from './ConnectionEdit';
const styles = composes(
css`
TableColumnValue {
composes: theme-border-color-color-positive from global;
}
`,
css`
[|new] {
border-left: solid 3px;
}
`
);
type Props = {
connection: ConnectionInfo;
}
export const Connection = observer(function Connection({ connection }: Props) {
return styled(useStyles())(
<TableItem item={connection.id}>
<TableColumnValue centerContent><TableItemSelect /></TableColumnValue>
const connectionInfoResource = useService(ConnectionsResource);
const isNew = connectionInfoResource.isNew(connection.id);
return styled(useStyles(styles))(
<TableItem item={connection.id} expandElement={ConnectionEdit}>
<TableColumnValue centerContent flex {...use({ new: isNew })}>
<TableItemSelect />
<TableItemExpand />
</TableColumnValue>
<TableColumnValue>{connection.name}</TableColumnValue>
<TableColumnValue>{connection.host}</TableColumnValue>
<TableColumnValue>{connection.port}</TableColumnValue>
<TableColumnValue><input type="checkbox" checked={!connection.template} disabled/></TableColumnValue>
<TableColumnValue><input type="checkbox" checked={connection.template === false} disabled/></TableColumnValue>
<TableColumnValue></TableColumnValue>
</TableItem>
);
@@ -0,0 +1,192 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import { useState, useContext, useCallback } from 'react';
import styled, { css, use } 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 { useStyles, composes } from '@cloudbeaver/core-theming';
import { ConnectionsResource } from '../../ConnectionsResource';
import { ConnectionEditController } from './ConnectionEditController';
import { ConnectionForm } from './ConnectionForm/ConnectionForm';
import { DriverProperties } from './DriverProperties/DriverProperties';
const styles = composes(
css`
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
TabList {
composes: theme-border-color-background from global;
}
ErrorMessage {
composes: theme-background-secondary from global;
}
SubmittingForm {
composes: theme-background-secondary from global;
}
`,
css`
custom-connection {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
CommonDialogWrapper {
max-height: 500px;
min-height: 500px;
}
SubmittingForm, BaseTabPanel {
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;
border-bottom: solid 1px;
}
Tab {
composes: theme-typography--body2 from global;
text-transform: uppercase;
font-weight: normal;
&:global([aria-selected=true]) {
font-weight: normal !important;
}
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
fill {
flex: 1;
}
connection-form[|new] {
border-left: solid 3px;
}
SubmittingForm, Loader {
min-height: 320px;
max-height: 500px;
}
IconButton {
height: 32px;
width: 32px;
margin-right: 16px;
}
Button:not(:first-child) {
margin-right: 24px;
}
`
);
type Props = {
item: string;
}
export const ConnectionEdit = observer(function ConnectionEdit({
item,
}: Props) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
const translate = useTranslate();
const controller = useController(ConnectionEditController, item);
const connectionsResource = useService(ConnectionsResource);
const [loadProperties, setLoadProperties] = useState(false);
const handleCancel = useCallback(() => {
tableContext?.setItemExpand(context?.item, false);
if (controller.isNew) {
connectionsResource.delete(item);
}
}, [tableContext, context]);
return styled(useStyles(styles))(
<TabsState selectedId='options'>
<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>
<fill as="div" />
<Button
type="button"
disabled={controller.isSaving}
mod={['outlined']}
onClick={handleCancel}
>
{translate('connections_connection_edit_cancel')}
</Button>
<Button
type="button"
disabled={controller.isSaving}
mod={['unelevated']}
onClick={controller.onSaveConnection}
>
{translate(controller.isNew ? 'connections_connection_edit_add' : 'connections_connection_edit_save')}
</Button>
</TabList>
{controller.isLoading
? <Loader />
: (
<SubmittingForm onSubmit={controller.onSaveConnection}>
<TabPanel tabId='options'>
<ConnectionForm controller={controller} />
</TabPanel>
{controller.driver && (
<TabPanel tabId='driver_properties'>
<DriverProperties
driver={controller.driver}
state={controller.config.properties!}
loadProperties={loadProperties}
/>
</TabPanel>
)}
</SubmittingForm>
)
}
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.onShowDetails}
/>
)}
</TabsState>
);
});
@@ -0,0 +1,230 @@
/*
* 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 } from 'mobx';
import {
injectable, IInitializableController, IDestructibleController, Bootstrap
} 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
} from '@cloudbeaver/core-sdk';
import { DatabaseAuthModelsResource } from '../../../DatabaseAuthModelsResource';
import { DBDriver, DBDriverResource } from '../../../DBDriverResource';
import { ConnectionsResource } from '../../ConnectionsResource';
export enum ConnectionType {
Attributes,
URL
}
@injectable()
export class ConnectionEditController
implements IInitializableController, IDestructibleController {
@observable connectionType = ConnectionType.Attributes
@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: {},
};
get isNew() {
return this.connectionsResource.isNew(this.connectionId);
}
get drivers() {
return Array.from(this.dbDriverResource.data.values());
}
connectionId!: string;
readonly error = new GQLErrorCatcher();
private isDistructed = false;
private connectionInfo!: ConnectionInfo;
constructor(
private connectionsResource: ConnectionsResource,
private notificationService: NotificationService,
private commonDialogService: CommonDialogService,
private dbAuthModelsResource: DatabaseAuthModelsResource,
private dbDriverResource: DBDriverResource,
) { }
init(id: string) {
this.connectionId = id;
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;
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);
this.notificationService.logInfo({ title: `Connection ${connection.name} created` });
} else {
const connection = await this.connectionsResource.update(this.connectionId, this.getConnectionConfig());
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);
}
}
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 = Object.values(this.config.credentials).some(Boolean);
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() {
this.onChange('name', this.connectionInfo?.name || (this.driver ? `${this.driver?.name} (custom)` : ''));
this.onChange('template', this.connectionInfo?.template);
this.onChange('driverId', this.connectionInfo?.driverId || this.driver?.id || '');
this.onChange('host', this.connectionInfo?.host || '');
this.onChange('port', this.connectionInfo?.port || this.driver?.defaultPort || '');
this.onChange('databaseName', this.connectionInfo?.databaseName || '');
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 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)!;
} catch (exception) {
this.notificationService.logException(exception, `Can't load ConnectionInfo ${this.connectionId}`);
} finally {
this.isLoading = false;
}
await this.loadDriver(this.connectionInfo.driverId);
}
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
);
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver auth model');
} finally {
this.isSaving = false;
}
}
}
@@ -0,0 +1,143 @@
/*
* 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 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;
}
export const ConnectionForm = observer(function ConnectionForm({
controller,
}: ConnectionFormProps) {
const translate = useTranslate();
const [focusedRef] = useFocus({ focusFirstChild: true });
return styled(useStyles(formStyles))(
<connection-form as='div' ref={focusedRef as React.RefObject<HTMLDivElement>}>
<left-box as='div'>
<group as="div">
<Checkbox
name="template"
value={controller.connectionId}
checkboxLabel={translate('connections_connection_in_tree')}
checked={controller.config.template === false}
onChange={value => controller.onChange('template', !value)}
disabled={controller.isSaving}
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.isNew}
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.isSaving}
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.isSaving}
mod='surface'
>
{translate('connections_connection_description')}
</Textarea>
</group>
</left-box>
<right-box 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.isSaving}
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.isSaving}
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.isSaving}
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 || ''}`}
autofillToken={`section-${controller.driver?.id || ''} section-auth`}
properties={controller.authModel.properties}
credentials={controller.config.credentials}
processing={controller.isSaving}
/>
</>
)}
</right-box>
</connection-form>
);
});
@@ -0,0 +1,26 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { DBDriver } from '@cloudbeaver/core-connections';
import { ConnectionConfig, DatabaseAuthModel } from '@cloudbeaver/core-sdk';
import { ConnectionType } from '../ConnectionEditController';
export interface IFormController {
isNew: boolean;
connectionId: string;
drivers: DBDriver[];
driver: DBDriver | null;
authModel: DatabaseAuthModel | null;
config: ConnectionConfig;
connectionType: ConnectionType;
isSaving: boolean;
onChangeType(type: ConnectionType): void;
onSelectDriver(driver: DBDriver): void;
onChange<T extends keyof ConnectionConfig>(property: T, value: ConnectionConfig[T]): void;
}
@@ -0,0 +1,72 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import styled, { use } from 'reshadow';
import { InputField } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { formStyles } from './formStyles';
import { IFormController } from './IFormController';
type ParametersFormProps = {
controller: IFormController;
embedded?: boolean;
}
export const ParametersForm = observer(function ParametersForm({
controller,
embedded,
}: ParametersFormProps) {
const translate = useTranslate();
return styled(useStyles(formStyles))(
<>
{ !embedded && (
<group as="div">
<InputField
type="text"
name="host"
value={controller.config.host}
onChange={value => controller.onChange('host', value)}
disabled={controller.isSaving}
mod='surface'
>
{translate('customConnection_custom_host')}
<sub-label as="div">{translate('customConnection_custom_obligatory')}</sub-label>
</InputField>
<InputField
type="number"
name="port"
value={controller.config.port}
onChange={value => controller.onChange('port', value)}
disabled={controller.isSaving}
{...use({ short: true })}
mod='surface'
>
{translate('customConnection_custom_port')}
</InputField>
</group>
)}
<group as="div">
<InputField
type="text"
name="databaseName"
value={controller.config.databaseName}
onChange={value => controller.onChange('databaseName', value)}
disabled={controller.isSaving}
mod='surface'
>
{translate('customConnection_custom_database')}
</InputField>
</group>
</>
);
});
@@ -0,0 +1,46 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { css } from 'reshadow';
export const formStyles = css`
connection-form {
flex: 1;
display: flex;
flex-direction: row;
padding: 18px 36px;
}
left-box {
display: flex;
flex-direction: column;
}
right-box {
flex: 1;
display: flex;
flex-direction: column;
}
connection-type {
margin-left: 150px;
}
Radio {
composes: theme-typography--body1 from global;
}
sub-label {
composes: theme-typography--caption from global;
line-height: 14px;
}
group {
box-sizing: border-box;
display: flex;
margin: 0 12px;
}
InputField[|short] {
min-width: unset;
max-width: 250px;
}
`;
@@ -30,12 +30,17 @@ const styles = composes(
type Props = {
connections: ConnectionInfo[];
selectedItems: Map<string, boolean>;
expandedItems: Map<string, boolean>;
}
export const ConnectionsTable = observer(function ConnectionsTable({ connections, selectedItems }: Props) {
export const ConnectionsTable = observer(function ConnectionsTable({
connections,
selectedItems,
expandedItems,
}: Props) {
const translate = useTranslate();
return styled(useStyles(styles))(
<Table selectedItems={selectedItems}>
<Table selectedItems={selectedItems} expandedItems={expandedItems}>
<TableHeader>
<TableColumnHeader min/>
<TableColumnHeader>{translate('connections_connection_name')}</TableColumnHeader>
@@ -0,0 +1,64 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { Loader, PropertiesTable } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { DBDriver } from '../../../../DBDriverResource';
import { DriverPropertiesController } from './DriverPropertiesController';
const styles = css`
properties {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
`;
type DriverPropertyState = {
[key: string]: string;
}
type DriverPropertiesProps = {
driver: DBDriver;
state: DriverPropertyState;
loadProperties: boolean;
}
export const DriverProperties = observer(function DriverProperties({
driver,
state,
loadProperties,
}: DriverPropertiesProps) {
const controller = useController(DriverPropertiesController, driver);
useEffect(() => {
if (loadProperties) {
controller.loadDriverProperties();
}
}, [loadProperties]);
return styled(useStyles(styles))(
<properties as="div">
{controller.isLoading && <Loader />}
{!controller.isLoading && (
<PropertiesTable
properties={controller.driverProperties}
propertiesState={state}
onAdd={controller.onAddProperty}
/>
)}
</properties>
);
});
@@ -0,0 +1,79 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { IProperty } from '@cloudbeaver/core-blocks';
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import { DBDriver } from '../../../../DBDriverResource';
import { DriverPropertiesService } from '../../../../DriverPropertiesService';
export type DriverPropertyState = {
[key: string]: string;
}
type StaticId = {
staticId: string;
}
export type DriverPropertyInfoWithStaticId = ObjectPropertyInfo & StaticId
@injectable()
export class DriverPropertiesController implements IInitializableController {
@observable isLoading = false;
@observable driver!: DBDriver
@observable hasDetails = false
@observable responseMessage: string | null = null
@observable driverProperties = observable<IProperty>([])
private loaded = false;
constructor(
private driverPropertiesService: DriverPropertiesService,
private notificationService: NotificationService
) { }
init(driver: DBDriver) {
this.driver = driver;
}
onAddProperty = () => {
this.driverProperties.unshift({
id: uuid(),
key: 'property',
defaultValue: '',
});
}
async loadDriverProperties() {
if (this.isLoading || this.loaded) {
return;
}
this.isLoading = true;
try {
const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driver.id);
this.driverProperties = observable(driverProperties.map(property => ({
id: property.id!,
key: property.id!,
displayName: property.displayName!,
defaultValue: property.defaultValue,
description: property.description,
validValues: property.validValues,
})));
this.loaded = true;
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver properties');
} finally {
this.isLoading = false;
}
}
}
@@ -9,42 +9,94 @@
import { injectable } from '@cloudbeaver/core-di';
import {
GraphQLService,
CachedDataResource,
ConnectionInfo,
ConnectionConfig,
CachedMapResource,
ResourceKey,
isResourceKeyList,
} from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
@injectable()
export class ConnectionsResource extends CachedDataResource<ConnectionInfo[], string | undefined> {
export class ConnectionsResource extends CachedMapResource<string, ConnectionInfo> {
constructor(
private graphQLService: GraphQLService,
) {
super([]);
super(new Map());
}
isLoaded(id?: string) {
return id
? this.data.some(connection => connection.id === id)
: !!this.data.length;
isNew(id: string) {
return id.startsWith('new-');
}
async create(config: ConnectionConfig) {
addNew() {
const connectionInfo = {
id: `new-${uuid()}`,
name: 'New connection',
} as ConnectionInfo;
this.data.set(connectionInfo.id, connectionInfo);
this.markUpdated(connectionInfo.id);
return connectionInfo;
}
async loadAll() {
await this.load('all');
return this.data;
}
async create(config: ConnectionConfig, id?: string) {
const { connection } = await this.graphQLService.gql.createConnectionConfiguration({ config });
this.data.push(connection as ConnectionInfo);
if (id) {
this.data.delete(id);
}
this.data.set(connection.id, connection as ConnectionInfo);
return connection as ConnectionInfo;
return this.get(connection.id)!;
}
async delete(id: string) {
await this.graphQLService.gql.deleteConnectionConfiguration({ id });
this.data.splice(this.data.findIndex(connection => connection.id === id), 1);
async update(id: string, config: ConnectionConfig) {
await this.performUpdate(id, async () => {
await this.setActivePromise<void>(id, this.updateConnection(id, config));
});
return this.get(id)!;
}
protected async loader(id?: string): Promise<ConnectionInfo[]> {
async delete(key: ResourceKey<string>) {
if (isResourceKeyList(key)) {
for (let i = 0; i < key.list.length; i++) {
this.data.delete(key.list[i]);
if (!this.isNew(key.list[i])) {
await this.graphQLService.gql.deleteConnectionConfiguration({ id: key.list[i] });
}
}
} else {
this.data.delete(key);
if (!this.isNew(key)) {
await this.graphQLService.gql.deleteConnectionConfiguration({ id: key });
}
}
this.markUpdated(key);
this.itemDeleteSubject.next(key);
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, ConnectionInfo>> {
const { connections } = await this.graphQLService.gql.getConnections();
this.data.clear();
return connections as ConnectionInfo[];
for (const connection of connections) {
this.set(connection.id, connection as ConnectionInfo);
}
this.markUpdated(key);
return this.data;
}
private async updateConnection(id: string, config: ConnectionConfig) {
const { connection } = await this.graphQLService.gql.updateConnectionConfiguration({ id, config });
this.set(id, connection as ConnectionInfo);
}
}
@@ -47,7 +47,7 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver> {
for (const driver of driverList) {
this.data.set(driver.id, driver);
}
this.data.set('all', {} as any);
// this.data.set('all', {} as any);
this.markUpdated(key);
return this.data;
}
@@ -0,0 +1,31 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@cloudbeaver/core-di';
import { GraphQLService, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
@injectable()
export class DriverPropertiesService {
constructor(
private graphQLService: GraphQLService,
) {
}
async loadDriverProperties(driverId: string): Promise<ObjectPropertyInfo[]> {
const response = await this.graphQLService.gql.driverProperties({
driverId,
});
if (response.driver.length === 0) {
throw new Error('Driver properties loading failed');
}
return response.driver[0].driverProperties! as ObjectPropertyInfo[];
}
}
@@ -7,6 +7,7 @@ export * from './extensions/IConnectionSetter';
export * from './ConnectionsManagerService';
export * from './ConnectionInfoResource';
export * from './ConnectionAuthService';
export * from './DriverPropertiesService';
export * from './EConnectionFeature';
export * from './ContainerResource';
export * from './ConnectionsLocaleService';
@@ -1,6 +1,12 @@
export default [
['connections_administration_item', 'Connection Management'],
['connections_connection_edit_save', 'Save'],
['connections_connection_edit_add', 'Add'],
['connections_connection_edit_cancel', 'Cancel'],
['connections_connection_edit_authentication', 'Authentication'],
['connections_connection_name', 'Name'],
['connections_connection_description', 'Description'],
['connections_connection_driver', 'Driver'],
['connections_connection_host', 'Host'],
['connections_connection_port', 'Port'],
['connections_connection_in_tree', 'Show in navigation tree'],
@@ -1,6 +1,12 @@
export default [
['connections_administration_item', 'Управление подключениями'],
['connections_connection_edit_save', 'Сохранить'],
['connections_connection_edit_add', 'Добавить'],
['connections_connection_edit_cancel', 'Отменить'],
['connections_connection_edit_authentication', 'Авторизация'],
['connections_connection_name', 'Название'],
['connections_connection_description', 'Описапние'],
['connections_connection_driver', 'Driver'],
['connections_connection_host', 'Хост'],
['connections_connection_port', 'Порт'],
['connections_connection_in_tree', 'Показывать в дереве навигации'],
+11 -15
View File
@@ -21,33 +21,29 @@ export function useController<T extends IInitializableController>(
): T;
export function useController<T>(ctor: IServiceConstructor<T>): T;
export function useController<T>(ctor: IServiceConstructor<T>, ...args: any[]): T {
const app = useContext(appContext);
const isInitialized = useRef<boolean>(false);
const [controller] = useState(() => {
const controllerRef = useRef<T>();
useMemo(() => {
if (controllerRef.current && isDestructibleController(controllerRef.current)) {
controllerRef.current.destruct();
}
const controller = app.resolveServiceByClass(ctor);
if (isInitializableController(controller)) {
controller.init(...args);
}
return controller;
});
useMemo(() => {
if (isInitialized.current) {
throw new Error(`${ctor.name}: Controller initialization arguments can't be updated`);
}
isInitialized.current = true;
controllerRef.current = controller;
}, [...args]);
useEffect(() => () => {
if (isDestructibleController(controller)) {
controller.destruct();
if (isDestructibleController(controllerRef.current)) {
controllerRef.current.destruct();
}
}, []);
return controller as T;
return controllerRef.current!;
}
function isDestructibleController(obj: any): obj is IDestructibleController {
@@ -0,0 +1,23 @@
query updateConnectionConfiguration($id: ID!, $config: ConnectionConfig!) {
connection: updateConnectionConfiguration(id: $id, config: $config) {
id
name
description
driverId
template
connected
readOnly
host
port
databaseName
url
properties
features
authNeeded
authModel
}
}
+32
View File
@@ -498,6 +498,7 @@ export type ConnectionConfig = {
properties?: Maybe<Scalars['Object']>;
template?: Maybe<Scalars['Boolean']>;
readOnly?: Maybe<Scalars['Boolean']>;
saveCredentials?: Maybe<Scalars['Boolean']>;
authModelId?: Maybe<Scalars['ID']>;
credentials?: Maybe<Scalars['Object']>;
dataSourceId?: Maybe<Scalars['ID']>;
@@ -838,6 +839,13 @@ export type GetConnectionsQueryVariables = Exact<{ [key: string]: never }>;
export type GetConnectionsQuery = { connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'readOnly' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel'>> };
export type UpdateConnectionConfigurationQueryVariables = Exact<{
id: Scalars['ID'];
config: ConnectionConfig;
}>;
export type UpdateConnectionConfigurationQuery = { connection: Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'readOnly' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel'> };
export type CloseConnectionMutationVariables = Exact<{
id: Scalars['ID'];
}>;
@@ -1343,6 +1351,27 @@ export const GetConnectionsDocument = `
}
}
`;
export const UpdateConnectionConfigurationDocument = `
query updateConnectionConfiguration($id: ID!, $config: ConnectionConfig!) {
connection: updateConnectionConfiguration(id: $id, config: $config) {
id
name
description
driverId
template
connected
readOnly
host
port
databaseName
url
properties
features
authNeeded
authModel
}
}
`;
export const CloseConnectionDocument = `
mutation closeConnection($id: ID!) {
connection: closeConnection(id: $id) {
@@ -2021,6 +2050,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getConnections(variables?: GetConnectionsQueryVariables): Promise<GetConnectionsQuery> {
return withWrapper(() => client.request<GetConnectionsQuery>(GetConnectionsDocument, variables));
},
updateConnectionConfiguration(variables: UpdateConnectionConfigurationQueryVariables): Promise<UpdateConnectionConfigurationQuery> {
return withWrapper(() => client.request<UpdateConnectionConfigurationQuery>(UpdateConnectionConfigurationDocument, variables));
},
closeConnection(variables: CloseConnectionMutationVariables): Promise<CloseConnectionMutation> {
return withWrapper(() => client.request<CloseConnectionMutation>(CloseConnectionDocument, variables));
},
@@ -19,8 +19,8 @@
@include mdc-theme-prop(border-color, background, false);
&:global([type="checkbox"]) {
height: 26px;
width: 14px;
height: 16px;
width: 16px;
}
&:global([disabled]),
@@ -36,6 +36,7 @@
label {
display: block;
line-height: 15px;
box-sizing: border-box;
padding: 0;
overflow: hidden;
@@ -11,6 +11,7 @@
@import '@material/button/mixins';
@import "@material/elevation/mixins";
@import "@material/typography/mixins";
@import 'variables';
@import 'branding';
@import 'theme-ripple';
@import 'ripple';
@@ -20,17 +20,21 @@
@include mdc-theme-prop(border-color, background, false);
}
tbody tr:hover {
@include stripes-background($mdc-theme-secondary);
}
tbody tr {
&:focus {
@include mdc-theme-prop(border-color, background, false);
}
tbody tr:focus {
@include mdc-theme-prop(border-color, background, false);
}
&:not([use|noHover]):hover,
&:not([use|noHover])[use|selected],
&:not([use|noHover])[use|expanded] {
@include stripes-background($mdc-theme-secondary);
}
tbody tr[use|isSelected] {
@include stripes-background($mdc-theme-secondary);
// @include mdc-theme-prop(border-color, $color-positive, false);
&[use|expanded] td {
padding-top: 24px;
padding-bottom: 24px;
}
}
th {
@@ -44,29 +48,50 @@
text-transform: uppercase;
text-align: left;
text-decoration: none !important;
}
th:last-child {
border-right: none;
}
&[use|min] {
width: 1%;
}
th[use|min] {
width: 1%;
&:last-child {
border-right: none;
}
}
td {
height: 24px;
height: 28px;
padding: 0 16px;
transition: padding ease-in-out 0.24s;
&[use|centerContent] {
align-items: center;
vertical-align: middle;
}
&[use|flex] {
display: flex;
}
&[use|expandArea] {
padding: 0;
}
> input[type="checkbox"] {
display: block;
height: 16px;
width: 16px;
}
}
td[use|centerContent] {
align-items: center;
vertical-align: middle;
}
td > input[type="checkbox"] {
display: block;
height: 16px;
TableItemExpand {
cursor: pointer;
margin-left: 16px;
width: 16px;
height: 100%;
padding: 0;
&[|expanded] {
transform: rotate(180deg);
}
}
}
@@ -0,0 +1,15 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
$mdc-theme-extra-property-values: (
color-positive: $color-positive,
color-negative: $color-negative,
input-border-color: $input-border-color,
);
$mdc-theme-property-values: map-merge($mdc-theme-property-values, $mdc-theme-extra-property-values);
@@ -135,7 +135,7 @@ export const CreateUserDialog = observer(function CreateUserDialog({
{controller.roles.map((role, i) => (
<group as="div" key={role.roleId}>
<Checkbox
type='checkbox'
value={role.roleId}
name='role'
checkboxLabel={role.roleName || role.roleId}
onChange={checked => handleRoleChange(role.roleId, checked)}
@@ -9,14 +9,12 @@
import { observable } from 'mobx';
import { IProperty } from '@cloudbeaver/core-blocks';
import { DBDriver } from '@cloudbeaver/core-connections';
import { DBDriver, DriverPropertiesService } from '@cloudbeaver/core-connections';
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import { CustomConnectionService } from '../../../CustomConnectionService';
export type DriverPropertyState = {
[key: string]: string;
}
@@ -37,8 +35,10 @@ export class DriverPropertiesController implements IInitializableController {
private loaded = false;
constructor(private customConnectionService: CustomConnectionService,
private notificationService: NotificationService) { }
constructor(
private driverPropertiesService: DriverPropertiesService,
private notificationService: NotificationService
) { }
init(driver: DBDriver) {
this.driver = driver;
@@ -58,7 +58,7 @@ export class DriverPropertiesController implements IInitializableController {
}
this.isLoading = true;
try {
const driverProperties = await this.customConnectionService.loadDriverProperties(this.driver.id);
const driverProperties = await this.driverPropertiesService.loadDriverProperties(this.driver.id);
this.driverProperties = observable(driverProperties.map(property => ({
id: property.id!,
key: property.id!,
@@ -8,7 +8,7 @@
import { ConnectionsManagerService, Connection } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { ConnectionConfig, GraphQLService, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { ConnectionConfig, GraphQLService } from '@cloudbeaver/core-sdk';
@injectable()
export class CustomConnectionService {
@@ -19,18 +19,6 @@ export class CustomConnectionService {
) {
}
async loadDriverProperties(driverId: string): Promise<ObjectPropertyInfo[]> {
const response = await this.graphQLService.gql.driverProperties({
driverId,
});
if (response.driver.length === 0) {
throw new Error('Driver properties loading failed');
}
return response.driver[0].driverProperties! as ObjectPropertyInfo[];
}
async createConnectionAsync(config: ConnectionConfig): Promise<Connection> {
const response = await this.graphQLService.gql.createConnection({
config,