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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,15 +18,17 @@
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 { serverSummarySelectedTab } from '../stores';
export let conid; export let conid;
@@ -78,26 +80,31 @@
<LoadingInfo message="Loading server details" wrapper /> <LoadingInfo message="Loading server details" wrapper />
{:then summary} {:then summary}
<div class="wrapper"> <div class="wrapper">
<ObjectListControl <TabControl
collection={summary.databases} isInline
hideDisplayName inlineTabs={true}
title={`Databases (${summary.databases.length})`} containerMaxWidth="100%"
emptyMessage={'No databases'} flex1={true}
columns={summary.columns.map(col => ({ value={$serverSummarySelectedTab}
...col, onUserChange={(index) => serverSummarySelectedTab.set(index)}
slot: col.columnType == 'bytes' ? 1 : col.columnType == 'actions' ? 2 : null, tabs={[
}))} {
> label: 'Variables',
<svelte:fragment slot="1" let:row let:col>{formatFileSize(row?.[col.fieldName])}</svelte:fragment> component: SummaryVariables,
<svelte:fragment slot="2" let:row let:col> props: { variables: summary.variables || [] },
{#each col.actions as action, index} },
{#if index > 0} {
<span class="action-separator">|</span> label: 'Processes',
{/if} component: SummaryProcesses,
<Link onClick={() => runAction(action, row)}>{action.header}</Link> props: { processes: summary.processes || [], conid },
{/each} },
</svelte:fragment> {
</ObjectListControl> label: 'Databases',
component: SummaryDatabases,
props: { databases: summary.databases || [] },
},
]}
/>
</div> </div>
{/await} {/await}

View File

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