Merge pull request #245 from dbeaver/fix/avoid-same-connection-names

fix(core-connections): CB-595 add index to connection name in case of…
This commit is contained in:
Alexey
2020-12-21 17:14:31 +03:00
committed by GitHub
10 changed files with 203 additions and 78 deletions
@@ -19,6 +19,10 @@ import { DBDriver, DBDriverResource } from '../../../DBDriverResource';
import { ConnectionsResource, isLocalConnection } from '../../ConnectionsResource';
import { IConnectionFormModel } from './IConnectionFormModel';
interface IValidationStatus {
status: boolean;
errorMessage: string;
}
@injectable()
export class ConnectionFormController
implements IInitializableController {
@@ -67,15 +71,23 @@ implements IInitializableController {
}
save = async (): Promise<void> => {
const connectionConfig = this.getConnectionConfig();
const validation = this.validate(connectionConfig);
if (!validation.status) {
this.notificationService.logError({ title: this.model.editing ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', message: validation.errorMessage });
return;
}
this.isSaving = true;
try {
if (this.model.editing) {
const connection = await this.connectionsResource.update(this.model.connection.id, this.getConnectionConfig());
const connection = await this.connectionsResource.update(this.model.connection.id, connectionConfig);
await this.afterSave.execute(connection.id);
this.notificationService.logSuccess({ title: `Connection ${connection.name} updated` });
} else {
const connection = await this.connectionsResource.create(this.getConnectionConfig());
connectionConfig.name = this.getUniqueName(connectionConfig.name!);
const connection = await this.connectionsResource.create(connectionConfig);
await this.afterSave.execute(connection.id);
this.close();
this.notificationService.logSuccess({ title: `Connection ${connection.name} created` });
@@ -99,6 +111,48 @@ implements IInitializableController {
}
};
private isConnectionNameAlreadyExists(name: string) {
for (const connection of this.connectionsResource.data.values()) {
if (connection.id !== this.model.connection.id && connection.name === name) {
return true;
}
}
return false;
}
private validate(config: ConnectionConfig) {
const validationStatus: IValidationStatus = { status: true, errorMessage: '' };
if (!config.name?.length) {
validationStatus.errorMessage = "Field 'name' can't be empty";
} else if (this.model.editing && this.isConnectionNameAlreadyExists(config.name)) {
validationStatus.errorMessage = 'Connection with this name already exists';
}
validationStatus.status = !validationStatus.errorMessage;
return validationStatus;
}
private getUniqueName(baseName: string) {
let index = 1;
let name = baseName;
const connectionsNames = new Set();
for (const connection of this.connectionsResource.data.values()) {
connectionsNames.add(connection.name);
}
while (true) {
if (!connectionsNames.has(name)) {
break;
}
name = `${baseName} (${index})`;
index++;
}
return name;
}
private getConnectionConfig(): ConnectionConfig {
const config: ConnectionConfig = {};
@@ -106,7 +160,7 @@ implements IInitializableController {
config.connectionId = this.model.connection.id;
}
config.name = this.model.connection.name;
config.name = this.model.connection.name.trim();
config.description = this.model.connection.description;
config.template = this.model.connection.template;
config.driverId = this.model.connection.driverId;
@@ -10,7 +10,7 @@ import { action, computed } from 'mobx';
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { DatabaseAuthModel, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { AdminConnectionFragment, DatabaseAuthModel, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { DatabaseAuthModelsResource } from '../../../../DatabaseAuthModelsResource';
import { DBDriver, DBDriverResource } from '../../../../DBDriverResource';
@@ -47,12 +47,14 @@ implements IInitializableController {
}
private model!: IConnectionFormModel;
private nameTemplate = /^(.*?)\s(|\(.*?\)\s)connection$/;
/** we want to save prev generated connection name to detect if user changed "name" field to turn off autofill */
private prevGeneratedName: string | null = null;
private maxHostLength = 20;
constructor(
private notificationService: NotificationService,
private dbAuthModelsResource: DatabaseAuthModelsResource,
private dbDriverResource: DBDriverResource
private dbDriverResource: DBDriverResource,
) { }
init(model: IConnectionFormModel): void {
@@ -67,7 +69,9 @@ implements IInitializableController {
): Promise<void> => this.loadDriver(driverId, prevValue);
onFormChange = (value?: unknown, name?: string): void => {
this.updateName(name);
if (name !== 'name') {
this.updateNameTemplate(this.model.connection);
}
};
@action
@@ -104,39 +108,37 @@ implements IInitializableController {
this.model.connection.url = this.driver?.sampleURL;
}
this.updateName();
this.updateNameTemplate(this.model.connection);
}
private updateName(name?: string) {
if (name === 'name') {
private updateNameTemplate(connection: AdminConnectionFragment) {
const isAutoFill = connection.name === this.prevGeneratedName || this.prevGeneratedName === null;
if (this.model.editing || !isAutoFill) {
return;
}
const databaseNames = ['New', ...this.drivers.map(driver => driver.name!)]
.filter(Boolean);
const matches = this.nameTemplate.exec(this.model.connection.name);
if (this.model.connection.name === undefined
|| (matches?.length && databaseNames.includes(matches[1]))) {
this.model.connection.name = this.getNameTemplate();
const isUrlConnection = !this.driver?.sampleURL;
if (isUrlConnection) {
this.prevGeneratedName = connection.url || '';
connection.name = connection.url || '';
return;
}
}
private getNameTemplate() {
if (this.driver) {
let address = [this.model.connection.host, this.model.connection.host && this.model.connection.port]
.filter(Boolean)
.join(':');
if (!this.driver) {
connection.name = 'New connection';
return;
}
if (address) {
address = ` (${address})`;
let name = this.driver.name || '';
if (connection.host) {
name += '@' + connection.host.slice(0, this.maxHostLength);
if (connection.port && connection.port !== this.driver.defaultPort) {
name += ':' + connection.port;
}
return `${this.driver.name}${address} connection`;
}
return 'New connection';
this.prevGeneratedName = name;
connection.name = name;
}
private async loadDrivers() {
@@ -3,6 +3,8 @@ export default [
['connections_administration_connection_create', 'Add Database'],
['connections_administration_search_database_tip', 'Type your hosts here, e.g. \'localhost host1.myhost.com 192.168.0.1\' and press Enter'],
['connections_administration_new_connection', 'New connection'],
['connections_administration_connection_create_error', 'Create connection error'],
['connections_administration_connection_save_error', 'Save connection error'],
['connections_administration_connection_access_empty', 'No available users and roles'],
['connections_administration_configuration_wizard_step_title', 'Database connections'],
['connections_administration_configuration_wizard_step_description', 'Add database connections'],
@@ -3,6 +3,8 @@ export default [
['connections_administration_connection_create', 'Создание подключения'],
['connections_administration_search_database_tip', 'Укажите ваши хосты, например \'localhost host1.myhost.com 192.168.0.1\' и нажмите Enter'],
['connections_administration_new_connection', 'Создание подключения'],
['connections_administration_connection_create_error', 'Ошибка при создании подключения'],
['connections_administration_connection_save_error', 'Ошибка при сохранении подключения'],
['connections_administration_connection_access_empty', 'Нет доступных пользователей и подключений'],
['connections_administration_configuration_wizard_step_title', 'Подключения к базам'],
['connections_administration_configuration_wizard_step_description', 'Подключения к базам'],
@@ -52,18 +52,28 @@ export const ConnectionForm = observer(function ConnectionForm({
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
return styled(useStyles(connectionFormStyles))(
<SubmittingForm ref={focusedRef}>
<SubmittingForm ref={focusedRef} onChange={controller.onChange}>
<FormBox>
<FormBoxElement>
<FormGroup>
<InputField
type="text"
name="name"
disabled={controller.isConnecting}
mod='surface'
state={controller.config}
>
{translate('customConnection_custom_name')}
</InputField>
</FormGroup>
{controller.isUrlConnection ? (
<FormGroup>
<InputField
type="text"
name="url"
value={controller.config.url}
state={controller.config}
disabled={controller.isConnecting}
mod='surface'
onChange={value => controller.onChange('url', value)}
>
{translate('customConnection_url_JDBC')}
</InputField>
@@ -13,5 +13,5 @@ export interface IFormController {
config: ConnectionConfig;
isUrlConnection: boolean;
isConnecting: boolean;
onChange: <T extends keyof ConnectionConfig>(property: T, value: ConnectionConfig[T]) => void;
onChange: (value?: unknown, name?: string) => void;
}
@@ -40,10 +40,9 @@ export const ParametersForm = observer(function ParametersForm({
<InputField
type="text"
name="host"
value={controller.config.host}
disabled={controller.isConnecting}
mod='surface'
onChange={value => controller.onChange('host', value)}
state={controller.config}
>
{translate('customConnection_custom_host')}
<sub-label as="div">{translate('customConnection_custom_obligatory')}</sub-label>
@@ -53,11 +52,10 @@ export const ParametersForm = observer(function ParametersForm({
<InputField
type="number"
name="port"
value={controller.config.port}
disabled={controller.isConnecting}
mod='surface'
state={controller.config}
short
onChange={value => controller.onChange('port', value)}
>
{translate('customConnection_custom_port')}
</InputField>
@@ -68,10 +66,9 @@ export const ParametersForm = observer(function ParametersForm({
<InputField
type="text"
name="databaseName"
value={controller.config.databaseName}
disabled={controller.isConnecting}
mod='surface'
onChange={value => controller.onChange('databaseName', value)}
state={controller.config}
>
{translate('customConnection_custom_database')}
</InputField>
@@ -8,7 +8,7 @@
import { observable, action } from 'mobx';
import { DBDriver, DatabaseAuthModelsResource } from '@cloudbeaver/core-connections';
import { DBDriver, DatabaseAuthModelsResource, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -16,6 +16,12 @@ import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { ConnectionConfig, GQLErrorCatcher, DatabaseAuthModel } from '@cloudbeaver/core-sdk';
import { CustomConnectionService } from '../../CustomConnectionService';
interface IValidationStatus {
status: boolean;
errorMessage: string;
}
@injectable()
export class ConnectionFormDialogController
implements IInitializableController, IDestructibleController {
@@ -42,12 +48,16 @@ implements IInitializableController, IDestructibleController {
readonly error = new GQLErrorCatcher();
private onClose!: () => void;
private isDistructed = false;
/** we want to save prev generated config name to detect if user changed "name" field to turn off autofill */
private prevGeneratedName: string | null = null;
private maxHostLength = 20;
constructor(
private customConnectionService: CustomConnectionService,
private notificationService: NotificationService,
private commonDialogService: CommonDialogService,
private dbAuthModelsResource: DatabaseAuthModelsResource
private dbAuthModelsResource: DatabaseAuthModelsResource,
private connectionInfoResource: ConnectionInfoResource,
) { }
init(driver: DBDriver, onClose: () => void) {
@@ -61,8 +71,10 @@ implements IInitializableController, IDestructibleController {
this.isDistructed = true;
}
onChange = (property: keyof ConnectionConfig, value: any) => {
this.config[property] = value;
onChange = (value?: unknown, name?: string): void => {
if (name !== 'name') {
this.updateNameTemplate(this.config);
}
};
onTestConnection = async () => {
@@ -80,10 +92,18 @@ implements IInitializableController, IDestructibleController {
};
onCreateConnection = async () => {
const connectionConfig = this.getConnectionConfig();
const validation = this.validate(connectionConfig);
if (!validation.status) {
this.notificationService.logError({ title: 'customConnection_create_error', message: validation.errorMessage });
return;
}
this.isConnecting = true;
this.error.clear();
try {
const connection = await this.customConnectionService.createConnectionAsync(this.getConnectionConfig());
connectionConfig.name = this.getUniqueName(connectionConfig.name!);
const connection = await this.customConnectionService.createConnectionAsync(connectionConfig);
this.notificationService.logSuccess({ title: `Connection ${connection.name} created` });
this.onClose();
@@ -100,21 +120,67 @@ implements IInitializableController, IDestructibleController {
}
};
private updateNameTemplate(config: ConnectionConfig) {
const isAutoFill = config.name === this.prevGeneratedName || this.prevGeneratedName === null;
if (!isAutoFill) {
return;
}
if (this.isUrlConnection) {
this.prevGeneratedName = config.url || '';
config.name = config.url || '';
return;
}
if (!this.driver) {
config.name = 'New connection';
return;
}
let name = this.driver.name || '';
if (config.host) {
name += '@' + config.host.slice(0, this.maxHostLength);
if (config.port && config.port !== this.driver.defaultPort) {
name += ':' + config.port;
}
}
this.prevGeneratedName = name;
config.name = name;
}
private getUniqueName(baseName: string) {
let index = 1;
let name = baseName;
const connectionsNames = new Set();
for (const connection of this.connectionInfoResource.data.values()) {
connectionsNames.add(connection.name);
}
while (true) {
if (!connectionsNames.has(name)) {
break;
}
name = `${baseName} (${index})`;
index++;
}
return name;
}
private getConnectionConfig(): ConnectionConfig {
const config: ConnectionConfig = {};
config.name = this.config.name;
config.name = this.config.name?.trim();
config.driverId = this.config.driverId;
if (!this.isUrlConnection) {
config.name = this.config.databaseName;
if (!this.driver?.embedded) {
config.host = this.config.host;
config.port = this.config.port;
config.name += `@${this.config.host}`;
}
config.databaseName = this.config.databaseName;
} else {
config.name = this.urlToConnectionName(this.config.name, this.config.url);
config.url = this.config.url;
}
if (this.authModel) {
@@ -129,46 +195,34 @@ implements IInitializableController, IDestructibleController {
return config;
}
private validate(config: ConnectionConfig) {
const validationStatus: IValidationStatus = { status: true, errorMessage: '' };
if (!config.name?.length) {
validationStatus.errorMessage = "Field 'name' can't be empty";
}
validationStatus.status = !validationStatus.errorMessage;
return validationStatus;
}
@action
private setDriverDefaults() {
this.config.name = `${this.driver.name} (custom)`;
this.config.driverId = this.driver.id;
this.config.host = this.driver.defaultServer || 'localhost';
this.config.port = this.driver.defaultPort || '';
this.config.databaseName = this.driver.defaultDatabase;
this.config.url = this.driver.sampleURL || '';
this.config.driverId = this.driver.id;
this.config.databaseName = this.driver.defaultDatabase;
this.config.properties = {};
this.config.authModelId = this.driver.defaultAuthModel;
this.config.credentials = {};
this.updateNameTemplate(this.config);
}
/**
* Creates connection name based on connection url
* @param defaultName default connection name if url parsing failed
* @param url connection url
*/
private urlToConnectionName(defaultName?: string, url?: string) {
let name = defaultName;
if (!url) {
return name;
}
const matches = /^.*:\/\/(.*?)(:.*?|)(\/(.*)?|)$/.exec(url);
if (!matches) {
return name;
}
name = matches[1];
if (matches[4]) {
name += `@${matches[4]}`;
}
return name;
}
private showError(exception: Error, message: string) {
private showError(exception: Error, title: string) {
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, message);
this.notificationService.logException(exception, title);
}
}
@@ -3,6 +3,7 @@ export default [
['customConnection_connectionType_url', 'URL'],
['customConnection_options', 'Options'],
['customConnection_properties', 'Driver Properties'],
['customConnection_custom_name', 'Name'],
['customConnection_custom_host', 'Host'],
['customConnection_custom_obligatory', '(obligatory)'],
['customConnection_custom_port', 'Port'],
@@ -13,4 +14,5 @@ export default [
['customConnection_Password', 'Password'],
['customConnection_test', 'Test Connection'],
['customConnection_create', 'Create'],
['customConnection_create_error', 'Сreate connection error'],
];
@@ -3,6 +3,7 @@ export default [
['customConnection_connectionType_url', 'URL'],
['customConnection_options', 'Подключение'],
['customConnection_properties', 'Параметры драйвера'],
['customConnection_custom_name', 'Имя'],
['customConnection_custom_host', 'Хост'],
['customConnection_custom_obligatory', '(обязательное)'],
['customConnection_custom_port', 'Порт'],
@@ -13,4 +14,5 @@ export default [
['customConnection_Password', 'Пароль'],
['customConnection_test', 'Проверить'],
['customConnection_create', 'Создать'],
['customConnection_create_error', 'Ошибка при создании подключения'],
];