mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-03 08:13:57 +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,
|
serverSummary_meta: true,
|
||||||
async serverSummary({ conid }, req) {
|
async serverSummary({ conid }, req) {
|
||||||
|
logger.info({ conid }, 'DBGM-00260 Processing server summary');
|
||||||
testConnectionPermission(conid, req);
|
testConnectionPermission(conid, req);
|
||||||
return this.loadDataCore('serverSummary', { conid });
|
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,
|
summaryCommand_meta: true,
|
||||||
async summaryCommand({ conid, command, row }, req) {
|
async summaryCommand({ conid, command, row }, req) {
|
||||||
testConnectionPermission(conid, req);
|
testConnectionPermission(conid, req);
|
||||||
|
|||||||
@@ -146,6 +146,30 @@ async function handleServerSummary({ msgid }) {
|
|||||||
return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan));
|
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 }) {
|
async function handleSummaryCommand({ msgid, command, row }) {
|
||||||
return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row));
|
return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row));
|
||||||
}
|
}
|
||||||
@@ -154,6 +178,8 @@ const messageHandlers = {
|
|||||||
connect: handleConnect,
|
connect: handleConnect,
|
||||||
ping: handlePing,
|
ping: handlePing,
|
||||||
serverSummary: handleServerSummary,
|
serverSummary: handleServerSummary,
|
||||||
|
killDatabaseProcess: handleKillDatabaseProcess,
|
||||||
|
listDatabaseProcesses: handleListDatabaseProcesses,
|
||||||
summaryCommand: handleSummaryCommand,
|
summaryCommand: handleSummaryCommand,
|
||||||
createDatabase: props => handleDatabaseOp('createDatabase', props),
|
createDatabase: props => handleDatabaseOp('createDatabase', props),
|
||||||
dropDatabase: props => handleDatabaseOp('dropDatabase', 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;
|
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 {
|
export interface SqlBackupDumper {
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SummaryColumn {
|
export interface ServerSummaryDatabases {
|
||||||
fieldName: string;
|
rows: any[];
|
||||||
header: string;
|
columns: SummaryDatabaseColumn[];
|
||||||
dataType: 'string' | 'number' | 'bytes';
|
|
||||||
}
|
}
|
||||||
export interface ServerSummaryDatabase {}
|
|
||||||
|
export type SummaryDatabaseColumn = {
|
||||||
|
header: string;
|
||||||
|
fieldName: string;
|
||||||
|
type: 'data' | 'fileSize';
|
||||||
|
filterable?: boolean;
|
||||||
|
sortable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ServerSummary {
|
export interface ServerSummary {
|
||||||
columns: SummaryColumn[];
|
processes: DatabaseProcess[];
|
||||||
databases: ServerSummaryDatabase[];
|
variables: DatabaseVariable[];
|
||||||
|
databases: ServerSummaryDatabases;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
|
export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
|
||||||
@@ -161,12 +188,12 @@ export interface FilterBehaviourProvider {
|
|||||||
getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour;
|
getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseHandle<TClient = any> {
|
export interface DatabaseHandle<TClient = any, TDataBase = any> {
|
||||||
client: TClient;
|
client: TClient;
|
||||||
database?: string;
|
database?: string;
|
||||||
conid?: string;
|
conid?: string;
|
||||||
feedback?: (message: any) => void;
|
feedback?: (message: any) => void;
|
||||||
getDatabase?: () => any;
|
getDatabase?: () => TDataBase;
|
||||||
connectionType?: string;
|
connectionType?: string;
|
||||||
treeKeySeparator?: string;
|
treeKeySeparator?: string;
|
||||||
}
|
}
|
||||||
@@ -196,7 +223,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase {
|
|||||||
inputFile: string;
|
inputFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBehaviourProvider {
|
||||||
engine: string;
|
engine: string;
|
||||||
title: string;
|
title: string;
|
||||||
defaultPort?: number;
|
defaultPort?: number;
|
||||||
@@ -242,61 +269,88 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
|||||||
defaultSocketPath?: string;
|
defaultSocketPath?: string;
|
||||||
authTypeLabel?: string;
|
authTypeLabel?: string;
|
||||||
importExportArgs?: any[];
|
importExportArgs?: any[];
|
||||||
connect({ server, port, user, password, database, connectionDefinition }): Promise<DatabaseHandle<TClient>>;
|
connect({
|
||||||
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
server,
|
||||||
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
port,
|
||||||
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
user,
|
||||||
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
password,
|
||||||
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
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
|
// eg. PostgreSQL COPY FROM stdin
|
||||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<StreamResult>;
|
writeQueryFromStream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string): Promise<StreamResult>;
|
||||||
writeTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo, options: WriteTableOptions): Promise<StreamResult>;
|
writeTable(
|
||||||
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
|
name: NamedObjectInfo,
|
||||||
|
options: WriteTableOptions
|
||||||
|
): Promise<StreamResult>;
|
||||||
analyseSingleObject(
|
analyseSingleObject(
|
||||||
dbhan: DatabaseHandle<TClient>,
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
name: NamedObjectInfo,
|
name: NamedObjectInfo,
|
||||||
objectTypeField: keyof DatabaseInfo
|
objectTypeField: keyof DatabaseInfo
|
||||||
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
|
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
|
||||||
analyseSingleTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo): Promise<TableInfo>;
|
analyseSingleTable(dbhan: DatabaseHandle<TClient, TDataBase>, name: NamedObjectInfo): Promise<TableInfo>;
|
||||||
getVersion(dbhan: DatabaseHandle<TClient>): Promise<{ version: string; versionText?: string }>;
|
getVersion(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<{ version: string; versionText?: string }>;
|
||||||
listDatabases(dbhan: DatabaseHandle<TClient>): Promise<
|
listDatabases(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
|
sizeOnDisk?: number;
|
||||||
|
empty?: boolean;
|
||||||
}[]
|
}[]
|
||||||
>;
|
>;
|
||||||
loadKeys(dbhan: DatabaseHandle<TClient>, root: string, filter?: string): Promise;
|
loadKeys(dbhan: DatabaseHandle<TClient, TDataBase>, root: string, filter?: string): Promise;
|
||||||
scanKeys(dbhan: DatabaseHandle<TClient>, root: string, pattern: string, cursor: string, count: number): Promise;
|
scanKeys(
|
||||||
exportKeys(dbhan: DatabaseHandle<TClient>, options: {}): Promise;
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
loadKeyInfo(dbhan: DatabaseHandle<TClient>, key): Promise;
|
root: string,
|
||||||
loadKeyTableRange(dbhan: DatabaseHandle<TClient>, key, cursor, count): Promise;
|
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(
|
loadFieldValues(
|
||||||
dbhan: DatabaseHandle<TClient>,
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
name: NamedObjectInfo,
|
name: NamedObjectInfo,
|
||||||
field: string,
|
field: string,
|
||||||
search: string,
|
search: string,
|
||||||
dataType: string
|
dataType: string
|
||||||
): Promise;
|
): Promise;
|
||||||
analyseFull(dbhan: DatabaseHandle<TClient>, serverVersion): Promise<DatabaseInfo>;
|
analyseFull(dbhan: DatabaseHandle<TClient, TDataBase>, serverVersion): Promise<DatabaseInfo>;
|
||||||
analyseIncremental(dbhan: DatabaseHandle<TClient>, structure: DatabaseInfo, serverVersion): Promise<DatabaseInfo>;
|
analyseIncremental(
|
||||||
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
|
structure: DatabaseInfo,
|
||||||
|
serverVersion
|
||||||
|
): Promise<DatabaseInfo>;
|
||||||
dialect: SqlDialect;
|
dialect: SqlDialect;
|
||||||
dialectByVersion(version): SqlDialect;
|
dialectByVersion(version): SqlDialect;
|
||||||
createDumper(options = null): SqlDumper;
|
createDumper(options = null): SqlDumper;
|
||||||
createBackupDumper(dbhan: DatabaseHandle<TClient>, options): Promise<SqlBackupDumper>;
|
createBackupDumper(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<SqlBackupDumper>;
|
||||||
getAuthTypes(): EngineAuthType[];
|
getAuthTypes(): EngineAuthType[];
|
||||||
readCollection(dbhan: DatabaseHandle<TClient>, options: ReadCollectionOptions): Promise<any>;
|
readCollection(dbhan: DatabaseHandle<TClient, TDataBase>, options: ReadCollectionOptions): Promise<any>;
|
||||||
updateCollection(dbhan: DatabaseHandle<TClient>, changeSet: any): Promise<any>;
|
updateCollection(dbhan: DatabaseHandle<TClient, TDataBase>, changeSet: any): Promise<any>;
|
||||||
getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string;
|
getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string;
|
||||||
createDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
createDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||||
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
dropDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||||
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
||||||
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
|
script(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: RunScriptOptions): Promise;
|
||||||
operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
|
operation(
|
||||||
|
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||||
|
operation: CollectionOperationInfo,
|
||||||
|
options?: RunScriptOptions
|
||||||
|
): Promise;
|
||||||
getNewObjectTemplates(): NewObjectTemplate[];
|
getNewObjectTemplates(): NewObjectTemplate[];
|
||||||
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
||||||
callMethod(dbhan: DatabaseHandle<TClient>, method, args);
|
callMethod(dbhan: DatabaseHandle<TClient, TDataBase>, method, args);
|
||||||
serverSummary(dbhan: DatabaseHandle<TClient>): Promise<ServerSummary>;
|
serverSummary(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<ServerSummary>;
|
||||||
summaryCommand(dbhan: DatabaseHandle<TClient>, command, row): Promise<void>;
|
summaryCommand(dbhan: DatabaseHandle<TClient, TDataBase>, command, row): Promise<void>;
|
||||||
startProfiler(dbhan: DatabaseHandle<TClient>, options): Promise<any>;
|
startProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<any>;
|
||||||
stopProfiler(dbhan: DatabaseHandle<TClient>, profiler): Promise<void>;
|
stopProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, profiler): Promise<void>;
|
||||||
getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>;
|
getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>;
|
||||||
getAuthTokenFromCode(connection, options): Promise<string>;
|
getAuthTokenFromCode(connection, options): Promise<string>;
|
||||||
getAccessTokenFromAuth(connection, req): Promise<string | null>;
|
getAccessTokenFromAuth(connection, req): Promise<string | null>;
|
||||||
@@ -313,7 +367,10 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
|||||||
adaptTableInfo(table: TableInfo): TableInfo;
|
adaptTableInfo(table: TableInfo): TableInfo;
|
||||||
// simple data type adapter
|
// simple data type adapter
|
||||||
adaptDataType(dataType: string): string;
|
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(
|
backupDatabaseCommand(
|
||||||
connection: any,
|
connection: any,
|
||||||
settings: BackupDatabaseSettings,
|
settings: BackupDatabaseSettings,
|
||||||
@@ -337,7 +394,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
|||||||
analyserClass?: any;
|
analyserClass?: any;
|
||||||
dumperClass?: any;
|
dumperClass?: any;
|
||||||
singleConnectionOnly?: boolean;
|
singleConnectionOnly?: boolean;
|
||||||
getLogDbInfo(dbhan: DatabaseHandle<TClient>): {
|
getLogDbInfo(dbhan: DatabaseHandle<TClient, TDataBase>): {
|
||||||
database?: string;
|
database?: string;
|
||||||
engine: string;
|
engine: string;
|
||||||
conid?: 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 menu = null;
|
||||||
export let isInline = false;
|
export let isInline = false;
|
||||||
export let containerMaxWidth = undefined;
|
export let containerMaxWidth = undefined;
|
||||||
|
export let containerMaxHeight = undefined;
|
||||||
export let flex1 = true;
|
export let flex1 = true;
|
||||||
|
export let flexColContainer = true;
|
||||||
|
export let maxHeight100 = false;
|
||||||
export let contentTestId = undefined;
|
export let contentTestId = undefined;
|
||||||
export let inlineTabs = false;
|
export let inlineTabs = false;
|
||||||
export let onUserChange = null;
|
export let onUserChange = null;
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="main" class:flex1>
|
<div class="main" class:maxHeight100 class:flex1>
|
||||||
<div class="tabs" class:inlineTabs>
|
<div class="tabs" class:inlineTabs>
|
||||||
{#each _.compact(tabs) as tab, index}
|
{#each _.compact(tabs) as tab, index}
|
||||||
<div
|
<div
|
||||||
@@ -50,10 +53,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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}
|
{#each _.compact(tabs) as tab, index}
|
||||||
<div class="container" class:isInline class:tabVisible={index == value} style:max-width={containerMaxWidth}>
|
<div
|
||||||
<svelte:component this={tab.component} {...tab.props} tabControlHiddenTab={index != value} />
|
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 != null}
|
||||||
{#if tab.slot == 0}<slot name="0" />
|
{#if tab.slot == 0}<slot name="0" />
|
||||||
{:else if tab.slot == 1}<slot name="1" />
|
{:else if tab.slot == 1}<slot name="1" />
|
||||||
@@ -83,6 +98,10 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main.maxHeight100 {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: var(--dim-tabs-height);
|
height: var(--dim-tabs-height);
|
||||||
@@ -132,6 +151,15 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.maxHeight100 {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.flexColContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.container:not(.isInline) {
|
.container:not(.isInline) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -350,7 +350,7 @@
|
|||||||
{#if col.component}
|
{#if col.component}
|
||||||
<svelte:component this={col.component} {...rowProps} />
|
<svelte:component this={col.component} {...rowProps} />
|
||||||
{:else if col.formatter}
|
{:else if col.formatter}
|
||||||
{col.formatter(row)}
|
{col.formatter(row, col)}
|
||||||
{:else if col.slot != null}
|
{:else if col.slot != null}
|
||||||
{#key row[col.slotKey] || 'key'}
|
{#key row[col.slotKey] || 'key'}
|
||||||
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
|
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
export let key, value, isParentExpanded, isParentArray;
|
export let key, value, isParentExpanded, isParentArray;
|
||||||
export let expanded = false;
|
export let expanded = false;
|
||||||
|
export let labelOverride = null;
|
||||||
|
export let hideKey = false;
|
||||||
const filteredKey = new Set(['length']);
|
const filteredKey = new Set(['length']);
|
||||||
|
|
||||||
$: keys = Object.getOwnPropertyNames(value);
|
$: keys = Object.getOwnPropertyNames(value);
|
||||||
@@ -22,8 +24,10 @@
|
|||||||
{keys}
|
{keys}
|
||||||
{previewKeys}
|
{previewKeys}
|
||||||
{getValue}
|
{getValue}
|
||||||
label="Array({value.length})"
|
label={labelOverride || `Array(${value.length})`}
|
||||||
bracketOpen="["
|
bracketOpen="["
|
||||||
bracketClose="]"
|
bracketClose="]"
|
||||||
elementValue={value}
|
elementValue={value}
|
||||||
|
{labelOverride}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import JSONNested from './JSONNested.svelte';
|
import JSONNested from './JSONNested.svelte';
|
||||||
|
|
||||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||||
|
export let labelOverride = null;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
let keys = [];
|
let keys = [];
|
||||||
|
|
||||||
@@ -29,7 +31,9 @@
|
|||||||
{getKey}
|
{getKey}
|
||||||
{getValue}
|
{getValue}
|
||||||
isArray={true}
|
isArray={true}
|
||||||
label="{nodeType}({keys.length})"
|
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||||
bracketOpen={'{'}
|
bracketOpen={'{'}
|
||||||
bracketClose={'}'}
|
bracketClose={'}'}
|
||||||
|
{labelOverride}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import MapEntry from './utils/MapEntry'
|
import MapEntry from './utils/MapEntry'
|
||||||
|
|
||||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||||
|
export let labelOverride = null;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
let keys = [];
|
let keys = [];
|
||||||
|
|
||||||
@@ -28,8 +30,10 @@
|
|||||||
{keys}
|
{keys}
|
||||||
{getKey}
|
{getKey}
|
||||||
{getValue}
|
{getValue}
|
||||||
label="{nodeType}({keys.length})"
|
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||||
colon=""
|
colon=""
|
||||||
bracketOpen={'{'}
|
bracketOpen={'{'}
|
||||||
bracketClose={'}'}
|
bracketClose={'}'}
|
||||||
|
{labelOverride}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
export let key, value, isParentExpanded, isParentArray;
|
export let key, value, isParentExpanded, isParentArray;
|
||||||
export let expanded = false;
|
export let expanded = false;
|
||||||
|
export let hideKey = false;
|
||||||
|
export let labelOverride = null;
|
||||||
|
|
||||||
const keys = ['key', 'value'];
|
const keys = ['key', 'value'];
|
||||||
|
|
||||||
@@ -17,7 +19,9 @@
|
|||||||
key={isParentExpanded ? String(key) : value.key}
|
key={isParentExpanded ? String(key) : value.key}
|
||||||
{keys}
|
{keys}
|
||||||
{getValue}
|
{getValue}
|
||||||
label={isParentExpanded ? 'Entry ' : '=> '}
|
label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')}
|
||||||
bracketOpen={'{'}
|
bracketOpen={'{'}
|
||||||
bracketClose={'}'}
|
bracketClose={'}'}
|
||||||
|
{labelOverride}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
@@ -21,11 +21,14 @@
|
|||||||
expandable = true;
|
expandable = true;
|
||||||
export let elementValue = null;
|
export let elementValue = null;
|
||||||
export let onRootExpandedChanged = null;
|
export let onRootExpandedChanged = null;
|
||||||
|
export let labelOverride = null;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
const context = getContext('json-tree-context-key');
|
const context = getContext('json-tree-context-key');
|
||||||
setContext('json-tree-context-key', { ...context, colon });
|
setContext('json-tree-context-key', { ...context, colon });
|
||||||
const elementData = getContext('json-tree-element-data');
|
const elementData = getContext('json-tree-element-data');
|
||||||
const slicedKeyCount = getContext('json-tree-sliced-key-count');
|
const slicedKeyCount = getContext('json-tree-sliced-key-count');
|
||||||
|
const keyLabel = labelOverride ?? key;
|
||||||
|
|
||||||
$: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5);
|
$: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5);
|
||||||
|
|
||||||
@@ -56,7 +59,16 @@
|
|||||||
{#if expandable && isParentExpanded}
|
{#if expandable && isParentExpanded}
|
||||||
<JSONArrow on:click={toggleExpand} {expanded} />
|
<JSONArrow on:click={toggleExpand} {expanded} />
|
||||||
{/if}
|
{/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>
|
<span on:click={toggleExpand}><span>{label}</span>{bracketOpen}</span>
|
||||||
</label>
|
</label>
|
||||||
{#if isParentExpanded}
|
{#if isParentExpanded}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
export let expanded = !!getContext('json-tree-default-expanded');
|
export let expanded = !!getContext('json-tree-default-expanded');
|
||||||
export let labelOverride = null;
|
export let labelOverride = null;
|
||||||
export let onRootExpandedChanged = null;
|
export let onRootExpandedChanged = null;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
$: nodeType = objType(value);
|
$: nodeType = objType(value);
|
||||||
$: componentType = getComponent(nodeType);
|
$: componentType = getComponent(nodeType);
|
||||||
@@ -85,4 +86,5 @@
|
|||||||
{expanded}
|
{expanded}
|
||||||
{labelOverride}
|
{labelOverride}
|
||||||
{onRootExpandedChanged}
|
{onRootExpandedChanged}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
export let expanded = false;
|
export let expanded = false;
|
||||||
export let labelOverride = null;
|
export let labelOverride = null;
|
||||||
export let onRootExpandedChanged = null;
|
export let onRootExpandedChanged = null;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
$: keys = Object.getOwnPropertyNames(value);
|
$: keys = Object.getOwnPropertyNames(value);
|
||||||
|
|
||||||
@@ -26,4 +27,5 @@
|
|||||||
bracketClose={'}'}
|
bracketClose={'}'}
|
||||||
elementValue={value}
|
elementValue={value}
|
||||||
{onRootExpandedChanged}
|
{onRootExpandedChanged}
|
||||||
|
{hideKey}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import JSONNode from './JSONNode.svelte';
|
import JSONNode from './JSONNode.svelte';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
||||||
import openNewTab from '../utility/openNewTab';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { copyTextToClipboard } from '../utility/clipboard';
|
import { copyTextToClipboard } from '../utility/clipboard';
|
||||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||||
@@ -23,6 +22,7 @@
|
|||||||
export let isDeleted = false;
|
export let isDeleted = false;
|
||||||
export let isInserted = false;
|
export let isInserted = false;
|
||||||
export let isModified = false;
|
export let isModified = false;
|
||||||
|
export let hideKey = false;
|
||||||
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
|
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
class:wrap
|
class:wrap
|
||||||
>
|
>
|
||||||
<JSONNode
|
<JSONNode
|
||||||
|
{hideKey}
|
||||||
{key}
|
{key}
|
||||||
{value}
|
{value}
|
||||||
isParentExpanded={true}
|
isParentExpanded={true}
|
||||||
|
|||||||
@@ -3,10 +3,25 @@
|
|||||||
|
|
||||||
import JSONKey from './JSONKey.svelte';
|
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');
|
const { colon } = getContext('json-tree-context-key');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<li class:indent={isParentExpanded}>
|
||||||
|
<JSONKey key={label} {colon} {isParentExpanded} {isParentArray} />
|
||||||
|
<span class={nodeType}>
|
||||||
|
{valueGetter ? valueGetter(value) : value}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
li {
|
li {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
@@ -45,9 +60,4 @@
|
|||||||
color: var(--symbol-color);
|
color: var(--symbol-color);
|
||||||
}
|
}
|
||||||
</style>
|
</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'
|
'connectionAppObjectSearchSettings2'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab');
|
||||||
|
|
||||||
let currentThemeValue = null;
|
let currentThemeValue = null;
|
||||||
currentTheme.subscribe(value => {
|
currentTheme.subscribe(value => {
|
||||||
currentThemeValue = value;
|
currentThemeValue = value;
|
||||||
|
|||||||
@@ -18,17 +18,22 @@
|
|||||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||||
import registerCommand from '../commands/registerCommand';
|
import registerCommand from '../commands/registerCommand';
|
||||||
import Link from '../elements/Link.svelte';
|
|
||||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||||
|
import TabControl from '../elements/TabControl.svelte';
|
||||||
|
|
||||||
import ObjectListControl from '../elements/ObjectListControl.svelte';
|
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
import formatFileSize from '../utility/formatFileSize';
|
|
||||||
import openNewTab from '../utility/openNewTab';
|
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;
|
export let conid;
|
||||||
|
const tabid = getContext('tabid');
|
||||||
|
$: isActiveTab = tabid === $activeTabId;
|
||||||
|
|
||||||
let refreshToken = 0;
|
let refreshToken = 0;
|
||||||
|
|
||||||
@@ -75,30 +80,54 @@
|
|||||||
|
|
||||||
<ToolStripContainer>
|
<ToolStripContainer>
|
||||||
{#await apiCall('server-connections/server-summary', { conid, refreshToken })}
|
{#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}
|
{:then summary}
|
||||||
<div class="wrapper">
|
{#if 'errorMessage' in summary}
|
||||||
<ObjectListControl
|
<div class="wrapper error-wrapper">
|
||||||
collection={summary.databases}
|
<div class="error-message">
|
||||||
hideDisplayName
|
<h3>{_t('serverSummaryTab.errorTitle', { defaultMessage: 'Error loading server summary' })}</h3>
|
||||||
title={`Databases (${summary.databases.length})`}
|
<p>{summary.errorMessage}</p>
|
||||||
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>
|
</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}
|
{/await}
|
||||||
|
|
||||||
<svelte:fragment slot="toolstrip">
|
<svelte:fragment slot="toolstrip">
|
||||||
@@ -114,10 +143,33 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: var(--theme-bg-0);
|
background-color: var(--theme-bg-0);
|
||||||
overflow: auto;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-separator {
|
.action-separator {
|
||||||
margin: 0 5px;
|
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>
|
</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 isPromise = require('is-promise');
|
||||||
const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb');
|
const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb');
|
||||||
const { EJSON } = require('bson');
|
const { EJSON } = require('bson');
|
||||||
const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse } = require('dbgate-tools');
|
const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse, getLogger } = require('dbgate-tools');
|
||||||
const createBulkInsertStream = require('./createBulkInsertStream');
|
const createBulkInsertStream = require('./createBulkInsertStream');
|
||||||
const {
|
const {
|
||||||
convertToMongoCondition,
|
convertToMongoCondition,
|
||||||
@@ -16,6 +16,8 @@ const {
|
|||||||
|
|
||||||
let isProApp;
|
let isProApp;
|
||||||
|
|
||||||
|
const logger = getLogger('mongoDriver');
|
||||||
|
|
||||||
function serializeMongoData(row) {
|
function serializeMongoData(row) {
|
||||||
return EJSON.serialize(
|
return EJSON.serialize(
|
||||||
serializeJsTypesForJsonStringify(row, (value) => {
|
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 = {
|
const driver = {
|
||||||
...driverBase,
|
...driverBase,
|
||||||
analyserClass: Analyser,
|
analyserClass: Analyser,
|
||||||
@@ -193,7 +195,10 @@ const driver = {
|
|||||||
let exprValue;
|
let exprValue;
|
||||||
|
|
||||||
try {
|
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);
|
const runtime = new ElectronRuntime(serviceProvider);
|
||||||
await runtime.evaluate(`use ${dbhan.database}`);
|
await runtime.evaluate(`use ${dbhan.database}`);
|
||||||
exprValue = await runtime.evaluate(sql);
|
exprValue = await runtime.evaluate(sql);
|
||||||
@@ -629,86 +634,107 @@ const driver = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async serverSummary(dbhan) {
|
async serverSummary(dbhan) {
|
||||||
const res = await dbhan.getDatabase().admin().listDatabases();
|
const [processes, variables, databases] = await Promise.all([
|
||||||
const profiling = await Promise.all(res.databases.map((x) => dbhan.client.db(x.name).command({ profile: -1 })));
|
this.listProcesses(dbhan),
|
||||||
|
this.listVariables(dbhan),
|
||||||
|
this.listDatabases(dbhan),
|
||||||
|
]);
|
||||||
|
|
||||||
function formatProfiling(info) {
|
/** @type {import('dbgate-types').ServerSummary} */
|
||||||
switch (info.was) {
|
const data = {
|
||||||
case 0:
|
processes,
|
||||||
return 'No profiling';
|
variables,
|
||||||
case 1:
|
databases: {
|
||||||
return `Filtered (>${info.slowms} ms)`;
|
rows: databases,
|
||||||
case 2:
|
|
||||||
return 'Profile all';
|
|
||||||
default:
|
|
||||||
return '???';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
fieldName: 'name',
|
filterable: true,
|
||||||
columnType: 'string',
|
sortable: true,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
fieldName: 'name',
|
||||||
|
type: 'data',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
sortable: true,
|
||||||
|
header: 'Size on disk',
|
||||||
fieldName: 'sizeOnDisk',
|
fieldName: 'sizeOnDisk',
|
||||||
columnType: 'bytes',
|
type: 'fileSize',
|
||||||
header: 'Size',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'profiling',
|
filterable: true,
|
||||||
columnType: 'string',
|
sortable: true,
|
||||||
header: 'Profiling',
|
header: 'Empty',
|
||||||
},
|
fieldName: 'empty',
|
||||||
{
|
type: 'data',
|
||||||
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]),
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async close(dbhan) {
|
async close(dbhan) {
|
||||||
return dbhan.client.close();
|
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) => {
|
driver.initialize = (dbgateEnv) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const lock = new AsyncLock();
|
|||||||
const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver');
|
const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver');
|
||||||
const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = require('./nativeDriver');
|
const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = require('./nativeDriver');
|
||||||
const { getLogger } = global.DBGATE_PACKAGES['dbgate-tools'];
|
const { getLogger } = global.DBGATE_PACKAGES['dbgate-tools'];
|
||||||
|
const sql = require('./sql');
|
||||||
|
|
||||||
const logger = getLogger('mssqlDriver');
|
const logger = getLogger('mssqlDriver');
|
||||||
|
|
||||||
@@ -148,9 +149,89 @@ const driver = {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
async listDatabases(dbhan) {
|
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;
|
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) {
|
getRedirectAuthUrl(connection, options) {
|
||||||
if (connection.authType != 'msentra') return null;
|
if (connection.authType != 'msentra') return null;
|
||||||
return authProxy.authProxyGetRedirectUrl({
|
return authProxy.authProxyGetRedirectUrl({
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ const viewColumns = require('./viewColumns');
|
|||||||
const indexes = require('./indexes');
|
const indexes = require('./indexes');
|
||||||
const indexcols = require('./indexcols');
|
const indexcols = require('./indexcols');
|
||||||
const triggers = require('./triggers');
|
const triggers = require('./triggers');
|
||||||
|
const listVariables = require('./listVariables');
|
||||||
|
const listDatabases = require('./listDatabases');
|
||||||
|
const listProcesses = require('./listProcesses');
|
||||||
const baseColumns = require('./baseColumns');
|
const baseColumns = require('./baseColumns');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -31,5 +34,8 @@ module.exports = {
|
|||||||
indexcols,
|
indexcols,
|
||||||
tableSizes,
|
tableSizes,
|
||||||
triggers,
|
triggers,
|
||||||
|
listVariables,
|
||||||
|
listDatabases,
|
||||||
|
listProcesses,
|
||||||
baseColumns,
|
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} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const driver = {
|
const driver = {
|
||||||
...driverBase,
|
...driverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
dumperClass: MsSqlDumper,
|
dumperClass: MsSqlDumper,
|
||||||
dialect,
|
dialect,
|
||||||
readOnlySessions: false,
|
readOnlySessions: false,
|
||||||
|
|||||||
@@ -200,6 +200,69 @@ const drivers = driverBases.map(driverBase => ({
|
|||||||
const { rows } = await this.query(dbhan, 'show databases');
|
const { rows } = await this.query(dbhan, 'show databases');
|
||||||
return rows.map(x => ({ name: x.Database }));
|
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) {
|
async writeTable(dbhan, name, options) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return createBulkInsertStreamBase(this, stream, dbhan, name, options);
|
return createBulkInsertStreamBase(this, stream, dbhan, name, options);
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ const mysqlDriverBase = {
|
|||||||
/** @type {import('dbgate-types').EngineDriver} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const mysqlDriver = {
|
const mysqlDriver = {
|
||||||
...mysqlDriverBase,
|
...mysqlDriverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
dialect: mysqlDialect,
|
dialect: mysqlDialect,
|
||||||
engine: 'mysql@dbgate-plugin-mysql',
|
engine: 'mysql@dbgate-plugin-mysql',
|
||||||
title: 'MySQL',
|
title: 'MySQL',
|
||||||
@@ -425,6 +426,7 @@ const mariaDbDialect = {
|
|||||||
/** @type {import('dbgate-types').EngineDriver} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const mariaDriver = {
|
const mariaDriver = {
|
||||||
...mysqlDriverBase,
|
...mysqlDriverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
dialect: mariaDbDialect,
|
dialect: mariaDbDialect,
|
||||||
engine: 'mariadb@dbgate-plugin-mysql',
|
engine: 'mariadb@dbgate-plugin-mysql',
|
||||||
title: 'MariaDB',
|
title: 'MariaDB',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const Analyser = require('./Analyser');
|
|||||||
const wkx = require('wkx');
|
const wkx = require('wkx');
|
||||||
const pg = require('pg');
|
const pg = require('pg');
|
||||||
const pgCopyStreams = require('pg-copy-streams');
|
const pgCopyStreams = require('pg-copy-streams');
|
||||||
|
const sql = require('./sql');
|
||||||
const {
|
const {
|
||||||
getLogger,
|
getLogger,
|
||||||
createBulkInsertStreamBase,
|
createBulkInsertStreamBase,
|
||||||
@@ -351,11 +352,65 @@ const drivers = driverBases.map(driverBase => ({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return createBulkInsertStreamBase(this, stream, dbhan, name, options);
|
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) {
|
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;
|
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() {
|
getAuthTypes() {
|
||||||
const res = [
|
const res = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const geographyColumns = require('./geographyColumns');
|
|||||||
const proceduresParameters = require('./proceduresParameters');
|
const proceduresParameters = require('./proceduresParameters');
|
||||||
const foreignKeys = require('./foreignKeys');
|
const foreignKeys = require('./foreignKeys');
|
||||||
const triggers = require('./triggers');
|
const triggers = require('./triggers');
|
||||||
|
const listDatabases = require('./listDatabases');
|
||||||
|
const listVariables = require('./listVariables');
|
||||||
|
const listProcesses = require('./listProcesses');
|
||||||
|
|
||||||
const fk_keyColumnUsage = require('./fk_key_column_usage');
|
const fk_keyColumnUsage = require('./fk_key_column_usage');
|
||||||
|
|
||||||
@@ -41,4 +44,7 @@ module.exports = {
|
|||||||
geographyColumns,
|
geographyColumns,
|
||||||
proceduresParameters,
|
proceduresParameters,
|
||||||
triggers,
|
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} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const postgresDriver = {
|
const postgresDriver = {
|
||||||
...postgresDriverBase,
|
...postgresDriverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
engine: 'postgres@dbgate-plugin-postgres',
|
engine: 'postgres@dbgate-plugin-postgres',
|
||||||
title: 'PostgreSQL',
|
title: 'PostgreSQL',
|
||||||
defaultPort: 5432,
|
defaultPort: 5432,
|
||||||
@@ -388,6 +389,7 @@ const postgresDriver = {
|
|||||||
/** @type {import('dbgate-types').EngineDriver} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const cockroachDriver = {
|
const cockroachDriver = {
|
||||||
...postgresDriverBase,
|
...postgresDriverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
engine: 'cockroach@dbgate-plugin-postgres',
|
engine: 'cockroach@dbgate-plugin-postgres',
|
||||||
title: 'CockroachDB',
|
title: 'CockroachDB',
|
||||||
defaultPort: 26257,
|
defaultPort: 26257,
|
||||||
@@ -403,6 +405,7 @@ const cockroachDriver = {
|
|||||||
/** @type {import('dbgate-types').EngineDriver} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const redshiftDriver = {
|
const redshiftDriver = {
|
||||||
...postgresDriverBase,
|
...postgresDriverBase,
|
||||||
|
supportsServerSummary: true,
|
||||||
dialect: {
|
dialect: {
|
||||||
...dialect,
|
...dialect,
|
||||||
stringAgg: false,
|
stringAgg: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user