mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-24 11:35:54 +08:00
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:
+57
-3
@@ -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;
|
||||
|
||||
+29
-27
@@ -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', 'Подключения к базам'],
|
||||
|
||||
+13
-3
@@ -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>
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+3
-6
@@ -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>
|
||||
|
||||
+92
-38
@@ -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', 'Ошибка при создании подключения'],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user