Merge branch 'summary'

This commit is contained in:
Jan Prochazka 2022-12-09 15:49:22 +01:00
commit a1032138da
10 changed files with 329 additions and 36 deletions

View File

@ -1,6 +1,7 @@
const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const uuidv1 = require('uuid/v1');
const _ = require('lodash');
const AsyncLock = require('async-lock');
const { handleProcessCommunication } = require('../utility/processComm');
@ -13,6 +14,7 @@ module.exports = {
opened: [],
closed: {},
lastPinged: {},
requests: {},
handle_databases(conid, { databases }) {
const existing = this.opened.find(x => x.conid == conid);
@ -33,6 +35,11 @@ module.exports = {
socket.emitChanged(`server-status-changed`);
},
handle_ping() {},
handle_response(conid, { msgid, ...response }) {
const [resolve, reject] = this.requests[msgid];
resolve(response);
delete this.requests[msgid];
},
async ensureOpened(conid) {
const res = await lock.acquire(conid, async () => {
@ -161,4 +168,41 @@ module.exports = {
opened.subprocess.send({ msgtype: 'dropDatabase', name });
return { status: 'ok' };
},
sendRequest(conn, message) {
const msgid = uuidv1();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject];
conn.subprocess.send({ msgid, ...message });
});
return promise;
},
async loadDataCore(msgtype, { conid, ...args }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
const res = await this.sendRequest(opened, { msgtype, ...args });
if (res.errorMessage) {
console.error(res.errorMessage);
return {
errorMessage: res.errorMessage,
};
}
return res.result || null;
},
serverSummary_meta: true,
async serverSummary({ conid }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('serverSummary', { conid });
},
summaryCommand_meta: true,
async summaryCommand({ conid, command, row }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (opened.connection.isReadOnly) return false;
return this.loadDataCore('summaryCommand', { conid, command, row });
},
};

View File

@ -10,6 +10,7 @@ let storedConnection;
let lastDatabases = null;
let lastStatus = null;
let lastPing = null;
let afterConnectCallbacks = [];
async function handleRefresh() {
const driver = requireEngineDriver(storedConnection);
@ -74,6 +75,18 @@ async function handleConnect(connection) {
// console.error(err);
setTimeout(() => process.exit(1), 1000);
}
for (const [resolve] of afterConnectCallbacks) {
resolve();
}
afterConnectCallbacks = [];
}
function waitConnected() {
if (systemConnection) return Promise.resolve();
return new Promise((resolve, reject) => {
afterConnectCallbacks.push([resolve, reject]);
});
}
function handlePing() {
@ -94,9 +107,30 @@ async function handleDatabaseOp(op, { name }) {
await handleRefresh();
}
async function handleDriverDataCore(msgid, callMethod) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
const result = await callMethod(driver);
process.send({ msgtype: 'response', msgid, result });
} catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
}
}
async function handleServerSummary({ msgid }) {
return handleDriverDataCore(msgid, driver => driver.serverSummary(systemConnection));
}
async function handleSummaryCommand({ msgid, command, row }) {
return handleDriverDataCore(msgid, driver => driver.summaryCommand(systemConnection, command, row));
}
const messageHandlers = {
connect: handleConnect,
ping: handlePing,
serverSummary: handleServerSummary,
summaryCommand: handleSummaryCommand,
createDatabase: props => handleDatabaseOp('createDatabase', props),
dropDatabase: props => handleDatabaseOp('dropDatabase', props),
};

View File

@ -55,6 +55,17 @@ export interface SqlBackupDumper {
run();
}
export interface SummaryColumn {
fieldName: string;
header: string;
dataType: 'string' | 'number' | 'bytes';
}
export interface ServerSummaryDatabase {}
export interface ServerSummary {
columns: SummaryColumn[];
databases: ServerSummaryDatabase[];
}
export interface EngineDriver {
engine: string;
title: string;
@ -65,6 +76,7 @@ export interface EngineDriver {
supportedKeyTypes: SupportedDbKeyType[];
supportsDatabaseUrl?: boolean;
supportsDatabaseDump?: boolean;
supportsServerSummary?: boolean;
isElectronOnly?: boolean;
supportedCreateDatabase?: boolean;
showConnectionField?: (field: string, values: any) => boolean;
@ -81,7 +93,7 @@ export interface EngineDriver {
stream(pool: any, sql: string, options: StreamOptions);
readQuery(pool: any, sql: string, structure?: TableInfo): Promise<stream.Readable>;
readJsonQuery(pool: any, query: any, structure?: TableInfo): Promise<stream.Readable>;
writeTable(pool: any, name: NamedObjectInfo, options: WriteTableOptions): Promise<stream.Writeable>;
writeTable(pool: any, name: NamedObjectInfo, options: WriteTableOptions): Promise<stream.Writable>;
analyseSingleObject(
pool: any,
name: NamedObjectInfo,
@ -116,6 +128,8 @@ export interface EngineDriver {
getNewObjectTemplates(): NewObjectTemplate[];
// direct call of pool method, only some methods could be supported, on only some drivers
callMethod(pool, method, args);
serverSummary(pool): Promise<ServerSummary>;
summaryCommand(pool, command, row): Promise<void>;
analyserClass?: any;
dumperClass?: any;

View File

@ -104,7 +104,7 @@
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
import AboutModal from '../modals/AboutModal.svelte';
import { tick } from 'svelte';
import { tick } from 'svelte';
export let data;
export let passProps;
@ -195,6 +195,16 @@ import { tick } from 'svelte';
}),
});
};
const handleServerSummary = () => {
openNewTab({
title: getConnectionLabel(data),
icon: 'img server',
tabComponent: 'ServerSummaryTab',
props: {
conid: data._id,
},
});
};
const handleNewQuery = () => {
const tooltip = `${getConnectionLabel(data)}`;
openNewTab({
@ -244,6 +254,11 @@ import { tick } from 'svelte';
text: 'Create database',
onClick: handleCreateDatabase,
},
$openedConnections.includes(data._id) &&
driver?.supportsServerSummary && {
text: 'Server summary',
onClick: handleServerSummary,
},
],
data.singleDatabase && [
{ divider: true },

View File

@ -1,8 +1,7 @@
<script lang="ts">
import _ from 'lodash';
import FontIcon from '../icons/FontIcon.svelte';
import Link from './Link.svelte';
import TableControl from './TableControl.svelte';
export let title;
@ -10,6 +9,7 @@
export let columns;
export let showIfEmpty = false;
export let emptyMessage = null;
export let hideDisplayName = false;
export let clickable;
export let onAddNew;
</script>
@ -31,43 +31,43 @@
<div class="body">
<TableControl
rows={collection || []}
columns={[
{
columns={_.compact([
!hideDisplayName && {
fieldName: 'displayName',
header: 'Name',
slot: -1,
},
...columns,
]}
])}
{clickable}
on:clickrow
>
<svelte:fragment slot="-1" let:row>
<slot name="name" {row} />
<svelte:fragment slot="-1" let:row let:col>
<slot name="name" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="0" let:row>
<slot name="0" {row} />
<svelte:fragment slot="0" let:row let:col>
<slot name="0" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="1" let:row>
<slot name="1" {row} />
<svelte:fragment slot="1" let:row let:col>
<slot name="1" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="2" let:row>
<slot name="2" {row} />
<svelte:fragment slot="2" let:row let:col>
<slot name="2" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="3" let:row>
<slot name="3" {row} />
<svelte:fragment slot="3" let:row let:col>
<slot name="3" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="4" let:row>
<slot name="4" {row} />
<svelte:fragment slot="4" let:row let:col>
<slot name="4" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="5" let:row>
<slot name="5" {row} />
<svelte:fragment slot="5" let:row let:col>
<slot name="5" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="6" let:row>
<slot name="6" {row} />
<svelte:fragment slot="6" let:row let:col>
<slot name="6" {row} {col} />
</svelte:fragment>
<svelte:fragment slot="7" let:row>
<slot name="7" {row} />
<svelte:fragment slot="7" let:row let:col>
<slot name="7" {row} {col} />
</svelte:fragment>
</TableControl>
</div>

View File

@ -86,17 +86,17 @@
{:else if col.formatter}
{col.formatter(row)}
{:else if col.slot != null}
{#if col.slot == -1}<slot name="-1" {row} {index} />
{:else if col.slot == 0}<slot name="0" {row} {index} {...rowProps} />
{:else if col.slot == 1}<slot name="1" {row} {index} {...rowProps} />
{:else if col.slot == 2}<slot name="2" {row} {index} {...rowProps} />
{:else if col.slot == 3}<slot name="3" {row} {index} {...rowProps} />
{:else if col.slot == 4}<slot name="4" {row} {index} {...rowProps} />
{:else if col.slot == 5}<slot name="5" {row} {index} {...rowProps} />
{:else if col.slot == 6}<slot name="6" {row} {index} {...rowProps} />
{:else if col.slot == 7}<slot name="7" {row} {index} {...rowProps} />
{:else if col.slot == 8}<slot name="8" {row} {index} {...rowProps} />
{:else if col.slot == 9}<slot name="9" {row} {index} {...rowProps} />
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
{:else if col.slot == 0}<slot name="0" {row} {col} {index} {...rowProps} />
{:else if col.slot == 1}<slot name="1" {row} {col} {index} {...rowProps} />
{:else if col.slot == 2}<slot name="2" {row} {col} {index} {...rowProps} />
{:else if col.slot == 3}<slot name="3" {row} {col} {index} {...rowProps} />
{:else if col.slot == 4}<slot name="4" {row} {col} {index} {...rowProps} />
{:else if col.slot == 5}<slot name="5" {row} {col} {index} {...rowProps} />
{:else if col.slot == 6}<slot name="6" {row} {col} {index} {...rowProps} />
{:else if col.slot == 7}<slot name="7" {row} {col} {index} {...rowProps} />
{:else if col.slot == 8}<slot name="8" {row} {col} {index} {...rowProps} />
{:else if col.slot == 9}<slot name="9" {row} {col} {index} {...rowProps} />
{/if}
{:else}
{row[col.fieldName] || ''}

View File

@ -0,0 +1,103 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('ServerSummaryTab');
registerCommand({
id: 'serverSummary.refresh',
category: 'Server sumnmary',
name: 'Refresh',
keyText: 'F5 | CtrlOrCommand+R',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
onClick: () => getCurrentEditor().refresh(),
});
</script>
<script>
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 ObjectListControl from '../elements/ObjectListControl.svelte';
import { apiCall } from '../utility/api';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import formatFileSize from '../utility/formatFileSize';
import openNewTab from '../utility/openNewTab';
export let conid;
let refreshToken = 0;
export const activator = createActivator('ServerSummaryTab', true);
export function refresh() {
refreshToken += 1;
}
async function runAction(action, row) {
const { command, openQuery } = action;
if (command) {
await apiCall('server-connections/summary-command', { conid, refreshToken, command, row });
refresh();
}
if (openQuery) {
openNewTab({
title: action.tabTitle || row.name,
icon: 'img query-data',
tabComponent: 'QueryDataTab',
props: {
conid,
database: row.name,
sql: openQuery,
},
});
}
}
</script>
<ToolStripContainer>
{#await apiCall('server-connections/server-summary', { conid, refreshToken })}
<LoadingInfo message="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> | </span>
{/if}
<Link onClick={() => runAction(action, row)}>{action.header}</Link>
{/each}
</svelte:fragment>
</ObjectListControl>
</div>
{/await}
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="serverSummary.refresh" />
</svelte:fragment>
</ToolStripContainer>
<style>
.wrapper {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: var(--theme-bg-0);
overflow: auto;
}
</style>

View File

@ -26,6 +26,7 @@ import * as QueryDataTab from './QueryDataTab.svelte';
import * as ConnectionTab from './ConnectionTab.svelte';
import * as MapTab from './MapTab.svelte';
import * as PerspectiveTab from './PerspectiveTab.svelte';
import * as ServerSummaryTab from './ServerSummaryTab.svelte';
export default {
TableDataTab,
@ -56,4 +57,5 @@ export default {
ConnectionTab,
MapTab,
PerspectiveTab,
ServerSummaryTab,
};

View File

@ -351,6 +351,86 @@ const driver = {
return res;
},
async summaryCommand(pool, command, row) {
switch (command) {
case 'profileOff':
await pool.db(row.name).command({ profile: 0 });
return;
case 'profileFiltered':
await pool.db(row.name).command({ profile: 1, slowms: 100 });
return;
case 'profileAll':
await pool.db(row.name).command({ profile: 2 });
return;
}
},
async serverSummary(pool) {
const res = await pool.__getDatabase().admin().listDatabases();
const profiling = await Promise.all(res.databases.map((x) => pool.db(x.name).command({ profile: -1 })));
function formatProfiling(info) {
switch (info.was) {
case 0:
return 'No profiling';
case 1:
return `Filtered (>${info.slowms} ms)`;
case 2:
return 'Profile all';
default:
return '???';
}
}
return {
columns: [
{
fieldName: 'name',
columnType: 'string',
header: 'Name',
},
{
fieldName: 'sizeOnDisk',
columnType: 'bytes',
header: 'Size',
},
{
fieldName: 'profiling',
columnType: 'string',
header: 'Profiling',
},
{
fieldName: 'setProfile',
columnType: 'actions',
header: 'Profiling actions',
actions: [
{
header: 'Off',
command: 'profileOff',
},
{
header: 'Filtered',
command: 'profileFiltered',
},
{
header: 'All',
command: 'profileAll',
},
{
header: 'View',
openQuery: "db['system.profile'].find()",
tabTitle: 'Profile data',
},
],
},
],
databases: res.databases.map((db, i) => ({
...db,
profiling: formatProfiling(profiling[i]),
})),
};
},
};
module.exports = driver;

View File

@ -32,6 +32,7 @@ const driver = {
editorMode: 'javascript',
defaultPort: 27017,
supportsDatabaseUrl: true,
supportsServerSummary: true,
databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname',
getQuerySplitterOptions: () => mongoSplitterOptions,