mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core): add PropertiesTable block
This commit is contained in:
@@ -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?',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
+11
-11
@@ -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;
|
||||
}
|
||||
`
|
||||
);
|
||||
@@ -16,3 +16,4 @@ export * from './useClipboard';
|
||||
export * from './SubmittingForm';
|
||||
export * from './SanitizedHTML/SanitizedHTML';
|
||||
export * from './ItemList';
|
||||
export * from './PropertiesTable';
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+21
-80
@@ -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>
|
||||
);
|
||||
|
||||
+14
-52
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user