From d66fc06403dcc7d9f5a7ae586c5054afc7ae3ece Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 10:21:19 +0100 Subject: [PATCH 01/10] ask password logic & modal --- packages/api/src/controllers/connections.js | 26 ++++++ .../src/controllers/databaseConnections.js | 4 + .../api/src/controllers/serverConnections.js | 4 + packages/api/src/utility/exceptions.js | 9 ++ packages/api/src/utility/useController.js | 18 +++- .../web/src/modals/DatabaseLoginModal.svelte | 82 +++++++++++++++++++ .../settings/ConnectionDriverFields.svelte | 12 ++- packages/web/src/utility/api.ts | 26 +++++- 8 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 packages/api/src/utility/exceptions.js create mode 100644 packages/web/src/modals/DatabaseLoginModal.svelte diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 5447a6f4d..bf967bd0c 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -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++) { @@ -126,6 +129,7 @@ function getPortalCollections() { return null; } + const portalConnections = getPortalCollections(); function getSingleDatabase() { @@ -199,6 +203,24 @@ module.exports = { }); }, + saveVolatile_meta: true, + async saveVolatile({ conid, user, password }) { + const old = await this.getCore({ conid }); + const res = { + ...old, + _id: crypto.randomUUID(), + password, + passwordMode: undefined, + unsaved: true, + }; + if (old.passwordMode == 'askUser') { + res.user = user; + } + + volatileConnections[res._id] = res; + return res; + }, + save_meta: true, async save(connection) { if (portalConnections) return; @@ -258,6 +280,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; diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 6be1a25c7..2ace4869c 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -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[]} */ @@ -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', diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 36a751ba9..2ed0189a3 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -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: [], @@ -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', diff --git a/packages/api/src/utility/exceptions.js b/packages/api/src/utility/exceptions.js new file mode 100644 index 000000000..fca5679da --- /dev/null +++ b/packages/api/src/utility/exceptions.js @@ -0,0 +1,9 @@ +class MissingCredentialsError { + constructor(detail) { + this.detail = detail; + } +} + +module.exports = { + MissingCredentialsError, +}; diff --git a/packages/api/src/utility/useController.js b/packages/api/src/utility/useController.js index 8ee431a42..1c3e562c8 100644 --- a/packages/api/src/utility/useController.js +++ b/packages/api/src/utility/useController.js @@ -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 }); + } } }); } diff --git a/packages/web/src/modals/DatabaseLoginModal.svelte b/packages/web/src/modals/DatabaseLoginModal.svelte new file mode 100644 index 000000000..e5c14ddc9 --- /dev/null +++ b/packages/web/src/modals/DatabaseLoginModal.svelte @@ -0,0 +1,82 @@ + + + + + + + Database Log In + + + + + + + + + + + diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte index ebe5e5186..eb3e1a6ab 100644 --- a/packages/web/src/settings/ConnectionDriverFields.svelte +++ b/packages/web/src/settings/ConnectionDriverFields.svelte @@ -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); @@ -159,7 +163,7 @@ {/if} -{#if !disabledFields.includes('password') && showPassword} +{#if !disabledFields.includes('password') && showPasswordMode} {/if} diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index e55c839c5..aa331267a 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -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,8 @@ let apiLogging = false; let apiDisabled = false; const disabledOnOauth = isOauthCallback(); +const volatileConnectionMap = {}; + export function disableApi() { apiDisabled = true; } @@ -20,6 +25,10 @@ export function enableApi() { apiDisabled = false; } +export function setVolatileConnectionRemapping(existingConnectionId, volatileConnectionId) { + volatileConnectionMap[existingConnectionId] = volatileConnectionId; +} + function wantEventSource() { if (!eventSource) { eventSource = new EventSource(`${resolveApi()}/stream`); @@ -32,7 +41,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 +60,10 @@ function processApiResponse(route, args, resp) { return resp; } +function transformApiArgs(args) { + return _.mapValues(args, (v, k) => (k == 'conid' && v && volatileConnectionMap[v] ? volatileConnectionMap[v] : v)); +} + export async function apiCall(route: string, args: {} = undefined) { if (apiLogging) { console.log('>>> API CALL', route, args); @@ -55,6 +77,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); From 1eea117062fee9f4001a4fcbd7ec29dcddaafe11 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 12:48:10 +0100 Subject: [PATCH 02/10] connection testing --- package.json | 1 + packages/api/env/dblogin/.env | 11 ++++ packages/api/package.json | 1 + packages/api/src/controllers/connections.js | 19 +++++- .../api/src/controllers/serverConnections.js | 4 +- .../web/src/modals/DatabaseLoginModal.svelte | 65 +++++++++++++++++-- packages/web/src/utility/api.ts | 10 ++- packages/web/src/utility/cache.ts | 2 +- packages/web/src/utility/connectionsPinger.js | 2 +- .../web/src/widgets/ConnectionList.svelte | 4 +- 10 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 packages/api/env/dblogin/.env diff --git a/package.json b/package.json index d3746f15d..f6e4d37e1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/api/env/dblogin/.env b/packages/api/env/dblogin/.env new file mode 100644 index 000000000..db6b9bb01 --- /dev/null +++ b/packages/api/env/dblogin/.env @@ -0,0 +1,11 @@ +DEVMODE=1 + +CONNECTIONS=mysql + +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 diff --git a/packages/api/package.json b/packages/api/package.json index 27e7ba1cf..64caf1d12 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -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", diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index bf967bd0c..8757deec0 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -52,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}`], @@ -204,7 +205,7 @@ module.exports = { }, saveVolatile_meta: true, - async saveVolatile({ conid, user, password }) { + async saveVolatile({ conid, user, password, test }) { const old = await this.getCore({ conid }); const res = { ...old, @@ -217,8 +218,20 @@ module.exports = { res.user = user; } - volatileConnections[res._id] = res; - return res; + 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, diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 2ed0189a3..831dda448 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -131,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(); diff --git a/packages/web/src/modals/DatabaseLoginModal.svelte b/packages/web/src/modals/DatabaseLoginModal.svelte index e5c14ddc9..d4991aea5 100644 --- a/packages/web/src/modals/DatabaseLoginModal.svelte +++ b/packages/web/src/modals/DatabaseLoginModal.svelte @@ -11,15 +11,20 @@ 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 { 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 } from './modalTools'; + import { closeCurrentModal, showModal } from './modalTools'; export let conid; export let passwordMode; @@ -27,6 +32,10 @@ const values = writable({}); let connection; + let isTesting; + let sqlConnectResult; + const testIdRef = createRef(0); + currentModalConid = conid; onMount(async () => { @@ -44,14 +53,31 @@ currentModalConid = null; }); + function handleCancelTest() { + testIdRef.update(x => x + 1); // invalidate current test + isTesting = false; + } + async function handleSubmit(ev) { - const con = await apiCall('connections/save-volatile', { + isTesting = true; + testIdRef.update(x => x + 1); + const testid = testIdRef.get(); + const resp = await apiCall('connections/save-volatile', { conid, user: ev.detail.user, password: ev.detail.password, + test: true, }); - setVolatileConnectionRemapping(conid, con._id); - closeCurrentModal(); + if (testIdRef.get() != testid) return; + isTesting = false; + if (resp.msgtype == 'connected') { + setVolatileConnectionRemapping(conid, resp._id); + dispatchCacheChange(`database-list-changed-${conid}`); + dispatchCacheChange(`server-status-changed`); + closeCurrentModal(); + } else { + sqlConnectResult = resp; + } } @@ -74,9 +100,36 @@ focused={passwordMode == 'askPassword'} /> + {#if isTesting} +
+ Testing connection +
+ {/if} + + {#if !isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error'} +
+ Connect failed: + {sqlConnectResult.error} + + showModal(ErrorMessageModal, { + message: sqlConnectResult.detail, + showAsCode: true, + title: 'Database connection error', + })} + > + Show detail + +
+ {/if} + - - + {#if isTesting} + + {:else} + + {/if} + diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index aa331267a..45f3165f2 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -29,6 +29,10 @@ export function setVolatileConnectionRemapping(existingConnectionId, volatileCon volatileConnectionMap[existingConnectionId] = volatileConnectionId; } +export function getVolatileRemapping(conid) { + return volatileConnectionMap[conid] || conid; +} + function wantEventSource() { if (!eventSource) { eventSource = new EventSource(`${resolveApi()}/stream`); @@ -61,7 +65,11 @@ function processApiResponse(route, args, resp) { } function transformApiArgs(args) { - return _.mapValues(args, (v, k) => (k == 'conid' && v && volatileConnectionMap[v] ? volatileConnectionMap[v] : v)); + 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 async function apiCall(route: string, args: {} = undefined) { diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index d6dcc6702..4de67e25b 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -105,7 +105,7 @@ export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHand } } -function dispatchCacheChange(reloadTrigger) { +export function dispatchCacheChange(reloadTrigger) { // console.log('CHANGE', reloadTrigger); cacheClean(reloadTrigger); diff --git a/packages/web/src/utility/connectionsPinger.js b/packages/web/src/utility/connectionsPinger.js index c37b5c8d9..426a6a9fe 100644 --- a/packages/web/src/utility/connectionsPinger.js +++ b/packages/web/src/utility/connectionsPinger.js @@ -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 => { diff --git a/packages/web/src/widgets/ConnectionList.svelte b/packages/web/src/widgets/ConnectionList.svelte index f475ba7f7..77b85ee89 100644 --- a/packages/web/src/widgets/ConnectionList.svelte +++ b/packages/web/src/widgets/ConnectionList.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( From 783f26b500bbb3654b29139edfe023233daf637e Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 15:35:56 +0100 Subject: [PATCH 03/10] structured reload trigger --- packages/api/src/controllers/apps.js | 12 +++--- packages/api/src/controllers/archive.js | 12 +++--- .../src/controllers/databaseConnections.js | 10 ++--- packages/api/src/controllers/files.js | 14 +++---- .../api/src/controllers/serverConnections.js | 4 +- packages/api/src/utility/socket.js | 7 ++-- packages/web/src/utility/cache.ts | 36 +++++++++------- packages/web/src/utility/metadataLoaders.ts | 42 +++++++++---------- 8 files changed, 73 insertions(+), 64 deletions(-) diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js index ba2afe587..43c2d6e28 100644 --- a/packages/api/src/controllers/apps.js +++ b/packages/api/src/controllers/apps.js @@ -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; } diff --git a/packages/api/src/controllers/archive.js b/packages/api/src/controllers/archive.js index a9147169a..50bb96fa6 100644 --- a/packages/api/src/controllers/archive.js +++ b/packages/api/src/controllers/archive.js @@ -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; }, diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 2ace4869c..3601b4734 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -43,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) { @@ -73,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() {}, @@ -317,7 +317,7 @@ module.exports = { }, structure: existing.structure, }; - socket.emitChanged(`database-status-changed-${conid}-${database}`); + socket.emitChanged(`database-status-changed`, { conid, database }); } }, diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index 484429db5..1e3c76b7a 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -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(); diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 831dda448..ccdbefe1b 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -21,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); diff --git a/packages/api/src/utility/socket.js b/packages/api/src/utility/socket.js index 5b519f107..b0f9fc3cb 100644 --- a/packages/api/src/utility/socket.js +++ b/packages/api/src/utility/socket.js @@ -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); }, }; diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index 4de67e25b..8e4443eaf 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -1,5 +1,6 @@ import { apiOn } 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(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]; } } @@ -87,20 +90,24 @@ 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]; } } } @@ -110,8 +117,9 @@ export function dispatchCacheChange(reloadTrigger) { cacheClean(reloadTrigger); for (const item of getAsArray(reloadTrigger)) { - if (subscriptionsByReloadTrigger[item]) { - for (const handler of subscriptionsByReloadTrigger[item]) { + const itemString = stableStringify(item); + if (subscriptionsByReloadTrigger[itemString]) { + for (const handler of subscriptionsByReloadTrigger[itemString]) { handler(); } } diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index a29404a53..3ea4c2764 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -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, }); From 8d1d6537a4a006cc3b5102e2a0b9395961a62087 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 17:12:12 +0100 Subject: [PATCH 04/10] ask password works! --- .../web/src/forms/FormPasswordFieldRaw.svelte | 6 ++++++ .../web/src/forms/FormTextFieldRaw.svelte | 6 ++++++ .../web/src/modals/DatabaseLoginModal.svelte | 12 +++++++----- packages/web/src/utility/api.ts | 16 +++++++++++++++- packages/web/src/utility/cache.ts | 19 ++++++++++++++----- packages/web/src/utility/connectionsPinger.js | 4 ++-- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/web/src/forms/FormPasswordFieldRaw.svelte b/packages/web/src/forms/FormPasswordFieldRaw.svelte index 5244373f2..b2a8f0e39 100644 --- a/packages/web/src/forms/FormPasswordFieldRaw.svelte +++ b/packages/web/src/forms/FormPasswordFieldRaw.svelte @@ -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'} /> diff --git a/packages/web/src/forms/FormTextFieldRaw.svelte b/packages/web/src/forms/FormTextFieldRaw.svelte index 215c0317f..320dd7b20 100644 --- a/packages/web/src/forms/FormTextFieldRaw.svelte +++ b/packages/web/src/forms/FormTextFieldRaw.svelte @@ -4,6 +4,7 @@ export let name; export let defaultValue; + export let saveOnInput = false; const { values, setFieldValue } = getFormContext(); @@ -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']); + } + }} /> diff --git a/packages/web/src/modals/DatabaseLoginModal.svelte b/packages/web/src/modals/DatabaseLoginModal.svelte index d4991aea5..53380fe41 100644 --- a/packages/web/src/modals/DatabaseLoginModal.svelte +++ b/packages/web/src/modals/DatabaseLoginModal.svelte @@ -18,7 +18,7 @@ import FormTextField from '../forms/FormTextField.svelte'; import FontIcon from '../icons/FontIcon.svelte'; import { apiCall, setVolatileConnectionRemapping } from '../utility/api'; - import { dispatchCacheChange } from '../utility/cache'; + import { batchDispatchCacheTriggers, dispatchCacheChange } from '../utility/cache'; import createRef from '../utility/createRef'; import { getConnectionInfo } from '../utility/metadataLoaders'; @@ -64,16 +64,16 @@ const testid = testIdRef.get(); const resp = await apiCall('connections/save-volatile', { conid, - user: ev.detail.user, - password: ev.detail.password, + user: $values['user'], + password: $values['password'], test: true, }); if (testIdRef.get() != testid) return; isTesting = false; if (resp.msgtype == 'connected') { setVolatileConnectionRemapping(conid, resp._id); - dispatchCacheChange(`database-list-changed-${conid}`); - dispatchCacheChange(`server-status-changed`); + dispatchCacheChange({ key: `server-status-changed` }); + batchDispatchCacheTriggers(x => x.conid == conid); closeCurrentModal(); } else { sqlConnectResult = resp; @@ -92,12 +92,14 @@ autocomplete="username" disabled={passwordMode == 'askPassword'} focused={passwordMode == 'askUser'} + saveOnInput /> {#if isTesting} diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 45f3165f2..c95f2c146 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -16,6 +16,7 @@ let apiDisabled = false; const disabledOnOauth = isOauthCallback(); const volatileConnectionMap = {}; +const volatileConnectionMapInv = {}; export function disableApi() { apiDisabled = true; @@ -27,12 +28,17 @@ export function enableApi() { 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; +} + function wantEventSource() { if (!eventSource) { eventSource = new EventSource(`${resolveApi()}/stream`); @@ -64,7 +70,7 @@ function processApiResponse(route, args, resp) { return resp; } -function transformApiArgs(args) { +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); @@ -72,6 +78,14 @@ function transformApiArgs(args) { }); } +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); diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index 8e4443eaf..19a100d08 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -1,4 +1,4 @@ -import { apiOn } from './api'; +import { apiOn, transformApiArgsInv } from './api'; import getAsArray from './getAsArray'; import stableStringify from 'json-stable-stringify'; @@ -34,7 +34,7 @@ function cacheSet(cacheKey, value, reloadTrigger, generation) { function cacheClean(reloadTrigger) { cacheGeneration += 1; for (const item of getAsArray(reloadTrigger)) { - const itemString = stableStringify(item); + const itemString = stableStringify(transformApiArgsInv(item)); const keys = cachedKeysByReloadTrigger[itemString]; if (keys) { for (const key of keys) { @@ -80,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; @@ -113,11 +114,10 @@ export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHand } export function dispatchCacheChange(reloadTrigger) { - // console.log('CHANGE', reloadTrigger); cacheClean(reloadTrigger); for (const item of getAsArray(reloadTrigger)) { - const itemString = stableStringify(item); + const itemString = stableStringify(transformApiArgsInv(item)); if (subscriptionsByReloadTrigger[itemString]) { for (const handler of subscriptionsByReloadTrigger[itemString]) { handler(); @@ -126,4 +126,13 @@ export function dispatchCacheChange(reloadTrigger) { } } +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)); diff --git a/packages/web/src/utility/connectionsPinger.js b/packages/web/src/utility/connectionsPinger.js index 426a6a9fe..f22ee3d25 100644 --- a/packages/web/src/utility/connectionsPinger.js +++ b/packages/web/src/utility/connectionsPinger.js @@ -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); }); } From 3792f1001e0d355e43f1f8405d42e99a5c1e5652 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 17:32:12 +0100 Subject: [PATCH 05/10] .env file --- packages/api/env/dblogin/.env | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api/env/dblogin/.env b/packages/api/env/dblogin/.env index db6b9bb01..9cf91c3f8 100644 --- a/packages/api/env/dblogin/.env +++ b/packages/api/env/dblogin/.env @@ -4,8 +4,9 @@ CONNECTIONS=mysql LABEL_mysql=MySql localhost SERVER_mysql=localhost -USER_mysql=root +# USER_mysql=root PORT_mysql=3306 # PASSWORD_mysql=Pwd2020Db ENGINE_mysql=mysql@dbgate-plugin-mysql -PASSWORD_MODE_mysql=askPassword +# PASSWORD_MODE_mysql=askPassword +PASSWORD_MODE_mysql=askUser From a6fa116b5e4bfd61a590aeb6aa010707975ab06a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 17:32:14 +0100 Subject: [PATCH 06/10] renamed singleDatabaseMode to lockedDatabaseMode --- packages/web/src/icons/FontIcon.svelte | 4 ++-- packages/web/src/settings/SettingsModal.svelte | 6 +++--- packages/web/src/stores.ts | 12 ++++++------ packages/web/src/utility/changeCurrentDbByTab.ts | 6 +++--- packages/web/src/widgets/TabsPanel.svelte | 16 ++++++++-------- packages/web/src/widgets/WidgetIconPanel.svelte | 8 ++++---- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 2c5cc0bfb..6593016be 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -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', diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 674774c90..c111d67c7 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -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; }, }} > - ($singleDatabaseMode = e.target.checked)} /> + ($lockedDatabaseMode = e.target.checked)} /> (false, 'singleDatabaseMode'); +export const lockedDatabaseMode = writableWithStorage(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 => { diff --git a/packages/web/src/utility/changeCurrentDbByTab.ts b/packages/web/src/utility/changeCurrentDbByTab.ts index 46424320f..3f4ba1a23 100644 --- a/packages/web/src/utility/changeCurrentDbByTab.ts +++ b/packages/web/src/utility/changeCurrentDbByTab.ts @@ -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, diff --git a/packages/web/src/widgets/TabsPanel.svelte b/packages/web/src/widgets/TabsPanel.svelte index 5df04a1d5..39bbba912 100644 --- a/packages/web/src/widgets/TabsPanel.svelte +++ b/packages/web/src/widgets/TabsPanel.svelte @@ -1,11 +1,11 @@ - {#if !$config?.singleDatabase} + {#if !$config?.singleDbConnection} From 5c1c4e1fa6d759d6aa4f0e94bb50b9e0b4534600 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 18:57:08 +0100 Subject: [PATCH 09/10] single connection multi db layout --- .../web/src/widgets/ConnectionList.svelte | 2 +- .../web/src/widgets/DatabaseWidget.svelte | 9 ++++-- .../SingleConnectionDatabaseList.svelte | 32 +++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 packages/web/src/widgets/SingleConnectionDatabaseList.svelte diff --git a/packages/web/src/widgets/ConnectionList.svelte b/packages/web/src/widgets/ConnectionList.svelte index 77b85ee89..001f89a51 100644 --- a/packages/web/src/widgets/ConnectionList.svelte +++ b/packages/web/src/widgets/ConnectionList.svelte @@ -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'; diff --git a/packages/web/src/widgets/DatabaseWidget.svelte b/packages/web/src/widgets/DatabaseWidget.svelte index 2e554bbdc..c64b7ea5a 100644 --- a/packages/web/src/widgets/DatabaseWidget.svelte +++ b/packages/web/src/widgets/DatabaseWidget.svelte @@ -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; @@ -21,12 +22,14 @@ $: config = useConfig(); $: singleDatabase = $currentDatabase?.connection?.singleDatabase; $: database = $currentDatabase?.name; - - $: console.log('CFG', $config); - {#if !$config?.singleDbConnection} + {#if $config?.singleConnection} + + + + {:else if !$config?.singleDbConnection} diff --git a/packages/web/src/widgets/SingleConnectionDatabaseList.svelte b/packages/web/src/widgets/SingleConnectionDatabaseList.svelte new file mode 100644 index 000000000..106e661f2 --- /dev/null +++ b/packages/web/src/widgets/SingleConnectionDatabaseList.svelte @@ -0,0 +1,32 @@ + + + + + + + + + + + + From c340ac91126b758e92f9c036ab07d729625d6995 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 25 Dec 2022 19:27:24 +0100 Subject: [PATCH 10/10] disconnect command --- app/src/mainMenuDefinition.js | 1 + packages/api/env/dblogin/.env | 6 +++--- packages/web/src/appobj/ConnectionAppObject.svelte | 3 ++- packages/web/src/commands/stdCommands.ts | 9 +++++++++ packages/web/src/utility/api.ts | 8 ++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/mainMenuDefinition.js b/app/src/mainMenuDefinition.js index 8ce7056e0..04d846c61 100644 --- a/app/src/mainMenuDefinition.js +++ b/app/src/mainMenuDefinition.js @@ -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 }, ], }, { diff --git a/packages/api/env/dblogin/.env b/packages/api/env/dblogin/.env index 5c47a7d05..da933f163 100644 --- a/packages/api/env/dblogin/.env +++ b/packages/api/env/dblogin/.env @@ -6,9 +6,9 @@ SINGLE_CONNECTION=mysql LABEL_mysql=MySql localhost SERVER_mysql=localhost -USER_mysql=root +# USER_mysql=root PORT_mysql=3306 -PASSWORD_mysql=Pwd2020Db +# PASSWORD_mysql=Pwd2020Db ENGINE_mysql=mysql@dbgate-plugin-mysql # PASSWORD_MODE_mysql=askPassword -# PASSWORD_MODE_mysql=askUser +PASSWORD_MODE_mysql=askUser diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index 5ff11e381..5c6c94dfb 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -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'; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 6a0b7f054..039cb005d 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -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, diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index c95f2c146..eb47fd55a 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -39,6 +39,14 @@ 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`);