mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-23 06:56:01 +00:00
feat: new server summary tab
This commit is contained in:
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>
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray;
|
||||
export let expanded = false;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
const filteredKey = new Set(['length']);
|
||||
|
||||
$: keys = Object.getOwnPropertyNames(value);
|
||||
@@ -22,8 +24,10 @@
|
||||
{keys}
|
||||
{previewKeys}
|
||||
{getValue}
|
||||
label="Array({value.length})"
|
||||
label={labelOverride || `Array(${value.length})`}
|
||||
bracketOpen="["
|
||||
bracketClose="]"
|
||||
elementValue={value}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import JSONNested from './JSONNested.svelte';
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
let keys = [];
|
||||
|
||||
@@ -29,7 +31,9 @@
|
||||
{getKey}
|
||||
{getValue}
|
||||
isArray={true}
|
||||
label="{nodeType}({keys.length})"
|
||||
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import MapEntry from './utils/MapEntry'
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
let keys = [];
|
||||
|
||||
@@ -28,8 +30,10 @@
|
||||
{keys}
|
||||
{getKey}
|
||||
{getValue}
|
||||
label="{nodeType}({keys.length})"
|
||||
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||
colon=""
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray;
|
||||
export let expanded = false;
|
||||
export let hideKey = false;
|
||||
export let labelOverride = null;
|
||||
|
||||
const keys = ['key', 'value'];
|
||||
|
||||
@@ -17,7 +19,9 @@
|
||||
key={isParentExpanded ? String(key) : value.key}
|
||||
{keys}
|
||||
{getValue}
|
||||
label={isParentExpanded ? 'Entry ' : '=> '}
|
||||
label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')}
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -21,11 +21,14 @@
|
||||
expandable = true;
|
||||
export let elementValue = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
const context = getContext('json-tree-context-key');
|
||||
setContext('json-tree-context-key', { ...context, colon });
|
||||
const elementData = getContext('json-tree-element-data');
|
||||
const slicedKeyCount = getContext('json-tree-sliced-key-count');
|
||||
const keyLabel = labelOverride ?? key;
|
||||
|
||||
$: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5);
|
||||
|
||||
@@ -56,7 +59,16 @@
|
||||
{#if expandable && isParentExpanded}
|
||||
<JSONArrow on:click={toggleExpand} {expanded} />
|
||||
{/if}
|
||||
<JSONKey {key} colon={context.colon} {isParentExpanded} {isParentArray} on:click={toggleExpand} />
|
||||
{#if !hideKey}
|
||||
<JSONKey
|
||||
key={keyLabel}
|
||||
colon={context.colon}
|
||||
{isParentExpanded}
|
||||
{isParentArray}
|
||||
{hideKey}
|
||||
on:click={toggleExpand}
|
||||
/>
|
||||
{/if}
|
||||
<span on:click={toggleExpand}><span>{label}</span>{bracketOpen}</span>
|
||||
</label>
|
||||
{#if isParentExpanded}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let expanded = !!getContext('json-tree-default-expanded');
|
||||
export let labelOverride = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let hideKey = false;
|
||||
|
||||
$: nodeType = objType(value);
|
||||
$: componentType = getComponent(nodeType);
|
||||
@@ -85,4 +86,5 @@
|
||||
{expanded}
|
||||
{labelOverride}
|
||||
{onRootExpandedChanged}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let expanded = false;
|
||||
export let labelOverride = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let hideKey = false;
|
||||
|
||||
$: keys = Object.getOwnPropertyNames(value);
|
||||
|
||||
@@ -26,4 +27,5 @@
|
||||
bracketClose={'}'}
|
||||
elementValue={value}
|
||||
{onRootExpandedChanged}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import JSONNode from './JSONNode.svelte';
|
||||
import { setContext } from 'svelte';
|
||||
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import _ from 'lodash';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
@@ -23,6 +22,7 @@
|
||||
export let isDeleted = false;
|
||||
export let isInserted = false;
|
||||
export let isModified = false;
|
||||
export let hideKey = false;
|
||||
|
||||
const settings = useSettings();
|
||||
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
|
||||
@@ -73,6 +73,7 @@
|
||||
class:wrap
|
||||
>
|
||||
<JSONNode
|
||||
{hideKey}
|
||||
{key}
|
||||
{value}
|
||||
isParentExpanded={true}
|
||||
|
||||
@@ -3,10 +3,25 @@
|
||||
|
||||
import JSONKey from './JSONKey.svelte';
|
||||
|
||||
export let key, value, valueGetter = null, isParentExpanded, isParentArray, nodeType;
|
||||
export let key,
|
||||
value,
|
||||
valueGetter = null,
|
||||
labelOverride,
|
||||
isParentExpanded,
|
||||
isParentArray,
|
||||
nodeType;
|
||||
|
||||
const label = labelOverride ?? key;
|
||||
const { colon } = getContext('json-tree-context-key');
|
||||
</script>
|
||||
|
||||
<li class:indent={isParentExpanded}>
|
||||
<JSONKey key={label} {colon} {isParentExpanded} {isParentArray} />
|
||||
<span class={nodeType}>
|
||||
{valueGetter ? valueGetter(value) : value}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
li {
|
||||
user-select: text;
|
||||
@@ -45,9 +60,4 @@
|
||||
color: var(--symbol-color);
|
||||
}
|
||||
</style>
|
||||
<li class:indent={isParentExpanded}>
|
||||
<JSONKey {key} {colon} {isParentExpanded} {isParentArray} />
|
||||
<span class={nodeType}>
|
||||
{valueGetter ? valueGetter(value) : value}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ export const connectionAppObjectSearchSettings = writableWithStorage(
|
||||
'connectionAppObjectSearchSettings2'
|
||||
);
|
||||
|
||||
export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab');
|
||||
|
||||
let currentThemeValue = null;
|
||||
currentTheme.subscribe(value => {
|
||||
currentThemeValue = value;
|
||||
|
||||
@@ -18,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}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
defaultActionId: 'openTable',
|
||||
},
|
||||
});
|
||||
}}>Data</ToolStripButton
|
||||
}}>DataX</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripButton
|
||||
|
||||
77
packages/web/src/widgets/SummaryDatabases.svelte
Normal file
77
packages/web/src/widgets/SummaryDatabases.svelte
Normal 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>
|
||||
77
packages/web/src/widgets/SummaryProcesses.svelte
Normal file
77
packages/web/src/widgets/SummaryProcesses.svelte
Normal 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>
|
||||
29
packages/web/src/widgets/SummaryVariables.svelte
Normal file
29
packages/web/src/widgets/SummaryVariables.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user