mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
Merge pull request #1186 from dbgate/feature/mongo-server-summary
Feature/mongo server summary
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
145
packages/types/engines.d.ts
vendored
145
packages/types/engines.d.ts
vendored
@@ -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;
|
||||
|
||||
46
packages/web/src/buttons/CtaButton.svelte
Normal file
46
packages/web/src/buttons/CtaButton.svelte
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ export const connectionAppObjectSearchSettings = writableWithStorage(
|
||||
'connectionAppObjectSearchSettings2'
|
||||
);
|
||||
|
||||
export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab');
|
||||
|
||||
let currentThemeValue = null;
|
||||
currentTheme.subscribe(value => {
|
||||
currentThemeValue = value;
|
||||
|
||||
@@ -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>
|
||||
|
||||
36
packages/web/src/widgets/SummaryDatabases.svelte
Normal file
36
packages/web/src/widgets/SummaryDatabases.svelte
Normal 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>
|
||||
206
packages/web/src/widgets/SummaryProcesses.svelte
Normal file
206
packages/web/src/widgets/SummaryProcesses.svelte
Normal 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>
|
||||
44
packages/web/src/widgets/SummaryVariables.svelte
Normal file
44
packages/web/src/widgets/SummaryVariables.svelte
Normal 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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
17
plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js
Normal file
17
plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js
Normal 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
|
||||
`;
|
||||
11
plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js
Normal file
11
plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js
Normal 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
|
||||
`;
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = `
|
||||
SELECT name as variable, value FROM sys.configurations ORDER BY name
|
||||
`;
|
||||
@@ -131,6 +131,7 @@ const dialect = {
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const driver = {
|
||||
...driverBase,
|
||||
supportsServerSummary: true,
|
||||
dumperClass: MsSqlDumper,
|
||||
dialect,
|
||||
readOnlySessions: false,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
@@ -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"
|
||||
`;
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = `
|
||||
SELECT "name" AS "variable", "setting" AS "value"
|
||||
FROM "pg_settings"
|
||||
ORDER BY "name"
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user