mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-20 07:46:00 +00:00
Merge branch 'profiler'
This commit is contained in:
@@ -41,7 +41,10 @@
|
||||
}
|
||||
|
||||
export const extractKey = data => data.fileName;
|
||||
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
||||
export const createMatcher =
|
||||
({ fileName }) =>
|
||||
filter =>
|
||||
filterName(filter, fileName);
|
||||
const ARCHIVE_ICONS = {
|
||||
'table.yaml': 'img table',
|
||||
'view.sql': 'img view',
|
||||
@@ -67,7 +70,7 @@
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
|
||||
import { archiveFilesAsDataSheets, currentArchive, extensions, getCurrentDatabase } from '../stores';
|
||||
import { archiveFilesAsDataSheets, currentArchive, extensions, getCurrentDatabase, getExtensions } from '../stores';
|
||||
|
||||
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
||||
import { exportQuickExportFile } from '../utility/exportFileTools';
|
||||
@@ -198,6 +201,29 @@
|
||||
),
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
data.fileType == 'jsonl' && {
|
||||
text: 'Open in profiler',
|
||||
submenu: getExtensions()
|
||||
.drivers.filter(eng => eng.profilerFormatterFunction)
|
||||
.map(eng => ({
|
||||
text: eng.title,
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
jslidLoad: `archive://${data.folderName}/${data.fileName}`,
|
||||
engine: eng.engine,
|
||||
// profilerFormatterFunction: eng.profilerFormatterFunction,
|
||||
// profilerTimestampFunction: eng.profilerTimestampFunction,
|
||||
// profilerChartAggregateFunction: eng.profilerChartAggregateFunction,
|
||||
// profilerChartMeasures: eng.profilerChartMeasures,
|
||||
},
|
||||
});
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -254,6 +254,18 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleDatabaseProfiler = () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
saveScriptToDatabase({ conid: connection._id, database: name }, sql, false);
|
||||
}
|
||||
@@ -284,7 +296,8 @@
|
||||
!connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' },
|
||||
{ divider: true },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' },
|
||||
isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||
driver?.supportsDatabaseProfiler && { onClick: handleDatabaseProfiler, text: 'Database profiler' },
|
||||
isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' },
|
||||
isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' },
|
||||
isSqlOrDoc &&
|
||||
|
||||
@@ -75,11 +75,17 @@
|
||||
{:else if value.$oid}
|
||||
<span class="value">ObjectId("{value.$oid}")</span>
|
||||
{:else if _.isPlainObject(value)}
|
||||
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
||||
{@const svalue = JSON.stringify(value, undefined, 2)}
|
||||
<span class="null" title={svalue}
|
||||
>{#if svalue.length < 100}{JSON.stringify(value)}{:else}(JSON){/if}</span
|
||||
>
|
||||
{:else if _.isArray(value)}
|
||||
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
|
||||
{:else if _.isPlainObject(jsonParsedValue)}
|
||||
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
|
||||
{@const svalue = JSON.stringify(jsonParsedValue, undefined, 2)}
|
||||
<span class="null" title={svalue}
|
||||
>{#if svalue.length < 100}{JSON.stringify(jsonParsedValue)}{:else}(JSON){/if}</span
|
||||
>
|
||||
{:else if _.isArray(jsonParsedValue)}
|
||||
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
|
||||
>[{jsonParsedValue.length} items]</span
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
export let customCommandIcon = null;
|
||||
export let onCustomCommand = null;
|
||||
export let customCommandTooltip = null;
|
||||
export let formatterFunction = null;
|
||||
|
||||
export let pureName = null;
|
||||
export let schemaName = null;
|
||||
@@ -276,6 +277,7 @@
|
||||
schemaName,
|
||||
pureName,
|
||||
field: columnName || uniqueName,
|
||||
formatterFunction,
|
||||
onConfirm: keys => setFilter(keys.map(x => getFilterValueExpression(x)).join(',')),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,6 +355,7 @@
|
||||
export let pureName = undefined;
|
||||
export let schemaName = undefined;
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let formatterFunction;
|
||||
|
||||
export let isLoadedAll;
|
||||
export let loadedTime;
|
||||
@@ -1743,6 +1744,7 @@
|
||||
{conid}
|
||||
{database}
|
||||
{jslid}
|
||||
{formatterFunction}
|
||||
driver={display?.driver}
|
||||
filterType={useEvalFilters ? 'eval' : col.filterType || getFilterType(col.dataType)}
|
||||
filter={display.getFilter(col.uniqueName)}
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
});
|
||||
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { jslid, display } = props;
|
||||
const { jslid, display, formatterFunction } = props;
|
||||
|
||||
const response = await apiCall('jsldata/get-rows', {
|
||||
jslid,
|
||||
offset,
|
||||
limit,
|
||||
formatterFunction,
|
||||
filters: display ? display.compileFilters() : null,
|
||||
});
|
||||
|
||||
@@ -34,6 +35,9 @@
|
||||
const response = await apiCall('jsldata/get-stats', { jslid });
|
||||
return response.rowCount;
|
||||
}
|
||||
|
||||
export let formatterPlugin;
|
||||
export let formatterFunction;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -56,6 +60,7 @@
|
||||
|
||||
export let jslid;
|
||||
export let display;
|
||||
export let formatterFunction;
|
||||
|
||||
export const activator = createActivator('JslDataGridCore', false);
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
'icon close': 'mdi mdi-close',
|
||||
'icon unsaved': 'mdi mdi-record',
|
||||
'icon stop': 'mdi mdi-close-octagon',
|
||||
'icon play': 'mdi mdi-play',
|
||||
'icon play-stop': 'mdi mdi-stop',
|
||||
'icon pause': 'mdi mdi-pause',
|
||||
'icon filter': 'mdi mdi-filter',
|
||||
'icon filter-off': 'mdi mdi-filter-off',
|
||||
'icon reload': 'mdi mdi-reload',
|
||||
@@ -177,6 +180,7 @@
|
||||
'img app-command': 'mdi mdi-flash color-icon-green',
|
||||
'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
|
||||
'img connection': 'mdi mdi-connection color-icon-blue',
|
||||
'img profiler': 'mdi mdi-gauge color-icon-blue',
|
||||
|
||||
'img add': 'mdi mdi-plus-circle color-icon-green',
|
||||
'img minus': 'mdi mdi-minus-circle color-icon-red',
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
export let driver;
|
||||
export let multiselect = false;
|
||||
export let jslid;
|
||||
export let formatterFunction;
|
||||
|
||||
// console.log('ValueLookupModal', conid, database, pureName, schemaName, columnName, driver);
|
||||
|
||||
@@ -42,6 +43,7 @@
|
||||
jslid,
|
||||
search,
|
||||
field,
|
||||
formatterFunction,
|
||||
});
|
||||
} else {
|
||||
rows = await apiCall('database-connections/load-field-values', {
|
||||
|
||||
@@ -216,7 +216,7 @@ export const getCurrentDatabase = () => currentDatabaseValue;
|
||||
let currentSettingsValue = null;
|
||||
export const getCurrentSettings = () => currentSettingsValue || {};
|
||||
|
||||
let extensionsValue = null;
|
||||
let extensionsValue: ExtensionsDirectory = null;
|
||||
extensions.subscribe(value => {
|
||||
extensionsValue = value;
|
||||
});
|
||||
|
||||
250
packages/web/src/tabs/ProfilerTab.svelte
Normal file
250
packages/web/src/tabs/ProfilerTab.svelte
Normal file
@@ -0,0 +1,250 @@
|
||||
<script lang="ts" context="module">
|
||||
const getCurrentEditor = () => getActiveComponent('ProfilerTab');
|
||||
|
||||
registerCommand({
|
||||
id: 'profiler.start',
|
||||
category: 'Profiler',
|
||||
name: 'Start profiling',
|
||||
icon: 'icon play',
|
||||
testEnabled: () => getCurrentEditor()?.startProfilingEnabled(),
|
||||
onClick: () => getCurrentEditor().startProfiling(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'profiler.stop',
|
||||
category: 'Profiler',
|
||||
name: 'Stop profiling',
|
||||
icon: 'icon play-stop',
|
||||
testEnabled: () => getCurrentEditor()?.stopProfilingEnabled(),
|
||||
onClick: () => getCurrentEditor().stopProfiling(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'profiler.save',
|
||||
category: 'Profiler',
|
||||
name: 'Save',
|
||||
icon: 'icon save',
|
||||
testEnabled: () => getCurrentEditor()?.saveEnabled(),
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import JslDataGrid from '../datagrid/JslDataGrid.svelte';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
|
||||
import { currentArchive, selectedWidget } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import { useConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { extensions } from '../stores';
|
||||
import ChartCore from '../charts/ChartCore.svelte';
|
||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||
import randomcolor from 'randomcolor';
|
||||
|
||||
export const activator = createActivator('ProfilerTab', true);
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
export let engine;
|
||||
export let jslidLoad;
|
||||
|
||||
let jslidSession;
|
||||
|
||||
let isProfiling = false;
|
||||
let sessionId;
|
||||
let isLoadingChart = false;
|
||||
|
||||
let intervalId;
|
||||
let chartData;
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: driver = findEngineDriver(engine || $connection, $extensions);
|
||||
$: jslid = jslidSession || jslidLoad;
|
||||
|
||||
onMount(() => {
|
||||
intervalId = setInterval(() => {
|
||||
if (sessionId) {
|
||||
apiCall('sessions/ping', {
|
||||
sesid: sessionId,
|
||||
});
|
||||
}
|
||||
}, 15 * 1000);
|
||||
});
|
||||
|
||||
$: {
|
||||
if (jslidLoad && driver) {
|
||||
loadChart();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
|
||||
export async function startProfiling() {
|
||||
isProfiling = true;
|
||||
|
||||
let sesid = sessionId;
|
||||
if (!sesid) {
|
||||
const resp = await apiCall('sessions/create', {
|
||||
conid,
|
||||
database,
|
||||
});
|
||||
sesid = resp.sesid;
|
||||
sessionId = sesid;
|
||||
}
|
||||
|
||||
const resp = await apiCall('sessions/start-profiler', {
|
||||
sesid,
|
||||
});
|
||||
jslidSession = resp.jslid;
|
||||
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function startProfilingEnabled() {
|
||||
return conid && database && !isProfiling;
|
||||
}
|
||||
|
||||
async function loadChart() {
|
||||
isLoadingChart = true;
|
||||
|
||||
const colors = randomcolor({
|
||||
count: driver.profilerChartMeasures.length,
|
||||
seed: 5,
|
||||
});
|
||||
|
||||
const data = await apiCall('jsldata/extract-timeline-chart', {
|
||||
jslid,
|
||||
timestampFunction: driver.profilerTimestampFunction,
|
||||
aggregateFunction: driver.profilerChartAggregateFunction,
|
||||
measures: driver.profilerChartMeasures,
|
||||
});
|
||||
chartData = {
|
||||
...data,
|
||||
labels: data.labels.map(x => new Date(x)),
|
||||
datasets: data.datasets.map((x, i) => ({
|
||||
...x,
|
||||
borderColor: colors[i],
|
||||
})),
|
||||
};
|
||||
isLoadingChart = false;
|
||||
}
|
||||
|
||||
export async function stopProfiling() {
|
||||
isProfiling = false;
|
||||
await apiCall('sessions/stop-profiler', { sesid: sessionId });
|
||||
await apiCall('sessions/kill', { sesid: sessionId });
|
||||
sessionId = null;
|
||||
|
||||
invalidateCommands();
|
||||
|
||||
loadChart();
|
||||
}
|
||||
|
||||
export function stopProfilingEnabled() {
|
||||
return conid && database && isProfiling;
|
||||
}
|
||||
|
||||
export function saveEnabled() {
|
||||
return !!jslidSession;
|
||||
}
|
||||
|
||||
async function doSave(folder, file) {
|
||||
await apiCall('archive/save-jsl-data', { folder, file, jslid });
|
||||
currentArchive.set(folder);
|
||||
selectedWidget.set('archive');
|
||||
}
|
||||
|
||||
export function save() {
|
||||
showModal(SaveArchiveModal, {
|
||||
// folder: archiveFolder,
|
||||
// file: archiveFile,
|
||||
onSave: doSave,
|
||||
});
|
||||
}
|
||||
|
||||
// const data = [
|
||||
// { year: 2010, count: 10 },
|
||||
// { year: 2011, count: 20 },
|
||||
// { year: 2012, count: 15 },
|
||||
// { year: 2013, count: 25 },
|
||||
// { year: 2014, count: 22 },
|
||||
// { year: 2015, count: 30 },
|
||||
// { year: 2016, count: 28 },
|
||||
// ];
|
||||
// {
|
||||
// labels: data.map(row => row.year),
|
||||
// datasets: [
|
||||
// {
|
||||
// label: 'Acquisitions by year',
|
||||
// data: data.map(row => row.count),
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
{#if jslid}
|
||||
<VerticalSplitter allowCollapseChild1 allowCollapseChild2>
|
||||
<svelte:fragment slot="1">
|
||||
{#key jslid}
|
||||
<JslDataGrid {jslid} listenInitializeFile formatterFunction={driver?.profilerFormatterFunction} />
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
{#if isLoadingChart}
|
||||
<LoadingInfo wrapper message="Loading chart" />
|
||||
{:else}
|
||||
<ChartCore
|
||||
title="Profile data"
|
||||
data={chartData}
|
||||
options={{
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
distribution: 'linear',
|
||||
|
||||
time: {
|
||||
tooltipFormat: 'D. M. YYYY HH:mm',
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss.SSS',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'D.M hA',
|
||||
day: 'D. M.',
|
||||
week: 'D. M. YYYY',
|
||||
month: 'MM-YYYY',
|
||||
quarter: '[Q]Q - YYYY',
|
||||
year: 'YYYY',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</VerticalSplitter>
|
||||
{:else}
|
||||
<ErrorInfo message="Profiler not yet started" alignTop />
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="profiler.start" />
|
||||
<ToolStripCommandButton command="profiler.stop" />
|
||||
<ToolStripCommandButton command="profiler.save" />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
@@ -27,6 +27,7 @@ import * as ConnectionTab from './ConnectionTab.svelte';
|
||||
import * as MapTab from './MapTab.svelte';
|
||||
import * as PerspectiveTab from './PerspectiveTab.svelte';
|
||||
import * as ServerSummaryTab from './ServerSummaryTab.svelte';
|
||||
import * as ProfilerTab from './ProfilerTab.svelte';
|
||||
|
||||
export default {
|
||||
TableDataTab,
|
||||
@@ -58,4 +59,5 @@ export default {
|
||||
MapTab,
|
||||
PerspectiveTab,
|
||||
ServerSummaryTab,
|
||||
ProfilerTab,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user