From a6b6b5eb701bf5ed999d8700b4330fa4f7c197d5 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 12 Aug 2025 22:19:16 +0200 Subject: [PATCH 01/28] feat: add typing for listVariables, listProccess, killProccess, update server summary typings --- packages/types/engines.d.ts | 124 +++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 4d8fd53b5..a1a864c99 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -99,6 +99,25 @@ export interface SupportedDbKeyType { showItemList?: boolean; } +export type DatabaseProcess = { + processId: number; + connectionId: number; + client: string; + operation?: string; + namespace?: string; + command?: any; + runningTime: number; + state?: any; + waitingFor?: boolean; + locks?: any; + progress?: any; +}; + +export type DatabaseVariable = { + variable: string; + value: any; +}; + export interface SqlBackupDumper { run(); } @@ -110,7 +129,8 @@ export interface SummaryColumn { } export interface ServerSummaryDatabase {} export interface ServerSummary { - columns: SummaryColumn[]; + processes: DatabaseProcess[]; + variables: DatabaseVariable[]; databases: ServerSummaryDatabase[]; } @@ -161,12 +181,12 @@ export interface FilterBehaviourProvider { getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour; } -export interface DatabaseHandle { +export interface DatabaseHandle { client: TClient; database?: string; conid?: string; feedback?: (message: any) => void; - getDatabase?: () => any; + getDatabase?: () => TDataBase; connectionType?: string; treeKeySeparator?: string; } @@ -196,7 +216,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase { inputFile: string; } -export interface EngineDriver extends FilterBehaviourProvider { +export interface EngineDriver extends FilterBehaviourProvider { engine: string; title: string; defaultPort?: number; @@ -242,61 +262,86 @@ export interface EngineDriver extends FilterBehaviourProvider { defaultSocketPath?: string; authTypeLabel?: string; importExportArgs?: any[]; - connect({ server, port, user, password, database, connectionDefinition }): Promise>; - close(dbhan: DatabaseHandle): Promise; - query(dbhan: DatabaseHandle, sql: string, options?: QueryOptions): Promise; - stream(dbhan: DatabaseHandle, sql: string, options: StreamOptions); - readQuery(dbhan: DatabaseHandle, sql: string, structure?: TableInfo): Promise; - readJsonQuery(dbhan: DatabaseHandle, query: any, structure?: TableInfo): Promise; + connect({ + server, + port, + user, + password, + database, + connectionDefinition, + }): Promise>; + close(dbhan: DatabaseHandle): Promise; + query(dbhan: DatabaseHandle, sql: string, options?: QueryOptions): Promise; + stream(dbhan: DatabaseHandle, sql: string, options: StreamOptions); + readQuery(dbhan: DatabaseHandle, sql: string, structure?: TableInfo): Promise; + readJsonQuery(dbhan: DatabaseHandle, query: any, structure?: TableInfo): Promise; // eg. PostgreSQL COPY FROM stdin - writeQueryFromStream(dbhan: DatabaseHandle, sql: string): Promise; - writeTable(dbhan: DatabaseHandle, name: NamedObjectInfo, options: WriteTableOptions): Promise; + writeQueryFromStream(dbhan: DatabaseHandle, sql: string): Promise; + writeTable( + dbhan: DatabaseHandle, + name: NamedObjectInfo, + options: WriteTableOptions + ): Promise; analyseSingleObject( - dbhan: DatabaseHandle, + dbhan: DatabaseHandle, name: NamedObjectInfo, objectTypeField: keyof DatabaseInfo ): Promise; - analyseSingleTable(dbhan: DatabaseHandle, name: NamedObjectInfo): Promise; - getVersion(dbhan: DatabaseHandle): Promise<{ version: string; versionText?: string }>; - listDatabases(dbhan: DatabaseHandle): Promise< + analyseSingleTable(dbhan: DatabaseHandle, name: NamedObjectInfo): Promise; + getVersion(dbhan: DatabaseHandle): Promise<{ version: string; versionText?: string }>; + listDatabases(dbhan: DatabaseHandle): Promise< { name: string; }[] >; - loadKeys(dbhan: DatabaseHandle, root: string, filter?: string): Promise; - scanKeys(dbhan: DatabaseHandle, root: string, pattern: string, cursor: string, count: number): Promise; - exportKeys(dbhan: DatabaseHandle, options: {}): Promise; - loadKeyInfo(dbhan: DatabaseHandle, key): Promise; - loadKeyTableRange(dbhan: DatabaseHandle, key, cursor, count): Promise; + loadKeys(dbhan: DatabaseHandle, root: string, filter?: string): Promise; + scanKeys( + dbhan: DatabaseHandle, + root: string, + pattern: string, + cursor: string, + count: number + ): Promise; + exportKeys(dbhan: DatabaseHandle, options: {}): Promise; + loadKeyInfo(dbhan: DatabaseHandle, key): Promise; + loadKeyTableRange(dbhan: DatabaseHandle, key, cursor, count): Promise; loadFieldValues( - dbhan: DatabaseHandle, + dbhan: DatabaseHandle, name: NamedObjectInfo, field: string, search: string, dataType: string ): Promise; - analyseFull(dbhan: DatabaseHandle, serverVersion): Promise; - analyseIncremental(dbhan: DatabaseHandle, structure: DatabaseInfo, serverVersion): Promise; + analyseFull(dbhan: DatabaseHandle, serverVersion): Promise; + analyseIncremental( + dbhan: DatabaseHandle, + structure: DatabaseInfo, + serverVersion + ): Promise; dialect: SqlDialect; dialectByVersion(version): SqlDialect; createDumper(options = null): SqlDumper; - createBackupDumper(dbhan: DatabaseHandle, options): Promise; + createBackupDumper(dbhan: DatabaseHandle, options): Promise; getAuthTypes(): EngineAuthType[]; - readCollection(dbhan: DatabaseHandle, options: ReadCollectionOptions): Promise; - updateCollection(dbhan: DatabaseHandle, changeSet: any): Promise; + readCollection(dbhan: DatabaseHandle, options: ReadCollectionOptions): Promise; + updateCollection(dbhan: DatabaseHandle, changeSet: any): Promise; getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string; - createDatabase(dbhan: DatabaseHandle, name: string): Promise; - dropDatabase(dbhan: DatabaseHandle, name: string): Promise; + createDatabase(dbhan: DatabaseHandle, name: string): Promise; + dropDatabase(dbhan: DatabaseHandle, name: string): Promise; getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any; - script(dbhan: DatabaseHandle, sql: string, options?: RunScriptOptions): Promise; - operation(dbhan: DatabaseHandle, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise; + script(dbhan: DatabaseHandle, sql: string, options?: RunScriptOptions): Promise; + operation( + dbhan: DatabaseHandle, + operation: CollectionOperationInfo, + options?: RunScriptOptions + ): Promise; getNewObjectTemplates(): NewObjectTemplate[]; // direct call of dbhan.client method, only some methods could be supported, on only some drivers - callMethod(dbhan: DatabaseHandle, method, args); - serverSummary(dbhan: DatabaseHandle): Promise; - summaryCommand(dbhan: DatabaseHandle, command, row): Promise; - startProfiler(dbhan: DatabaseHandle, options): Promise; - stopProfiler(dbhan: DatabaseHandle, profiler): Promise; + callMethod(dbhan: DatabaseHandle, method, args); + serverSummary(dbhan: DatabaseHandle): Promise; + summaryCommand(dbhan: DatabaseHandle, command, row): Promise; + startProfiler(dbhan: DatabaseHandle, options): Promise; + stopProfiler(dbhan: DatabaseHandle, profiler): Promise; getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>; getAuthTokenFromCode(connection, options): Promise; getAccessTokenFromAuth(connection, req): Promise; @@ -313,7 +358,10 @@ export interface EngineDriver extends FilterBehaviourProvider { adaptTableInfo(table: TableInfo): TableInfo; // simple data type adapter adaptDataType(dataType: string): string; - listSchemas(dbhan: DatabaseHandle): Promise; + listSchemas(dbhan: DatabaseHandle): Promise; + listProcesses(dbhan: DatabaseHandle): Promise; + listVariables(dbhan: DatabaseHandle): Promise; + killProcess(dbhan: DatabaseHandle, pid: number): Promise; backupDatabaseCommand( connection: any, settings: BackupDatabaseSettings, @@ -337,7 +385,7 @@ export interface EngineDriver extends FilterBehaviourProvider { analyserClass?: any; dumperClass?: any; singleConnectionOnly?: boolean; - getLogDbInfo(dbhan: DatabaseHandle): { + getLogDbInfo(dbhan: DatabaseHandle): { database?: string; engine: string; conid?: string; From a293eeb3984ff566e182b5ca08ff9deab132d4c5 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 12 Aug 2025 22:19:28 +0200 Subject: [PATCH 02/28] feat: new mongo server summary --- .../dbgate-plugin-mongo/src/backend/driver.js | 153 +++++++++--------- 1 file changed, 77 insertions(+), 76 deletions(-) diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index d275f5611..c886d5c18 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -6,7 +6,7 @@ const Analyser = require('./Analyser'); const isPromise = require('is-promise'); const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb'); const { EJSON } = require('bson'); -const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse } = require('dbgate-tools'); +const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse, getLogger } = require('dbgate-tools'); const createBulkInsertStream = require('./createBulkInsertStream'); const { convertToMongoCondition, @@ -16,6 +16,8 @@ const { let isProApp; +const logger = getLogger('mongoDriver'); + function serializeMongoData(row) { return EJSON.serialize( serializeJsTypesForJsonStringify(row, (value) => { @@ -80,7 +82,7 @@ async function getScriptableDb(dbhan) { // } // } -/** @type {import('dbgate-types').EngineDriver} */ +/** @type {import('dbgate-types').EngineDriver} */ const driver = { ...driverBase, analyserClass: Analyser, @@ -193,7 +195,10 @@ const driver = { let exprValue; try { - const serviceProvider = new NodeDriverServiceProvider(dbhan.client, new EventEmitter(), { productDocsLink: '', productName: 'DbGate' }); + const serviceProvider = new NodeDriverServiceProvider(dbhan.client, new EventEmitter(), { + productDocsLink: '', + productName: 'DbGate', + }); const runtime = new ElectronRuntime(serviceProvider); await runtime.evaluate(`use ${dbhan.database}`); exprValue = await runtime.evaluate(sql); @@ -629,86 +634,82 @@ const driver = { }, async serverSummary(dbhan) { - const res = await dbhan.getDatabase().admin().listDatabases(); - const profiling = await Promise.all(res.databases.map((x) => dbhan.client.db(x.name).command({ profile: -1 }))); + const [processes, variables, databases] = await Promise.all([ + this.listProcesses(dbhan), + this.listVariables(dbhan), + this.listDatabases(dbhan), + ]); - 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', - // }, - { - header: 'View', - openTab: { - title: 'system.profile', - icon: 'img collection', - tabComponent: 'CollectionDataTab', - props: { - pureName: 'system.profile', - }, - }, - addDbProps: true, - }, - ], - }, - ], - databases: res.databases.map((db, i) => ({ - ...db, - profiling: formatProfiling(profiling[i]), - })), + const data = { + processes, + variables, + databases, }; + + return data; }, async close(dbhan) { return dbhan.client.close(); }, + + async listProcesses(dbhan) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const currentOp = await adminDb.command({ + currentOp: { + $all: true, + active: true, + idle: true, + system: true, + killPending: true, + }, + }); + + const processes = currentOp.inprog.map((op) => ({ + processId: op.opid, + connectionId: op.connectionId, + client: op.client, + operation: op.op, + namespace: op.ns, + command: op.command, + runningTime: op.secs_running, + state: op.state, + waitingFor: op.waitingForLock, + locks: op.locks, + progress: op.progress, + })); + + return processes; + }, + + async listVariables(dbhan) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const variables = await adminDb + .command({ getParameter: '*' }) + .then((params) => + Object.entries(params).map(([key, value]) => ({ variable: key, value: value?.value || value })) + ); + + return variables; + }, + + async killProcess(dbhan, processId) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const result = await adminDb.command({ + killOp: 1, + op: processId, + }); + + logger.info(`Killed process with ID ${processId}`, result); + + return result; + }, }; driver.initialize = (dbgateEnv) => { From 5dd62ad2aadbf93ff9472b1e10e59e54f85496c4 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 12 Aug 2025 23:45:41 +0200 Subject: [PATCH 03/28] feat: new server summary tab --- packages/web/src/buttons/CtaButton.svelte | 46 +++++++++++ .../web/src/jsontree/JSONArrayNode.svelte | 8 +- .../src/jsontree/JSONIterableArrayNode.svelte | 8 +- .../src/jsontree/JSONIterableMapNode.svelte | 6 +- .../web/src/jsontree/JSONMapEntryNode.svelte | 8 +- packages/web/src/jsontree/JSONNested.svelte | 14 +++- packages/web/src/jsontree/JSONNode.svelte | 2 + .../web/src/jsontree/JSONObjectNode.svelte | 2 + packages/web/src/jsontree/JSONTree.svelte | 3 +- .../web/src/jsontree/JSONValueNode.svelte | 24 ++++-- packages/web/src/stores.ts | 2 + packages/web/src/tabs/ServerSummaryTab.svelte | 53 +++++++------ .../web/src/tabs/TableStructureTab.svelte | 2 +- .../web/src/widgets/SummaryDatabases.svelte | 77 +++++++++++++++++++ .../web/src/widgets/SummaryProcesses.svelte | 77 +++++++++++++++++++ .../web/src/widgets/SummaryVariables.svelte | 29 +++++++ 16 files changed, 321 insertions(+), 40 deletions(-) create mode 100644 packages/web/src/buttons/CtaButton.svelte create mode 100644 packages/web/src/widgets/SummaryDatabases.svelte create mode 100644 packages/web/src/widgets/SummaryProcesses.svelte create mode 100644 packages/web/src/widgets/SummaryVariables.svelte diff --git a/packages/web/src/buttons/CtaButton.svelte b/packages/web/src/buttons/CtaButton.svelte new file mode 100644 index 000000000..2b77f7ca8 --- /dev/null +++ b/packages/web/src/buttons/CtaButton.svelte @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/web/src/jsontree/JSONArrayNode.svelte b/packages/web/src/jsontree/JSONArrayNode.svelte index 1f57ac377..205ff32f8 100644 --- a/packages/web/src/jsontree/JSONArrayNode.svelte +++ b/packages/web/src/jsontree/JSONArrayNode.svelte @@ -3,6 +3,8 @@ export let key, value, isParentExpanded, isParentArray; export let expanded = false; + export let labelOverride = null; + export let hideKey = false; const filteredKey = new Set(['length']); $: keys = Object.getOwnPropertyNames(value); @@ -22,8 +24,10 @@ {keys} {previewKeys} {getValue} - label="Array({value.length})" + label={labelOverride || `Array(${value.length})`} bracketOpen="[" bracketClose="]" elementValue={value} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONIterableArrayNode.svelte b/packages/web/src/jsontree/JSONIterableArrayNode.svelte index 927455bce..75bc356ea 100644 --- a/packages/web/src/jsontree/JSONIterableArrayNode.svelte +++ b/packages/web/src/jsontree/JSONIterableArrayNode.svelte @@ -2,6 +2,8 @@ import JSONNested from './JSONNested.svelte'; export let key, value, isParentExpanded, isParentArray, nodeType; + export let labelOverride = null; + export let hideKey = false; let keys = []; @@ -29,7 +31,9 @@ {getKey} {getValue} isArray={true} - label="{nodeType}({keys.length})" + label={labelOverride || `${nodeType}(${keys.length})`} bracketOpen={'{'} bracketClose={'}'} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONIterableMapNode.svelte b/packages/web/src/jsontree/JSONIterableMapNode.svelte index 907ee0326..8c0aad697 100644 --- a/packages/web/src/jsontree/JSONIterableMapNode.svelte +++ b/packages/web/src/jsontree/JSONIterableMapNode.svelte @@ -3,6 +3,8 @@ import MapEntry from './utils/MapEntry' export let key, value, isParentExpanded, isParentArray, nodeType; + export let labelOverride = null; + export let hideKey = false; let keys = []; @@ -28,8 +30,10 @@ {keys} {getKey} {getValue} - label="{nodeType}({keys.length})" + label={labelOverride || `${nodeType}(${keys.length})`} colon="" bracketOpen={'{'} bracketClose={'}'} + {labelOverride} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONMapEntryNode.svelte b/packages/web/src/jsontree/JSONMapEntryNode.svelte index cccd283fa..1f012e10b 100644 --- a/packages/web/src/jsontree/JSONMapEntryNode.svelte +++ b/packages/web/src/jsontree/JSONMapEntryNode.svelte @@ -3,6 +3,8 @@ export let key, value, isParentExpanded, isParentArray; export let expanded = false; + export let hideKey = false; + export let labelOverride = null; const keys = ['key', 'value']; @@ -17,7 +19,9 @@ key={isParentExpanded ? String(key) : value.key} {keys} {getValue} - label={isParentExpanded ? 'Entry ' : '=> '} + label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')} bracketOpen={'{'} bracketClose={'}'} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONNested.svelte b/packages/web/src/jsontree/JSONNested.svelte index 2c0a71bec..0efa828ac 100644 --- a/packages/web/src/jsontree/JSONNested.svelte +++ b/packages/web/src/jsontree/JSONNested.svelte @@ -21,11 +21,14 @@ expandable = true; export let elementValue = null; export let onRootExpandedChanged = null; + export let labelOverride = null; + export let hideKey = false; const context = getContext('json-tree-context-key'); setContext('json-tree-context-key', { ...context, colon }); const elementData = getContext('json-tree-element-data'); const slicedKeyCount = getContext('json-tree-sliced-key-count'); + const keyLabel = labelOverride ?? key; $: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5); @@ -56,7 +59,16 @@ {#if expandable && isParentExpanded} {/if} - + {#if !hideKey} + + {/if} {label}{bracketOpen} {#if isParentExpanded} diff --git a/packages/web/src/jsontree/JSONNode.svelte b/packages/web/src/jsontree/JSONNode.svelte index bf526e1ca..a7b179c16 100644 --- a/packages/web/src/jsontree/JSONNode.svelte +++ b/packages/web/src/jsontree/JSONNode.svelte @@ -16,6 +16,7 @@ export let expanded = !!getContext('json-tree-default-expanded'); export let labelOverride = null; export let onRootExpandedChanged = null; + export let hideKey = false; $: nodeType = objType(value); $: componentType = getComponent(nodeType); @@ -85,4 +86,5 @@ {expanded} {labelOverride} {onRootExpandedChanged} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONObjectNode.svelte b/packages/web/src/jsontree/JSONObjectNode.svelte index 71f0dd536..33320a8ff 100644 --- a/packages/web/src/jsontree/JSONObjectNode.svelte +++ b/packages/web/src/jsontree/JSONObjectNode.svelte @@ -5,6 +5,7 @@ export let expanded = false; export let labelOverride = null; export let onRootExpandedChanged = null; + export let hideKey = false; $: keys = Object.getOwnPropertyNames(value); @@ -26,4 +27,5 @@ bracketClose={'}'} elementValue={value} {onRootExpandedChanged} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONTree.svelte b/packages/web/src/jsontree/JSONTree.svelte index fddfda32e..311e54c6b 100644 --- a/packages/web/src/jsontree/JSONTree.svelte +++ b/packages/web/src/jsontree/JSONTree.svelte @@ -2,7 +2,6 @@ import JSONNode from './JSONNode.svelte'; import { setContext } from 'svelte'; import contextMenu, { getContextMenu } from '../utility/contextMenu'; - import openNewTab from '../utility/openNewTab'; import _ from 'lodash'; import { copyTextToClipboard } from '../utility/clipboard'; import { openJsonLinesData } from '../utility/openJsonLinesData'; @@ -23,6 +22,7 @@ export let isDeleted = false; export let isInserted = false; export let isModified = false; + export let hideKey = false; const settings = useSettings(); $: wrap = $settings?.['behaviour.jsonPreviewWrap']; @@ -73,6 +73,7 @@ class:wrap > + +
  • + + + {valueGetter ? valueGetter(value) : value} + +
  • + -
  • - - - {valueGetter ? valueGetter(value) : value} - -
  • \ No newline at end of file + diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index badfa1ce7..34188e2cc 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -215,6 +215,8 @@ export const connectionAppObjectSearchSettings = writableWithStorage( 'connectionAppObjectSearchSettings2' ); +export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab'); + let currentThemeValue = null; currentTheme.subscribe(value => { currentThemeValue = value; diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index 0686a5edc..3043ac358 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -18,15 +18,17 @@ import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte'; import ToolStripContainer from '../buttons/ToolStripContainer.svelte'; import registerCommand from '../commands/registerCommand'; - import Link from '../elements/Link.svelte'; import LoadingInfo from '../elements/LoadingInfo.svelte'; + import TabControl from '../elements/TabControl.svelte'; - import ObjectListControl from '../elements/ObjectListControl.svelte'; import { _t } from '../translations'; import { apiCall } from '../utility/api'; import createActivator, { getActiveComponent } from '../utility/createActivator'; - import formatFileSize from '../utility/formatFileSize'; import openNewTab from '../utility/openNewTab'; + import SummaryVariables from '../widgets/SummaryVariables.svelte'; + import SummaryProcesses from '../widgets/SummaryProcesses.svelte'; + import SummaryDatabases from '../widgets/SummaryDatabases.svelte'; + import { serverSummarySelectedTab } from '../stores'; export let conid; @@ -78,26 +80,31 @@ {: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} - - + serverSummarySelectedTab.set(index)} + tabs={[ + { + label: 'Variables', + component: SummaryVariables, + props: { variables: summary.variables || [] }, + }, + { + label: 'Processes', + component: SummaryProcesses, + props: { processes: summary.processes || [], conid }, + }, + { + label: 'Databases', + component: SummaryDatabases, + props: { databases: summary.databases || [] }, + }, + ]} + />
    {/await} diff --git a/packages/web/src/tabs/TableStructureTab.svelte b/packages/web/src/tabs/TableStructureTab.svelte index 1e8ab9efb..f5dfca571 100644 --- a/packages/web/src/tabs/TableStructureTab.svelte +++ b/packages/web/src/tabs/TableStructureTab.svelte @@ -197,7 +197,7 @@ defaultActionId: 'openTable', }, }); - }}>DataDataX + import TableControl from '../elements/TableControl.svelte'; + import CtaButton from '../buttons/CtaButton.svelte'; + import { _t } from '../translations'; + import formatFileSize from '../utility/formatFileSize'; + + export let databases: any[] = []; + + async function profileOff(database: any) { + // TODO: Implement profile off functionality + console.log('Profile off:', database.name); + } + + async function profileFiltered(database: any) { + // TODO: Implement profile filtered functionality + console.log('Profile filtered:', database.name); + } + + async function profileAll(database: any) { + // TODO: Implement profile all functionality + console.log('Profile all:', database.name); + } + + +
    + + + profileOff(row)}>Profile Off + | + profileFiltered(row)}>Profile Filtered + | + profileAll(row)}>Profile All + + + + {row.name} + + + + {formatFileSize(row.sizeOnDisk)} + + + + {formatFileSize(row.dataSize)} + + + + {formatFileSize(row.indexSize)} + + +
    + + diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte new file mode 100644 index 000000000..c9e765bd3 --- /dev/null +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -0,0 +1,77 @@ + + +
    + + + killProcess(row.processId)}> + {_t('common.kill', { defaultMessage: 'Kill' })} + + + + + {row.processId} + + + + {formatRunningTime(row.runningTime)} + + + + {row.waitingFor ? 'Yes' : 'No'} + + +
    + + diff --git a/packages/web/src/widgets/SummaryVariables.svelte b/packages/web/src/widgets/SummaryVariables.svelte new file mode 100644 index 000000000..7a37b6a34 --- /dev/null +++ b/packages/web/src/widgets/SummaryVariables.svelte @@ -0,0 +1,29 @@ + + +
    + + + + + +
    + + From 9d456992cf5af8007473b6d1bcd13bc276a804f4 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 13 Aug 2025 04:54:15 +0200 Subject: [PATCH 04/28] feat: kill db process --- packages/api/src/controllers/serverConnections.js | 14 ++++++++++++++ packages/api/src/proc/serverConnectionProcess.js | 13 +++++++++++++ packages/web/src/widgets/SummaryProcesses.svelte | 12 +++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 86e461feb..cdc4bcb76 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -274,6 +274,20 @@ module.exports = { return this.loadDataCore('serverSummary', { conid }); }, + killDatabaseProcess_meta: true, + async killDatabaseProcess(ctx, req) { + const { conid, pid } = ctx; + testConnectionPermission(conid, req); + + const opened = await this.ensureOpened(conid); + if (!opened) { + return null; + } + if (opened.connection.isReadOnly) return false; + + return this.sendRequest(opened, { msgtype: 'killDatabaseProcess', pid }); + }, + summaryCommand_meta: true, async summaryCommand({ conid, command, row }, req) { testConnectionPermission(conid, req); diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index d58aa18ed..99435f274 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -146,6 +146,18 @@ async function handleServerSummary({ msgid }) { return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan)); } +async function handleKillDatabaseProccess({ msgid, pid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + try { + const result = await driver.killProcess(dbhan, Number(pid)); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + async function handleSummaryCommand({ msgid, command, row }) { return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row)); } @@ -154,6 +166,7 @@ const messageHandlers = { connect: handleConnect, ping: handlePing, serverSummary: handleServerSummary, + killDatabaseProcess: handleKillDatabaseProccess, summaryCommand: handleSummaryCommand, createDatabase: props => handleDatabaseOp('createDatabase', props), dropDatabase: props => handleDatabaseOp('dropDatabase', props), diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index c9e765bd3..b1b19bc85 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -3,12 +3,22 @@ import TableControl from '../elements/TableControl.svelte'; import { _t } from '../translations'; import CtaButton from '../buttons/CtaButton.svelte'; + import { apiCall } from '../utility/api'; + import runCommand from '../commands/runCommand'; + export let conid; export let processes: DatabaseProcess[] = []; - async function killProcess(processId: string) { + async function killProcess(processId: number) { // TODO: Implement kill process functionality console.log('Kill process:', processId); + + await apiCall('server-connections/kill-database-process', { + pid: processId, + conid, + }); + + runCommand('serverSummary.refresh'); } function formatRunningTime(seconds: number): string { From 61f1c99791b007865750f86e1f854742b2ad376c Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 16:39:54 +0200 Subject: [PATCH 05/28] feat: send cols from list databases --- packages/types/engines.d.ts | 16 ++++- packages/web/src/tabs/ServerSummaryTab.svelte | 2 +- .../web/src/widgets/SummaryDatabases.svelte | 70 ++----------------- .../dbgate-plugin-mongo/src/backend/driver.js | 9 ++- 4 files changed, 30 insertions(+), 67 deletions(-) diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index a1a864c99..4a848af9c 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -127,11 +127,21 @@ export interface SummaryColumn { header: string; dataType: 'string' | 'number' | 'bytes'; } -export interface ServerSummaryDatabase {} +export interface ServerSummaryDatabases { + rows: any[]; + columns: DatabaseColumn[]; +} + +export type DatabaseColumn = { + header: string; + fieldName: string; + type: 'data' | 'fileSize'; +}; + export interface ServerSummary { processes: DatabaseProcess[]; variables: DatabaseVariable[]; - databases: ServerSummaryDatabase[]; + databases: ServerSummaryDatabases; } export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'; @@ -292,6 +302,8 @@ export interface EngineDriver extends FilterBeha listDatabases(dbhan: DatabaseHandle): Promise< { name: string; + sizeOnDisk?: number; + empty?: boolean; }[] >; loadKeys(dbhan: DatabaseHandle, root: string, filter?: string): Promise; diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index 3043ac358..6d828d7ad 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -101,7 +101,7 @@ { label: 'Databases', component: SummaryDatabases, - props: { databases: summary.databases || [] }, + props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] }, }, ]} /> diff --git a/packages/web/src/widgets/SummaryDatabases.svelte b/packages/web/src/widgets/SummaryDatabases.svelte index cd576fe1e..9fe06bcb8 100644 --- a/packages/web/src/widgets/SummaryDatabases.svelte +++ b/packages/web/src/widgets/SummaryDatabases.svelte @@ -1,77 +1,21 @@
    - - - profileOff(row)}>Profile Off - | - profileFiltered(row)}>Profile Filtered - | - profileAll(row)}>Profile All - - - - {row.name} - - - - {formatFileSize(row.sizeOnDisk)} - - - - {formatFileSize(row.dataSize)} - - - - {formatFileSize(row.indexSize)} - - +
    diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index c886d5c18..9be451ddf 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -643,7 +643,14 @@ const driver = { const data = { processes, variables, - databases, + databases: { + rows: databases, + columns: [ + { header: 'Name', fieldName: 'name' }, + { header: 'Size on disk', fieldName: 'sizeOnDisk' }, + { header: 'Empty', fieldName: 'empty' }, + ], + }, }; return data; From b1696ed1cd957de050c65996f94f33435725dfe1 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 16:40:20 +0200 Subject: [PATCH 06/28] feat: autorefresh processes, refresh processes w/o displaying loader --- .../api/src/controllers/serverConnections.js | 14 +++++++++++ .../api/src/proc/serverConnectionProcess.js | 13 +++++++++++ .../web/src/widgets/SummaryProcesses.svelte | 23 ++++++++++++++----- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index cdc4bcb76..b486d25e9 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -274,6 +274,20 @@ module.exports = { return this.loadDataCore('serverSummary', { conid }); }, + listDatabaseProcesses_meta: true, + async listDatabaseProcesses(ctx, req) { + const { conid } = ctx; + testConnectionPermission(conid, req); + + const opened = await this.ensureOpened(conid); + if (!opened) { + return null; + } + if (opened.connection.isReadOnly) return false; + + return this.sendRequest(opened, { msgtype: 'listDatabaseProcesses' }); + }, + killDatabaseProcess_meta: true, async killDatabaseProcess(ctx, req) { const { conid, pid } = ctx; diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index 99435f274..5540dbbf5 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -158,6 +158,18 @@ async function handleKillDatabaseProccess({ msgid, pid }) { } } +async function handleListDatabaseProcesses({ msgid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + try { + const result = await driver.listProcesses(dbhan); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + async function handleSummaryCommand({ msgid, command, row }) { return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row)); } @@ -167,6 +179,7 @@ const messageHandlers = { ping: handlePing, serverSummary: handleServerSummary, killDatabaseProcess: handleKillDatabaseProccess, + listDatabaseProcesses: handleListDatabaseProcesses, summaryCommand: handleSummaryCommand, createDatabase: props => handleDatabaseOp('createDatabase', props), dropDatabase: props => handleDatabaseOp('dropDatabase', props), diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index b1b19bc85..e049bd1bd 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -4,21 +4,26 @@ import { _t } from '../translations'; import CtaButton from '../buttons/CtaButton.svelte'; import { apiCall } from '../utility/api'; - import runCommand from '../commands/runCommand'; + import { onMount } from 'svelte'; export let conid; export let processes: DatabaseProcess[] = []; + export let refreshInterval: number = 1000; + + let internalProcesses = [...processes]; + + async function refreshProcesses() { + const data = await apiCall('server-connections/list-database-processes', { conid }); + internalProcesses = data.result; + } async function killProcess(processId: number) { - // TODO: Implement kill process functionality - console.log('Kill process:', processId); - await apiCall('server-connections/kill-database-process', { pid: processId, conid, }); - runCommand('serverSummary.refresh'); + refreshProcesses(); } function formatRunningTime(seconds: number): string { @@ -27,11 +32,17 @@ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; } + + onMount(() => { + const intervalId = setInterval(() => refreshProcesses(), refreshInterval); + + return () => clearInterval(intervalId); + });
    Date: Thu, 14 Aug 2025 18:46:19 +0200 Subject: [PATCH 07/28] feat: use _t translations in server summary --- packages/web/src/tabs/ServerSummaryTab.svelte | 13 ++++++---- .../web/src/widgets/SummaryProcesses.svelte | 26 ++++++++++++------- .../web/src/widgets/SummaryVariables.svelte | 5 ++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index 6d828d7ad..b16058fd1 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -77,7 +77,10 @@ {#await apiCall('server-connections/server-summary', { conid, refreshToken })} - + {:then summary}
    serverSummarySelectedTab.set(index)} + onUserChange={index => serverSummarySelectedTab.set(index)} tabs={[ { - label: 'Variables', + label: _t('serverSummaryTab.variables', { defaultMessage: 'Variables' }), component: SummaryVariables, props: { variables: summary.variables || [] }, }, { - label: 'Processes', + label: _t('serverSummaryTab.processes', { defaultMessage: 'Processes' }), component: SummaryProcesses, props: { processes: summary.processes || [], conid }, }, { - label: 'Databases', + label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }), component: SummaryDatabases, props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] }, }, diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index e049bd1bd..eda44885e 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -44,16 +44,24 @@ import TableControl from '../elements/TableControl.svelte'; import JSONTree from '../jsontree/JSONTree.svelte'; + import { _t } from '../translations'; export let variables: { variable: string; value: any }[] = []; @@ -8,9 +9,9 @@ Date: Thu, 14 Aug 2025 19:18:14 +0200 Subject: [PATCH 08/28] feat: pass tabVisible to tabs --- packages/web/src/elements/TabControl.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web/src/elements/TabControl.svelte b/packages/web/src/elements/TabControl.svelte index 4c7ee15aa..cf7b8cd93 100644 --- a/packages/web/src/elements/TabControl.svelte +++ b/packages/web/src/elements/TabControl.svelte @@ -53,7 +53,12 @@
    {#each _.compact(tabs) as tab, index}
    - + {#if tab.slot != null} {#if tab.slot == 0} {:else if tab.slot == 1} From 164a112e0cad055b3e2dacfd45861767949e11cb Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 19:18:33 +0200 Subject: [PATCH 09/28] feat: refresh processes only if processes tab is open --- packages/web/src/widgets/SummaryProcesses.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index eda44885e..e73e208a6 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -9,6 +9,7 @@ export let conid; export let processes: DatabaseProcess[] = []; export let refreshInterval: number = 1000; + export let tabVisible: boolean = false; let internalProcesses = [...processes]; @@ -34,7 +35,11 @@ } onMount(() => { - const intervalId = setInterval(() => refreshProcesses(), refreshInterval); + const intervalId = setInterval(() => { + if (!tabVisible) return; + + refreshProcesses(); + }, refreshInterval); return () => clearInterval(intervalId); }); From d8081277eea81a0f1d04ca2e447af68a32460e6a Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 19:18:42 +0200 Subject: [PATCH 10/28] feat: parse col to row formatter --- packages/web/src/elements/TableControl.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte index 6da2e8914..f760c30b9 100644 --- a/packages/web/src/elements/TableControl.svelte +++ b/packages/web/src/elements/TableControl.svelte @@ -350,7 +350,7 @@ {#if col.component} {:else if col.formatter} - {col.formatter(row)} + {col.formatter(row, col)} {:else if col.slot != null} {#key row[col.slotKey] || 'key'} {#if col.slot == -1} From 02ee3275954bb6421d6fd07ba35ff06b622b5ce8 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 19:21:04 +0200 Subject: [PATCH 11/28] feat: format fileSize cols in summary databases --- packages/web/src/widgets/SummaryDatabases.svelte | 8 ++++++++ plugins/dbgate-plugin-mongo/src/backend/driver.js | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/web/src/widgets/SummaryDatabases.svelte b/packages/web/src/widgets/SummaryDatabases.svelte index 9fe06bcb8..91ecf2fa1 100644 --- a/packages/web/src/widgets/SummaryDatabases.svelte +++ b/packages/web/src/widgets/SummaryDatabases.svelte @@ -1,5 +1,6 @@ diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index 9be451ddf..0057d5bfd 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -640,15 +640,16 @@ const driver = { this.listDatabases(dbhan), ]); + /** @type {import('dbgate-types').ServerSummary} */ const data = { processes, variables, databases: { rows: databases, columns: [ - { header: 'Name', fieldName: 'name' }, - { header: 'Size on disk', fieldName: 'sizeOnDisk' }, - { header: 'Empty', fieldName: 'empty' }, + { header: 'Name', fieldName: 'name', type: 'data' }, + { header: 'Size on disk', fieldName: 'sizeOnDisk', type: 'fileSize' }, + { header: 'Empty', fieldName: 'empty', type: 'data' }, ], }, }; From 939bbc3f2c8d3a58bd8cac3b417e2010aa440c0d Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 20:09:47 +0200 Subject: [PATCH 12/28] chore: update summary typing --- packages/types/engines.d.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 4a848af9c..b19d1aa88 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -122,17 +122,12 @@ export interface SqlBackupDumper { run(); } -export interface SummaryColumn { - fieldName: string; - header: string; - dataType: 'string' | 'number' | 'bytes'; -} export interface ServerSummaryDatabases { rows: any[]; - columns: DatabaseColumn[]; + columns: SummaryDatabaseColumn[]; } -export type DatabaseColumn = { +export type SummaryDatabaseColumn = { header: string; fieldName: string; type: 'data' | 'fileSize'; From 90546ad4a7019f5a358e128be1ec65123d2e09a8 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 21:16:53 +0200 Subject: [PATCH 13/28] feat: toasts when killing db proc --- .../web/src/widgets/SummaryProcesses.svelte | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index e73e208a6..0a162b4cb 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -5,6 +5,7 @@ import CtaButton from '../buttons/CtaButton.svelte'; import { apiCall } from '../utility/api'; import { onMount } from 'svelte'; + import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar'; export let conid; export let processes: DatabaseProcess[] = []; @@ -19,17 +20,33 @@ } async function killProcess(processId: number) { - await apiCall('server-connections/kill-database-process', { + const result = await apiCall('server-connections/kill-database-process', { pid: processId, conid, }); + if (result.errorMessage || result.error) { + showSnackbarError( + _t('summaryProcesses.killError', { + defaultMessage: 'Error while killing process {processId}: {errorMessage}', + values: { processId, errorMessage: result.errorMessage || result.error }, + }) + ); + } else { + showSnackbarSuccess( + _t('summaryProcesses.killSuccess', { + defaultMessage: 'Process {processId} killed successfully', + values: { processId }, + }) + ); + } + refreshProcesses(); } function formatRunningTime(seconds: number): string { if (!seconds) return '-'; - if (seconds < 60) return `${seconds}s`; + if (seconds < 60) return `${seconds.toFixed(3)}s`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; } From 25fe1d03a7c238821d3492ada060bb943975c410 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 21:33:33 +0200 Subject: [PATCH 14/28] feat: server summary for postgres --- .../src/backend/drivers.js | 57 ++++++++++++++++++- .../src/backend/sql/index.js | 6 ++ .../src/backend/sql/listDatabases.js | 11 ++++ .../src/backend/sql/listProcesses.js | 13 +++++ .../src/backend/sql/listVariables.js | 5 ++ .../src/frontend/drivers.js | 3 + 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js create mode 100644 plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js create mode 100644 plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 595b56b97..de81e3d19 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -6,6 +6,7 @@ const Analyser = require('./Analyser'); const wkx = require('wkx'); const pg = require('pg'); const pgCopyStreams = require('pg-copy-streams'); +const sql = require('./sql'); const { getLogger, createBulkInsertStreamBase, @@ -351,11 +352,65 @@ const drivers = driverBases.map(driverBase => ({ // @ts-ignore return createBulkInsertStreamBase(this, stream, dbhan, name, options); }, + + async serverSummary(dbhan) { + const [processes, variables, databases] = await Promise.all([ + this.listProcesses(dbhan), + this.listVariables(dbhan), + this.listDatabases(dbhan), + ]); + + /** @type {import('dbgate-types').ServerSummary} */ + const data = { + processes, + variables, + databases: { + rows: databases, + columns: [ + { header: 'Name', fieldName: 'name', type: 'data' }, + { header: 'Size on disk', fieldName: 'sizeOnDisk', type: 'fileSize' }, + ], + }, + }; + + return data; + }, + + async killProcess(dbhan, pid) { + const result = await this.query(dbhan, `SELECT pg_terminate_backend(${parseInt(pid)})`); + return result; + }, + async listDatabases(dbhan) { - const { rows } = await this.query(dbhan, 'SELECT datname AS name FROM pg_database WHERE datistemplate = false'); + const { rows } = await this.query(dbhan, sql.listDatabases); return rows; }, + async listVariables(dbhan) { + const result = await this.query(dbhan, sql.listVariables); + return result.rows.map(row => ({ + variable: row.variable, + value: row.value, + })); + }, + + async listProcesses(dbhan) { + const result = await this.query(dbhan, sql.listProcesses); + return result.rows.map(row => ({ + processId: row.processId, + connectionId: row.connectionId, + client: row.client, + operation: row.operation, + namespace: null, + command: row.operation, + runningTime: row.runningTime ? Math.max(Number(row.runningTime), 0) : null, + state: row.state, + waitingFor: row.waitingFor, + locks: null, + progress: null, + })); + }, + getAuthTypes() { const res = [ { diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js index 8f648c64f..833153196 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js @@ -17,6 +17,9 @@ const geographyColumns = require('./geographyColumns'); const proceduresParameters = require('./proceduresParameters'); const foreignKeys = require('./foreignKeys'); const triggers = require('./triggers'); +const listDatabases = require('./listDatabases'); +const listVariables = require('./listVariables'); +const listProcesses = require('./listProcesses'); const fk_keyColumnUsage = require('./fk_key_column_usage'); @@ -41,4 +44,7 @@ module.exports = { geographyColumns, proceduresParameters, triggers, + listDatabases, + listVariables, + listProcesses, }; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js new file mode 100644 index 000000000..a033b9297 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js @@ -0,0 +1,11 @@ +module.exports = ` +SELECT + "datname" AS "name", + pg_database_size("datname") AS "sizeOnDisk", + 0 AS "tableCount", + 0 AS "viewCount", + 0 AS "matviewCount" +FROM "pg_database" +WHERE "datistemplate" = false +ORDER BY pg_database_size("datname") DESC +`; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js new file mode 100644 index 000000000..807dcad38 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js @@ -0,0 +1,13 @@ +module.exports = ` +SELECT + "pid" AS "processId", + "application_name" AS "client", + "client_addr" AS "connectionId", + "state" AS "state", + "query" AS "operation", + EXTRACT(EPOCH FROM (NOW() - "state_change")) AS "runningTime", + "wait_event" IS NOT NULL AS "waitingFor" +FROM "pg_stat_activity" +WHERE "state" IS NOT NULL +ORDER BY "pid" +`; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js new file mode 100644 index 000000000..0973f035b --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js @@ -0,0 +1,5 @@ +module.exports = ` +SELECT "name" AS "variable", "setting" AS "value" +FROM "pg_settings" +ORDER BY "name" +`; diff --git a/plugins/dbgate-plugin-postgres/src/frontend/drivers.js b/plugins/dbgate-plugin-postgres/src/frontend/drivers.js index e44bf8b6f..c67f1577e 100644 --- a/plugins/dbgate-plugin-postgres/src/frontend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/frontend/drivers.js @@ -361,6 +361,7 @@ EXECUTE FUNCTION function_name();`, /** @type {import('dbgate-types').EngineDriver} */ const postgresDriver = { ...postgresDriverBase, + supportsServerSummary: true, engine: 'postgres@dbgate-plugin-postgres', title: 'PostgreSQL', defaultPort: 5432, @@ -388,6 +389,7 @@ const postgresDriver = { /** @type {import('dbgate-types').EngineDriver} */ const cockroachDriver = { ...postgresDriverBase, + supportsServerSummary: true, engine: 'cockroach@dbgate-plugin-postgres', title: 'CockroachDB', defaultPort: 26257, @@ -403,6 +405,7 @@ const cockroachDriver = { /** @type {import('dbgate-types').EngineDriver} */ const redshiftDriver = { ...postgresDriverBase, + supportsServerSummary: true, dialect: { ...dialect, stringAgg: false, From b5ab1d6b33fb845127fdc4bc72bf5a7745224eeb Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 21:36:34 +0200 Subject: [PATCH 15/28] fix: always convert seconds to fixed 3 when printing running time --- packages/web/src/widgets/SummaryProcesses.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index 0a162b4cb..a6f51c832 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -47,7 +47,7 @@ function formatRunningTime(seconds: number): string { if (!seconds) return '-'; if (seconds < 60) return `${seconds.toFixed(3)}s`; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(3)}s`; return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; } From 8f6783792fce44d70cd4374c27c90ae731c83728 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 21:48:56 +0200 Subject: [PATCH 16/28] feat: add error handler for server summary tab --- packages/web/src/tabs/ServerSummaryTab.svelte | 83 +++++++++++++------ 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index b16058fd1..e716d5123 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -82,33 +82,42 @@ wrapper /> {:then summary} -
    - serverSummarySelectedTab.set(index)} - tabs={[ - { - label: _t('serverSummaryTab.variables', { defaultMessage: 'Variables' }), - component: SummaryVariables, - props: { variables: summary.variables || [] }, - }, - { - label: _t('serverSummaryTab.processes', { defaultMessage: 'Processes' }), - component: SummaryProcesses, - props: { processes: summary.processes || [], conid }, - }, - { - label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }), - component: SummaryDatabases, - props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] }, - }, - ]} - /> -
    + {#if 'errorMessage' in summary} +
    +
    +

    {_t('serverSummaryTab.errorTitle', { defaultMessage: 'Error loading server summary' })}

    +

    {summary.errorMessage}

    +
    +
    + {:else} +
    + serverSummarySelectedTab.set(index)} + tabs={[ + { + label: _t('serverSummaryTab.variables', { defaultMessage: 'Variables' }), + component: SummaryVariables, + props: { variables: summary.variables || [] }, + }, + { + label: _t('serverSummaryTab.processes', { defaultMessage: 'Processes' }), + component: SummaryProcesses, + props: { processes: summary.processes || [], conid }, + }, + { + label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }), + component: SummaryDatabases, + props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] }, + }, + ]} + /> +
    + {/if} {/await} @@ -130,4 +139,24 @@ .action-separator { margin: 0 5px; } + + .error-wrapper { + display: flex; + align-items: center; + justify-content: center; + } + + .error-message { + background: var(--theme-bg-1); + border: 1px solid var(--theme-border); + border-radius: 4px; + padding: 20px; + max-width: 500px; + text-align: center; + } + + .error-message h3 { + color: var(--theme-font-error); + margin-top: 0; + } From 6a5672673488f8568b48d19d97e864c24bc5195a Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 22:48:35 +0200 Subject: [PATCH 17/28] fix: refresh proccesses only if summary tab is opened --- packages/web/src/tabs/ServerSummaryTab.svelte | 11 +++++++++-- packages/web/src/widgets/SummaryProcesses.svelte | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index e716d5123..e923e73d8 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -28,9 +28,12 @@ import SummaryVariables from '../widgets/SummaryVariables.svelte'; import SummaryProcesses from '../widgets/SummaryProcesses.svelte'; import SummaryDatabases from '../widgets/SummaryDatabases.svelte'; - import { serverSummarySelectedTab } from '../stores'; + import { activeTabId, serverSummarySelectedTab } from '../stores'; + import { getContext } from 'svelte'; export let conid; + const tabid = getContext('tabid'); + $: isActiveTab = tabid === $activeTabId; let refreshToken = 0; @@ -107,7 +110,11 @@ { label: _t('serverSummaryTab.processes', { defaultMessage: 'Processes' }), component: SummaryProcesses, - props: { processes: summary.processes || [], conid }, + props: { + processes: summary.processes || [], + isSummaryOpened: isActiveTab, + conid, + }, }, { label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }), diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index a6f51c832..175546c58 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -8,6 +8,7 @@ import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar'; export let conid; + export let isSummaryOpened: boolean = false; export let processes: DatabaseProcess[] = []; export let refreshInterval: number = 1000; export let tabVisible: boolean = false; @@ -53,7 +54,7 @@ onMount(() => { const intervalId = setInterval(() => { - if (!tabVisible) return; + if (!tabVisible || !isSummaryOpened) return; refreshProcesses(); }, refreshInterval); From c49b1a46f8796f58a5e7a6fdc50a8e4572977f4c Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 22:48:42 +0200 Subject: [PATCH 18/28] feat: mysql server summary --- .../src/backend/drivers.js | 55 +++++++++++++++++++ .../src/frontend/drivers.js | 2 + 2 files changed, 57 insertions(+) diff --git a/plugins/dbgate-plugin-mysql/src/backend/drivers.js b/plugins/dbgate-plugin-mysql/src/backend/drivers.js index bd6bb5a02..95f87f8a6 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/drivers.js +++ b/plugins/dbgate-plugin-mysql/src/backend/drivers.js @@ -200,6 +200,61 @@ const drivers = driverBases.map(driverBase => ({ const { rows } = await this.query(dbhan, 'show databases'); return rows.map(x => ({ name: x.Database })); }, + + async listVariables(dbhan) { + const { rows } = await this.query(dbhan, 'SHOW VARIABLES'); + return rows.map(row => ({ + variable: row.Variable_name, + value: row.Value, + })); + }, + + async listProcesses(dbhan) { + const { rows } = await this.query(dbhan, 'SHOW PROCESSLIST'); + return rows.map(row => ({ + processId: row.Id, + connectionId: null, + client: row.Host, + operation: row.Command, + namespace: row.Database, + runningTime: row.Time, + state: row.State, + waitingFor: row.State && row.State.includes('Waiting'), + })); + }, + + async killProcess(dbhan, processId) { + await this.query(dbhan, `KILL ${processId}`); + }, + + async serverSummary(dbhan) { + const [variables, processes, databases] = await Promise.all([ + this.listVariables(dbhan), + this.listProcesses(dbhan), + this.listDatabases(dbhan), + ]); + + return { + variables, + processes: processes.map(p => ({ + processId: p.processId, + connectionId: p.connectionId, + client: p.client, + operation: p.operation, + namespace: p.namespace, + runningTime: p.runningTime, + state: p.state, + waitingFor: p.waitingFor, + })), + databases: { + rows: databases.map(db => ({ + name: db.name, + })), + columns: [{ header: 'Database', fieldName: 'name', type: 'data' }], + }, + }; + }, + async writeTable(dbhan, name, options) { // @ts-ignore return createBulkInsertStreamBase(this, stream, dbhan, name, options); diff --git a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js index 486942207..8e605068c 100644 --- a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js +++ b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js @@ -385,6 +385,7 @@ const mysqlDriverBase = { /** @type {import('dbgate-types').EngineDriver} */ const mysqlDriver = { ...mysqlDriverBase, + supportsServerSummary: true, dialect: mysqlDialect, engine: 'mysql@dbgate-plugin-mysql', title: 'MySQL', @@ -425,6 +426,7 @@ const mariaDbDialect = { /** @type {import('dbgate-types').EngineDriver} */ const mariaDriver = { ...mysqlDriverBase, + supportsServerSummary: true, dialect: mariaDbDialect, engine: 'mariadb@dbgate-plugin-mysql', title: 'MariaDB', From e4bf2b4c9b6903394627d25f535f3b36fa7e05f9 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 22:48:51 +0200 Subject: [PATCH 19/28] feat: mssql server summary --- .../dbgate-plugin-mssql/src/backend/driver.js | 43 ++++++++++++++++++- .../src/backend/sql/index.js | 6 +++ .../src/backend/sql/listDatabases.js | 17 ++++++++ .../src/backend/sql/listProcesses.js | 11 +++++ .../src/backend/sql/listVariables.js | 3 ++ .../src/frontend/driver.js | 1 + 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js create mode 100644 plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js create mode 100644 plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js diff --git a/plugins/dbgate-plugin-mssql/src/backend/driver.js b/plugins/dbgate-plugin-mssql/src/backend/driver.js index 8d16e35b8..7360b998c 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/driver.js @@ -9,6 +9,7 @@ const lock = new AsyncLock(); const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver'); const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = require('./nativeDriver'); const { getLogger } = global.DBGATE_PACKAGES['dbgate-tools']; +const sql = require('./sql'); const logger = getLogger('mssqlDriver'); @@ -148,9 +149,49 @@ const driver = { return res; }, async listDatabases(dbhan) { - const { rows } = await this.query(dbhan, 'SELECT name FROM sys.databases order by name'); + const { rows } = await this.query(dbhan, sql.listDatabases); return rows; }, + + async listProcesses(dbhan) { + const { rows } = await this.query(dbhan, sql.listProcesses); + return rows; + }, + + async listVariables(dbhan) { + const { rows } = await this.query(dbhan, sql.listVariables); + return rows; + }, + + async killProcess(dbhan, processId) { + await this.query(dbhan, `KILL ${processId}`); + }, + + async serverSummary(dbhan) { + const [variables, processes, databases] = await Promise.all([ + this.listVariables(dbhan), + this.listProcesses(dbhan), + this.listDatabases(dbhan), + ]); + + return { + variables: variables, + processes: processes, + databases: { + rows: databases, + columns: [ + { header: 'Database', fieldName: 'name', type: 'data' }, + { header: 'Status', fieldName: 'status', type: 'data' }, + { header: 'Recovery Model', fieldName: 'recoveryModel', type: 'data' }, + { header: 'Compatibility Level', fieldName: 'compatibilityLevel', type: 'data' }, + { header: 'Read Only', fieldName: 'isReadOnly', type: 'data' }, + { header: 'Data Size', fieldName: 'sizeOnDisk', type: 'fileSize' }, + { header: 'Log Size', fieldName: 'logSizeOnDisk', type: 'fileSize' }, + ], + }, + }; + }, + getRedirectAuthUrl(connection, options) { if (connection.authType != 'msentra') return null; return authProxy.authProxyGetRedirectUrl({ diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js index 4125b1ebd..f9426419f 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js @@ -13,6 +13,9 @@ const viewColumns = require('./viewColumns'); const indexes = require('./indexes'); const indexcols = require('./indexcols'); const triggers = require('./triggers'); +const listVariables = require('./listVariables'); +const listDatabases = require('./listDatabases'); +const listProcesses = require('./listProcesses'); module.exports = { columns, @@ -30,4 +33,7 @@ module.exports = { indexcols, tableSizes, triggers, + listVariables, + listDatabases, + listProcesses, }; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js new file mode 100644 index 000000000..751c54b39 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js @@ -0,0 +1,17 @@ +module.exports = ` + SELECT + d.name, + d.database_id, + d.state_desc as status, + d.recovery_model_desc as recoveryModel, + d.collation_name as collation, + d.compatibility_level as compatibilityLevel, + d.is_read_only as isReadOnly, + CAST(SUM(CASE WHEN mf.type = 0 THEN mf.size * 8192.0 ELSE 0 END) AS BIGINT) AS sizeOnDisk, + CAST(SUM(CASE WHEN mf.type = 1 THEN mf.size * 8192.0 ELSE 0 END) AS BIGINT) AS logSizeOnDisk + FROM sys.databases d + LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id + GROUP BY d.name, d.database_id, d.state_desc, d.recovery_model_desc, d.collation_name, + d.compatibility_level, d.is_read_only + ORDER BY d.name +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js new file mode 100644 index 000000000..30ffe5768 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js @@ -0,0 +1,11 @@ +module.exports = ` +SELECT + session_id as processId, + ISNULL(host_name, 'Unknown') + ':' + ISNULL(CAST(host_process_id AS VARCHAR(10)), '?') as client, + ISNULL(DB_NAME(database_id), 'master') as namespace, + ISNULL(DATEDIFF(SECOND, last_request_start_time, GETDATE()), 0) as runningTime, + status as state +FROM sys.dm_exec_sessions +WHERE is_user_process = 1 +ORDER BY session_id +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js new file mode 100644 index 000000000..a6543c759 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js @@ -0,0 +1,3 @@ +module.exports = ` + SELECT name as variable, value FROM sys.configurations ORDER BY name +`; diff --git a/plugins/dbgate-plugin-mssql/src/frontend/driver.js b/plugins/dbgate-plugin-mssql/src/frontend/driver.js index 56c315e14..fd979122e 100644 --- a/plugins/dbgate-plugin-mssql/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/frontend/driver.js @@ -116,6 +116,7 @@ const dialect = { /** @type {import('dbgate-types').EngineDriver} */ const driver = { ...driverBase, + supportsServerSummary: true, dumperClass: MsSqlDumper, dialect, readOnlySessions: false, From ab924f6b48df3fb1924f3f92f19fcdff8e213fb3 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 14 Aug 2025 22:55:36 +0200 Subject: [PATCH 20/28] feat: confirm kill process --- packages/web/src/widgets/SummaryProcesses.svelte | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index 175546c58..995497ebe 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -6,6 +6,8 @@ import { apiCall } from '../utility/api'; import { onMount } from 'svelte'; import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar'; + import { showModal } from '../modals/modalTools'; + import ConfirmModal from '../modals/ConfirmModal.svelte'; export let conid; export let isSummaryOpened: boolean = false; @@ -45,6 +47,18 @@ refreshProcesses(); } + async function killProcessWithConfirm(processId: number) { + showModal(ConfirmModal, { + message: _t('summaryProcesses.killConfirm', { + defaultMessage: 'Are you sure you want to kill process {processId}?', + values: { processId }, + }), + onConfirm: async () => { + await killProcess(processId); + }, + }); + } + function formatRunningTime(seconds: number): string { if (!seconds) return '-'; if (seconds < 60) return `${seconds.toFixed(3)}s`; @@ -91,7 +105,7 @@ ]} > - killProcess(row.processId)}> + killProcessWithConfirm(row.processId)}> {_t('common.kill', { defaultMessage: 'Kill' })} From 78215552bf3ecfa3fb1b9e8550b3860f0bf3ae5d Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 19 Aug 2025 17:08:07 +0200 Subject: [PATCH 21/28] chore: add logging for server summary --- packages/api/src/controllers/serverConnections.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index b486d25e9..e490aa2e9 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -270,6 +270,7 @@ module.exports = { serverSummary_meta: true, async serverSummary({ conid }, req) { + logger.info({ conid }, 'DBGM-00260 Processing server summary'); testConnectionPermission(conid, req); return this.loadDataCore('serverSummary', { conid }); }, @@ -277,6 +278,7 @@ module.exports = { listDatabaseProcesses_meta: true, async listDatabaseProcesses(ctx, req) { const { conid } = ctx; + logger.info({ conid }, 'DBGM-00261 Processing server summary'); testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); From 114ce1ea3acc907089912b20df137c2ac0b98663 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 19 Aug 2025 17:08:38 +0200 Subject: [PATCH 22/28] feat: make summary table sortable, filtrable, with sticky header --- packages/types/engines.d.ts | 2 + .../web/src/widgets/SummaryDatabases.svelte | 7 ++- .../web/src/widgets/SummaryProcesses.svelte | 49 ++++++++++++++--- .../web/src/widgets/SummaryVariables.svelte | 14 ++++- .../dbgate-plugin-mongo/src/backend/driver.js | 23 ++++++-- .../dbgate-plugin-mssql/src/backend/driver.js | 54 ++++++++++++++++--- .../src/backend/drivers.js | 10 +++- 7 files changed, 140 insertions(+), 19 deletions(-) diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index b19d1aa88..0daf4c172 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -131,6 +131,8 @@ export type SummaryDatabaseColumn = { header: string; fieldName: string; type: 'data' | 'fileSize'; + filterable?: boolean; + sortable?: boolean; }; export interface ServerSummary { diff --git a/packages/web/src/widgets/SummaryDatabases.svelte b/packages/web/src/widgets/SummaryDatabases.svelte index 91ecf2fa1..be29fd98f 100644 --- a/packages/web/src/widgets/SummaryDatabases.svelte +++ b/packages/web/src/widgets/SummaryDatabases.svelte @@ -1,11 +1,16 @@
    - +
    diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index 133134e14..9966f2b4b 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -80,7 +80,7 @@ }); -
    +
    From 176d75768f8ebdd76659934ddb404e9925b408ba Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 19 Aug 2025 18:40:08 +0200 Subject: [PATCH 24/28] feat: vertical split for process operation --- .../web/src/widgets/SummaryProcesses.svelte | 166 ++++++++++-------- 1 file changed, 92 insertions(+), 74 deletions(-) diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte index 9966f2b4b..9ce57e33c 100644 --- a/packages/web/src/widgets/SummaryProcesses.svelte +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -1,6 +1,7 @@
    - - - killProcessWithConfirm(row.processId)}> - {_t('common.kill', { defaultMessage: 'Kill' })} - - + + +
    + { + selectedProcess = e.detail; + }} + {filters} + stickyHeader + rows={internalProcesses} + columns={[ + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.processId', { defaultMessage: 'Process ID' }), + fieldName: 'processId', + slot: 1, + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.connectionId', { defaultMessage: 'Connection ID' }), + fieldName: 'connectionId', + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.client', { defaultMessage: 'Client' }), + fieldName: 'client', + }, + { + filterable: true, + header: _t('summaryProcesses.operation', { defaultMessage: 'Operation' }), + fieldName: 'operation', + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.namespace', { defaultMessage: 'Namespace' }), + fieldName: 'namespace', + }, + { + sortable: true, + header: _t('summaryProcesses.runningTime', { defaultMessage: 'Running Time' }), + fieldName: 'runningTime', + slot: 2, + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.state', { defaultMessage: 'State' }), + fieldName: 'state', + }, + { + sortable: true, + header: _t('summaryProcesses.waitingFor', { defaultMessage: 'Waiting For' }), + fieldName: 'waitingFor', + slot: 3, + }, + { + header: _t('summaryProcesses.actions', { defaultMessage: 'Actions' }), + fieldName: 'processId', + slot: 0, + }, + ]} + > + + killProcessWithConfirm(row.processId)}> + {_t('common.kill', { defaultMessage: 'Kill' })} + + - - {row.processId} - + + {row.processId} + - - {formatRunningTime(row.runningTime)} - + + {formatRunningTime(row.runningTime)} + - - {row.waitingFor ? 'Yes' : 'No'} + + {row.waitingFor ? 'Yes' : 'No'} + + +
    -
    + + {#if !!selectedProcess} + + {/if} + +