more flexible connection dialog, improved UX when connecting to redshift

This commit is contained in:
Jan Prochazka 2021-05-16 11:22:48 +02:00
parent 6d7cc7d441
commit 8bffa4a7dd
10 changed files with 201 additions and 102 deletions

View File

@ -38,7 +38,10 @@ export interface EngineDriver {
title: string;
defaultPort?: number;
supportsDatabaseUrl?: boolean;
isFileDatabase?: boolean;
isElectronOnly?: boolean;
showConnectionField?: (field: string, values: any) => boolean;
showConnectionTab?: (tab: 'ssl' | 'sshTunnel', values: any) => boolean;
beforeConnectionSave?: (values: any) => any;
databaseUrlPlaceholder?: string;
connect({ server, port, user, password, database }): any;
query(pool: any, sql: string): Promise<QueryResult>;

View File

@ -51,19 +51,29 @@
}
async function handleSubmit(e) {
const connection = driver?.isFileDatabase
? {
..._.omit(e.detail, ['server', 'port', 'defaultDatabase']),
singleDatabase: true,
defaultDatabase: getDatabaseFileLabel(e.detail.databaseFile),
}
: {
..._.omit(e.detail, ['databaseFile']),
singleDatabase: e.detail.defaultDatabase ? e.detail.singleDatabase : false,
};
const allProps = [
'databaseFile',
'useDatabaseUrl',
'databaseUrl',
'authType',
'server',
'port',
'user',
'password',
'defaultDatabase',
'singleDatabase',
];
const visibleProps = allProps.filter(x => !driver?.showConnectionField || driver.showConnectionField(x, $values));
const omitProps = _.difference(allProps, visibleProps);
if (!$values.defaultDatabase) omitProps.push('singleDatabase');
let connection = _.omit(e.detail, omitProps);
if (driver?.beforeConnectionSave) connection = driver?.beforeConnectionSave(connection);
axiosInstance.post('connections/save', connection);
closeCurrentModal();
}
</script>
<FormProviderCore template={FormFieldTemplateLarge} {values}>
@ -77,11 +87,11 @@
label: 'Main',
component: ConnectionModalDriverFields,
},
!driver?.isFileDatabase && {
(!driver?.showConnectionTab || driver?.showConnectionTab('sshTunnel', $values)) && {
label: 'SSH Tunnel',
component: ConnectionModalSshTunnelFields,
},
!driver?.isFileDatabase && {
(!driver?.showConnectionTab || driver?.showConnectionTab('ssl', $values)) && {
label: 'SSL',
component: ConnectionModalSslFields,
},
@ -146,4 +156,5 @@
.error-result {
white-space: normal;
}
</style>

View File

@ -24,6 +24,7 @@
$: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
$: driver = $extensions.drivers.find(x => x.engine == engine);
$: defaultDatabase = $values.defaultDatabase;
</script>
<FormSelectField
@ -33,7 +34,7 @@
options={[
{ label: '(select driver)', value: '' },
...$extensions.drivers
.filter(driver => !driver.isFileDatabase || electron)
.filter(driver => !driver.isElectronOnly || electron)
.map(driver => ({
value: driver.engine,
label: driver.title,
@ -41,44 +42,48 @@
]}
/>
{#if driver?.isFileDatabase}
{#if !driver?.showConnectionField || driver.showConnectionField('databaseFile', $values)}
<FormElectronFileSelector label="Database file" name="databaseFile" disabled={!electron} />
{:else}
{#if driver?.supportsDatabaseUrl}
<div class="radio">
<FormRadioGroupField
name="useDatabaseUrl"
options={[
{ label: 'Fill database connection details', value: '', default: true },
{ label: 'Use database URL', value: '1' },
]}
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('useDatabaseUrl', $values)}
<div class="radio">
<FormRadioGroupField
name="useDatabaseUrl"
options={[
{ label: 'Fill database connection details', value: '', default: true },
{ label: 'Use database URL', value: '1' },
]}
/>
</div>
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('databaseUrl', $values)}
<FormTextField label="Database URL" name="databaseUrl" placeholder={driver?.databaseUrlPlaceholder} />
{/if}
{#if $authTypes && (!driver?.showConnectionField || driver.showConnectionField('authType', $values))}
<FormSelectField
label="Authentication"
name="authType"
options={$authTypes.map(auth => ({
value: auth.name,
label: auth.title,
}))}
/>
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('server', $values)}
<div class="row">
<div class="col-9 mr-1">
<FormTextField
label="Server"
name="server"
disabled={disabledFields.includes('server')}
templateProps={{ noMargin: true }}
/>
</div>
{/if}
{#if driver?.supportsDatabaseUrl && useDatabaseUrl}
<FormTextField label="Database URL" name="databaseUrl" placeholder={driver?.databaseUrlPlaceholder} />
{:else}
{#if $authTypes}
<FormSelectField
label="Authentication"
name="authType"
options={$authTypes.map(auth => ({
value: auth.name,
label: auth.title,
}))}
/>
{/if}
<div class="row">
<div class="col-9 mr-1">
<FormTextField
label="Server"
name="server"
disabled={disabledFields.includes('server')}
templateProps={{ noMargin: true }}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('port', $values)}
<div class="col-3 mr-1">
<FormTextField
label="Port"
@ -88,17 +93,21 @@
placeholder={driver && driver.defaultPort}
/>
</div>
</div>
{/if}
</div>
{/if}
<div class="row">
<div class="col-6 mr-1">
<FormTextField
label="User"
name="user"
disabled={disabledFields.includes('user')}
templateProps={{ noMargin: true }}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('user', $values)}
<div class="row">
<div class="col-6 mr-1">
<FormTextField
label="User"
name="user"
disabled={disabledFields.includes('user')}
templateProps={{ noMargin: true }}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('password', $values)}
<div class="col-6 mr-1">
<FormPasswordField
label="Password"
@ -107,26 +116,28 @@
templateProps={{ noMargin: true }}
/>
</div>
</div>
{#if !disabledFields.includes('password')}
<FormSelectField
label="Password mode"
isNative
name="passwordMode"
options={[
{ value: 'saveEncrypted', label: 'Save and encrypt' },
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
]}
/>
{/if}
{/if}
</div>
{/if}
{#if !disabledFields.includes('password') && (!driver?.showConnectionField || driver.showConnectionField('password', $values))}
<FormSelectField
label="Password mode"
isNative
name="passwordMode"
options={[
{ value: 'saveEncrypted', label: 'Save and encrypt' },
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
]}
/>
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('defaultDatabase', $values)}
<FormTextField label="Default database" name="defaultDatabase" />
{/if}
{#if defaultDatabase}
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
{/if}
{#if defaultDatabase && (!driver?.showConnectionField || driver.showConnectionField('singleDatabase', $values))}
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
{/if}
<FormTextField label="Display name" name="displayName" />
@ -143,4 +154,5 @@
.radio :global(label) {
margin-right: 10px;
}
</style>

View File

@ -21,5 +21,9 @@ export default function getConnectionLabel(connection, { allowExplicitDatabase =
if (connection.server) {
return connection.server;
}
if (connection.singleDatabase && connection.defaultDatabase) {
return `${connection.defaultDatabase}`;
}
return '';
}

View File

@ -34,6 +34,14 @@ const driver = {
supportsDatabaseUrl: true,
databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname',
showConnectionField: (field, values) => {
if (field == 'useDatabaseUrl') return true;
if (values.useDatabaseUrl) {
return ['databaseUrl', 'defaultDatabase', 'singleDatabase'].includes(field);
}
return ['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field);
},
getCollectionUpdateScript(changeSet) {
let res = '';
for (const insert of changeSet.inserts) {

View File

@ -32,6 +32,9 @@ const driver = {
}
return dialect;
},
showConnectionField: (field, values) =>
['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
engine: 'mssql@dbgate-plugin-mssql',
title: 'Microsoft SQL Server',
defaultPort: 1433,

View File

@ -14,24 +14,27 @@ const dialect = {
},
};
/** @type {import('dbgate-types').EngineDriver} */
const mysqlDriver = {
const mysqlDriverBase = {
...driverBase,
showConnectionField: (field, values) =>
['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
dumperClass: Dumper,
dialect,
defaultPort: 3306,
};
/** @type {import('dbgate-types').EngineDriver} */
const mysqlDriver = {
...mysqlDriverBase,
engine: 'mysql@dbgate-plugin-mysql',
title: 'MySQL',
defaultPort: 3306,
};
/** @type {import('dbgate-types').EngineDriver} */
const mariaDriver = {
...driverBase,
dumperClass: Dumper,
dialect,
...mysqlDriverBase,
engine: 'mariadb@dbgate-plugin-mysql',
title: 'MariaDB',
defaultPort: 3306,
};
module.exports = [mysqlDriver, mariaDriver];

View File

@ -97,16 +97,38 @@ const drivers = driverBases.map(driverBase => ({
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, ssl }) {
const client = new pg.Client({
// connectionString: 'postgres://root@localhost:26257/postgres?sslmode=disabke'
host: server,
port,
user,
password,
database: database || 'postgres',
ssl,
});
async connect({ engine, server, port, user, password, database, databaseUrl, ssl }) {
let options = null;
if (engine == 'redshift@dbgate-plugin-postgres') {
let url = databaseUrl;
if (url && url.startsWith('jdbc:redshift://')) {
url = url.substring('jdbc:redshift://'.length);
}
if (user && password) {
url = `postgres://${user}:${password}@${url}`;
} else if (user) {
url = `postgres://${user}@${url}`;
} else {
url = `postgres://${url}`;
}
options = {
connectionString: url,
};
} else {
options = {
// connectionString: 'postgres://root@localhost:26257/postgres?sslmode=disabke'
host: server,
port,
user,
password,
database: database || 'postgres',
ssl,
};
}
const client = new pg.Client(options);
await client.connect();
return client;
},

View File

@ -15,11 +15,17 @@ const dialect = {
stringAgg: true,
};
/** @type {import('dbgate-types').EngineDriver} */
const postgresDriver = {
const postgresDriverBase = {
...driverBase,
dumperClass: Dumper,
dialect,
showConnectionField: (field, values) =>
['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
};
/** @type {import('dbgate-types').EngineDriver} */
const postgresDriver = {
...postgresDriverBase,
engine: 'postgres@dbgate-plugin-postgres',
title: 'Postgre SQL',
defaultPort: 5432,
@ -27,9 +33,7 @@ const postgresDriver = {
/** @type {import('dbgate-types').EngineDriver} */
const cockroachDriver = {
...driverBase,
dumperClass: Dumper,
dialect,
...postgresDriverBase,
engine: 'cockroach@dbgate-plugin-postgres',
title: 'CockroachDB',
defaultPort: 26257,
@ -37,15 +41,30 @@ const cockroachDriver = {
/** @type {import('dbgate-types').EngineDriver} */
const redshiftDriver = {
...driverBase,
dumperClass: Dumper,
...postgresDriverBase,
dialect: {
...dialect,
stringAgg: false,
},
engine: 'red@dbgate-plugin-postgres',
engine: 'redshift@dbgate-plugin-postgres',
title: 'Amazon Redshift',
defaultPort: 5439,
showConnectionField: (field, values) => ['databaseUrl', 'user', 'password'].includes(field),
beforeConnectionSave: connection => {
const { databaseUrl } = connection;
if (databaseUrl) {
const m = databaseUrl.match(/\/([^/]+)$/);
if (m) {
return {
...connection,
singleDatabase: true,
defaultDatabase: m[1],
// displayName: connection.displayName || `${m[1]} on Amazon Redshift`,
};
}
}
return connection;
},
};
module.exports = [postgresDriver, cockroachDriver, redshiftDriver];

View File

@ -1,6 +1,13 @@
const { driverBase } = require('dbgate-tools');
const Dumper = require('./Dumper');
function getDatabaseFileLabel(databaseFile) {
if (!databaseFile) return databaseFile;
const m = databaseFile.match(/[\/]([^\/]+)$/);
if (m) return m[1];
return databaseFile;
}
/** @type {import('dbgate-types').SqlDialect} */
const dialect = {
limitSelect: true,
@ -13,7 +20,6 @@ const dialect = {
},
};
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
@ -21,7 +27,15 @@ const driver = {
dialect,
engine: 'sqlite@dbgate-plugin-sqlite',
title: 'SQLite',
isFileDatabase: true,
showConnectionField: (field, values) => field == 'databaseFile',
showConnectionTab: (field) => false,
beforeConnectionSave: (connection) => ({
...connection,
singleDatabase: true,
defaultDatabase: getDatabaseFileLabel(connection.databaseFile),
}),
// isFileDatabase: true,
isElectronOnly: true,
};
module.exports = driver;