mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core): connections managment
CB-215
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+22
-23
@@ -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>
|
||||
);
|
||||
}
|
||||
+5
-3
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-1
@@ -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>
|
||||
</>
|
||||
|
||||
+23
-12
@@ -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;
|
||||
|
||||
+30
-7
@@ -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>
|
||||
);
|
||||
|
||||
+192
@@ -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>
|
||||
);
|
||||
});
|
||||
+230
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -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>
|
||||
);
|
||||
});
|
||||
+26
@@ -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;
|
||||
}
|
||||
+72
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
+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 { 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;
|
||||
}
|
||||
`;
|
||||
+7
-2
@@ -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>
|
||||
|
||||
+64
@@ -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>
|
||||
);
|
||||
});
|
||||
+79
@@ -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', 'Показывать в дереве навигации'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
+1
-1
@@ -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)}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user