Merge pull request #1186 from dbgate/feature/mongo-server-summary

Feature/mongo server summary
This commit is contained in:
Jan Prochazka
2025-08-21 15:46:34 +02:00
committed by GitHub
35 changed files with 1043 additions and 170 deletions

View File

@@ -270,10 +270,40 @@ 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 });
},
listDatabaseProcesses_meta: true,
async listDatabaseProcesses(ctx, req) {
const { conid } = ctx;
// logger.info({ conid }, 'DBGM-00261 Listing processes of database server');
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;
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);

View File

@@ -146,6 +146,30 @@ async function handleServerSummary({ msgid }) {
return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan));
}
async function handleKillDatabaseProcess({ 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 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));
}
@@ -154,6 +178,8 @@ const messageHandlers = {
connect: handleConnect,
ping: handlePing,
serverSummary: handleServerSummary,
killDatabaseProcess: handleKillDatabaseProcess,
listDatabaseProcesses: handleListDatabaseProcesses,
summaryCommand: handleSummaryCommand,
createDatabase: props => handleDatabaseOp('createDatabase', props),
dropDatabase: props => handleDatabaseOp('dropDatabase', props),

View File

@@ -99,19 +99,46 @@ 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();
}
export interface SummaryColumn {
fieldName: string;
header: string;
dataType: 'string' | 'number' | 'bytes';
export interface ServerSummaryDatabases {
rows: any[];
columns: SummaryDatabaseColumn[];
}
export interface ServerSummaryDatabase {}
export type SummaryDatabaseColumn = {
header: string;
fieldName: string;
type: 'data' | 'fileSize';
filterable?: boolean;
sortable?: boolean;
};
export interface ServerSummary {
columns: SummaryColumn[];
databases: ServerSummaryDatabase[];
processes: DatabaseProcess[];
variables: DatabaseVariable[];
databases: ServerSummaryDatabases;
}
export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
@@ -161,12 +188,12 @@ export interface FilterBehaviourProvider {
getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour;
}
export interface DatabaseHandle<TClient = any> {
export interface DatabaseHandle<TClient = any, TDataBase = any> {
client: TClient;
database?: string;
conid?: string;
feedback?: (message: any) => void;
getDatabase?: () => any;
getDatabase?: () => TDataBase;
connectionType?: string;
treeKeySeparator?: string;
}
@@ -196,7 +223,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase {
inputFile: string;
}
export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBehaviourProvider {
engine: string;
title: string;
defaultPort?: number;
@@ -242,61 +269,88 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
defaultSocketPath?: string;
authTypeLabel?: string;
importExportArgs?: any[];
connect({ server, port, user, password, database, connectionDefinition }): Promise<DatabaseHandle<TClient>>;
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<StreamResult>;
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<StreamResult>;
connect({
server,
port,
user,
password,
database,
connectionDefinition,
}): Promise<DatabaseHandle<TClient, TDataBase>>;
close(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<any>;
query(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: QueryOptions): Promise<QueryResult>;
stream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options: StreamOptions);
readQuery(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, structure?: TableInfo): Promise<StreamResult>;
readJsonQuery(dbhan: DatabaseHandle<TClient, TDataBase>, query: any, structure?: TableInfo): Promise<StreamResult>;
// eg. PostgreSQL COPY FROM stdin
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<StreamResult>;
writeTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo, options: WriteTableOptions): Promise<StreamResult>;
writeQueryFromStream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string): Promise<StreamResult>;
writeTable(
dbhan: DatabaseHandle<TClient, TDataBase>,
name: NamedObjectInfo,
options: WriteTableOptions
): Promise<StreamResult>;
analyseSingleObject(
dbhan: DatabaseHandle<TClient>,
dbhan: DatabaseHandle<TClient, TDataBase>,
name: NamedObjectInfo,
objectTypeField: keyof DatabaseInfo
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
analyseSingleTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo): Promise<TableInfo>;
getVersion(dbhan: DatabaseHandle<TClient>): Promise<{ version: string; versionText?: string }>;
listDatabases(dbhan: DatabaseHandle<TClient>): Promise<
analyseSingleTable(dbhan: DatabaseHandle<TClient, TDataBase>, name: NamedObjectInfo): Promise<TableInfo>;
getVersion(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<{ version: string; versionText?: string }>;
listDatabases(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<
{
name: string;
sizeOnDisk?: number;
empty?: boolean;
}[]
>;
loadKeys(dbhan: DatabaseHandle<TClient>, root: string, filter?: string): Promise;
scanKeys(dbhan: DatabaseHandle<TClient>, root: string, pattern: string, cursor: string, count: number): Promise;
exportKeys(dbhan: DatabaseHandle<TClient>, options: {}): Promise;
loadKeyInfo(dbhan: DatabaseHandle<TClient>, key): Promise;
loadKeyTableRange(dbhan: DatabaseHandle<TClient>, key, cursor, count): Promise;
loadKeys(dbhan: DatabaseHandle<TClient, TDataBase>, root: string, filter?: string): Promise;
scanKeys(
dbhan: DatabaseHandle<TClient, TDataBase>,
root: string,
pattern: string,
cursor: string,
count: number
): Promise;
exportKeys(dbhan: DatabaseHandle<TClient, TDataBase>, options: {}): Promise;
loadKeyInfo(dbhan: DatabaseHandle<TClient, TDataBase>, key): Promise;
loadKeyTableRange(dbhan: DatabaseHandle<TClient, TDataBase>, key, cursor, count): Promise;
loadFieldValues(
dbhan: DatabaseHandle<TClient>,
dbhan: DatabaseHandle<TClient, TDataBase>,
name: NamedObjectInfo,
field: string,
search: string,
dataType: string
): Promise;
analyseFull(dbhan: DatabaseHandle<TClient>, serverVersion): Promise<DatabaseInfo>;
analyseIncremental(dbhan: DatabaseHandle<TClient>, structure: DatabaseInfo, serverVersion): Promise<DatabaseInfo>;
analyseFull(dbhan: DatabaseHandle<TClient, TDataBase>, serverVersion): Promise<DatabaseInfo>;
analyseIncremental(
dbhan: DatabaseHandle<TClient, TDataBase>,
structure: DatabaseInfo,
serverVersion
): Promise<DatabaseInfo>;
dialect: SqlDialect;
dialectByVersion(version): SqlDialect;
createDumper(options = null): SqlDumper;
createBackupDumper(dbhan: DatabaseHandle<TClient>, options): Promise<SqlBackupDumper>;
createBackupDumper(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<SqlBackupDumper>;
getAuthTypes(): EngineAuthType[];
readCollection(dbhan: DatabaseHandle<TClient>, options: ReadCollectionOptions): Promise<any>;
updateCollection(dbhan: DatabaseHandle<TClient>, changeSet: any): Promise<any>;
readCollection(dbhan: DatabaseHandle<TClient, TDataBase>, options: ReadCollectionOptions): Promise<any>;
updateCollection(dbhan: DatabaseHandle<TClient, TDataBase>, changeSet: any): Promise<any>;
getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string;
createDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
createDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
dropDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
script(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: RunScriptOptions): Promise;
operation(
dbhan: DatabaseHandle<TClient, TDataBase>,
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<TClient>, method, args);
serverSummary(dbhan: DatabaseHandle<TClient>): Promise<ServerSummary>;
summaryCommand(dbhan: DatabaseHandle<TClient>, command, row): Promise<void>;
startProfiler(dbhan: DatabaseHandle<TClient>, options): Promise<any>;
stopProfiler(dbhan: DatabaseHandle<TClient>, profiler): Promise<void>;
callMethod(dbhan: DatabaseHandle<TClient, TDataBase>, method, args);
serverSummary(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<ServerSummary>;
summaryCommand(dbhan: DatabaseHandle<TClient, TDataBase>, command, row): Promise<void>;
startProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<any>;
stopProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, profiler): Promise<void>;
getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>;
getAuthTokenFromCode(connection, options): Promise<string>;
getAccessTokenFromAuth(connection, req): Promise<string | null>;
@@ -313,7 +367,10 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
adaptTableInfo(table: TableInfo): TableInfo;
// simple data type adapter
adaptDataType(dataType: string): string;
listSchemas(dbhan: DatabaseHandle<TClient>): Promise<SchemaInfo[] | null>;
listSchemas(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<SchemaInfo[] | null>;
listProcesses(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseProcess[] | null>;
listVariables(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseVariable[] | null>;
killProcess(dbhan: DatabaseHandle<TClient, TDataBase>, pid: number): Promise<any>;
backupDatabaseCommand(
connection: any,
settings: BackupDatabaseSettings,
@@ -337,7 +394,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
analyserClass?: any;
dumperClass?: any;
singleConnectionOnly?: boolean;
getLogDbInfo(dbhan: DatabaseHandle<TClient>): {
getLogDbInfo(dbhan: DatabaseHandle<TClient, TDataBase>): {
database?: string;
engine: string;
conid?: string;

View File

@@ -0,0 +1,46 @@
<script lang="ts">
export let disabled = false;
export let title = null;
let domButton;
export function getBoundingClientRect() {
return domButton.getBoundingClientRect();
}
</script>
<button
class="cta-button"
{title}
{disabled}
on:click
bind:this={domButton}
data-testid={$$props['data-testid']}
>
<slot />
</button>
<style>
.cta-button {
background: none;
border: none;
padding: 0;
margin: 0;
color: var(--theme-font-link);
text-decoration: underline;
cursor: pointer;
font-size: inherit;
font-family: inherit;
display: inline;
}
.cta-button:hover:not(:disabled) {
color: var(--theme-font-hover);
}
.cta-button:disabled {
color: var(--theme-font-3);
cursor: not-allowed;
text-decoration: none;
}
</style>

View File

@@ -15,7 +15,10 @@
export let menu = null;
export let isInline = false;
export let containerMaxWidth = undefined;
export let containerMaxHeight = undefined;
export let flex1 = true;
export let flexColContainer = true;
export let maxHeight100 = false;
export let contentTestId = undefined;
export let inlineTabs = false;
export let onUserChange = null;
@@ -28,7 +31,7 @@
}
</script>
<div class="main" class:flex1>
<div class="main" class:maxHeight100 class:flex1>
<div class="tabs" class:inlineTabs>
{#each _.compact(tabs) as tab, index}
<div
@@ -50,10 +53,22 @@
{/if}
</div>
<div class="content-container" data-testid={contentTestId}>
<div class="content-container" style:max-height={containerMaxHeight} data-testid={contentTestId}>
{#each _.compact(tabs) as tab, index}
<div class="container" class:isInline class:tabVisible={index == value} style:max-width={containerMaxWidth}>
<svelte:component this={tab.component} {...tab.props} tabControlHiddenTab={index != value} />
<div
class="container"
class:flexColContainer
class:maxHeight100
class:isInline
class:tabVisible={index == value}
style:max-width={containerMaxWidth}
>
<svelte:component
this={tab.component}
{...tab.props}
tabVisible={index == value}
tabControlHiddenTab={index != value}
/>
{#if tab.slot != null}
{#if tab.slot == 0}<slot name="0" />
{:else if tab.slot == 1}<slot name="1" />
@@ -83,6 +98,10 @@
max-width: 100%;
}
.main.maxHeight100 {
max-height: 100%;
}
.tabs {
display: flex;
height: var(--dim-tabs-height);
@@ -132,6 +151,15 @@
position: relative;
}
.container.maxHeight100 {
max-height: 100%;
}
.container.flexColContainer {
display: flex;
flex-direction: column;
}
.container:not(.isInline) {
position: absolute;
display: flex;

View File

@@ -350,7 +350,7 @@
{#if col.component}
<svelte:component this={col.component} {...rowProps} />
{: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}<slot name="-1" {row} {col} {index} />

View File

@@ -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}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -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={'}'}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -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}
/>

View File

@@ -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={'}'}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -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}
<JSONArrow on:click={toggleExpand} {expanded} />
{/if}
<JSONKey {key} colon={context.colon} {isParentExpanded} {isParentArray} on:click={toggleExpand} />
{#if !hideKey}
<JSONKey
key={keyLabel}
colon={context.colon}
{isParentExpanded}
{isParentArray}
{hideKey}
on:click={toggleExpand}
/>
{/if}
<span on:click={toggleExpand}><span>{label}</span>{bracketOpen}</span>
</label>
{#if isParentExpanded}

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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
>
<JSONNode
{hideKey}
{key}
{value}
isParentExpanded={true}

View File

@@ -3,10 +3,25 @@
import JSONKey from './JSONKey.svelte';
export let key, value, valueGetter = null, isParentExpanded, isParentArray, nodeType;
export let key,
value,
valueGetter = null,
labelOverride,
isParentExpanded,
isParentArray,
nodeType;
const label = labelOverride ?? key;
const { colon } = getContext('json-tree-context-key');
</script>
<li class:indent={isParentExpanded}>
<JSONKey key={label} {colon} {isParentExpanded} {isParentArray} />
<span class={nodeType}>
{valueGetter ? valueGetter(value) : value}
</span>
</li>
<style>
li {
user-select: text;
@@ -45,9 +60,4 @@
color: var(--symbol-color);
}
</style>
<li class:indent={isParentExpanded}>
<JSONKey {key} {colon} {isParentExpanded} {isParentArray} />
<span class={nodeType}>
{valueGetter ? valueGetter(value) : value}
</span>
</li>

View File

@@ -215,6 +215,8 @@ export const connectionAppObjectSearchSettings = writableWithStorage(
'connectionAppObjectSearchSettings2'
);
export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab');
let currentThemeValue = null;
currentTheme.subscribe(value => {
currentThemeValue = value;

View File

@@ -18,17 +18,22 @@
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 { activeTabId, serverSummarySelectedTab } from '../stores';
import { getContext } from 'svelte';
export let conid;
const tabid = getContext('tabid');
$: isActiveTab = tabid === $activeTabId;
let refreshToken = 0;
@@ -75,30 +80,54 @@
<ToolStripContainer>
{#await apiCall('server-connections/server-summary', { conid, refreshToken })}
<LoadingInfo message="Loading server details" wrapper />
<LoadingInfo
message={_t('serverSummaryTab.loadingMessage', { defaultMessage: 'Loading server details' })}
wrapper
/>
{:then summary}
<div class="wrapper">
<ObjectListControl
collection={summary.databases}
hideDisplayName
title={`Databases (${summary.databases.length})`}
emptyMessage={'No databases'}
columns={summary.columns.map(col => ({
...col,
slot: col.columnType == 'bytes' ? 1 : col.columnType == 'actions' ? 2 : null,
}))}
>
<svelte:fragment slot="1" let:row let:col>{formatFileSize(row?.[col.fieldName])}</svelte:fragment>
<svelte:fragment slot="2" let:row let:col>
{#each col.actions as action, index}
{#if index > 0}
<span class="action-separator">|</span>
{/if}
<Link onClick={() => runAction(action, row)}>{action.header}</Link>
{/each}
</svelte:fragment>
</ObjectListControl>
</div>
{#if 'errorMessage' in summary}
<div class="wrapper error-wrapper">
<div class="error-message">
<h3>{_t('serverSummaryTab.errorTitle', { defaultMessage: 'Error loading server summary' })}</h3>
<p>{summary.errorMessage}</p>
</div>
</div>
{:else}
<div class="wrapper">
<TabControl
isInline
inlineTabs
containerMaxWidth="100%"
containerMaxHeight="calc(100% - 34px)"
maxHeight100
flex1
flexColContainer
value={$serverSummarySelectedTab}
onUserChange={index => 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 || [],
isSummaryOpened: isActiveTab,
conid,
},
},
{
label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }),
component: SummaryDatabases,
props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] },
},
]}
/>
</div>
{/if}
{/await}
<svelte:fragment slot="toolstrip">
@@ -114,10 +143,33 @@
right: 0;
bottom: 0;
background-color: var(--theme-bg-0);
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
}
.action-separator {
margin: 0 5px;
}
.error-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.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;
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { writable } from 'svelte/store';
import TableControl from '../elements/TableControl.svelte';
import formatFileSize from '../utility/formatFileSize';
export let rows: any[] = [];
export let columns: any[] = [];
const filters = writable({});
const tableColumns = columns.map(col => ({
filterable: col.filterable,
sortable: col.sortable,
header: col.header,
fieldName: col.fieldName,
type: col.type || 'data',
formatter: (row, col) => {
const value = row[col.fieldName];
if (col.type === 'fileSize') return formatFileSize(value);
return value;
},
}));
</script>
<div class="wrapper">
<TableControl {filters} stickyHeader {rows} columns={tableColumns} />
</div>
<style>
.wrapper {
flex-grow: 1;
overflow-y: auto;
max-height: 100%;
}
</style>

View File

@@ -0,0 +1,206 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { DatabaseProcess } from 'dbgate-types';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import TableControl from '../elements/TableControl.svelte';
import { _t } from '../translations';
import CtaButton from '../buttons/CtaButton.svelte';
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';
import SqlEditor from '../query/SqlEditor.svelte';
export let conid;
export let isSummaryOpened: boolean = false;
export let processes: DatabaseProcess[] = [];
export let refreshInterval: number = 1000;
export let tabVisible: boolean = false;
let selectedProcess: DatabaseProcess | null = null;
const filters = writable({});
let internalProcesses = [...processes];
async function refreshProcesses() {
const data = await apiCall('server-connections/list-database-processes', { conid });
internalProcesses = data.result;
}
async function killProcess(processId: number) {
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();
}
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`;
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`;
}
onMount(() => {
const intervalId = setInterval(() => {
if (!tabVisible || !isSummaryOpened) return;
refreshProcesses();
}, refreshInterval);
return () => clearInterval(intervalId);
});
</script>
<div class="wrapper">
<VerticalSplitter initialValue="70%" isSplitter={!!selectedProcess}>
<svelte:fragment slot="1">
<div class="child1-wrapper">
<TableControl
clickable
on:clickrow={e => {
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,
},
]}
>
<svelte:fragment slot="0" let:row>
<CtaButton on:click={() => killProcessWithConfirm(row.processId)}>
{_t('common.kill', { defaultMessage: 'Kill' })}
</CtaButton>
</svelte:fragment>
<svelte:fragment slot="1" let:row>
<code>{row.processId}</code>
</svelte:fragment>
<svelte:fragment slot="2" let:row>
<span>{formatRunningTime(row.runningTime)}</span>
</svelte:fragment>
<svelte:fragment slot="3" let:row>
<span class:waiting={row.waitingFor}>{row.waitingFor ? 'Yes' : 'No'}</span>
</svelte:fragment>
</TableControl>
</div>
</svelte:fragment>
<svelte:fragment slot="2">
{#if !!selectedProcess}
<SqlEditor value={selectedProcess.operation ?? ''} readOnly />
{/if}
</svelte:fragment>
</VerticalSplitter>
</div>
<style>
.wrapper {
flex-grow: 1;
overflow-y: auto;
max-height: 100%;
}
.child1-wrapper {
width: 100%;
max-height: 100%;
overflow-y: auto;
}
code {
font-family: monospace;
background: var(--theme-bg-1);
padding: 2px 4px;
border-radius: 3px;
}
.waiting {
color: var(--theme-font-warning);
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { writable } from 'svelte/store';
import TableControl from '../elements/TableControl.svelte';
import JSONTree from '../jsontree/JSONTree.svelte';
import { _t } from '../translations';
export let variables: { variable: string; value: any }[] = [];
const filters = writable({});
</script>
<div class="wrapper">
<TableControl
stickyHeader
rows={variables}
{filters}
columns={[
{
sortable: true,
filterable: true,
header: _t('summaryVariables.variable', { defaultMessage: 'Variable' }),
fieldName: 'variable',
},
{
sortable: true,
filterable: true,
header: _t('summaryVariables.value', { defaultMessage: 'Value' }),
fieldName: 'value',
slot: 0,
},
]}
>
<svelte:fragment slot="0" let:row>
<JSONTree labelOverride="" hideKey key={row.variable} value={row.value} expandAll={false} />
</svelte:fragment>
</TableControl>
</div>
<style>
.wrapper {
flex-grow: 1;
overflow-y: auto;
max-height: 100%;
}
</style>

View File

@@ -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<MongoClient>} */
/** @type {import('dbgate-types').EngineDriver<MongoClient, import('mongodb').Db>} */
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,107 @@ 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]),
})),
/** @type {import('dbgate-types').ServerSummary} */
const data = {
processes,
variables,
databases: {
rows: databases,
columns: [
{
filterable: true,
sortable: true,
header: 'Name',
fieldName: 'name',
type: 'data',
},
{
sortable: true,
header: 'Size on disk',
fieldName: 'sizeOnDisk',
type: 'fileSize',
},
{
filterable: true,
sortable: true,
header: 'Empty',
fieldName: 'empty',
type: 'data',
},
],
},
};
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) => {

View File

@@ -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,89 @@ 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: [
{
filterable: true,
sortable: true,
header: 'Database',
fieldName: 'name',
type: 'data',
},
{
filterable: true,
sortable: true,
header: 'Status',
fieldName: 'status',
type: 'data',
},
{
filterable: true,
sortable: true,
header: 'Recovery Model',
fieldName: 'recoveryModel',
type: 'data',
},
{
filterable: true,
sortable: true,
header: 'Compatibility Level',
fieldName: 'compatibilityLevel',
type: 'data',
},
{
filterable: true,
sortable: true,
header: 'Read Only',
fieldName: 'isReadOnly',
type: 'data',
},
{
sortable: true,
header: 'Data Size',
fieldName: 'sizeOnDisk',
type: 'fileSize',
},
{
sortable: true,
header: 'Log Size',
fieldName: 'logSizeOnDisk',
type: 'fileSize',
},
],
},
};
},
getRedirectAuthUrl(connection, options) {
if (connection.authType != 'msentra') return null;
return authProxy.authProxyGetRedirectUrl({

View File

@@ -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');
const baseColumns = require('./baseColumns');
module.exports = {
@@ -31,5 +34,8 @@ module.exports = {
indexcols,
tableSizes,
triggers,
listVariables,
listDatabases,
listProcesses,
baseColumns,
};

View File

@@ -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
`;

View File

@@ -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
`;

View File

@@ -0,0 +1,3 @@
module.exports = `
SELECT name as variable, value FROM sys.configurations ORDER BY name
`;

View File

@@ -131,6 +131,7 @@ const dialect = {
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
supportsServerSummary: true,
dumperClass: MsSqlDumper,
dialect,
readOnlySessions: false,

View File

@@ -200,6 +200,69 @@ 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 FULL PROCESSLIST');
return rows.map(row => ({
processId: row.Id,
connectionId: null,
client: row.Host,
operation: row.Info,
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: [
{
filterable: true,
sortable: true,
header: 'Database',
fieldName: 'name',
type: 'data',
},
],
},
};
},
async writeTable(dbhan, name, options) {
// @ts-ignore
return createBulkInsertStreamBase(this, stream, dbhan, name, options);

View File

@@ -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',

View File

@@ -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 = [
{

View File

@@ -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,
};

View File

@@ -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
`;

View File

@@ -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"
`;

View File

@@ -0,0 +1,5 @@
module.exports = `
SELECT "name" AS "variable", "setting" AS "value"
FROM "pg_settings"
ORDER BY "name"
`;

View File

@@ -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,