feat: new server summary tab

This commit is contained in:
Pavel
2025-08-12 23:45:41 +02:00
parent a293eeb398
commit 5dd62ad2aa
16 changed files with 321 additions and 40 deletions

View File

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

View File

@@ -3,6 +3,8 @@
export let key, value, isParentExpanded, isParentArray;
export let expanded = false;
export let labelOverride = null;
export let hideKey = false;
const filteredKey = new Set(['length']);
$: keys = Object.getOwnPropertyNames(value);
@@ -22,8 +24,10 @@
{keys}
{previewKeys}
{getValue}
label="Array({value.length})"
label={labelOverride || `Array(${value.length})`}
bracketOpen="["
bracketClose="]"
elementValue={value}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -2,6 +2,8 @@
import JSONNested from './JSONNested.svelte';
export let key, value, isParentExpanded, isParentArray, nodeType;
export let labelOverride = null;
export let hideKey = false;
let keys = [];
@@ -29,7 +31,9 @@
{getKey}
{getValue}
isArray={true}
label="{nodeType}({keys.length})"
label={labelOverride || `${nodeType}(${keys.length})`}
bracketOpen={'{'}
bracketClose={'}'}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -3,6 +3,8 @@
import MapEntry from './utils/MapEntry'
export let key, value, isParentExpanded, isParentArray, nodeType;
export let labelOverride = null;
export let hideKey = false;
let keys = [];
@@ -28,8 +30,10 @@
{keys}
{getKey}
{getValue}
label="{nodeType}({keys.length})"
label={labelOverride || `${nodeType}(${keys.length})`}
colon=""
bracketOpen={'{'}
bracketClose={'}'}
{labelOverride}
{hideKey}
/>

View File

@@ -3,6 +3,8 @@
export let key, value, isParentExpanded, isParentArray;
export let expanded = false;
export let hideKey = false;
export let labelOverride = null;
const keys = ['key', 'value'];
@@ -17,7 +19,9 @@
key={isParentExpanded ? String(key) : value.key}
{keys}
{getValue}
label={isParentExpanded ? 'Entry ' : '=> '}
label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')}
bracketOpen={'{'}
bracketClose={'}'}
/>
{labelOverride}
{hideKey}
/>

View File

@@ -21,11 +21,14 @@
expandable = true;
export let elementValue = null;
export let onRootExpandedChanged = null;
export let labelOverride = null;
export let hideKey = false;
const context = getContext('json-tree-context-key');
setContext('json-tree-context-key', { ...context, colon });
const elementData = getContext('json-tree-element-data');
const slicedKeyCount = getContext('json-tree-sliced-key-count');
const keyLabel = labelOverride ?? key;
$: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5);
@@ -56,7 +59,16 @@
{#if expandable && isParentExpanded}
<JSONArrow on:click={toggleExpand} {expanded} />
{/if}
<JSONKey {key} colon={context.colon} {isParentExpanded} {isParentArray} on:click={toggleExpand} />
{#if !hideKey}
<JSONKey
key={keyLabel}
colon={context.colon}
{isParentExpanded}
{isParentArray}
{hideKey}
on:click={toggleExpand}
/>
{/if}
<span on:click={toggleExpand}><span>{label}</span>{bracketOpen}</span>
</label>
{#if isParentExpanded}

View File

@@ -16,6 +16,7 @@
export let expanded = !!getContext('json-tree-default-expanded');
export let labelOverride = null;
export let onRootExpandedChanged = null;
export let hideKey = false;
$: nodeType = objType(value);
$: componentType = getComponent(nodeType);
@@ -85,4 +86,5 @@
{expanded}
{labelOverride}
{onRootExpandedChanged}
{hideKey}
/>

View File

@@ -5,6 +5,7 @@
export let expanded = false;
export let labelOverride = null;
export let onRootExpandedChanged = null;
export let hideKey = false;
$: keys = Object.getOwnPropertyNames(value);
@@ -26,4 +27,5 @@
bracketClose={'}'}
elementValue={value}
{onRootExpandedChanged}
{hideKey}
/>

View File

@@ -2,7 +2,6 @@
import JSONNode from './JSONNode.svelte';
import { setContext } from 'svelte';
import contextMenu, { getContextMenu } from '../utility/contextMenu';
import openNewTab from '../utility/openNewTab';
import _ from 'lodash';
import { copyTextToClipboard } from '../utility/clipboard';
import { openJsonLinesData } from '../utility/openJsonLinesData';
@@ -23,6 +22,7 @@
export let isDeleted = false;
export let isInserted = false;
export let isModified = false;
export let hideKey = false;
const settings = useSettings();
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
@@ -73,6 +73,7 @@
class:wrap
>
<JSONNode
{hideKey}
{key}
{value}
isParentExpanded={true}

View File

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

View File

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

View File

@@ -18,15 +18,17 @@
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import registerCommand from '../commands/registerCommand';
import Link from '../elements/Link.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import TabControl from '../elements/TabControl.svelte';
import ObjectListControl from '../elements/ObjectListControl.svelte';
import { _t } from '../translations';
import { apiCall } from '../utility/api';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import formatFileSize from '../utility/formatFileSize';
import openNewTab from '../utility/openNewTab';
import SummaryVariables from '../widgets/SummaryVariables.svelte';
import SummaryProcesses from '../widgets/SummaryProcesses.svelte';
import SummaryDatabases from '../widgets/SummaryDatabases.svelte';
import { serverSummarySelectedTab } from '../stores';
export let conid;
@@ -78,26 +80,31 @@
<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 class="action-separator">|</span>
{/if}
<Link onClick={() => runAction(action, row)}>{action.header}</Link>
{/each}
</svelte:fragment>
</ObjectListControl>
<TabControl
isInline
inlineTabs={true}
containerMaxWidth="100%"
flex1={true}
value={$serverSummarySelectedTab}
onUserChange={(index) => serverSummarySelectedTab.set(index)}
tabs={[
{
label: 'Variables',
component: SummaryVariables,
props: { variables: summary.variables || [] },
},
{
label: 'Processes',
component: SummaryProcesses,
props: { processes: summary.processes || [], conid },
},
{
label: 'Databases',
component: SummaryDatabases,
props: { databases: summary.databases || [] },
},
]}
/>
</div>
{/await}

View File

@@ -197,7 +197,7 @@
defaultActionId: 'openTable',
},
});
}}>Data</ToolStripButton
}}>DataX</ToolStripButton
>
<ToolStripButton

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import TableControl from '../elements/TableControl.svelte';
import CtaButton from '../buttons/CtaButton.svelte';
import { _t } from '../translations';
import formatFileSize from '../utility/formatFileSize';
export let databases: any[] = [];
async function profileOff(database: any) {
// TODO: Implement profile off functionality
console.log('Profile off:', database.name);
}
async function profileFiltered(database: any) {
// TODO: Implement profile filtered functionality
console.log('Profile filtered:', database.name);
}
async function profileAll(database: any) {
// TODO: Implement profile all functionality
console.log('Profile all:', database.name);
}
</script>
<div>
<TableControl
rows={databases}
columns={[
{ header: 'Name', fieldName: 'name', slot: 1 },
{ header: 'Size', fieldName: 'sizeOnDisk', slot: 2 },
{ header: 'Collections', fieldName: 'collections' },
{ header: 'Indexes', fieldName: 'indexes' },
{ header: 'Data Size', fieldName: 'dataSize', slot: 3 },
{ header: 'Index Size', fieldName: 'indexSize', slot: 4 },
{
header: 'Actions',
fieldName: 'name',
slot: 0,
},
]}
>
<svelte:fragment slot="0" let:row>
<CtaButton on:click={() => profileOff(row)}>Profile Off</CtaButton>
<span class="action-separator">|</span>
<CtaButton on:click={() => profileFiltered(row)}>Profile Filtered</CtaButton>
<span class="action-separator">|</span>
<CtaButton on:click={() => profileAll(row)}>Profile All</CtaButton>
</svelte:fragment>
<svelte:fragment slot="1" let:row>
<strong>{row.name}</strong>
</svelte:fragment>
<svelte:fragment slot="2" let:row>
<span>{formatFileSize(row.sizeOnDisk)}</span>
</svelte:fragment>
<svelte:fragment slot="3" let:row>
<span>{formatFileSize(row.dataSize)}</span>
</svelte:fragment>
<svelte:fragment slot="4" let:row>
<span>{formatFileSize(row.indexSize)}</span>
</svelte:fragment>
</TableControl>
</div>
<style>
div {
padding: 10px;
}
.action-separator {
margin: 0 5px;
color: var(--theme-font-3);
}
</style>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { DatabaseProcess } from 'dbgate-types';
import TableControl from '../elements/TableControl.svelte';
import { _t } from '../translations';
import CtaButton from '../buttons/CtaButton.svelte';
export let processes: DatabaseProcess[] = [];
async function killProcess(processId: string) {
// TODO: Implement kill process functionality
console.log('Kill process:', processId);
}
function formatRunningTime(seconds: number): string {
if (!seconds) return '-';
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
}
</script>
<div>
<TableControl
rows={processes}
columns={[
{ header: 'Process ID', fieldName: 'processId', slot: 1 },
{ header: 'Connection ID', fieldName: 'connectionId' },
{ header: 'Client', fieldName: 'client' },
{ header: 'Operation', fieldName: 'operation' },
{ header: 'Namespace', fieldName: 'namespace' },
{ header: 'Running Time', fieldName: 'runningTime', slot: 2 },
{ header: 'State', fieldName: 'state' },
{ header: 'Waiting For', fieldName: 'waitingFor', slot: 3 },
{
header: 'Actions',
fieldName: 'processId',
slot: 0,
},
]}
>
<svelte:fragment slot="0" let:row>
<CtaButton on:click={() => killProcess(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>
<style>
div {
padding: 10px;
}
code {
font-family: monospace;
background: var(--theme-bg-1);
padding: 2px 4px;
border-radius: 3px;
}
.waiting {
color: var(--theme-font-warning);
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import TableControl from '../elements/TableControl.svelte';
import JSONTree from '../jsontree/JSONTree.svelte';
export let variables: { variable: string; value: any }[] = [];
</script>
<div>
<TableControl
rows={variables}
columns={[
{ header: 'Variable', fieldName: 'variable' },
{
header: '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>
div {
padding: 10px;
}
</style>