Merge branch 'askpassword'

This commit is contained in:
Jan Prochazka
2022-12-25 19:33:21 +01:00
35 changed files with 501 additions and 118 deletions

View File

@@ -21,6 +21,7 @@ module.exports = ({ editMenu }) => [
{ divider: true }, { divider: true },
{ command: 'file.exit', hideDisabled: true }, { command: 'file.exit', hideDisabled: true },
{ command: 'app.logout', hideDisabled: true, skipInApp: true }, { command: 'app.logout', hideDisabled: true, skipInApp: true },
{ command: 'app.disconnect', hideDisabled: true, skipInApp: true },
], ],
}, },
{ {

View File

@@ -17,6 +17,7 @@
"start:api:portal": "yarn workspace dbgate-api start:portal", "start:api:portal": "yarn workspace dbgate-api start:portal",
"start:api:singledb": "yarn workspace dbgate-api start:singledb", "start:api:singledb": "yarn workspace dbgate-api start:singledb",
"start:api:auth": "yarn workspace dbgate-api start:auth", "start:api:auth": "yarn workspace dbgate-api start:auth",
"start:api:dblogin": "yarn workspace dbgate-api start:dblogin",
"start:web": "yarn workspace dbgate-web dev", "start:web": "yarn workspace dbgate-web dev",
"start:sqltree": "yarn workspace dbgate-sqltree start", "start:sqltree": "yarn workspace dbgate-sqltree start",
"start:tools": "yarn workspace dbgate-tools start", "start:tools": "yarn workspace dbgate-tools start",

14
packages/api/env/dblogin/.env vendored Normal file
View File

@@ -0,0 +1,14 @@
DEVMODE=1
CONNECTIONS=mysql
SINGLE_CONNECTION=mysql
# SINGLE_DATABASE=Chinook
LABEL_mysql=MySql localhost
SERVER_mysql=localhost
# USER_mysql=root
PORT_mysql=3306
# PASSWORD_mysql=Pwd2020Db
ENGINE_mysql=mysql@dbgate-plugin-mysql
# PASSWORD_MODE_mysql=askPassword
PASSWORD_MODE_mysql=askUser

View File

@@ -5,8 +5,8 @@ CONNECTIONS=mysql
LABEL_mysql=MySql localhost LABEL_mysql=MySql localhost
SERVER_mysql=localhost SERVER_mysql=localhost
USER_mysql=root USER_mysql=root
PASSWORD_mysql=test PASSWORD_mysql=Pwd2020Db
PORT_mysql=3307 PORT_mysql=3306
ENGINE_mysql=mysql@dbgate-plugin-mysql ENGINE_mysql=mysql@dbgate-plugin-mysql
DBCONFIG_mysql=[{"name":"Chinook","connectionColor":"cyan"}] DBCONFIG_mysql=[{"name":"Chinook","connectionColor":"cyan"}]

View File

@@ -60,6 +60,7 @@
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api", "start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api", "start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api", "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
"start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api", "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api", "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
"ts": "tsc", "ts": "tsc",

View File

@@ -58,7 +58,7 @@ module.exports = {
refreshFiles_meta: true, refreshFiles_meta: true,
async refreshFiles({ folder }) { async refreshFiles({ folder }) {
socket.emitChanged(`app-files-changed-${folder}`); socket.emitChanged('app-files-changed', { app: folder });
}, },
refreshFolders_meta: true, refreshFolders_meta: true,
@@ -69,7 +69,7 @@ module.exports = {
deleteFile_meta: true, deleteFile_meta: true,
async deleteFile({ folder, file, fileType }) { async deleteFile({ folder, file, fileType }) {
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`)); await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
socket.emitChanged(`app-files-changed-${folder}`); socket.emitChanged('app-files-changed', { app: folder });
this.emitChangedDbApp(folder); this.emitChangedDbApp(folder);
}, },
@@ -79,7 +79,7 @@ module.exports = {
path.join(path.join(appdir(), folder), `${file}.${fileType}`), path.join(path.join(appdir(), folder), `${file}.${fileType}`),
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`) path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
); );
socket.emitChanged(`app-files-changed-${folder}`); socket.emitChanged('app-files-changed', { app: folder });
this.emitChangedDbApp(folder); this.emitChangedDbApp(folder);
}, },
@@ -95,7 +95,7 @@ module.exports = {
if (!folder) throw new Error('Missing folder parameter'); if (!folder) throw new Error('Missing folder parameter');
await fs.rmdir(path.join(appdir(), folder), { recursive: true }); await fs.rmdir(path.join(appdir(), folder), { recursive: true });
socket.emitChanged(`app-folders-changed`); socket.emitChanged(`app-folders-changed`);
socket.emitChanged(`app-files-changed-${folder}`); socket.emitChanged('app-files-changed', { app: folder });
socket.emitChanged('used-apps-changed'); socket.emitChanged('used-apps-changed');
}, },
@@ -219,7 +219,7 @@ module.exports = {
await fs.writeFile(file, JSON.stringify(json, undefined, 2)); await fs.writeFile(file, JSON.stringify(json, undefined, 2));
socket.emitChanged(`app-files-changed-${appFolder}`); socket.emitChanged('app-files-changed', { app: appFolder });
socket.emitChanged('used-apps-changed'); socket.emitChanged('used-apps-changed');
}, },
@@ -271,7 +271,7 @@ module.exports = {
const file = path.join(appdir(), appFolder, fileName); const file = path.join(appdir(), appFolder, fileName);
if (!(await fs.exists(file))) { if (!(await fs.exists(file))) {
await fs.writeFile(file, JSON.stringify(content, undefined, 2)); await fs.writeFile(file, JSON.stringify(content, undefined, 2));
socket.emitChanged(`app-files-changed-${appFolder}`); socket.emitChanged('app-files-changed', { app: appFolder });
socket.emitChanged('used-apps-changed'); socket.emitChanged('used-apps-changed');
return true; return true;
} }

View File

@@ -75,7 +75,7 @@ module.exports = {
refreshFiles_meta: true, refreshFiles_meta: true,
async refreshFiles({ folder }) { async refreshFiles({ folder }) {
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged('archive-files-changed', { folder });
}, },
refreshFolders_meta: true, refreshFolders_meta: true,
@@ -86,7 +86,7 @@ module.exports = {
deleteFile_meta: true, deleteFile_meta: true,
async deleteFile({ folder, file, fileType }) { async deleteFile({ folder, file, fileType }) {
await fs.unlink(path.join(resolveArchiveFolder(folder), `${file}.${fileType}`)); await fs.unlink(path.join(resolveArchiveFolder(folder), `${file}.${fileType}`));
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged(`archive-files-changed`, { folder });
}, },
renameFile_meta: true, renameFile_meta: true,
@@ -95,7 +95,7 @@ module.exports = {
path.join(resolveArchiveFolder(folder), `${file}.${fileType}`), path.join(resolveArchiveFolder(folder), `${file}.${fileType}`),
path.join(resolveArchiveFolder(folder), `${newFile}.${fileType}`) path.join(resolveArchiveFolder(folder), `${newFile}.${fileType}`)
); );
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged(`archive-files-changed`, { folder });
}, },
renameFolder_meta: true, renameFolder_meta: true,
@@ -119,7 +119,7 @@ module.exports = {
saveFreeTable_meta: true, saveFreeTable_meta: true,
async saveFreeTable({ folder, file, data }) { async saveFreeTable({ folder, file, data }) {
await saveFreeTableData(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), data); await saveFreeTableData(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), data);
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged(`archive-files-changed`, { folder });
return true; return true;
}, },
@@ -147,7 +147,7 @@ module.exports = {
saveText_meta: true, saveText_meta: true,
async saveText({ folder, file, text }) { async saveText({ folder, file, text }) {
await fs.writeFile(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), text); await fs.writeFile(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), text);
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged(`archive-files-changed`, { folder });
return true; return true;
}, },
@@ -156,7 +156,7 @@ module.exports = {
const source = getJslFileName(jslid); const source = getJslFileName(jslid);
const target = path.join(resolveArchiveFolder(folder), `${file}.jsonl`); const target = path.join(resolveArchiveFolder(folder), `${file}.jsonl`);
await fs.copyFile(source, target); await fs.copyFile(source, target);
socket.emitChanged(`archive-files-changed-${folder}`); socket.emitChanged(`archive-files-changed`, { folder });
return true; return true;
}, },

View File

@@ -38,7 +38,8 @@ module.exports = {
return { return {
runAsPortal: !!connections.portalConnections, runAsPortal: !!connections.portalConnections,
singleDatabase: connections.singleDatabase, singleDbConnection: connections.singleDbConnection,
singleConnection: connections.singleConnection,
// hideAppEditor: !!process.env.HIDE_APP_EDITOR, // hideAppEditor: !!process.env.HIDE_APP_EDITOR,
allowShellConnection: platformInfo.allowShellConnection, allowShellConnection: platformInfo.allowShellConnection,
allowShellScripting: platformInfo.allowShellScripting, allowShellScripting: platformInfo.allowShellScripting,

View File

@@ -2,6 +2,7 @@ const path = require('path');
const { fork } = require('child_process'); const { fork } = require('child_process');
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs-extra'); const fs = require('fs-extra');
const crypto = require('crypto');
const { datadir, filesdir } = require('../utility/directories'); const { datadir, filesdir } = require('../utility/directories');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
@@ -15,6 +16,8 @@ const { safeJsonParse } = require('dbgate-tools');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
let volatileConnections = {};
function getNamedArgs() { function getNamedArgs() {
const res = {}; const res = {};
for (let i = 0; i < process.argv.length; i++) { for (let i = 0; i < process.argv.length; i++) {
@@ -49,6 +52,7 @@ function getPortalCollections() {
server: process.env[`SERVER_${id}`], server: process.env[`SERVER_${id}`],
user: process.env[`USER_${id}`], user: process.env[`USER_${id}`],
password: process.env[`PASSWORD_${id}`], password: process.env[`PASSWORD_${id}`],
passwordMode: process.env[`PASSWORD_MODE_${id}`],
port: process.env[`PORT_${id}`], port: process.env[`PORT_${id}`],
databaseUrl: process.env[`URL_${id}`], databaseUrl: process.env[`URL_${id}`],
useDatabaseUrl: !!process.env[`URL_${id}`], useDatabaseUrl: !!process.env[`URL_${id}`],
@@ -126,9 +130,10 @@ function getPortalCollections() {
return null; return null;
} }
const portalConnections = getPortalCollections(); const portalConnections = getPortalCollections();
function getSingleDatabase() { function getSingleDbConnection() {
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) { if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
// @ts-ignore // @ts-ignore
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION); const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
@@ -152,12 +157,31 @@ function getSingleDatabase() {
return null; return null;
} }
const singleDatabase = getSingleDatabase(); function getSingleConnection() {
if (getSingleDbConnection()) return null;
if (process.env.SINGLE_CONNECTION) {
// @ts-ignore
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
if (connection) {
return connection;
}
}
// @ts-ignore
const arg0 = (portalConnections || []).find(x => x._id == 'argv');
if (arg0) {
return arg0;
}
return null;
}
const singleDbConnection = getSingleDbConnection();
const singleConnection = getSingleConnection();
module.exports = { module.exports = {
datastore: null, datastore: null,
opened: [], opened: [],
singleDatabase, singleDbConnection,
singleConnection,
portalConnections, portalConnections,
async _init() { async _init() {
@@ -199,6 +223,36 @@ module.exports = {
}); });
}, },
saveVolatile_meta: true,
async saveVolatile({ conid, user, password, test }) {
const old = await this.getCore({ conid });
const res = {
...old,
_id: crypto.randomUUID(),
password,
passwordMode: undefined,
unsaved: true,
};
if (old.passwordMode == 'askUser') {
res.user = user;
}
if (test) {
const testRes = await this.test(res);
if (testRes.msgtype == 'connected') {
volatileConnections[res._id] = res;
return {
...res,
msgtype: 'connected',
};
}
return testRes;
} else {
volatileConnections[res._id] = res;
return res;
}
},
save_meta: true, save_meta: true,
async save(connection) { async save(connection) {
if (portalConnections) return; if (portalConnections) return;
@@ -258,6 +312,10 @@ module.exports = {
async getCore({ conid, mask = false }) { async getCore({ conid, mask = false }) {
if (!conid) return null; if (!conid) return null;
const volatile = volatileConnections[conid];
if (volatile) {
return volatile;
}
if (portalConnections) { if (portalConnections) {
const res = portalConnections.find(x => x._id == conid) || null; const res = portalConnections.find(x => x._id == conid) || null;
return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res; return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res;

View File

@@ -27,6 +27,7 @@ const { createTwoFilesPatch } = require('diff');
const diff2htmlPage = require('../utility/diff2htmlPage'); const diff2htmlPage = require('../utility/diff2htmlPage');
const processArgs = require('../utility/processArgs'); const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission'); const { testConnectionPermission } = require('../utility/hasPermission');
const { MissingCredentialsError } = require('../utility/exceptions');
module.exports = { module.exports = {
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */ /** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
@@ -42,19 +43,19 @@ module.exports = {
const existing = this.opened.find(x => x.conid == conid && x.database == database); const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return; if (!existing) return;
existing.structure = structure; existing.structure = structure;
socket.emitChanged(`database-structure-changed-${conid}-${database}`); socket.emitChanged('database-structure-changed', { conid, database });
}, },
handle_structureTime(conid, database, { analysedTime }) { handle_structureTime(conid, database, { analysedTime }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database); const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return; if (!existing) return;
existing.analysedTime = analysedTime; existing.analysedTime = analysedTime;
socket.emitChanged(`database-status-changed-${conid}-${database}`); socket.emitChanged(`database-status-changed`, { conid, database });
}, },
handle_version(conid, database, { version }) { handle_version(conid, database, { version }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database); const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return; if (!existing) return;
existing.serverVersion = version; existing.serverVersion = version;
socket.emitChanged(`database-server-version-changed-${conid}-${database}`); socket.emitChanged(`database-server-version-changed`, { conid, database });
}, },
handle_error(conid, database, props) { handle_error(conid, database, props) {
@@ -72,7 +73,7 @@ module.exports = {
if (!existing) return; if (!existing) return;
if (existing.status && status && existing.status.counter > status.counter) return; if (existing.status && status && existing.status.counter > status.counter) return;
existing.status = status; existing.status = status;
socket.emitChanged(`database-status-changed-${conid}-${database}`); socket.emitChanged(`database-status-changed`, { conid, database });
}, },
handle_ping() {}, handle_ping() {},
@@ -81,6 +82,9 @@ module.exports = {
const existing = this.opened.find(x => x.conid == conid && x.database == database); const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) return existing; if (existing) return existing;
const connection = await connections.getCore({ conid }); const connection = await connections.getCore({ conid });
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
}
const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [ const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [
'--is-forked-api', '--is-forked-api',
'--start-process', '--start-process',
@@ -313,7 +317,7 @@ module.exports = {
}, },
structure: existing.structure, structure: existing.structure,
}; };
socket.emitChanged(`database-status-changed-${conid}-${database}`); socket.emitChanged(`database-status-changed`, { conid, database });
} }
}, },

View File

@@ -49,7 +49,7 @@ module.exports = {
async delete({ folder, file }, req) { async delete({ folder, file }, req) {
if (!hasPermission(`files/${folder}/write`, req)) return false; if (!hasPermission(`files/${folder}/write`, req)) return false;
await fs.unlink(path.join(filesdir(), folder, file)); await fs.unlink(path.join(filesdir(), folder, file));
socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`files-changed`, { folder });
socket.emitChanged(`all-files-changed`); socket.emitChanged(`all-files-changed`);
return true; return true;
}, },
@@ -58,7 +58,7 @@ module.exports = {
async rename({ folder, file, newFile }, req) { async rename({ folder, file, newFile }, req) {
if (!hasPermission(`files/${folder}/write`, req)) return false; if (!hasPermission(`files/${folder}/write`, req)) return false;
await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile)); await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`files-changed`, { folder });
socket.emitChanged(`all-files-changed`); socket.emitChanged(`all-files-changed`);
return true; return true;
}, },
@@ -66,7 +66,7 @@ module.exports = {
refresh_meta: true, refresh_meta: true,
async refresh({ folders }, req) { async refresh({ folders }, req) {
for (const folder of folders) { for (const folder of folders) {
socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`files-changed`, { folder });
socket.emitChanged(`all-files-changed`); socket.emitChanged(`all-files-changed`);
} }
return true; return true;
@@ -76,7 +76,7 @@ module.exports = {
async copy({ folder, file, newFile }, req) { async copy({ folder, file, newFile }, req) {
if (!hasPermission(`files/${folder}/write`, req)) return false; if (!hasPermission(`files/${folder}/write`, req)) return false;
await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile)); await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`files-changed`, { folder });
socket.emitChanged(`all-files-changed`); socket.emitChanged(`all-files-changed`);
return true; return true;
}, },
@@ -112,13 +112,13 @@ module.exports = {
if (!hasPermission(`archive/write`, req)) return false; if (!hasPermission(`archive/write`, req)) return false;
const dir = resolveArchiveFolder(folder.substring('archive:'.length)); const dir = resolveArchiveFolder(folder.substring('archive:'.length));
await fs.writeFile(path.join(dir, file), serialize(format, data)); await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`); socket.emitChanged(`archive-files-changed`, { folder: folder.substring('archive:'.length) });
return true; return true;
} else if (folder.startsWith('app:')) { } else if (folder.startsWith('app:')) {
if (!hasPermission(`apps/write`, req)) return false; if (!hasPermission(`apps/write`, req)) return false;
const app = folder.substring('app:'.length); const app = folder.substring('app:'.length);
await fs.writeFile(path.join(appdir(), app, file), serialize(format, data)); await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
socket.emitChanged(`app-files-changed-${app}`); socket.emitChanged(`app-files-changed`, { app });
socket.emitChanged('used-apps-changed'); socket.emitChanged('used-apps-changed');
apps.emitChangedDbApp(folder); apps.emitChangedDbApp(folder);
return true; return true;
@@ -129,7 +129,7 @@ module.exports = {
await fs.mkdir(dir); await fs.mkdir(dir);
} }
await fs.writeFile(path.join(dir, file), serialize(format, data)); await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`files-changed`, { folder });
socket.emitChanged(`all-files-changed`); socket.emitChanged(`all-files-changed`);
if (folder == 'shell') { if (folder == 'shell') {
scheduler.reload(); scheduler.reload();

View File

@@ -9,6 +9,7 @@ const lock = new AsyncLock();
const config = require('./config'); const config = require('./config');
const processArgs = require('../utility/processArgs'); const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission'); const { testConnectionPermission } = require('../utility/hasPermission');
const { MissingCredentialsError } = require('../utility/exceptions');
module.exports = { module.exports = {
opened: [], opened: [],
@@ -20,13 +21,13 @@ module.exports = {
const existing = this.opened.find(x => x.conid == conid); const existing = this.opened.find(x => x.conid == conid);
if (!existing) return; if (!existing) return;
existing.databases = databases; existing.databases = databases;
socket.emitChanged(`database-list-changed-${conid}`); socket.emitChanged(`database-list-changed`, { conid });
}, },
handle_version(conid, { version }) { handle_version(conid, { version }) {
const existing = this.opened.find(x => x.conid == conid); const existing = this.opened.find(x => x.conid == conid);
if (!existing) return; if (!existing) return;
existing.version = version; existing.version = version;
socket.emitChanged(`server-version-changed-${conid}`); socket.emitChanged(`server-version-changed`, { conid });
}, },
handle_status(conid, { status }) { handle_status(conid, { status }) {
const existing = this.opened.find(x => x.conid == conid); const existing = this.opened.find(x => x.conid == conid);
@@ -46,6 +47,9 @@ module.exports = {
const existing = this.opened.find(x => x.conid == conid); const existing = this.opened.find(x => x.conid == conid);
if (existing) return existing; if (existing) return existing;
const connection = await connections.getCore({ conid }); const connection = await connections.getCore({ conid });
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
}
const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [ const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [
'--is-forked-api', '--is-forked-api',
'--start-process', '--start-process',
@@ -127,9 +131,9 @@ module.exports = {
}, },
ping_meta: true, ping_meta: true,
async ping({ connections }) { async ping({ conidArray }) {
await Promise.all( await Promise.all(
_.uniq(connections).map(async conid => { _.uniq(conidArray).map(async conid => {
const last = this.lastPinged[conid]; const last = this.lastPinged[conid];
if (last && new Date().getTime() - last < 30 * 1000) { if (last && new Date().getTime() - last < 30 * 1000) {
return Promise.resolve(); return Promise.resolve();

View File

@@ -0,0 +1,9 @@
class MissingCredentialsError {
constructor(detail) {
this.detail = detail;
}
}
module.exports = {
MissingCredentialsError,
};

View File

@@ -1,4 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const stableStringify = require('json-stable-stringify');
const sseResponses = []; const sseResponses = [];
let electronSender = null; let electronSender = null;
@@ -27,12 +28,12 @@ module.exports = {
electronSender.send(message, data == null ? null : data); electronSender.send(message, data == null ? null : data);
} }
for (const res of sseResponses) { for (const res of sseResponses) {
res.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`); res.write(`event: ${message}\ndata: ${stableStringify(data == null ? null : data)}\n\n`);
} }
}, },
emitChanged(key) { emitChanged(key, params = undefined) {
// console.log('EMIT CHANGED', key); // console.log('EMIT CHANGED', key);
this.emit('changed-cache', key); this.emit('changed-cache', { key, ...params });
// this.emit(key); // this.emit(key);
}, },
}; };

View File

@@ -1,6 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const express = require('express'); const express = require('express');
const getExpressPath = require('./getExpressPath'); const getExpressPath = require('./getExpressPath');
const { MissingCredentialsError } = require('./exceptions');
/** /**
* @param {string} route * @param {string} route
@@ -37,6 +38,13 @@ module.exports = function useController(app, electron, route, controller) {
if (data === undefined) return null; if (data === undefined) return null;
return data; return data;
} catch (err) { } catch (err) {
if (err instanceof MissingCredentialsError) {
return {
missingCredentials: true,
apiErrorMessage: 'Missing credentials',
detail: err.detail,
};
}
return { apiErrorMessage: err.message }; return { apiErrorMessage: err.message };
} }
}); });
@@ -69,7 +77,15 @@ module.exports = function useController(app, electron, route, controller) {
res.json(data); res.json(data);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
res.status(500).json({ apiErrorMessage: e.message }); if (e instanceof MissingCredentialsError) {
res.json({
missingCredentials: true,
apiErrorMessage: 'Missing credentials',
detail: e.detail,
});
} else {
res.status(500).json({ apiErrorMessage: e.message });
}
} }
}); });
} }

View File

@@ -52,6 +52,7 @@
const electron = getElectron(); const electron = getElectron();
const currentDb = getCurrentDatabase(); const currentDb = getCurrentDatabase();
openedConnections.update(list => list.filter(x => x != conid)); openedConnections.update(list => list.filter(x => x != conid));
removeVolatileMapping(conid);
if (electron) { if (electron) {
apiCall('server-connections/disconnect', { conid }); apiCall('server-connections/disconnect', { conid });
} }
@@ -100,7 +101,7 @@
import getConnectionLabel from '../utility/getConnectionLabel'; import getConnectionLabel from '../utility/getConnectionLabel';
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders'; import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache'; import { getLocalStorage } from '../utility/storageCache';
import { apiCall } from '../utility/api'; import { apiCall, removeVolatileMapping } from '../utility/api';
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte'; import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte'; import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
import AboutModal from '../modals/AboutModal.svelte'; import AboutModal from '../modals/AboutModal.svelte';

View File

@@ -40,7 +40,7 @@
for (const connection of connectionList || []) { for (const connection of connectionList || []) {
const conid = connection._id; const conid = connection._id;
if (connection.singleDatabase) continue; if (connection.singleDatabase) continue;
if (getCurrentConfig()?.singleDatabase) continue; if (getCurrentConfig()?.singleDbConnection) continue;
const databases = getLocalStorage(`database_list_${conid}`) || []; const databases = getLocalStorage(`database_list_${conid}`) || [];
for (const db of databases) { for (const db of databases) {
databaseList.push({ databaseList.push({

View File

@@ -37,6 +37,7 @@ import { openWebLink } from '../utility/exportFileTools';
import { getSettings } from '../utility/metadataLoaders'; import { getSettings } from '../utility/metadataLoaders';
import { isMac } from '../utility/common'; import { isMac } from '../utility/common';
import { doLogout, internalRedirectTo } from '../clientAuth'; import { doLogout, internalRedirectTo } from '../clientAuth';
import { disconnectServerConnection } from '../appobj/ConnectionAppObject.svelte';
// function themeCommand(theme: ThemeDefinition) { // function themeCommand(theme: ThemeDefinition) {
// return { // return {
@@ -552,6 +553,14 @@ registerCommand({
onClick: doLogout, onClick: doLogout,
}); });
registerCommand({
id: 'app.disconnect',
category: 'App',
name: 'Disconnect',
testEnabled: () => getCurrentConfig()?.singleConnection != null,
onClick: () => disconnectServerConnection(getCurrentConfig()?.singleConnection?._id),
});
export function registerFileCommands({ export function registerFileCommands({
idPrefix, idPrefix,
category, category,

View File

@@ -8,6 +8,7 @@
export let name; export let name;
export let disabled = false; export let disabled = false;
export let saveOnInput = false;
const { values, setFieldValue } = getFormContext(); const { values, setFieldValue } = getFormContext();
@@ -23,6 +24,11 @@
{disabled} {disabled}
value={isCrypted ? '' : value} value={isCrypted ? '' : value}
on:change={e => setFieldValue(name, e.target['value'])} on:change={e => setFieldValue(name, e.target['value'])}
on:input={e => {
if (saveOnInput) {
setFieldValue(name, e.target['value']);
}
}}
placeholder={isCrypted ? '(Password is encrypted)' : undefined} placeholder={isCrypted ? '(Password is encrypted)' : undefined}
type={isCrypted || showPassword ? 'text' : 'password'} type={isCrypted || showPassword ? 'text' : 'password'}
/> />

View File

@@ -4,6 +4,7 @@
export let name; export let name;
export let defaultValue; export let defaultValue;
export let saveOnInput = false;
const { values, setFieldValue } = getFormContext(); const { values, setFieldValue } = getFormContext();
</script> </script>
@@ -12,4 +13,9 @@
{...$$restProps} {...$$restProps}
value={$values[name] ?? defaultValue} value={$values[name] ?? defaultValue}
on:input={e => setFieldValue(name, e.target['value'])} on:input={e => setFieldValue(name, e.target['value'])}
on:input={e => {
if (saveOnInput) {
setFieldValue(name, e.target['value']);
}
}}
/> />

View File

@@ -40,8 +40,8 @@
'icon columns': 'mdi mdi-view-column', 'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline', 'icon columns-outline': 'mdi mdi-view-column-outline',
'icon single-database-mode': 'mdi mdi-database-lock', 'icon locked-database-mode': 'mdi mdi-database-lock',
'icon multi-database-mode': 'mdi mdi-database-eye', 'icon unlocked-database-mode': 'mdi mdi-database-eye',
'icon database': 'mdi mdi-database', 'icon database': 'mdi mdi-database',
'icon server': 'mdi mdi-server', 'icon server': 'mdi mdi-server',

View File

@@ -0,0 +1,137 @@
<script lang="ts" context="module">
let currentModalConid = null;
export function isDatabaseLoginVisible() {
return !!currentModalConid;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import Link from '../elements/Link.svelte';
import FormPasswordField from '../forms/FormPasswordField.svelte';
import FormProviderCore from '../forms/FormProviderCore.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { apiCall, setVolatileConnectionRemapping } from '../utility/api';
import { batchDispatchCacheTriggers, dispatchCacheChange } from '../utility/cache';
import createRef from '../utility/createRef';
import { getConnectionInfo } from '../utility/metadataLoaders';
import ErrorMessageModal from './ErrorMessageModal.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal, showModal } from './modalTools';
export let conid;
export let passwordMode;
const values = writable({});
let connection;
let isTesting;
let sqlConnectResult;
const testIdRef = createRef(0);
currentModalConid = conid;
onMount(async () => {
connection = await getConnectionInfo({ conid });
if (passwordMode == 'askPassword') {
$values = {
...$values,
user: connection.user,
server: connection.server,
};
}
});
onDestroy(() => {
currentModalConid = null;
});
function handleCancelTest() {
testIdRef.update(x => x + 1); // invalidate current test
isTesting = false;
}
async function handleSubmit(ev) {
isTesting = true;
testIdRef.update(x => x + 1);
const testid = testIdRef.get();
const resp = await apiCall('connections/save-volatile', {
conid,
user: $values['user'],
password: $values['password'],
test: true,
});
if (testIdRef.get() != testid) return;
isTesting = false;
if (resp.msgtype == 'connected') {
setVolatileConnectionRemapping(conid, resp._id);
dispatchCacheChange({ key: `server-status-changed` });
batchDispatchCacheTriggers(x => x.conid == conid);
closeCurrentModal();
} else {
sqlConnectResult = resp;
}
}
</script>
<FormProviderCore {values}>
<ModalBase {...$$restProps} simple>
<svelte:fragment slot="header">Database Log In</svelte:fragment>
<FormTextField label="Server" name="server" disabled />
<FormTextField
label="Username"
name="user"
autocomplete="username"
disabled={passwordMode == 'askPassword'}
focused={passwordMode == 'askUser'}
saveOnInput
/>
<FormPasswordField
label="Password"
name="password"
autocomplete="current-password"
focused={passwordMode == 'askPassword'}
saveOnInput
/>
{#if isTesting}
<div>
<FontIcon icon="icon loading" /> Testing connection
</div>
{/if}
{#if !isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error'}
<div class="error-result">
Connect failed: <FontIcon icon="img error" />
{sqlConnectResult.error}
<Link
onClick={() =>
showModal(ErrorMessageModal, {
message: sqlConnectResult.detail,
showAsCode: true,
title: 'Database connection error',
})}
>
Show detail
</Link>
</div>
{/if}
<svelte:fragment slot="footer">
{#if isTesting}
<FormStyledButton value="Stop connecting" on:click={handleCancelTest} />
{:else}
<FormSubmit value="Connect" on:click={handleSubmit} />
{/if}
<FormStyledButton value="Close" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProviderCore>

View File

@@ -28,8 +28,12 @@
$: driver = $extensions.drivers.find(x => x.engine == engine); $: driver = $extensions.drivers.find(x => x.engine == engine);
$: defaultDatabase = $values.defaultDatabase; $: defaultDatabase = $values.defaultDatabase;
$: showUser = driver?.showConnectionField('user', $values); $: showUser = driver?.showConnectionField('user', $values) && $values.passwordMode != 'askUser';
$: showPassword = driver?.showConnectionField('password', $values); $: showPassword =
driver?.showConnectionField('password', $values) &&
$values.passwordMode != 'askPassword' &&
$values.passwordMode != 'askUser';
$: showPasswordMode = driver?.showConnectionField('password', $values);
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id); $: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
</script> </script>
@@ -159,7 +163,7 @@
<FormPasswordField label="Password" name="password" disabled={isConnected || disabledFields.includes('password')} /> <FormPasswordField label="Password" name="password" disabled={isConnected || disabledFields.includes('password')} />
{/if} {/if}
{#if !disabledFields.includes('password') && showPassword} {#if !disabledFields.includes('password') && showPasswordMode}
<FormSelectField <FormSelectField
label="Password mode" label="Password mode"
isNative isNative
@@ -169,6 +173,8 @@
options={[ options={[
{ value: 'saveEncrypted', label: 'Save and encrypt' }, { value: 'saveEncrypted', label: 'Save and encrypt' },
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' }, { value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
{ value: 'askPassword', label: "Don't save, ask for password" },
{ value: 'askUser', label: "Don't save, ask for login and password" },
]} ]}
/> />
{/if} {/if}

View File

@@ -24,7 +24,7 @@
currentEditorTheme, currentEditorTheme,
extensions, extensions,
selectedWidget, selectedWidget,
singleDatabaseMode, lockedDatabaseMode,
visibleWidgetSideBar, visibleWidgetSideBar,
} from '../stores'; } from '../stores';
import { isMac } from '../utility/common'; import { isMac } from '../utility/common';
@@ -115,11 +115,11 @@ ORDER BY
type="checkbox" type="checkbox"
labelProps={{ labelProps={{
onClick: () => { onClick: () => {
$singleDatabaseMode = !$singleDatabaseMode; $lockedDatabaseMode = !$lockedDatabaseMode;
}, },
}} }}
> >
<CheckboxField checked={$singleDatabaseMode} on:change={e => ($singleDatabaseMode = e.target.checked)} /> <CheckboxField checked={$lockedDatabaseMode} on:change={e => ($lockedDatabaseMode = e.target.checked)} />
</FormFieldTemplateLarge> </FormFieldTemplateLarge>
<FormCheckboxField <FormCheckboxField

View File

@@ -50,7 +50,7 @@ function subscribeCssVariable(store, transform, cssVariable) {
} }
export const selectedWidget = writableWithStorage('database', 'selectedWidget'); export const selectedWidget = writableWithStorage('database', 'selectedWidget');
export const singleDatabaseMode = writableWithStorage<boolean>(false, 'singleDatabaseMode'); export const lockedDatabaseMode = writableWithStorage<boolean>(false, 'lockedDatabaseMode');
export const visibleWidgetSideBar = writableWithStorage(true, 'visibleWidgetSideBar'); export const visibleWidgetSideBar = writableWithStorage(true, 'visibleWidgetSideBar');
export const visibleSelectedWidget = derived( export const visibleSelectedWidget = derived(
[selectedWidget, visibleWidgetSideBar], [selectedWidget, visibleWidgetSideBar],
@@ -138,7 +138,7 @@ subscribeCssVariable(visibleSelectedWidget, x => (x ? 1 : 0), '--dim-visible-lef
// subscribeCssVariable(visibleToolbar, x => (x ? 1 : 0), '--dim-visible-toolbar'); // subscribeCssVariable(visibleToolbar, x => (x ? 1 : 0), '--dim-visible-toolbar');
subscribeCssVariable(leftPanelWidth, x => `${x}px`, '--dim-left-panel-width'); subscribeCssVariable(leftPanelWidth, x => `${x}px`, '--dim-left-panel-width');
subscribeCssVariable(visibleTitleBar, x => (x ? 1 : 0), '--dim-visible-titlebar'); subscribeCssVariable(visibleTitleBar, x => (x ? 1 : 0), '--dim-visible-titlebar');
subscribeCssVariable(singleDatabaseMode, x => (x ? 0 : 1), '--dim-visible-tabs-databases'); subscribeCssVariable(lockedDatabaseMode, x => (x ? 0 : 1), '--dim-visible-tabs-databases');
let activeTabIdValue = null; let activeTabIdValue = null;
activeTabId.subscribe(value => { activeTabId.subscribe(value => {
@@ -200,11 +200,11 @@ pinnedDatabases.subscribe(value => {
}); });
export const getPinnedDatabases = () => _.compact(pinnedDatabasesValue); export const getPinnedDatabases = () => _.compact(pinnedDatabasesValue);
let singleDatabaseModeValue = null; let lockedDatabaseModeValue = null;
singleDatabaseMode.subscribe(value => { lockedDatabaseMode.subscribe(value => {
singleDatabaseModeValue = value; lockedDatabaseModeValue = value;
}); });
export const getSingleDatabaseMode = () => singleDatabaseModeValue; export const getLockedDatabaseMode = () => lockedDatabaseModeValue;
let currentDatabaseValue = null; let currentDatabaseValue = null;
currentDatabase.subscribe(value => { currentDatabase.subscribe(value => {
@@ -246,8 +246,8 @@ export function subscribeApiDependendStores() {
useConfig().subscribe(value => { useConfig().subscribe(value => {
currentConfigValue = value; currentConfigValue = value;
invalidateCommands(); invalidateCommands();
if (value.singleDatabase) { if (value.singleDbConnection) {
currentDatabase.set(value.singleDatabase); currentDatabase.set(value.singleDbConnection);
} }
}); });
} }

View File

@@ -5,6 +5,9 @@ import getElectron from './getElectron';
// import socket from './socket'; // import socket from './socket';
import { showSnackbarError } from '../utility/snackbar'; import { showSnackbarError } from '../utility/snackbar';
import { isOauthCallback, redirectToLogin } from '../clientAuth'; import { isOauthCallback, redirectToLogin } from '../clientAuth';
import { showModal } from '../modals/modalTools';
import DatabaseLoginModal, { isDatabaseLoginVisible } from '../modals/DatabaseLoginModal.svelte';
import _ from 'lodash';
let eventSource; let eventSource;
let apiLogging = false; let apiLogging = false;
@@ -12,6 +15,9 @@ let apiLogging = false;
let apiDisabled = false; let apiDisabled = false;
const disabledOnOauth = isOauthCallback(); const disabledOnOauth = isOauthCallback();
const volatileConnectionMap = {};
const volatileConnectionMapInv = {};
export function disableApi() { export function disableApi() {
apiDisabled = true; apiDisabled = true;
} }
@@ -20,6 +26,27 @@ export function enableApi() {
apiDisabled = false; apiDisabled = false;
} }
export function setVolatileConnectionRemapping(existingConnectionId, volatileConnectionId) {
volatileConnectionMap[existingConnectionId] = volatileConnectionId;
volatileConnectionMapInv[volatileConnectionId] = existingConnectionId;
}
export function getVolatileRemapping(conid) {
return volatileConnectionMap[conid] || conid;
}
export function getVolatileRemappingInv(conid) {
return volatileConnectionMapInv[conid] || conid;
}
export function removeVolatileMapping(conid) {
const mapped = volatileConnectionMap[conid];
if (mapped) {
delete volatileConnectionMap[conid];
delete volatileConnectionMapInv[mapped];
}
}
function wantEventSource() { function wantEventSource() {
if (!eventSource) { if (!eventSource) {
eventSource = new EventSource(`${resolveApi()}/stream`); eventSource = new EventSource(`${resolveApi()}/stream`);
@@ -32,7 +59,16 @@ function processApiResponse(route, args, resp) {
// console.log('<<< API RESPONSE', route, args, resp); // console.log('<<< API RESPONSE', route, args, resp);
// } // }
if (resp?.apiErrorMessage) { if (resp?.missingCredentials) {
if (!isDatabaseLoginVisible()) {
showModal(DatabaseLoginModal, resp.detail);
}
return null;
// return {
// errorMessage: resp.apiErrorMessage,
// missingCredentials: true,
// };
} else if (resp?.apiErrorMessage) {
showSnackbarError('API error:' + resp?.apiErrorMessage); showSnackbarError('API error:' + resp?.apiErrorMessage);
return { return {
errorMessage: resp.apiErrorMessage, errorMessage: resp.apiErrorMessage,
@@ -42,6 +78,22 @@ function processApiResponse(route, args, resp) {
return resp; return resp;
} }
export function transformApiArgs(args) {
return _.mapValues(args, (v, k) => {
if (k == 'conid' && v && volatileConnectionMap[v]) return volatileConnectionMap[v];
if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMap[x] || x);
return v;
});
}
export function transformApiArgsInv(args) {
return _.mapValues(args, (v, k) => {
if (k == 'conid' && v && volatileConnectionMapInv[v]) return volatileConnectionMapInv[v];
if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMapInv[x] || x);
return v;
});
}
export async function apiCall(route: string, args: {} = undefined) { export async function apiCall(route: string, args: {} = undefined) {
if (apiLogging) { if (apiLogging) {
console.log('>>> API CALL', route, args); console.log('>>> API CALL', route, args);
@@ -55,6 +107,8 @@ export async function apiCall(route: string, args: {} = undefined) {
return; return;
} }
args = transformApiArgs(args);
const electron = getElectron(); const electron = getElectron();
if (electron) { if (electron) {
const resp = await electron.invoke(route.replace('/', '-'), args); const resp = await electron.invoke(route.replace('/', '-'), args);

View File

@@ -1,5 +1,6 @@
import { apiOn } from './api'; import { apiOn, transformApiArgsInv } from './api';
import getAsArray from './getAsArray'; import getAsArray from './getAsArray';
import stableStringify from 'json-stable-stringify';
const cachedByKey = {}; const cachedByKey = {};
const cachedPromisesByKey = {}; const cachedPromisesByKey = {};
@@ -15,10 +16,11 @@ function cacheGet(key) {
function addCacheKeyToReloadTrigger(cacheKey, reloadTrigger) { function addCacheKeyToReloadTrigger(cacheKey, reloadTrigger) {
for (const item of getAsArray(reloadTrigger)) { for (const item of getAsArray(reloadTrigger)) {
if (!(item in cachedKeysByReloadTrigger)) { const itemString = stableStringify(item);
cachedKeysByReloadTrigger[item] = []; if (!(itemString in cachedKeysByReloadTrigger)) {
cachedKeysByReloadTrigger[itemString] = [];
} }
cachedKeysByReloadTrigger[item].push(cacheKey); cachedKeysByReloadTrigger[itemString].push(cacheKey);
} }
} }
@@ -32,7 +34,8 @@ function cacheSet(cacheKey, value, reloadTrigger, generation) {
function cacheClean(reloadTrigger) { function cacheClean(reloadTrigger) {
cacheGeneration += 1; cacheGeneration += 1;
for (const item of getAsArray(reloadTrigger)) { for (const item of getAsArray(reloadTrigger)) {
const keys = cachedKeysByReloadTrigger[item]; const itemString = stableStringify(transformApiArgsInv(item));
const keys = cachedKeysByReloadTrigger[itemString];
if (keys) { if (keys) {
for (const key of keys) { for (const key of keys) {
delete cachedByKey[key]; delete cachedByKey[key];
@@ -40,7 +43,7 @@ function cacheClean(reloadTrigger) {
cacheGenerationByKey[key] = cacheGeneration; cacheGenerationByKey[key] = cacheGeneration;
} }
} }
delete cachedKeysByReloadTrigger[item]; delete cachedKeysByReloadTrigger[itemString];
} }
} }
@@ -77,7 +80,8 @@ export async function loadCachedValue(reloadTrigger, cacheKey, func) {
} }
} catch (err) { } catch (err) {
console.error('Error when using cached promise', err); console.error('Error when using cached promise', err);
cacheClean(cacheKey); // cacheClean(cacheKey);
cacheClean(reloadTrigger);
const res = await func(); const res = await func();
cacheSet(cacheKey, res, reloadTrigger, generation); cacheSet(cacheKey, res, reloadTrigger, generation);
return res; return res;
@@ -87,35 +91,48 @@ export async function loadCachedValue(reloadTrigger, cacheKey, func) {
export async function subscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) { export async function subscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) {
for (const item of getAsArray(reloadTrigger)) { for (const item of getAsArray(reloadTrigger)) {
if (!subscriptionsByReloadTrigger[item]) { const itemString = stableStringify(item);
subscriptionsByReloadTrigger[item] = []; if (!subscriptionsByReloadTrigger[itemString]) {
subscriptionsByReloadTrigger[itemString] = [];
} }
subscriptionsByReloadTrigger[item].push(reloadHandler); subscriptionsByReloadTrigger[itemString].push(reloadHandler);
} }
} }
export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) { export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) {
for (const item of getAsArray(reloadTrigger)) { for (const item of getAsArray(reloadTrigger)) {
if (subscriptionsByReloadTrigger[item]) { const itemString = stableStringify(item);
subscriptionsByReloadTrigger[item] = subscriptionsByReloadTrigger[item].filter(x => x != reloadHandler); if (subscriptionsByReloadTrigger[itemString]) {
subscriptionsByReloadTrigger[itemString] = subscriptionsByReloadTrigger[itemString].filter(
x => x != reloadHandler
);
} }
if (subscriptionsByReloadTrigger[item].length == 0) { if (subscriptionsByReloadTrigger[itemString].length == 0) {
delete subscriptionsByReloadTrigger[item]; delete subscriptionsByReloadTrigger[itemString];
} }
} }
} }
function dispatchCacheChange(reloadTrigger) { export function dispatchCacheChange(reloadTrigger) {
// console.log('CHANGE', reloadTrigger);
cacheClean(reloadTrigger); cacheClean(reloadTrigger);
for (const item of getAsArray(reloadTrigger)) { for (const item of getAsArray(reloadTrigger)) {
if (subscriptionsByReloadTrigger[item]) { const itemString = stableStringify(transformApiArgsInv(item));
for (const handler of subscriptionsByReloadTrigger[item]) { if (subscriptionsByReloadTrigger[itemString]) {
for (const handler of subscriptionsByReloadTrigger[itemString]) {
handler(); handler();
} }
} }
} }
} }
export function batchDispatchCacheTriggers(predicate) {
for (const key in subscriptionsByReloadTrigger) {
const relaodTrigger = JSON.parse(key);
if (predicate(relaodTrigger)) {
dispatchCacheChange(relaodTrigger);
}
}
}
apiOn('changed-cache', reloadTrigger => dispatchCacheChange(reloadTrigger)); apiOn('changed-cache', reloadTrigger => dispatchCacheChange(reloadTrigger));

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { currentDatabase, getCurrentDatabase, getSingleDatabaseMode, openedTabs } from '../stores'; import { currentDatabase, getCurrentDatabase, getLockedDatabaseMode, openedTabs } from '../stores';
import { shouldShowTab } from '../widgets/TabsPanel.svelte'; import { shouldShowTab } from '../widgets/TabsPanel.svelte';
import { callWhenAppLoaded } from './appLoadManager'; import { callWhenAppLoaded } from './appLoadManager';
import { getConnectionInfo } from './metadataLoaders'; import { getConnectionInfo } from './metadataLoaders';
@@ -9,7 +9,7 @@ let lastCurrentTab = null;
openedTabs.subscribe(value => { openedTabs.subscribe(value => {
const newCurrentTab = (value || []).find(x => x.selected); const newCurrentTab = (value || []).find(x => x.selected);
if (newCurrentTab == lastCurrentTab) return; if (newCurrentTab == lastCurrentTab) return;
if (getSingleDatabaseMode() && getCurrentDatabase()) return; if (getLockedDatabaseMode() && getCurrentDatabase()) return;
const lastTab = lastCurrentTab; const lastTab = lastCurrentTab;
lastCurrentTab = newCurrentTab; lastCurrentTab = newCurrentTab;
@@ -31,7 +31,7 @@ openedTabs.subscribe(value => {
}); });
currentDatabase.subscribe(currentDb => { currentDatabase.subscribe(currentDb => {
if (!getSingleDatabaseMode()) return; if (!getLockedDatabaseMode()) return;
openedTabs.update(tabs => { openedTabs.update(tabs => {
const newTabs = tabs.map(tab => ({ const newTabs = tabs.map(tab => ({
...tab, ...tab,

View File

@@ -10,7 +10,7 @@ import { getConnectionList } from './metadataLoaders';
// }; // };
const doServerPing = value => { const doServerPing = value => {
apiCall('server-connections/ping', { connections: value }); apiCall('server-connections/ping', { conidArray: value });
}; };
const doDatabasePing = value => { const doDatabasePing = value => {
@@ -29,12 +29,12 @@ export function subscribeConnectionPingers() {
openedConnections.subscribe(value => { openedConnections.subscribe(value => {
doServerPing(value); doServerPing(value);
if (openedConnectionsHandle) window.clearInterval(openedConnectionsHandle); if (openedConnectionsHandle) window.clearInterval(openedConnectionsHandle);
openedConnectionsHandle = window.setInterval(() => doServerPing(value), 30 * 1000); openedConnectionsHandle = window.setInterval(() => doServerPing(value), 20 * 1000);
}); });
currentDatabase.subscribe(value => { currentDatabase.subscribe(value => {
doDatabasePing(value); doDatabasePing(value);
if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle); if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle);
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 30 * 1000); currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
}); });
} }

View File

@@ -9,7 +9,7 @@ import { apiCall, apiOff, apiOn } from './api';
const databaseInfoLoader = ({ conid, database }) => ({ const databaseInfoLoader = ({ conid, database }) => ({
url: 'database-connections/structure', url: 'database-connections/structure',
params: { conid, database }, params: { conid, database },
reloadTrigger: `database-structure-changed-${conid}-${database}`, reloadTrigger: { key: `database-structure-changed`, conid, database },
transform: extendDatabaseInfo, transform: extendDatabaseInfo,
}); });
@@ -28,31 +28,31 @@ const databaseInfoLoader = ({ conid, database }) => ({
const connectionInfoLoader = ({ conid }) => ({ const connectionInfoLoader = ({ conid }) => ({
url: 'connections/get', url: 'connections/get',
params: { conid }, params: { conid },
reloadTrigger: 'connection-list-changed', reloadTrigger: { key: 'connection-list-changed' },
}); });
const configLoader = () => ({ const configLoader = () => ({
url: 'config/get', url: 'config/get',
params: {}, params: {},
reloadTrigger: 'config-changed', reloadTrigger: { key: 'config-changed' },
}); });
const settingsLoader = () => ({ const settingsLoader = () => ({
url: 'config/get-settings', url: 'config/get-settings',
params: {}, params: {},
reloadTrigger: 'settings-changed', reloadTrigger: { key: 'settings-changed' },
}); });
const platformInfoLoader = () => ({ const platformInfoLoader = () => ({
url: 'config/platform-info', url: 'config/platform-info',
params: {}, params: {},
reloadTrigger: 'platform-info-changed', reloadTrigger: { key: 'platform-info-changed' },
}); });
const favoritesLoader = () => ({ const favoritesLoader = () => ({
url: 'files/favorites', url: 'files/favorites',
params: {}, params: {},
reloadTrigger: 'files-changed-favorites', reloadTrigger: { key: 'files-changed-favorites' },
}); });
// const sqlObjectListLoader = ({ conid, database }) => ({ // const sqlObjectListLoader = ({ conid, database }) => ({
@@ -64,13 +64,13 @@ const favoritesLoader = () => ({
const databaseStatusLoader = ({ conid, database }) => ({ const databaseStatusLoader = ({ conid, database }) => ({
url: 'database-connections/status', url: 'database-connections/status',
params: { conid, database }, params: { conid, database },
reloadTrigger: `database-status-changed-${conid}-${database}`, reloadTrigger: { key: `database-status-changed`, conid, database },
}); });
const databaseListLoader = ({ conid }) => ({ const databaseListLoader = ({ conid }) => ({
url: 'server-connections/list-databases', url: 'server-connections/list-databases',
params: { conid }, params: { conid },
reloadTrigger: `database-list-changed-${conid}`, reloadTrigger: { key: `database-list-changed`, conid },
onLoaded: value => { onLoaded: value => {
if (value?.length > 0) setLocalStorage(`database_list_${conid}`, value); if (value?.length > 0) setLocalStorage(`database_list_${conid}`, value);
}, },
@@ -85,37 +85,37 @@ const databaseListLoader = ({ conid }) => ({
const serverVersionLoader = ({ conid }) => ({ const serverVersionLoader = ({ conid }) => ({
url: 'server-connections/version', url: 'server-connections/version',
params: { conid }, params: { conid },
reloadTrigger: `server-version-changed-${conid}`, reloadTrigger: { key: `server-version-changed`, conid },
}); });
const databaseServerVersionLoader = ({ conid, database }) => ({ const databaseServerVersionLoader = ({ conid, database }) => ({
url: 'database-connections/server-version', url: 'database-connections/server-version',
params: { conid, database }, params: { conid, database },
reloadTrigger: `database-server-version-changed-${conid}-${database}`, reloadTrigger: { key: `database-server-version-changed`, conid, database },
}); });
const archiveFoldersLoader = () => ({ const archiveFoldersLoader = () => ({
url: 'archive/folders', url: 'archive/folders',
params: {}, params: {},
reloadTrigger: `archive-folders-changed`, reloadTrigger: { key: `archive-folders-changed` },
}); });
const archiveFilesLoader = ({ folder }) => ({ const archiveFilesLoader = ({ folder }) => ({
url: 'archive/files', url: 'archive/files',
params: { folder }, params: { folder },
reloadTrigger: `archive-files-changed-${folder}`, reloadTrigger: { key: `archive-files-changed`, folder },
}); });
const appFoldersLoader = () => ({ const appFoldersLoader = () => ({
url: 'apps/folders', url: 'apps/folders',
params: {}, params: {},
reloadTrigger: `app-folders-changed`, reloadTrigger: { key: `app-folders-changed` },
}); });
const appFilesLoader = ({ folder }) => ({ const appFilesLoader = ({ folder }) => ({
url: 'apps/files', url: 'apps/files',
params: { folder }, params: { folder },
reloadTrigger: `app-files-changed-${folder}`, reloadTrigger: { key: `app-files-changed`, app: folder },
}); });
// const dbAppsLoader = ({ conid, database }) => ({ // const dbAppsLoader = ({ conid, database }) => ({
@@ -127,41 +127,41 @@ const appFilesLoader = ({ folder }) => ({
const usedAppsLoader = ({ conid, database }) => ({ const usedAppsLoader = ({ conid, database }) => ({
url: 'apps/get-used-apps', url: 'apps/get-used-apps',
params: {}, params: {},
reloadTrigger: `used-apps-changed`, reloadTrigger: { key: `used-apps-changed` },
}); });
const serverStatusLoader = () => ({ const serverStatusLoader = () => ({
url: 'server-connections/server-status', url: 'server-connections/server-status',
params: {}, params: {},
reloadTrigger: `server-status-changed`, reloadTrigger: { key: `server-status-changed` },
}); });
const connectionListLoader = () => ({ const connectionListLoader = () => ({
url: 'connections/list', url: 'connections/list',
params: {}, params: {},
reloadTrigger: `connection-list-changed`, reloadTrigger: { key: `connection-list-changed` },
}); });
const installedPluginsLoader = () => ({ const installedPluginsLoader = () => ({
url: 'plugins/installed', url: 'plugins/installed',
params: {}, params: {},
reloadTrigger: `installed-plugins-changed`, reloadTrigger: { key: `installed-plugins-changed` },
}); });
const filesLoader = ({ folder }) => ({ const filesLoader = ({ folder }) => ({
url: 'files/list', url: 'files/list',
params: { folder }, params: { folder },
reloadTrigger: `files-changed-${folder}`, reloadTrigger: { key: `files-changed`, folder },
}); });
const allFilesLoader = () => ({ const allFilesLoader = () => ({
url: 'files/list-all', url: 'files/list-all',
params: {}, params: {},
reloadTrigger: `all-files-changed`, reloadTrigger: { key: `all-files-changed` },
}); });
const authTypesLoader = ({ engine }) => ({ const authTypesLoader = ({ engine }) => ({
url: 'plugins/auth-types', url: 'plugins/auth-types',
params: { engine }, params: { engine },
reloadTrigger: `installed-plugins-changed`, reloadTrigger: { key: `installed-plugins-changed` },
errorValue: null, errorValue: null,
}); });

View File

@@ -3,7 +3,7 @@
import InlineButton from '../buttons/InlineButton.svelte'; import InlineButton from '../buttons/InlineButton.svelte';
import SearchInput from '../elements/SearchInput.svelte'; import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { useConnectionList, useServerStatus } from '../utility/metadataLoaders'; import { useConfig, useConnectionList, useServerStatus } from '../utility/metadataLoaders';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte'; import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import AppObjectList from '../appobj/AppObjectList.svelte'; import AppObjectList from '../appobj/AppObjectList.svelte';
import * as connectionAppObject from '../appobj/ConnectionAppObject.svelte'; import * as connectionAppObject from '../appobj/ConnectionAppObject.svelte';
@@ -21,7 +21,7 @@
import { useConnectionColorFactory } from '../utility/useConnectionColor'; import { useConnectionColorFactory } from '../utility/useConnectionColor';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte'; import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { apiCall } from '../utility/api'; import { apiCall, getVolatileRemapping } from '../utility/api';
import LargeButton from '../buttons/LargeButton.svelte'; import LargeButton from '../buttons/LargeButton.svelte';
import { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons'; import { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons';
import { safeJsonParse } from 'dbgate-tools'; import { safeJsonParse } from 'dbgate-tools';
@@ -33,7 +33,7 @@
$: connectionsWithStatus = $: connectionsWithStatus =
$connections && $serverStatus $connections && $serverStatus
? $connections.map(conn => ({ ...conn, status: $serverStatus[conn._id] })) ? $connections.map(conn => ({ ...conn, status: $serverStatus[getVolatileRemapping(conn._id)] }))
: $connections; : $connections;
$: connectionsWithStatusFiltered = connectionsWithStatus?.filter( $: connectionsWithStatusFiltered = connectionsWithStatus?.filter(

View File

@@ -12,6 +12,7 @@
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte'; import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import SqlObjectList from './SqlObjectList.svelte'; import SqlObjectList from './SqlObjectList.svelte';
import DbKeysTree from './DbKeysTree.svelte'; import DbKeysTree from './DbKeysTree.svelte';
import SingleConnectionDatabaseList from './SingleConnectionDatabaseList.svelte';
export let hidden = false; export let hidden = false;
@@ -24,7 +25,11 @@
</script> </script>
<WidgetColumnBar {hidden}> <WidgetColumnBar {hidden}>
{#if !$config?.singleDatabase} {#if $config?.singleConnection}
<WidgetColumnBarItem title="Databases" name="databases" height="35%" storageName="databasesWidget">
<SingleConnectionDatabaseList connection={$config?.singleConnection} />
</WidgetColumnBarItem>
{:else if !$config?.singleDbConnection}
<WidgetColumnBarItem title="Connections" name="connections" height="35%" storageName="connectionsWidget"> <WidgetColumnBarItem title="Connections" name="connections" height="35%" storageName="connectionsWidget">
<ConnectionList /> <ConnectionList />
</WidgetColumnBarItem> </WidgetColumnBarItem>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import _ from 'lodash';
import InlineButton from '../buttons/InlineButton.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SubDatabaseList from '../appobj/SubDatabaseList.svelte';
import { openedConnections } from '../stores';
import { useConnectionColorFactory } from '../utility/useConnectionColor';
import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { apiCall } from '../utility/api';
export let connection;
let filter = '';
const handleRefreshDatabases = () => {
apiCall('server-connections/refresh', { conid: connection._id });
};
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search connection or database" bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton on:click={handleRefreshDatabases} title="Refresh database list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<SubDatabaseList data={connection} {filter} passProps={{}} />
</WidgetsInnerContainer>

View File

@@ -1,11 +1,11 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
const getCurrentValueMarker: any = {}; const getCurrentValueMarker: any = {};
export function shouldShowTab(tab, singleDbMode = getCurrentValueMarker, currentDb = getCurrentValueMarker) { export function shouldShowTab(tab, lockedDbMode = getCurrentValueMarker, currentDb = getCurrentValueMarker) {
if (singleDbMode == getCurrentValueMarker) { if (lockedDbMode == getCurrentValueMarker) {
singleDbMode = getSingleDatabaseMode(); lockedDbMode = getLockedDatabaseMode();
} }
if (singleDbMode) { if (lockedDbMode) {
if (currentDb == getCurrentValueMarker) { if (currentDb == getCurrentValueMarker) {
currentDb = getCurrentDatabase(); currentDb = getCurrentDatabase();
} }
@@ -250,8 +250,8 @@
activeTabId, activeTabId,
getActiveTabId, getActiveTabId,
getCurrentDatabase, getCurrentDatabase,
singleDatabaseMode, lockedDatabaseMode,
getSingleDatabaseMode, getLockedDatabaseMode,
} from '../stores'; } from '../stores';
import tabs from '../tabs'; import tabs from '../tabs';
import { setSelectedTab } from '../utility/common'; import { setSelectedTab } from '../utility/common';
@@ -264,7 +264,7 @@
import TabCloseButton from '../elements/TabCloseButton.svelte'; import TabCloseButton from '../elements/TabCloseButton.svelte';
import CloseTabModal from '../modals/CloseTabModal.svelte'; import CloseTabModal from '../modals/CloseTabModal.svelte';
$: showTabFilterFunc = tab => shouldShowTab(tab, $singleDatabaseMode, $currentDatabase); $: showTabFilterFunc = tab => shouldShowTab(tab, $lockedDatabaseMode, $currentDatabase);
$: connectionList = useConnectionList(); $: connectionList = useConnectionList();
$: currentDbKey = $: currentDbKey =
@@ -443,7 +443,7 @@
<div class="tabs" on:wheel={handleTabsWheel} bind:this={domTabs}> <div class="tabs" on:wheel={handleTabsWheel} bind:this={domTabs}>
{#each groupedTabs as tabGroup} {#each groupedTabs as tabGroup}
<div class="db-wrapper"> <div class="db-wrapper">
{#if !$singleDatabaseMode} {#if !$lockedDatabaseMode}
<div <div
class="db-name" class="db-name"
class:selected={draggingDbGroup class:selected={draggingDbGroup

View File

@@ -7,7 +7,7 @@
visibleSelectedWidget, visibleSelectedWidget,
visibleWidgetSideBar, visibleWidgetSideBar,
visibleHamburgerMenuWidget, visibleHamburgerMenuWidget,
singleDatabaseMode, lockedDatabaseMode,
} from '../stores'; } from '../stores';
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
@@ -112,12 +112,12 @@
<div <div
class="wrapper" class="wrapper"
title={`Toggle whether tabs from all databases are visible. Currently - ${$singleDatabaseMode ? 'NO' : 'YES'}`} title={`Toggle whether tabs from all databases are visible. Currently - ${$lockedDatabaseMode ? 'NO' : 'YES'}`}
on:click={() => { on:click={() => {
$singleDatabaseMode = !$singleDatabaseMode; $lockedDatabaseMode = !$lockedDatabaseMode;
}} }}
> >
<FontIcon icon={$singleDatabaseMode ? 'icon single-database-mode' : 'icon multi-database-mode'} /> <FontIcon icon={$lockedDatabaseMode ? 'icon locked-database-mode' : 'icon unlocked-database-mode'} />
</div> </div>
<div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings}> <div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings}>
<FontIcon icon="icon settings" /> <FontIcon icon="icon settings" />