diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 5b06d307..36a751ba 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -1,6 +1,7 @@ const connections = require('./connections'); const socket = require('../utility/socket'); const { fork } = require('child_process'); +const uuidv1 = require('uuid/v1'); const _ = require('lodash'); const AsyncLock = require('async-lock'); const { handleProcessCommunication } = require('../utility/processComm'); @@ -13,6 +14,7 @@ module.exports = { opened: [], closed: {}, lastPinged: {}, + requests: {}, handle_databases(conid, { databases }) { const existing = this.opened.find(x => x.conid == conid); @@ -33,6 +35,11 @@ module.exports = { socket.emitChanged(`server-status-changed`); }, handle_ping() {}, + handle_response(conid, { msgid, ...response }) { + const [resolve, reject] = this.requests[msgid]; + resolve(response); + delete this.requests[msgid]; + }, async ensureOpened(conid) { const res = await lock.acquire(conid, async () => { @@ -161,4 +168,41 @@ module.exports = { opened.subprocess.send({ msgtype: 'dropDatabase', name }); return { status: 'ok' }; }, + + sendRequest(conn, message) { + const msgid = uuidv1(); + const promise = new Promise((resolve, reject) => { + this.requests[msgid] = [resolve, reject]; + conn.subprocess.send({ msgid, ...message }); + }); + return promise; + }, + + async loadDataCore(msgtype, { conid, ...args }, req) { + testConnectionPermission(conid, req); + const opened = await this.ensureOpened(conid); + const res = await this.sendRequest(opened, { msgtype, ...args }); + if (res.errorMessage) { + console.error(res.errorMessage); + + return { + errorMessage: res.errorMessage, + }; + } + return res.result || null; + }, + + serverSummary_meta: true, + async serverSummary({ conid }, req) { + testConnectionPermission(conid, req); + return this.loadDataCore('serverSummary', { conid }); + }, + + summaryCommand_meta: true, + async summaryCommand({ conid, command, row }, req) { + testConnectionPermission(conid, req); + const opened = await this.ensureOpened(conid); + if (opened.connection.isReadOnly) return false; + return this.loadDataCore('summaryCommand', { conid, command, row }); + }, }; diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index a700b026..c720b888 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -10,6 +10,7 @@ let storedConnection; let lastDatabases = null; let lastStatus = null; let lastPing = null; +let afterConnectCallbacks = []; async function handleRefresh() { const driver = requireEngineDriver(storedConnection); @@ -74,6 +75,18 @@ async function handleConnect(connection) { // console.error(err); setTimeout(() => process.exit(1), 1000); } + + for (const [resolve] of afterConnectCallbacks) { + resolve(); + } + afterConnectCallbacks = []; +} + +function waitConnected() { + if (systemConnection) return Promise.resolve(); + return new Promise((resolve, reject) => { + afterConnectCallbacks.push([resolve, reject]); + }); } function handlePing() { @@ -94,9 +107,30 @@ async function handleDatabaseOp(op, { name }) { await handleRefresh(); } +async function handleDriverDataCore(msgid, callMethod) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + try { + const result = await callMethod(driver); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + +async function handleServerSummary({ msgid }) { + return handleDriverDataCore(msgid, driver => driver.serverSummary(systemConnection)); +} + +async function handleSummaryCommand({ msgid, command, row }) { + return handleDriverDataCore(msgid, driver => driver.summaryCommand(systemConnection, command, row)); +} + const messageHandlers = { connect: handleConnect, ping: handlePing, + serverSummary: handleServerSummary, + summaryCommand: handleSummaryCommand, createDatabase: props => handleDatabaseOp('createDatabase', props), dropDatabase: props => handleDatabaseOp('dropDatabase', props), }; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 6673b64a..e2da64c8 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -55,6 +55,17 @@ export interface SqlBackupDumper { run(); } +export interface SummaryColumn { + fieldName: string; + header: string; + dataType: 'string' | 'number' | 'bytes'; +} +export interface ServerSummaryDatabase {} +export interface ServerSummary { + columns: SummaryColumn[]; + databases: ServerSummaryDatabase[]; +} + export interface EngineDriver { engine: string; title: string; @@ -65,6 +76,7 @@ export interface EngineDriver { supportedKeyTypes: SupportedDbKeyType[]; supportsDatabaseUrl?: boolean; supportsDatabaseDump?: boolean; + supportsServerSummary?: boolean; isElectronOnly?: boolean; supportedCreateDatabase?: boolean; showConnectionField?: (field: string, values: any) => boolean; @@ -81,7 +93,7 @@ export interface EngineDriver { stream(pool: any, sql: string, options: StreamOptions); readQuery(pool: any, sql: string, structure?: TableInfo): Promise; readJsonQuery(pool: any, query: any, structure?: TableInfo): Promise; - writeTable(pool: any, name: NamedObjectInfo, options: WriteTableOptions): Promise; + writeTable(pool: any, name: NamedObjectInfo, options: WriteTableOptions): Promise; analyseSingleObject( pool: any, name: NamedObjectInfo, @@ -116,6 +128,8 @@ export interface EngineDriver { getNewObjectTemplates(): NewObjectTemplate[]; // direct call of pool method, only some methods could be supported, on only some drivers callMethod(pool, method, args); + serverSummary(pool): Promise; + summaryCommand(pool, command, row): Promise; analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index 5b00eeae..c3b415b2 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -104,7 +104,7 @@ import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte'; import { closeMultipleTabs } from '../widgets/TabsPanel.svelte'; import AboutModal from '../modals/AboutModal.svelte'; -import { tick } from 'svelte'; + import { tick } from 'svelte'; export let data; export let passProps; @@ -195,6 +195,16 @@ import { tick } from 'svelte'; }), }); }; + const handleServerSummary = () => { + openNewTab({ + title: getConnectionLabel(data), + icon: 'img server', + tabComponent: 'ServerSummaryTab', + props: { + conid: data._id, + }, + }); + }; const handleNewQuery = () => { const tooltip = `${getConnectionLabel(data)}`; openNewTab({ @@ -244,6 +254,11 @@ import { tick } from 'svelte'; text: 'Create database', onClick: handleCreateDatabase, }, + $openedConnections.includes(data._id) && + driver?.supportsServerSummary && { + text: 'Server summary', + onClick: handleServerSummary, + }, ], data.singleDatabase && [ { divider: true }, diff --git a/packages/web/src/elements/ObjectListControl.svelte b/packages/web/src/elements/ObjectListControl.svelte index f3b1770b..fe8022dc 100644 --- a/packages/web/src/elements/ObjectListControl.svelte +++ b/packages/web/src/elements/ObjectListControl.svelte @@ -1,8 +1,7 @@ @@ -31,43 +31,43 @@
- - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +
diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte index 1542b72c..c06d4c8b 100644 --- a/packages/web/src/elements/TableControl.svelte +++ b/packages/web/src/elements/TableControl.svelte @@ -86,17 +86,17 @@ {:else if col.formatter} {col.formatter(row)} {:else if col.slot != null} - {#if col.slot == -1} - {:else if col.slot == 0} - {:else if col.slot == 1} - {:else if col.slot == 2} - {:else if col.slot == 3} - {:else if col.slot == 4} - {:else if col.slot == 5} - {:else if col.slot == 6} - {:else if col.slot == 7} - {:else if col.slot == 8} - {:else if col.slot == 9} + {#if col.slot == -1} + {:else if col.slot == 0} + {:else if col.slot == 1} + {:else if col.slot == 2} + {:else if col.slot == 3} + {:else if col.slot == 4} + {:else if col.slot == 5} + {:else if col.slot == 6} + {:else if col.slot == 7} + {:else if col.slot == 8} + {:else if col.slot == 9} {/if} {:else} {row[col.fieldName] || ''} diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte new file mode 100644 index 00000000..e859b12d --- /dev/null +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -0,0 +1,103 @@ + + + + + + {#await apiCall('server-connections/server-summary', { conid, refreshToken })} + + {:then summary} +
+ ({ + ...col, + slot: col.columnType == 'bytes' ? 1 : col.columnType == 'actions' ? 2 : null, + }))} + > + {formatFileSize(row?.[col.fieldName])} + + {#each col.actions as action, index} + {#if index > 0} + | + {/if} + runAction(action, row)}>{action.header} + {/each} + + +
+ {/await} + + + + +
+ + diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index 0cc22df2..db9ab503 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -26,6 +26,7 @@ import * as QueryDataTab from './QueryDataTab.svelte'; import * as ConnectionTab from './ConnectionTab.svelte'; import * as MapTab from './MapTab.svelte'; import * as PerspectiveTab from './PerspectiveTab.svelte'; +import * as ServerSummaryTab from './ServerSummaryTab.svelte'; export default { TableDataTab, @@ -56,4 +57,5 @@ export default { ConnectionTab, MapTab, PerspectiveTab, + ServerSummaryTab, }; diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index a1041287..fbc2f6de 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -351,6 +351,86 @@ const driver = { return res; }, + + async summaryCommand(pool, command, row) { + switch (command) { + case 'profileOff': + await pool.db(row.name).command({ profile: 0 }); + return; + case 'profileFiltered': + await pool.db(row.name).command({ profile: 1, slowms: 100 }); + return; + case 'profileAll': + await pool.db(row.name).command({ profile: 2 }); + return; + } + }, + + async serverSummary(pool) { + const res = await pool.__getDatabase().admin().listDatabases(); + const profiling = await Promise.all(res.databases.map((x) => pool.db(x.name).command({ profile: -1 }))); + + function formatProfiling(info) { + switch (info.was) { + case 0: + return 'No profiling'; + case 1: + return `Filtered (>${info.slowms} ms)`; + case 2: + return 'Profile all'; + default: + return '???'; + } + } + + return { + columns: [ + { + fieldName: 'name', + columnType: 'string', + header: 'Name', + }, + { + fieldName: 'sizeOnDisk', + columnType: 'bytes', + header: 'Size', + }, + { + fieldName: 'profiling', + columnType: 'string', + header: 'Profiling', + }, + { + fieldName: 'setProfile', + columnType: 'actions', + header: 'Profiling actions', + actions: [ + { + header: 'Off', + command: 'profileOff', + }, + { + header: 'Filtered', + command: 'profileFiltered', + }, + { + header: 'All', + command: 'profileAll', + }, + { + header: 'View', + openQuery: "db['system.profile'].find()", + tabTitle: 'Profile data', + }, + ], + }, + ], + databases: res.databases.map((db, i) => ({ + ...db, + profiling: formatProfiling(profiling[i]), + })), + }; + }, }; module.exports = driver; diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index e2cc3701..ca50a073 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -32,6 +32,7 @@ const driver = { editorMode: 'javascript', defaultPort: 27017, supportsDatabaseUrl: true, + supportsServerSummary: true, databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname', getQuerySplitterOptions: () => mongoSplitterOptions,