SYNC: Merge pull request #4 from dbgate/feature/charts

This commit is contained in:
Jan Prochazka
2025-06-09 09:15:21 +02:00
committed by Diflow
parent 6f69205818
commit f03cffe3f8
22 changed files with 1687 additions and 122 deletions

View File

@@ -185,10 +185,6 @@
isImport: true,
requiresWriteAccess: true,
},
hasPermission('dbops/charts') && {
label: 'Open active chart',
isActiveChart: true,
},
];
case 'views':
return [
@@ -245,10 +241,6 @@
isExport: true,
functionName: 'tableReader',
},
{
label: 'Open active chart',
isActiveChart: true,
},
];
case 'matviews':
return [
@@ -299,10 +291,6 @@
isExport: true,
functionName: 'tableReader',
},
{
label: 'Open active chart',
isActiveChart: true,
},
];
case 'queries':
return [
@@ -472,28 +460,7 @@
return driver;
};
if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',

View File

@@ -41,16 +41,6 @@
label: 'Markdown file',
};
const charts: FileTypeHandler = {
icon: 'img chart',
format: 'json',
tabComponent: 'ChartTab',
folder: 'charts',
currentConnection: true,
extension: 'json',
label: 'Chart file',
};
const query: FileTypeHandler = {
icon: 'img query-design',
format: 'json',
@@ -139,7 +129,6 @@
sql,
shell,
markdown,
charts,
query,
sqlite,
diagrams,

View File

@@ -261,13 +261,6 @@
testEnabled: () => getCurrentDataGrid() != null,
onClick: () => getCurrentDataGrid().openFreeTable(),
});
registerCommand({
id: 'dataGrid.openChartFromSelection',
category: 'Data grid',
name: 'Open chart from selection',
testEnabled: () => getCurrentDataGrid() != null,
onClick: () => getCurrentDataGrid().openChartFromSelection(),
});
registerCommand({
id: 'dataGrid.newJson',
category: 'Data grid',
@@ -469,6 +462,7 @@
export let hideGridLeftColumn = false;
export let overlayDefinition = null;
export let onGetSelectionMenu = null;
export let onOpenChart = null;
export const activator = createActivator('DataGridCore', false);
@@ -715,23 +709,6 @@
openJsonLinesData(getSelectedFreeDataRows());
}
export function openChartFromSelection() {
openNewTab(
{
title: 'Chart #',
icon: 'img chart',
tabComponent: 'ChartTab',
props: {},
},
{
editor: {
data: getSelectedFreeData(),
config: { chartType: 'bar' },
},
}
);
}
export function viewJsonDocumentEnabled() {
return isDynamicStructure && _.uniq(selectedCells.map(x => x[0])).length == 1;
}
@@ -1869,9 +1846,13 @@
// ],
// },
isProApp() && { command: 'dataGrid.sendToDataDeploy' },
isProApp() &&
onOpenChart && {
text: 'Open chart',
onClick: () => onOpenChart(),
},
{ command: 'dataGrid.generateSqlFromData' },
{ command: 'dataGrid.openFreeTable' },
{ command: 'dataGrid.openChartFromSelection' },
{ command: 'dataGrid.openSelectionInMap', hideDisabled: true },
{ placeTag: 'chart' }
);

View File

@@ -1,14 +1,6 @@
<script context="module" lang="ts">
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openActiveChart',
category: 'Data grid',
name: 'Open active chart',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/charts'),
onClick: () => getCurrentEditor().openActiveChart(),
});
registerCommand({
id: 'sqlDataGrid.openQuery',
category: 'Data grid',
@@ -190,28 +182,6 @@
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)));
}
export function openActiveChart() {
openNewTab(
{
title: 'Chart #',
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid,
database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: display.getExportQuery(select => {
select.orderBy = null;
}),
},
}
);
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(

View File

@@ -39,7 +39,7 @@
$: size = computeSplitterSize(initialValue, clientWidth, customRatio, initialSizeRight);
$: if (onChangeSize) onChangeSize(size);
$: if (onChangeSize) onChangeSize(size, clientWidth - size);
</script>
<div class="container" bind:clientWidth>

View File

@@ -18,6 +18,7 @@
export let flex1 = true;
export let contentTestId = undefined;
export let inlineTabs = false;
export let onUserChange = null;
export function setValue(index) {
value = index;
@@ -30,8 +31,16 @@
<div class="main" class:flex1>
<div class="tabs" class:inlineTabs>
{#each _.compact(tabs) as tab, index}
<div class="tab-item" class:selected={value == index} on:click={() => (value = index)} data-testid={tab.testid}>
<span class="ml-2">
<div
class="tab-item"
class:selected={value == index}
on:click={() => {
value = index;
onUserChange?.(index);
}}
data-testid={tab.testid}
>
<span class="ml-2 noselect">
{tab.label}
</span>
</div>
@@ -139,5 +148,4 @@
.container.isInline:not(.tabVisible) {
display: none;
}
</style>

View File

@@ -71,6 +71,7 @@
'icon trigger': 'mdi mdi-lightning-bolt',
'icon scheduler-event': 'mdi mdi-calendar-blank',
'icon arrow-link': 'mdi mdi-arrow-top-right-thick',
'icon reset': 'mdi mdi-cancel',
'icon window-restore': 'mdi mdi-window-restore',
'icon window-maximize': 'mdi mdi-window-maximize',

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import _ from 'lodash';
import _, { result } from 'lodash';
import { onMount, tick } from 'svelte';
@@ -9,6 +9,7 @@
import { apiOff, apiOn } from '../utility/api';
import useEffect from '../utility/useEffect';
import AllResultsTab from './AllResultsTab.svelte';
import JslChart from '../charts/JslChart.svelte';
export let tabs = [];
export let sessionId;
@@ -16,6 +17,8 @@
export let driver;
export let resultCount;
export let onSetFrontMatterField;
export let onGetFrontMatter;
onMount(() => {
allResultsInOneTab = $allResultsInOneTabDefault;
@@ -23,6 +26,7 @@
let allResultsInOneTab = null;
let resultInfos = [];
let charts = [];
let domTabs;
$: resultCount = resultInfos.length;
@@ -35,6 +39,23 @@
if (!currentTab?.isResult) domTabs.setValue(_.findIndex(allTabs, x => x.isResult));
};
const handleCharts = async props => {
charts = [
...charts,
{
jslid: props.jslid,
charts: props.charts,
resultIndex: props.resultIndex,
},
];
const selectedChart = onGetFrontMatter?.()?.['selected-chart'];
await tick();
if (selectedChart && props.resultIndex == selectedChart - 1) {
domTabs.setValue(_.findIndex(allTabs, x => x.isChart && x.resultIndex === props.resultIndex));
}
// console.log('Charts received for jslid:', props.jslid, 'Charts:', props.charts);
};
$: oneTab = allResultsInOneTab ?? $allResultsInOneTabDefault;
$: allTabs = [
@@ -55,13 +76,27 @@
label: `Result ${index + 1}`,
isResult: true,
component: JslDataGrid,
props: { jslid: info.jslid, driver },
props: { jslid: info.jslid, driver, onOpenChart: () => handleOpenChart(info.resultIndex) },
}))),
...charts.map((info, index) => ({
label: `Chart ${info.resultIndex + 1}`,
isChart: true,
resultIndex: info.resultIndex,
component: JslChart,
props: {
jslid: info.jslid,
initialCharts: info.charts,
onEditDefinition: definition => {
onSetFrontMatterField?.(`chart-${info.resultIndex + 1}`, definition ?? undefined);
},
},
})),
];
$: {
if (executeNumber >= 0) {
resultInfos = [];
charts = [];
if (domTabs) domTabs.setValue(0);
}
}
@@ -72,8 +107,10 @@
function onSession(sid) {
if (sid) {
apiOn(`session-recordset-${sid}`, handleResultSet);
apiOn(`session-charts-${sid}`, handleCharts);
return () => {
apiOff(`session-recordset-${sid}`, handleResultSet);
apiOff(`session-charts-${sid}`, handleCharts);
};
}
return () => {};
@@ -84,6 +121,25 @@
allResultsInOneTab = value;
$allResultsInOneTabDefault = value;
}
async function handleOpenChart(resultIndex) {
const chartTab = _.find(allTabs, x => x.isChart && x.resultIndex === resultIndex);
if (chartTab) {
domTabs.setValue(_.findIndex(allTabs, x => x.isChart && x.resultIndex === resultIndex));
} else {
charts = [
...charts,
{
jslid: resultInfos[resultIndex].jslid,
charts: [],
resultIndex,
},
];
await tick();
domTabs.setValue(_.findIndex(allTabs, x => x.isChart && x.resultIndex === resultIndex));
}
onSetFrontMatterField?.('selected-chart', resultIndex + 1);
}
</script>
<TabControl
@@ -94,6 +150,13 @@
? { text: 'Every result in single tab', onClick: () => setOneTabValue(false) }
: { text: 'All results in one tab', onClick: () => setOneTabValue(true) },
]}
onUserChange={value => {
if (allTabs[value].isChart) {
onSetFrontMatterField?.(`selected-chart`, allTabs[value].resultIndex + 1);
} else {
onSetFrontMatterField?.(`selected-chart`, undefined);
}
}}
>
<slot name="0" slot="0" />
<slot name="1" slot="1" />

View File

@@ -1,6 +1,7 @@
<script lang="ts" context="module">
import registerCommand from '../commands/registerCommand';
import { copyTextToClipboard } from '../utility/clipboard';
import yaml from 'js-yaml';
const getCurrentEditor = () => getActiveComponent('QueryTab');
@@ -60,6 +61,13 @@
getCurrentEditor() != null && !getCurrentEditor()?.isBusy() && getCurrentEditor()?.hasConnection(),
onClick: () => getCurrentEditor().executeCurrent(),
});
registerCommand({
id: 'query.toggleAutoExecute',
category: 'Query',
name: 'Toggle auto execute',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().toggleAutoExecute(),
});
registerCommand({
id: 'query.beginTransaction',
category: 'Query',
@@ -126,7 +134,7 @@
import InsertJoinModal from '../modals/InsertJoinModal.svelte';
import useTimerLabel from '../utility/useTimerLabel';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import { findEngineDriver, safeJsonParse } from 'dbgate-tools';
import { findEngineDriver, getSqlFrontMatter, safeJsonParse, setSqlFrontMatter } from 'dbgate-tools';
import AceEditor from '../query/AceEditor.svelte';
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
import { showSnackbarError } from '../utility/snackbar';
@@ -147,6 +155,7 @@
import ToolStripButton from '../buttons/ToolStripButton.svelte';
import { getIntSettingsValue } from '../settings/settingsTools';
import RowsLimitModal from '../modals/RowsLimitModal.svelte';
import _ from 'lodash';
export let tabid;
export let conid;
@@ -199,6 +208,7 @@
let domAiAssistant;
let isInTransaction = false;
let isAutocommit = false;
let splitterInitialValue = undefined;
const queryRowsLimitLocalStorageKey = `tabdata_limitRows_${tabid}`;
function getInitialRowsLimit() {
@@ -350,6 +360,7 @@
executeStartLine = startLine;
executeNumber++;
visibleResultTabs = true;
const frontMatter = getSqlFrontMatter($editorValue, yaml);
busy = true;
timerLabel.start();
@@ -381,6 +392,7 @@
sql,
autoCommit: driver?.implicitTransactions && isAutocommit,
limitRows: queryRowsLimit ? queryRowsLimit : undefined,
frontMatter,
});
}
await apiCall('query-history/write', {
@@ -550,12 +562,47 @@
initialArgs && initialArgs.scriptTemplate
? () => applyScriptTemplate(initialArgs.scriptTemplate, $extensions, $$props)
: null,
onInitialData: value => {
const frontMatter = getSqlFrontMatter(value, yaml);
if (frontMatter?.autoExecute) {
executeCore(value, 0);
}
if (frontMatter?.splitterInitialValue) {
splitterInitialValue = frontMatter.splitterInitialValue;
}
},
});
function handleChangeErrors(errors) {
errorMessages = errors;
}
function handleSetFrontMatterField(field, value) {
const text = $editorValue;
setEditorData(
setSqlFrontMatter(
text,
{
...getSqlFrontMatter(text, yaml),
[field]: value,
},
yaml
)
);
}
export function toggleAutoExecute() {
const frontMatter = getSqlFrontMatter($editorValue, yaml);
setEditorData(
setSqlFrontMatter(
$editorValue,
{ ...frontMatter, autoExecute: frontMatter?.autoExecute ? undefined : true },
yaml
)
);
}
async function handleKeyDown(event) {
if (isProApp()) {
if (event.code == 'Space' && event.shiftKey && event.ctrlKey && !isAiAssistantVisible) {
@@ -584,6 +631,7 @@
{ command: 'query.execute' },
{ command: 'query.executeCurrent' },
{ command: 'query.kill' },
{ command: 'query.toggleAutoExecute' },
{ divider: true },
{ command: 'query.toggleComment' },
{ command: 'query.formatCode' },
@@ -625,7 +673,7 @@
<ToolStripContainer bind:this={domToolStrip}>
<HorizontalSplitter isSplitter={isAiAssistantVisible} initialSizeRight={300}>
<svelte:fragment slot="1">
<VerticalSplitter isSplitter={visibleResultTabs}>
<VerticalSplitter isSplitter={visibleResultTabs} initialValue={splitterInitialValue}>
<svelte:fragment slot="1">
{#if driver?.databaseEngineTypes?.includes('sql')}
<SqlEditor
@@ -678,7 +726,15 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="2">
<ResultTabs tabs={[{ label: 'Messages', slot: 0 }]} {sessionId} {executeNumber} bind:resultCount {driver}>
<ResultTabs
tabs={[{ label: 'Messages', slot: 0 }]}
{sessionId}
{executeNumber}
bind:resultCount
{driver}
onSetFrontMatterField={handleSetFrontMatterField}
onGetFrontMatter={() => getSqlFrontMatter($editorValue, yaml)}
>
<svelte:fragment slot="0">
<SocketMessageView
eventName={sessionId ? `session-info-${sessionId}` : null}