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/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..da933f163 --- /dev/null +++ b/packages/api/env/dblogin/.env @@ -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 diff --git a/packages/api/env/singledb/.env b/packages/api/env/singledb/.env index cf816aa11..a119453b4 100644 --- a/packages/api/env/singledb/.env +++ b/packages/api/env/singledb/.env @@ -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"}] 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/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/config.js b/packages/api/src/controllers/config.js index d1d4daddf..500f61423 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -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, diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 5447a6f4d..7ca5c67bb 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++) { @@ -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; diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 6be1a25c7..3601b4734 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[]} */ @@ -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 }); } }, 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 36a751ba9..ccdbefe1b 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: [], @@ -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(); 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/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/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/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/CommandPalette.svelte b/packages/web/src/commands/CommandPalette.svelte index b2adedf60..ba6ae25b1 100644 --- a/packages/web/src/commands/CommandPalette.svelte +++ b/packages/web/src/commands/CommandPalette.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({ 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/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/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/modals/DatabaseLoginModal.svelte b/packages/web/src/modals/DatabaseLoginModal.svelte new file mode 100644 index 000000000..53380fe41 --- /dev/null +++ b/packages/web/src/modals/DatabaseLoginModal.svelte @@ -0,0 +1,137 @@ + + + + + + + Database Log In + + + + + + {#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/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/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 => { @@ -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); } }); } diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index e55c839c5..eb47fd55a 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,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); diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index d6dcc6702..19a100d08 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -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)); 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/utility/connectionsPinger.js b/packages/web/src/utility/connectionsPinger.js index c37b5c8d9..f22ee3d25 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 => { @@ -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); }); } 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, }); diff --git a/packages/web/src/widgets/ConnectionList.svelte b/packages/web/src/widgets/ConnectionList.svelte index f475ba7f7..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'; @@ -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( diff --git a/packages/web/src/widgets/DatabaseWidget.svelte b/packages/web/src/widgets/DatabaseWidget.svelte index 807fe4ca8..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; @@ -24,7 +25,11 @@ - {#if !$config?.singleDatabase} + {#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 @@ + + + + + + + + + + + + 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 @@