feat(core): add PropertiesTable block

This commit is contained in:
Wroud
2020-05-03 20:24:18 +03:00
parent 586e361789
commit 609a870c19
12 changed files with 261 additions and 151 deletions
+3 -3
View File
@@ -17,9 +17,6 @@ module.exports = {
customConnection_connectionType_url: 'URL',
customConnection_options: 'Options',
customConnection_properties: 'Driver Properties',
customConnection_properties_name: 'Name',
customConnection_properties_value: 'Value',
customConnection_properties_add: 'Add',
customConnection_custom_host: 'Host',
customConnection_custom_obligatory: '(obligatory)',
customConnection_custom_port: 'Port',
@@ -39,6 +36,9 @@ module.exports = {
ui_errors_details: 'Details',
ui_search: 'Search...',
ui_data_saving_error: 'Error when saving changes',
block_properties_table_name: 'Name',
block_properties_table_value: 'Value',
block_properties_table_add: 'Add',
app_root_session_expired_title: 'Session expired',
app_root_session_expired_message: 'Session has been expired. Do you want to reload?',
+4 -4
View File
@@ -16,9 +16,6 @@ module.exports = {
customConnection_connectionType_url: 'URL',
customConnection_options: 'Подключение',
customConnection_properties: 'Параметры драйвера',
customConnection_properties_name: 'Название',
customConnection_properties_value: 'Значение',
customConnection_properties_add: 'Добавить',
customConnection_custom_host: 'Хост',
customConnection_custom_obligatory: '(обязательное)',
customConnection_custom_port: 'Порт',
@@ -32,12 +29,15 @@ module.exports = {
ui_stepper_back: 'Назад',
ui_processing_loading: 'Загрузка...',
ui_processing_cancel: 'Отменить...',
ui_processing_cancel: 'Отменить',
ui_processing_retry: 'Повторить',
ui_error_message: 'Ошибка:',
ui_errors_details: 'Информация',
ui_search: 'Поиск...',
ui_data_saving_error: 'Ошибка при сохранении изменений',
block_properties_name: 'Название',
block_properties_value: 'Значение',
block_properties_add: 'Добавить',
app_root_session_expired_title: 'Сесстия истекла',
app_root_session_expired_message: 'Сессия истекла. Хотите перезагрузить страницу?',
@@ -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.
*/
export interface IProperty {
id: string;
name: string;
defaultValue?: string;
description?: string;
validValues?: string[];
}
@@ -0,0 +1,129 @@
/*
* 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, useLocalStore } from 'mobx-react';
import { useCallback } from 'react';
import styled from 'reshadow';
import { Button } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { IProperty } from './IProperty';
import { PropertyItem } from './PropertyItem';
import { PROPERTIES_TABLE_STYLES } from './styles';
type PropertiesState = {
[key: string]: string;
}
type PropertiesTableProps = {
properties: IProperty[];
propertiesState?: PropertiesState;
onNameChange?(id: string, name: string): void;
onChange?(state: PropertiesState): void;
onAdd?(): void;
onRemove?(id: string): void;
className?: string;
}
export const PropertiesTable = observer(function PropertiesTable({
propertiesState,
properties,
onNameChange,
onChange,
onAdd,
onRemove,
className,
}: PropertiesTableProps) {
const translate = useTranslate();
const state = useLocalStore<PropertiesState>(() => (propertiesState || {}));
const changeName = useCallback((id: string, name: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
if (state[property.name] !== undefined) {
state[name] = state[property.name];
delete state[property.name];
}
if (onNameChange) {
onNameChange(property.name, name);
}
property.name = name;
}, [properties]);
const changeValue = useCallback((id: string, value: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
state[property.name] = value;
if (onChange) {
onChange(state);
}
}, [properties, onChange]);
const removeProperty = useCallback((id: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
if (state[property.name] !== undefined) {
delete state[property.name];
}
if (onRemove) {
onRemove(id);
}
properties.splice(properties.indexOf(property), 1);
}, [properties, onRemove]);
const isNameUnique = useCallback(
(name: string) => properties.filter(property => property.name === name).length === 1,
[]
);
return styled(useStyles(PROPERTIES_TABLE_STYLES))(
<properties as="div" className={className}>
<properties-header as="div">
<properties-header-name as="div">
{translate('block_properties_table_name')}
</properties-header-name>
<properties-header-value as="div">
{translate('block_properties_table_value')}
</properties-header-value>
<properties-header-right as="div">
{onAdd && <Button type='button' mod={['outlined']} onClick={onAdd}>{translate('block_properties_table_add')}</Button> }
</properties-header-right>
</properties-header>
<properties-list as="div">
{properties.map(property => (
<PropertyItem
key={property.id}
property={property}
value={state[property.name]}
onNameChange={changeName}
onValueChange={changeValue}
onRemove={removeProperty}
error={!isNameUnique(property.name)}
/>
))}
</properties-list>
</properties>
);
});
@@ -15,8 +15,8 @@ import styled, { css, use } from 'reshadow';
import { ShadowInput, Icon } from '@dbeaver/core/blocks';
import { composes, useStyles } from '@dbeaver/core/theming';
import { DriverPropertyInfoWithStaticId } from './DriverPropertiesController';
import { DriverPropertyValueSelector } from './DriverPropertyValueSelector';
import { IProperty } from './IProperty';
const styles = composes(
css`
@@ -104,8 +104,8 @@ const styles = composes(
`
);
type DriverPropertyProps = {
property: DriverPropertyInfoWithStaticId;
type PropertyItemProps = {
property: IProperty;
value?: string;
onNameChange(staticId: string, newId: string): void;
onValueChange(staticId: string, value: string): void;
@@ -113,24 +113,24 @@ type DriverPropertyProps = {
error?: boolean;
}
export const DriverProperty = observer(function DriverProperty({
export const PropertyItem = observer(function PropertyItem({
property,
value,
onNameChange,
onValueChange,
onRemove,
error,
}: DriverPropertyProps) {
const isEditable = !property.displayName;
}: PropertyItemProps) {
const isEditable = property.name !== property.id;
const edited = value !== undefined && value !== property.defaultValue;
const [focus, setFocus] = useState(false);
const nameInputRef = useRef<HTMLInputElement>(null);
const handleNameChange = useCallback((value: string) => onNameChange(property.staticId, value), [property.staticId]);
const handleNameChange = useCallback((value: string) => onNameChange(property.id, value), [property]);
const handleValueChange = useCallback(
(value: string) => onValueChange(property.staticId, value),
[property.staticId]
(value: string) => onValueChange(property.id, value),
[property]
);
const handleRemove = useCallback(() => onRemove(property.staticId), [property.staticId]);
const handleRemove = useCallback(() => onRemove(property.id), [property]);
useEffect(() => {
if (nameInputRef.current && isEditable) {
@@ -149,7 +149,7 @@ export const DriverProperty = observer(function DriverProperty({
readOnly={!isEditable}
autoComplete='none'
>
{property.displayName || property.id}
{property.name || property.id}
</ShadowInput>
</property-name>
<property-value as='div'>
@@ -0,0 +1,2 @@
export * from './PropertiesTable';
export * from './IProperty';
@@ -0,0 +1,60 @@
/*
* 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';
import { composes } from '@dbeaver/core/theming';
export const PROPERTIES_TABLE_STYLES = composes(
css`
properties-header {
composes: theme-background-secondary theme-text-on-secondary from global;
}
properties-header-name, properties-header-value {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
properties-header {
box-sizing: border-box;
display: inline-flex;
padding: 5px 1px;
position: sticky;
z-index: 1;
top: 0;
}
properties-header-name, properties-header-value {
composes: theme-typography--caption from global;
text-transform: uppercase;
box-sizing: border-box;
flex: 1;
padding: 4px 36px;
margin: 0px 1px;
}
properties-header-name {
flex: 0 0 auto;
width: 300px;
}
properties-header-right {
flex: 0 0 auto;
margin: 0px 1px;
}
properties-list {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 24px;
}
`
);
+1
View File
@@ -16,3 +16,4 @@ export * from './useClipboard';
export * from './SubmittingForm';
export * from './SanitizedHTML/SanitizedHTML';
export * from './ItemList';
export * from './PropertiesTable';
@@ -144,7 +144,7 @@ export const ConnectionFormDialog = observer(
<BaseTabPanel {...tab} stopId='driver_properties'>
<DriverProperties
driver={driver}
properties={controller.config.properties}
state={controller.config.properties}
isSelected={tab.selectedId === 'driver_properties'}
/>
</BaseTabPanel>
@@ -11,76 +11,36 @@ import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { DBDriver } from '@dbeaver/core/app';
import { Button, Loader } from '@dbeaver/core/blocks';
import { Loader, PropertiesTable } from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles, composes } from '@dbeaver/core/theming';
import { useStyles } from '@dbeaver/core/theming';
import { DriverPropertiesController, DriverPropertyState } from './DriverPropertiesController';
import { DriverProperty } from './DriverProperty';
import { DriverPropertiesController } from './DriverPropertiesController';
const styles = composes(
css`
properties-header {
composes: theme-background-secondary theme-text-on-secondary from global;
}
properties-header-name, properties-header-value {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
properties-header {
box-sizing: border-box;
display: inline-flex;
padding: 5px 1px;
position: sticky;
z-index: 1;
top: 0;
}
properties-header-name, properties-header-value {
composes: theme-typography--caption from global;
text-transform: uppercase;
box-sizing: border-box;
flex: 1;
padding: 4px 36px;
margin: 0px 1px;
}
properties-header-name {
flex: 0 0 auto;
width: 300px;
}
properties-header-right {
flex: 0 0 auto;
margin: 0px 1px;
}
const styles = css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
`;
properties-list {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 24px;
}
`
);
type DriverPropertyState = {
[key: string]: string;
}
type DriverPropertiesProps = {
driver: DBDriver;
properties: DriverPropertyState;
state: DriverPropertyState;
isSelected: boolean;
}
export const DriverProperties = observer(function DriverProperties({
driver,
properties,
state,
isSelected,
}: DriverPropertiesProps) {
const translate = useTranslate();
const controller = useController(DriverPropertiesController, driver, properties);
const controller = useController(DriverPropertiesController, driver);
useEffect(() => {
if (isSelected) {
@@ -90,32 +50,13 @@ export const DriverProperties = observer(function DriverProperties({
return styled(useStyles(styles))(
<properties as="div">
<properties-header as="div">
<properties-header-name as="div">
{translate('customConnection_properties_name')}
</properties-header-name>
<properties-header-value as="div">
{translate('customConnection_properties_value')}
</properties-header-value>
<properties-header-right as="div">
<Button type='button' mod={['outlined']} onClick={controller.onAddProperty}>{translate('customConnection_properties_add')}</Button>
</properties-header-right>
</properties-header>
{controller.isLoading && <Loader />}
{!controller.isLoading && (
<properties-list as="div">
{controller.driverProperties.map(property => (
<DriverProperty
key={property.staticId}
property={property}
value={controller.state[property.id]}
onNameChange={controller.onNameChange}
onValueChange={controller.onValueChange}
onRemove={controller.onRemove}
error={controller.getPropertyIdCount(property.id) > 1}
/>
))}
</properties-list>
<PropertiesTable
properties={controller.driverProperties}
propertiesState={state}
onAdd={controller.onAddProperty}
/>
)}
</properties>
);
@@ -6,9 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import { observable, action } from 'mobx';
import { observable } from 'mobx';
import { DBDriver } from '@dbeaver/core/app';
import { IProperty } from '@dbeaver/core/blocks';
import { injectable, IInitializableController } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { DriverPropertyInfo } from '@dbeaver/core/sdk';
@@ -32,70 +33,25 @@ export class DriverPropertiesController implements IInitializableController {
@observable driver!: DBDriver
@observable hasDetails = false
@observable responseMessage: string | null = null
@observable driverProperties = observable<DriverPropertyInfoWithStaticId>([])
@observable state: DriverPropertyState = {}
@observable driverProperties = observable<IProperty>([])
private loaded = false;
constructor(private customConnectionService: CustomConnectionService,
private notificationService: NotificationService) { }
init(driver: DBDriver, state: DriverPropertyState) {
init(driver: DBDriver) {
this.driver = driver;
this.state = state;
}
getPropertyIdCount(propertyId: string): number {
return this.driverProperties.filter(property => property.id === propertyId).length;
}
onAddProperty = () => {
this.driverProperties.unshift({
staticId: uuid(),
id: 'property',
id: uuid(),
name: 'property',
defaultValue: '',
});
}
onValueChange = (staticId: string, value: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
this.state[property.id] = value;
}
@action
onNameChange = (staticId: string, newId: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
if (this.state[property.id] !== undefined) {
this.state[newId] = this.state[property.id];
delete this.state[property.id];
}
property.id = newId;
}
@action
onRemove = (staticId: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
if (this.state[property.id] !== undefined) {
delete this.state[property.id];
}
this.driverProperties.remove(property);
}
async loadDriverProperties() {
if (this.isLoading || this.loaded) {
return;
@@ -103,7 +59,13 @@ export class DriverPropertiesController implements IInitializableController {
this.isLoading = true;
try {
const driverProperties = await this.customConnectionService.loadDriverProperties(this.driver.id);
this.driverProperties = observable(driverProperties.map(property => ({ ...property, staticId: property.id })));
this.driverProperties = observable(driverProperties.map(property => ({
id: property.displayName!,
name: 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');