mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-29 06:26:00 +00:00
Merge branch 'askpassword'
This commit is contained in:
@@ -21,6 +21,7 @@ module.exports = ({ editMenu }) => [
|
||||
{ divider: true },
|
||||
{ command: 'file.exit', hideDisabled: true },
|
||||
{ command: 'app.logout', hideDisabled: true, skipInApp: true },
|
||||
{ command: 'app.disconnect', hideDisabled: true, skipInApp: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:singledb": "yarn workspace dbgate-api start:singledb",
|
||||
"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:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
|
||||
14
packages/api/env/dblogin/.env
vendored
Normal file
14
packages/api/env/dblogin/.env
vendored
Normal 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
|
||||
4
packages/api/env/singledb/.env
vendored
4
packages/api/env/singledb/.env
vendored
@@ -5,8 +5,8 @@ CONNECTIONS=mysql
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
PASSWORD_mysql=Pwd2020Db
|
||||
PORT_mysql=3306
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
DBCONFIG_mysql=[{"name":"Chinook","connectionColor":"cyan"}]
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"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: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:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
|
||||
"ts": "tsc",
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
|
||||
refreshFiles_meta: true,
|
||||
async refreshFiles({ folder }) {
|
||||
socket.emitChanged(`app-files-changed-${folder}`);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
},
|
||||
|
||||
refreshFolders_meta: true,
|
||||
@@ -69,7 +69,7 @@ module.exports = {
|
||||
deleteFile_meta: true,
|
||||
async deleteFile({ 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);
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@ module.exports = {
|
||||
path.join(path.join(appdir(), folder), `${file}.${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);
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ module.exports = {
|
||||
if (!folder) throw new Error('Missing folder parameter');
|
||||
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
socket.emitChanged(`app-files-changed-${folder}`);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
},
|
||||
|
||||
@@ -219,7 +219,7 @@ module.exports = {
|
||||
|
||||
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');
|
||||
},
|
||||
|
||||
@@ -271,7 +271,7 @@ module.exports = {
|
||||
const file = path.join(appdir(), appFolder, fileName);
|
||||
if (!(await fs.exists(file))) {
|
||||
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');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ module.exports = {
|
||||
|
||||
refreshFiles_meta: true,
|
||||
async refreshFiles({ folder }) {
|
||||
socket.emitChanged(`archive-files-changed-${folder}`);
|
||||
socket.emitChanged('archive-files-changed', { folder });
|
||||
},
|
||||
|
||||
refreshFolders_meta: true,
|
||||
@@ -86,7 +86,7 @@ module.exports = {
|
||||
deleteFile_meta: true,
|
||||
async deleteFile({ 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,
|
||||
@@ -95,7 +95,7 @@ module.exports = {
|
||||
path.join(resolveArchiveFolder(folder), `${file}.${fileType}`),
|
||||
path.join(resolveArchiveFolder(folder), `${newFile}.${fileType}`)
|
||||
);
|
||||
socket.emitChanged(`archive-files-changed-${folder}`);
|
||||
socket.emitChanged(`archive-files-changed`, { folder });
|
||||
},
|
||||
|
||||
renameFolder_meta: true,
|
||||
@@ -119,7 +119,7 @@ module.exports = {
|
||||
saveFreeTable_meta: true,
|
||||
async saveFreeTable({ folder, file, data }) {
|
||||
await saveFreeTableData(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), data);
|
||||
socket.emitChanged(`archive-files-changed-${folder}`);
|
||||
socket.emitChanged(`archive-files-changed`, { folder });
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -147,7 +147,7 @@ module.exports = {
|
||||
saveText_meta: true,
|
||||
async saveText({ folder, file, 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;
|
||||
},
|
||||
|
||||
@@ -156,7 +156,7 @@ module.exports = {
|
||||
const source = getJslFileName(jslid);
|
||||
const target = path.join(resolveArchiveFolder(folder), `${file}.jsonl`);
|
||||
await fs.copyFile(source, target);
|
||||
socket.emitChanged(`archive-files-changed-${folder}`);
|
||||
socket.emitChanged(`archive-files-changed`, { folder });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ module.exports = {
|
||||
|
||||
return {
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
singleDbConnection: connections.singleDbConnection,
|
||||
singleConnection: connections.singleConnection,
|
||||
// hideAppEditor: !!process.env.HIDE_APP_EDITOR,
|
||||
allowShellConnection: platformInfo.allowShellConnection,
|
||||
allowShellScripting: platformInfo.allowShellScripting,
|
||||
|
||||
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const { fork } = require('child_process');
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs-extra');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const { datadir, filesdir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
@@ -15,6 +16,8 @@ const { safeJsonParse } = require('dbgate-tools');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
|
||||
|
||||
let volatileConnections = {};
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
@@ -49,6 +52,7 @@ function getPortalCollections() {
|
||||
server: process.env[`SERVER_${id}`],
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
passwordMode: process.env[`PASSWORD_MODE_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
useDatabaseUrl: !!process.env[`URL_${id}`],
|
||||
@@ -126,9 +130,10 @@ function getPortalCollections() {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const portalConnections = getPortalCollections();
|
||||
|
||||
function getSingleDatabase() {
|
||||
function getSingleDbConnection() {
|
||||
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
|
||||
// @ts-ignore
|
||||
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
|
||||
@@ -152,12 +157,31 @@ function getSingleDatabase() {
|
||||
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 = {
|
||||
datastore: null,
|
||||
opened: [],
|
||||
singleDatabase,
|
||||
singleDbConnection,
|
||||
singleConnection,
|
||||
portalConnections,
|
||||
|
||||
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,
|
||||
async save(connection) {
|
||||
if (portalConnections) return;
|
||||
@@ -258,6 +312,10 @@ module.exports = {
|
||||
|
||||
async getCore({ conid, mask = false }) {
|
||||
if (!conid) return null;
|
||||
const volatile = volatileConnections[conid];
|
||||
if (volatile) {
|
||||
return volatile;
|
||||
}
|
||||
if (portalConnections) {
|
||||
const res = portalConnections.find(x => x._id == conid) || null;
|
||||
return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res;
|
||||
|
||||
@@ -27,6 +27,7 @@ const { createTwoFilesPatch } = require('diff');
|
||||
const diff2htmlPage = require('../utility/diff2htmlPage');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
@@ -42,19 +43,19 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
socket.emitChanged('database-structure-changed', { conid, database });
|
||||
},
|
||||
handle_structureTime(conid, database, { analysedTime }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.analysedTime = analysedTime;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
socket.emitChanged(`database-status-changed`, { conid, database });
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.serverVersion = version;
|
||||
socket.emitChanged(`database-server-version-changed-${conid}-${database}`);
|
||||
socket.emitChanged(`database-server-version-changed`, { conid, database });
|
||||
},
|
||||
|
||||
handle_error(conid, database, props) {
|
||||
@@ -72,7 +73,7 @@ module.exports = {
|
||||
if (!existing) return;
|
||||
if (existing.status && status && existing.status.counter > status.counter) return;
|
||||
existing.status = status;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
socket.emitChanged(`database-status-changed`, { conid, database });
|
||||
},
|
||||
|
||||
handle_ping() {},
|
||||
@@ -81,6 +82,9 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing;
|
||||
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], [
|
||||
'--is-forked-api',
|
||||
'--start-process',
|
||||
@@ -313,7 +317,7 @@ module.exports = {
|
||||
},
|
||||
structure: existing.structure,
|
||||
};
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
socket.emitChanged(`database-status-changed`, { conid, database });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = {
|
||||
async delete({ folder, file }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
await fs.unlink(path.join(filesdir(), folder, file));
|
||||
socket.emitChanged(`files-changed-${folder}`);
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
return true;
|
||||
},
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
async rename({ folder, file, newFile }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
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`);
|
||||
return true;
|
||||
},
|
||||
@@ -66,7 +66,7 @@ module.exports = {
|
||||
refresh_meta: true,
|
||||
async refresh({ folders }, req) {
|
||||
for (const folder of folders) {
|
||||
socket.emitChanged(`files-changed-${folder}`);
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
}
|
||||
return true;
|
||||
@@ -76,7 +76,7 @@ module.exports = {
|
||||
async copy({ folder, file, newFile }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
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`);
|
||||
return true;
|
||||
},
|
||||
@@ -112,13 +112,13 @@ module.exports = {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
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;
|
||||
} else if (folder.startsWith('app:')) {
|
||||
if (!hasPermission(`apps/write`, req)) return false;
|
||||
const app = folder.substring('app:'.length);
|
||||
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');
|
||||
apps.emitChangedDbApp(folder);
|
||||
return true;
|
||||
@@ -129,7 +129,7 @@ module.exports = {
|
||||
await fs.mkdir(dir);
|
||||
}
|
||||
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`);
|
||||
if (folder == 'shell') {
|
||||
scheduler.reload();
|
||||
|
||||
@@ -9,6 +9,7 @@ const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
@@ -20,13 +21,13 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
existing.databases = databases;
|
||||
socket.emitChanged(`database-list-changed-${conid}`);
|
||||
socket.emitChanged(`database-list-changed`, { conid });
|
||||
},
|
||||
handle_version(conid, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
existing.version = version;
|
||||
socket.emitChanged(`server-version-changed-${conid}`);
|
||||
socket.emitChanged(`server-version-changed`, { conid });
|
||||
},
|
||||
handle_status(conid, { status }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
@@ -46,6 +47,9 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (existing) return existing;
|
||||
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], [
|
||||
'--is-forked-api',
|
||||
'--start-process',
|
||||
@@ -127,9 +131,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
ping_meta: true,
|
||||
async ping({ connections }) {
|
||||
async ping({ conidArray }) {
|
||||
await Promise.all(
|
||||
_.uniq(connections).map(async conid => {
|
||||
_.uniq(conidArray).map(async conid => {
|
||||
const last = this.lastPinged[conid];
|
||||
if (last && new Date().getTime() - last < 30 * 1000) {
|
||||
return Promise.resolve();
|
||||
|
||||
9
packages/api/src/utility/exceptions.js
Normal file
9
packages/api/src/utility/exceptions.js
Normal file
@@ -0,0 +1,9 @@
|
||||
class MissingCredentialsError {
|
||||
constructor(detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MissingCredentialsError,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
|
||||
const sseResponses = [];
|
||||
let electronSender = null;
|
||||
@@ -27,12 +28,12 @@ module.exports = {
|
||||
electronSender.send(message, data == null ? null : data);
|
||||
}
|
||||
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);
|
||||
this.emit('changed-cache', key);
|
||||
this.emit('changed-cache', { key, ...params });
|
||||
// this.emit(key);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const express = require('express');
|
||||
const getExpressPath = require('./getExpressPath');
|
||||
const { MissingCredentialsError } = require('./exceptions');
|
||||
|
||||
/**
|
||||
* @param {string} route
|
||||
@@ -37,6 +38,13 @@ module.exports = function useController(app, electron, route, controller) {
|
||||
if (data === undefined) return null;
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err instanceof MissingCredentialsError) {
|
||||
return {
|
||||
missingCredentials: true,
|
||||
apiErrorMessage: 'Missing credentials',
|
||||
detail: err.detail,
|
||||
};
|
||||
}
|
||||
return { apiErrorMessage: err.message };
|
||||
}
|
||||
});
|
||||
@@ -69,7 +77,15 @@ module.exports = function useController(app, electron, route, controller) {
|
||||
res.json(data);
|
||||
} catch (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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
const electron = getElectron();
|
||||
const currentDb = getCurrentDatabase();
|
||||
openedConnections.update(list => list.filter(x => x != conid));
|
||||
removeVolatileMapping(conid);
|
||||
if (electron) {
|
||||
apiCall('server-connections/disconnect', { conid });
|
||||
}
|
||||
@@ -100,7 +101,7 @@
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { apiCall, removeVolatileMapping } from '../utility/api';
|
||||
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
|
||||
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
for (const connection of connectionList || []) {
|
||||
const conid = connection._id;
|
||||
if (connection.singleDatabase) continue;
|
||||
if (getCurrentConfig()?.singleDatabase) continue;
|
||||
if (getCurrentConfig()?.singleDbConnection) continue;
|
||||
const databases = getLocalStorage(`database_list_${conid}`) || [];
|
||||
for (const db of databases) {
|
||||
databaseList.push({
|
||||
|
||||
@@ -37,6 +37,7 @@ import { openWebLink } from '../utility/exportFileTools';
|
||||
import { getSettings } from '../utility/metadataLoaders';
|
||||
import { isMac } from '../utility/common';
|
||||
import { doLogout, internalRedirectTo } from '../clientAuth';
|
||||
import { disconnectServerConnection } from '../appobj/ConnectionAppObject.svelte';
|
||||
|
||||
// function themeCommand(theme: ThemeDefinition) {
|
||||
// return {
|
||||
@@ -552,6 +553,14 @@ registerCommand({
|
||||
onClick: doLogout,
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.disconnect',
|
||||
category: 'App',
|
||||
name: 'Disconnect',
|
||||
testEnabled: () => getCurrentConfig()?.singleConnection != null,
|
||||
onClick: () => disconnectServerConnection(getCurrentConfig()?.singleConnection?._id),
|
||||
});
|
||||
|
||||
export function registerFileCommands({
|
||||
idPrefix,
|
||||
category,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
export let name;
|
||||
export let disabled = false;
|
||||
export let saveOnInput = false;
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
|
||||
@@ -23,6 +24,11 @@
|
||||
{disabled}
|
||||
value={isCrypted ? '' : 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}
|
||||
type={isCrypted || showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let name;
|
||||
export let defaultValue;
|
||||
export let saveOnInput = false;
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
</script>
|
||||
@@ -12,4 +13,9 @@
|
||||
{...$$restProps}
|
||||
value={$values[name] ?? defaultValue}
|
||||
on:input={e => setFieldValue(name, e.target['value'])}
|
||||
on:input={e => {
|
||||
if (saveOnInput) {
|
||||
setFieldValue(name, e.target['value']);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
'icon columns': 'mdi mdi-view-column',
|
||||
'icon columns-outline': 'mdi mdi-view-column-outline',
|
||||
|
||||
'icon single-database-mode': 'mdi mdi-database-lock',
|
||||
'icon multi-database-mode': 'mdi mdi-database-eye',
|
||||
'icon locked-database-mode': 'mdi mdi-database-lock',
|
||||
'icon unlocked-database-mode': 'mdi mdi-database-eye',
|
||||
|
||||
'icon database': 'mdi mdi-database',
|
||||
'icon server': 'mdi mdi-server',
|
||||
|
||||
137
packages/web/src/modals/DatabaseLoginModal.svelte
Normal file
137
packages/web/src/modals/DatabaseLoginModal.svelte
Normal 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>
|
||||
@@ -28,8 +28,12 @@
|
||||
$: driver = $extensions.drivers.find(x => x.engine == engine);
|
||||
$: defaultDatabase = $values.defaultDatabase;
|
||||
|
||||
$: showUser = driver?.showConnectionField('user', $values);
|
||||
$: showPassword = driver?.showConnectionField('password', $values);
|
||||
$: showUser = driver?.showConnectionField('user', $values) && $values.passwordMode != 'askUser';
|
||||
$: 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);
|
||||
</script>
|
||||
|
||||
@@ -159,7 +163,7 @@
|
||||
<FormPasswordField label="Password" name="password" disabled={isConnected || disabledFields.includes('password')} />
|
||||
{/if}
|
||||
|
||||
{#if !disabledFields.includes('password') && showPassword}
|
||||
{#if !disabledFields.includes('password') && showPasswordMode}
|
||||
<FormSelectField
|
||||
label="Password mode"
|
||||
isNative
|
||||
@@ -169,6 +173,8 @@
|
||||
options={[
|
||||
{ value: 'saveEncrypted', label: 'Save and encrypt' },
|
||||
{ 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}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
currentEditorTheme,
|
||||
extensions,
|
||||
selectedWidget,
|
||||
singleDatabaseMode,
|
||||
lockedDatabaseMode,
|
||||
visibleWidgetSideBar,
|
||||
} from '../stores';
|
||||
import { isMac } from '../utility/common';
|
||||
@@ -115,11 +115,11 @@ ORDER BY
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
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>
|
||||
|
||||
<FormCheckboxField
|
||||
|
||||
@@ -50,7 +50,7 @@ function subscribeCssVariable(store, transform, cssVariable) {
|
||||
}
|
||||
|
||||
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 visibleSelectedWidget = derived(
|
||||
[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(leftPanelWidth, x => `${x}px`, '--dim-left-panel-width');
|
||||
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;
|
||||
activeTabId.subscribe(value => {
|
||||
@@ -200,11 +200,11 @@ pinnedDatabases.subscribe(value => {
|
||||
});
|
||||
export const getPinnedDatabases = () => _.compact(pinnedDatabasesValue);
|
||||
|
||||
let singleDatabaseModeValue = null;
|
||||
singleDatabaseMode.subscribe(value => {
|
||||
singleDatabaseModeValue = value;
|
||||
let lockedDatabaseModeValue = null;
|
||||
lockedDatabaseMode.subscribe(value => {
|
||||
lockedDatabaseModeValue = value;
|
||||
});
|
||||
export const getSingleDatabaseMode = () => singleDatabaseModeValue;
|
||||
export const getLockedDatabaseMode = () => lockedDatabaseModeValue;
|
||||
|
||||
let currentDatabaseValue = null;
|
||||
currentDatabase.subscribe(value => {
|
||||
@@ -246,8 +246,8 @@ export function subscribeApiDependendStores() {
|
||||
useConfig().subscribe(value => {
|
||||
currentConfigValue = value;
|
||||
invalidateCommands();
|
||||
if (value.singleDatabase) {
|
||||
currentDatabase.set(value.singleDatabase);
|
||||
if (value.singleDbConnection) {
|
||||
currentDatabase.set(value.singleDbConnection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import getElectron from './getElectron';
|
||||
// import socket from './socket';
|
||||
import { showSnackbarError } from '../utility/snackbar';
|
||||
import { isOauthCallback, redirectToLogin } from '../clientAuth';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DatabaseLoginModal, { isDatabaseLoginVisible } from '../modals/DatabaseLoginModal.svelte';
|
||||
import _ from 'lodash';
|
||||
|
||||
let eventSource;
|
||||
let apiLogging = false;
|
||||
@@ -12,6 +15,9 @@ let apiLogging = false;
|
||||
let apiDisabled = false;
|
||||
const disabledOnOauth = isOauthCallback();
|
||||
|
||||
const volatileConnectionMap = {};
|
||||
const volatileConnectionMapInv = {};
|
||||
|
||||
export function disableApi() {
|
||||
apiDisabled = true;
|
||||
}
|
||||
@@ -20,6 +26,27 @@ export function enableApi() {
|
||||
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() {
|
||||
if (!eventSource) {
|
||||
eventSource = new EventSource(`${resolveApi()}/stream`);
|
||||
@@ -32,7 +59,16 @@ function processApiResponse(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);
|
||||
return {
|
||||
errorMessage: resp.apiErrorMessage,
|
||||
@@ -42,6 +78,22 @@ function processApiResponse(route, args, 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) {
|
||||
if (apiLogging) {
|
||||
console.log('>>> API CALL', route, args);
|
||||
@@ -55,6 +107,8 @@ export async function apiCall(route: string, args: {} = undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
args = transformApiArgs(args);
|
||||
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
const resp = await electron.invoke(route.replace('/', '-'), args);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiOn } from './api';
|
||||
import { apiOn, transformApiArgsInv } from './api';
|
||||
import getAsArray from './getAsArray';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
const cachedByKey = {};
|
||||
const cachedPromisesByKey = {};
|
||||
@@ -15,10 +16,11 @@ function cacheGet(key) {
|
||||
|
||||
function addCacheKeyToReloadTrigger(cacheKey, reloadTrigger) {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
if (!(item in cachedKeysByReloadTrigger)) {
|
||||
cachedKeysByReloadTrigger[item] = [];
|
||||
const itemString = stableStringify(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) {
|
||||
cacheGeneration += 1;
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
const keys = cachedKeysByReloadTrigger[item];
|
||||
const itemString = stableStringify(transformApiArgsInv(item));
|
||||
const keys = cachedKeysByReloadTrigger[itemString];
|
||||
if (keys) {
|
||||
for (const key of keys) {
|
||||
delete cachedByKey[key];
|
||||
@@ -40,7 +43,7 @@ function cacheClean(reloadTrigger) {
|
||||
cacheGenerationByKey[key] = cacheGeneration;
|
||||
}
|
||||
}
|
||||
delete cachedKeysByReloadTrigger[item];
|
||||
delete cachedKeysByReloadTrigger[itemString];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +80,8 @@ export async function loadCachedValue(reloadTrigger, cacheKey, func) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error when using cached promise', err);
|
||||
cacheClean(cacheKey);
|
||||
// cacheClean(cacheKey);
|
||||
cacheClean(reloadTrigger);
|
||||
const res = await func();
|
||||
cacheSet(cacheKey, res, reloadTrigger, generation);
|
||||
return res;
|
||||
@@ -87,35 +91,48 @@ export async function loadCachedValue(reloadTrigger, cacheKey, func) {
|
||||
|
||||
export async function subscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
if (!subscriptionsByReloadTrigger[item]) {
|
||||
subscriptionsByReloadTrigger[item] = [];
|
||||
const itemString = stableStringify(item);
|
||||
if (!subscriptionsByReloadTrigger[itemString]) {
|
||||
subscriptionsByReloadTrigger[itemString] = [];
|
||||
}
|
||||
subscriptionsByReloadTrigger[item].push(reloadHandler);
|
||||
subscriptionsByReloadTrigger[itemString].push(reloadHandler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
if (subscriptionsByReloadTrigger[item]) {
|
||||
subscriptionsByReloadTrigger[item] = subscriptionsByReloadTrigger[item].filter(x => x != reloadHandler);
|
||||
const itemString = stableStringify(item);
|
||||
if (subscriptionsByReloadTrigger[itemString]) {
|
||||
subscriptionsByReloadTrigger[itemString] = subscriptionsByReloadTrigger[itemString].filter(
|
||||
x => x != reloadHandler
|
||||
);
|
||||
}
|
||||
if (subscriptionsByReloadTrigger[item].length == 0) {
|
||||
delete subscriptionsByReloadTrigger[item];
|
||||
if (subscriptionsByReloadTrigger[itemString].length == 0) {
|
||||
delete subscriptionsByReloadTrigger[itemString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchCacheChange(reloadTrigger) {
|
||||
// console.log('CHANGE', reloadTrigger);
|
||||
export function dispatchCacheChange(reloadTrigger) {
|
||||
cacheClean(reloadTrigger);
|
||||
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
if (subscriptionsByReloadTrigger[item]) {
|
||||
for (const handler of subscriptionsByReloadTrigger[item]) {
|
||||
const itemString = stableStringify(transformApiArgsInv(item));
|
||||
if (subscriptionsByReloadTrigger[itemString]) {
|
||||
for (const handler of subscriptionsByReloadTrigger[itemString]) {
|
||||
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));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { currentDatabase, getCurrentDatabase, getSingleDatabaseMode, openedTabs } from '../stores';
|
||||
import { currentDatabase, getCurrentDatabase, getLockedDatabaseMode, openedTabs } from '../stores';
|
||||
import { shouldShowTab } from '../widgets/TabsPanel.svelte';
|
||||
import { callWhenAppLoaded } from './appLoadManager';
|
||||
import { getConnectionInfo } from './metadataLoaders';
|
||||
@@ -9,7 +9,7 @@ let lastCurrentTab = null;
|
||||
openedTabs.subscribe(value => {
|
||||
const newCurrentTab = (value || []).find(x => x.selected);
|
||||
if (newCurrentTab == lastCurrentTab) return;
|
||||
if (getSingleDatabaseMode() && getCurrentDatabase()) return;
|
||||
if (getLockedDatabaseMode() && getCurrentDatabase()) return;
|
||||
|
||||
const lastTab = lastCurrentTab;
|
||||
lastCurrentTab = newCurrentTab;
|
||||
@@ -31,7 +31,7 @@ openedTabs.subscribe(value => {
|
||||
});
|
||||
|
||||
currentDatabase.subscribe(currentDb => {
|
||||
if (!getSingleDatabaseMode()) return;
|
||||
if (!getLockedDatabaseMode()) return;
|
||||
openedTabs.update(tabs => {
|
||||
const newTabs = tabs.map(tab => ({
|
||||
...tab,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getConnectionList } from './metadataLoaders';
|
||||
// };
|
||||
|
||||
const doServerPing = value => {
|
||||
apiCall('server-connections/ping', { connections: value });
|
||||
apiCall('server-connections/ping', { conidArray: value });
|
||||
};
|
||||
|
||||
const doDatabasePing = value => {
|
||||
@@ -29,12 +29,12 @@ export function subscribeConnectionPingers() {
|
||||
openedConnections.subscribe(value => {
|
||||
doServerPing(value);
|
||||
if (openedConnectionsHandle) window.clearInterval(openedConnectionsHandle);
|
||||
openedConnectionsHandle = window.setInterval(() => doServerPing(value), 30 * 1000);
|
||||
openedConnectionsHandle = window.setInterval(() => doServerPing(value), 20 * 1000);
|
||||
});
|
||||
|
||||
currentDatabase.subscribe(value => {
|
||||
doDatabasePing(value);
|
||||
if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle);
|
||||
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 30 * 1000);
|
||||
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { apiCall, apiOff, apiOn } from './api';
|
||||
const databaseInfoLoader = ({ conid, database }) => ({
|
||||
url: 'database-connections/structure',
|
||||
params: { conid, database },
|
||||
reloadTrigger: `database-structure-changed-${conid}-${database}`,
|
||||
reloadTrigger: { key: `database-structure-changed`, conid, database },
|
||||
transform: extendDatabaseInfo,
|
||||
});
|
||||
|
||||
@@ -28,31 +28,31 @@ const databaseInfoLoader = ({ conid, database }) => ({
|
||||
const connectionInfoLoader = ({ conid }) => ({
|
||||
url: 'connections/get',
|
||||
params: { conid },
|
||||
reloadTrigger: 'connection-list-changed',
|
||||
reloadTrigger: { key: 'connection-list-changed' },
|
||||
});
|
||||
|
||||
const configLoader = () => ({
|
||||
url: 'config/get',
|
||||
params: {},
|
||||
reloadTrigger: 'config-changed',
|
||||
reloadTrigger: { key: 'config-changed' },
|
||||
});
|
||||
|
||||
const settingsLoader = () => ({
|
||||
url: 'config/get-settings',
|
||||
params: {},
|
||||
reloadTrigger: 'settings-changed',
|
||||
reloadTrigger: { key: 'settings-changed' },
|
||||
});
|
||||
|
||||
const platformInfoLoader = () => ({
|
||||
url: 'config/platform-info',
|
||||
params: {},
|
||||
reloadTrigger: 'platform-info-changed',
|
||||
reloadTrigger: { key: 'platform-info-changed' },
|
||||
});
|
||||
|
||||
const favoritesLoader = () => ({
|
||||
url: 'files/favorites',
|
||||
params: {},
|
||||
reloadTrigger: 'files-changed-favorites',
|
||||
reloadTrigger: { key: 'files-changed-favorites' },
|
||||
});
|
||||
|
||||
// const sqlObjectListLoader = ({ conid, database }) => ({
|
||||
@@ -64,13 +64,13 @@ const favoritesLoader = () => ({
|
||||
const databaseStatusLoader = ({ conid, database }) => ({
|
||||
url: 'database-connections/status',
|
||||
params: { conid, database },
|
||||
reloadTrigger: `database-status-changed-${conid}-${database}`,
|
||||
reloadTrigger: { key: `database-status-changed`, conid, database },
|
||||
});
|
||||
|
||||
const databaseListLoader = ({ conid }) => ({
|
||||
url: 'server-connections/list-databases',
|
||||
params: { conid },
|
||||
reloadTrigger: `database-list-changed-${conid}`,
|
||||
reloadTrigger: { key: `database-list-changed`, conid },
|
||||
onLoaded: value => {
|
||||
if (value?.length > 0) setLocalStorage(`database_list_${conid}`, value);
|
||||
},
|
||||
@@ -85,37 +85,37 @@ const databaseListLoader = ({ conid }) => ({
|
||||
const serverVersionLoader = ({ conid }) => ({
|
||||
url: 'server-connections/version',
|
||||
params: { conid },
|
||||
reloadTrigger: `server-version-changed-${conid}`,
|
||||
reloadTrigger: { key: `server-version-changed`, conid },
|
||||
});
|
||||
|
||||
const databaseServerVersionLoader = ({ conid, database }) => ({
|
||||
url: 'database-connections/server-version',
|
||||
params: { conid, database },
|
||||
reloadTrigger: `database-server-version-changed-${conid}-${database}`,
|
||||
reloadTrigger: { key: `database-server-version-changed`, conid, database },
|
||||
});
|
||||
|
||||
const archiveFoldersLoader = () => ({
|
||||
url: 'archive/folders',
|
||||
params: {},
|
||||
reloadTrigger: `archive-folders-changed`,
|
||||
reloadTrigger: { key: `archive-folders-changed` },
|
||||
});
|
||||
|
||||
const archiveFilesLoader = ({ folder }) => ({
|
||||
url: 'archive/files',
|
||||
params: { folder },
|
||||
reloadTrigger: `archive-files-changed-${folder}`,
|
||||
reloadTrigger: { key: `archive-files-changed`, folder },
|
||||
});
|
||||
|
||||
const appFoldersLoader = () => ({
|
||||
url: 'apps/folders',
|
||||
params: {},
|
||||
reloadTrigger: `app-folders-changed`,
|
||||
reloadTrigger: { key: `app-folders-changed` },
|
||||
});
|
||||
|
||||
const appFilesLoader = ({ folder }) => ({
|
||||
url: 'apps/files',
|
||||
params: { folder },
|
||||
reloadTrigger: `app-files-changed-${folder}`,
|
||||
reloadTrigger: { key: `app-files-changed`, app: folder },
|
||||
});
|
||||
|
||||
// const dbAppsLoader = ({ conid, database }) => ({
|
||||
@@ -127,41 +127,41 @@ const appFilesLoader = ({ folder }) => ({
|
||||
const usedAppsLoader = ({ conid, database }) => ({
|
||||
url: 'apps/get-used-apps',
|
||||
params: {},
|
||||
reloadTrigger: `used-apps-changed`,
|
||||
reloadTrigger: { key: `used-apps-changed` },
|
||||
});
|
||||
|
||||
const serverStatusLoader = () => ({
|
||||
url: 'server-connections/server-status',
|
||||
params: {},
|
||||
reloadTrigger: `server-status-changed`,
|
||||
reloadTrigger: { key: `server-status-changed` },
|
||||
});
|
||||
|
||||
const connectionListLoader = () => ({
|
||||
url: 'connections/list',
|
||||
params: {},
|
||||
reloadTrigger: `connection-list-changed`,
|
||||
reloadTrigger: { key: `connection-list-changed` },
|
||||
});
|
||||
|
||||
const installedPluginsLoader = () => ({
|
||||
url: 'plugins/installed',
|
||||
params: {},
|
||||
reloadTrigger: `installed-plugins-changed`,
|
||||
reloadTrigger: { key: `installed-plugins-changed` },
|
||||
});
|
||||
|
||||
const filesLoader = ({ folder }) => ({
|
||||
url: 'files/list',
|
||||
params: { folder },
|
||||
reloadTrigger: `files-changed-${folder}`,
|
||||
reloadTrigger: { key: `files-changed`, folder },
|
||||
});
|
||||
const allFilesLoader = () => ({
|
||||
url: 'files/list-all',
|
||||
params: {},
|
||||
reloadTrigger: `all-files-changed`,
|
||||
reloadTrigger: { key: `all-files-changed` },
|
||||
});
|
||||
const authTypesLoader = ({ engine }) => ({
|
||||
url: 'plugins/auth-types',
|
||||
params: { engine },
|
||||
reloadTrigger: `installed-plugins-changed`,
|
||||
reloadTrigger: { key: `installed-plugins-changed` },
|
||||
errorValue: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import SearchInput from '../elements/SearchInput.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 AppObjectList from '../appobj/AppObjectList.svelte';
|
||||
import * as connectionAppObject from '../appobj/ConnectionAppObject.svelte';
|
||||
@@ -21,7 +21,7 @@
|
||||
import { useConnectionColorFactory } from '../utility/useConnectionColor';
|
||||
import FontIcon from '../icons/FontIcon.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 { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons';
|
||||
import { safeJsonParse } from 'dbgate-tools';
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
$: connectionsWithStatus =
|
||||
$connections && $serverStatus
|
||||
? $connections.map(conn => ({ ...conn, status: $serverStatus[conn._id] }))
|
||||
? $connections.map(conn => ({ ...conn, status: $serverStatus[getVolatileRemapping(conn._id)] }))
|
||||
: $connections;
|
||||
|
||||
$: connectionsWithStatusFiltered = connectionsWithStatus?.filter(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||
import SqlObjectList from './SqlObjectList.svelte';
|
||||
import DbKeysTree from './DbKeysTree.svelte';
|
||||
import SingleConnectionDatabaseList from './SingleConnectionDatabaseList.svelte';
|
||||
|
||||
export let hidden = false;
|
||||
|
||||
@@ -24,7 +25,11 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<ConnectionList />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
32
packages/web/src/widgets/SingleConnectionDatabaseList.svelte
Normal file
32
packages/web/src/widgets/SingleConnectionDatabaseList.svelte
Normal 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>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" context="module">
|
||||
const getCurrentValueMarker: any = {};
|
||||
|
||||
export function shouldShowTab(tab, singleDbMode = getCurrentValueMarker, currentDb = getCurrentValueMarker) {
|
||||
if (singleDbMode == getCurrentValueMarker) {
|
||||
singleDbMode = getSingleDatabaseMode();
|
||||
export function shouldShowTab(tab, lockedDbMode = getCurrentValueMarker, currentDb = getCurrentValueMarker) {
|
||||
if (lockedDbMode == getCurrentValueMarker) {
|
||||
lockedDbMode = getLockedDatabaseMode();
|
||||
}
|
||||
if (singleDbMode) {
|
||||
if (lockedDbMode) {
|
||||
if (currentDb == getCurrentValueMarker) {
|
||||
currentDb = getCurrentDatabase();
|
||||
}
|
||||
@@ -250,8 +250,8 @@
|
||||
activeTabId,
|
||||
getActiveTabId,
|
||||
getCurrentDatabase,
|
||||
singleDatabaseMode,
|
||||
getSingleDatabaseMode,
|
||||
lockedDatabaseMode,
|
||||
getLockedDatabaseMode,
|
||||
} from '../stores';
|
||||
import tabs from '../tabs';
|
||||
import { setSelectedTab } from '../utility/common';
|
||||
@@ -264,7 +264,7 @@
|
||||
import TabCloseButton from '../elements/TabCloseButton.svelte';
|
||||
import CloseTabModal from '../modals/CloseTabModal.svelte';
|
||||
|
||||
$: showTabFilterFunc = tab => shouldShowTab(tab, $singleDatabaseMode, $currentDatabase);
|
||||
$: showTabFilterFunc = tab => shouldShowTab(tab, $lockedDatabaseMode, $currentDatabase);
|
||||
$: connectionList = useConnectionList();
|
||||
|
||||
$: currentDbKey =
|
||||
@@ -443,7 +443,7 @@
|
||||
<div class="tabs" on:wheel={handleTabsWheel} bind:this={domTabs}>
|
||||
{#each groupedTabs as tabGroup}
|
||||
<div class="db-wrapper">
|
||||
{#if !$singleDatabaseMode}
|
||||
{#if !$lockedDatabaseMode}
|
||||
<div
|
||||
class="db-name"
|
||||
class:selected={draggingDbGroup
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
visibleSelectedWidget,
|
||||
visibleWidgetSideBar,
|
||||
visibleHamburgerMenuWidget,
|
||||
singleDatabaseMode,
|
||||
lockedDatabaseMode,
|
||||
} from '../stores';
|
||||
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
@@ -112,12 +112,12 @@
|
||||
|
||||
<div
|
||||
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={() => {
|
||||
$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 class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings}>
|
||||
<FontIcon icon="icon settings" />
|
||||
|
||||
Reference in New Issue
Block a user