{
- skip += limit;
+ skip = parseInt(skip) + parseInt(limit);
dispatch('load');
}}
>
diff --git a/packages/web/src/elements/TabCloseButton.svelte b/packages/web/src/elements/TabCloseButton.svelte
new file mode 100644
index 000000000..93355ef61
--- /dev/null
+++ b/packages/web/src/elements/TabCloseButton.svelte
@@ -0,0 +1,31 @@
+
+
+ {
+ mousein = true;
+ }}
+ on:mouseleave={() => {
+ mousein = false;
+ }}
+>
+
+
+
+
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte
index 57302b6e8..21f5063f1 100644
--- a/packages/web/src/icons/FontIcon.svelte
+++ b/packages/web/src/icons/FontIcon.svelte
@@ -46,6 +46,7 @@
'icon file': 'mdi mdi-file',
'icon loading': 'mdi mdi-loading mdi-spin',
'icon close': 'mdi mdi-close',
+ 'icon unsaved': 'mdi mdi-record',
'icon stop': 'mdi mdi-close-octagon',
'icon filter': 'mdi mdi-filter',
'icon filter-off': 'mdi mdi-filter-off',
diff --git a/packages/web/src/modals/CloseTabModal.svelte b/packages/web/src/modals/CloseTabModal.svelte
new file mode 100644
index 000000000..ef8de7380
--- /dev/null
+++ b/packages/web/src/modals/CloseTabModal.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+ Confirm close tabs
+
+
+ Following files are modified, really close tabs? After closing, you could reopen them in history
+
+ widget
+
+
+ {#each tabs as tab}
+ {tab.title}
+ {/each}
+
+
+ {
+ closeCurrentModal();
+ onConfirm();
+ }}
+ />
+ {
+ closeCurrentModal();
+ onCancel();
+ }}
+ />
+
+
+
diff --git a/packages/web/src/modals/InsertJoinModal.svelte b/packages/web/src/modals/InsertJoinModal.svelte
index ed19eb7bf..e1cd01777 100644
--- a/packages/web/src/modals/InsertJoinModal.svelte
+++ b/packages/web/src/modals/InsertJoinModal.svelte
@@ -1,4 +1,5 @@
-
+ |
{#if value !== undefined}
{#if displayType == 'json'}
@@ -23,6 +23,8 @@
{:else}
(no image)
{/if}
+ {:else if !value.$oid && (_.isArray(value) || _.isPlainObject(value))}
+
{:else}
{/if}
diff --git a/packages/web/src/perspectives/PerspectiveDesigner.svelte b/packages/web/src/perspectives/PerspectiveDesigner.svelte
index 7a8b8b1c4..7fb2fd625 100644
--- a/packages/web/src/perspectives/PerspectiveDesigner.svelte
+++ b/packages/web/src/perspectives/PerspectiveDesigner.svelte
@@ -3,10 +3,12 @@
createPerspectiveNodeConfig,
MultipleDatabaseInfo,
PerspectiveConfig,
+ PerspectiveDataPatternDict,
perspectiveNodesHaveStructure,
PerspectiveTreeNode,
switchPerspectiveReferenceDirection,
} from 'dbgate-datalib';
+ import { CollectionInfo } from 'dbgate-types';
import _ from 'lodash';
import { tick } from 'svelte';
import runCommand from '../commands/runCommand';
@@ -18,6 +20,7 @@
export let config: PerspectiveConfig;
export let dbInfos: MultipleDatabaseInfo;
+ export let dataPatterns: PerspectiveDataPatternDict;
export let root: PerspectiveTreeNode;
export let conid;
@@ -27,22 +30,39 @@
export let onClickTableHeader = null;
- function createDesignerModel(config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo) {
+ function createDesignerModel(
+ config: PerspectiveConfig,
+ dbInfos: MultipleDatabaseInfo,
+ dataPatterns: PerspectiveDataPatternDict
+ ) {
return {
...config,
tables: _.compact(
config.nodes.map(node => {
- const table = dbInfos?.[node.conid || conid]?.[node.database || database]?.tables?.find(
+ const db = dbInfos?.[node.conid || conid]?.[node.database || database];
+ const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
+ const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
+ let collection: CollectionInfo & { columns?: any[] } = db?.collections?.find(
x => x.pureName == node.pureName && x.schemaName == node.schemaName
);
- const view = dbInfos?.[node.conid || conid]?.[node.database || database]?.views?.find(
- x => x.pureName == node.pureName && x.schemaName == node.schemaName
- );
- if (!table && !view) return null;
+
+ if (collection) {
+ const pattern = dataPatterns?.[node.designerId];
+ if (!pattern) return null;
+ collection = {
+ ...collection,
+ columns:
+ pattern?.columns.map(x => ({
+ columnName: x.name,
+ })) || [],
+ };
+ }
+
+ if (!table && !view && !collection) return null;
const { designerId } = node;
return {
- ...(table || view),
+ ...(table || view || collection),
left: node?.position?.x || 0,
top: node?.position?.y || 0,
alias: node.alias,
@@ -55,7 +75,7 @@
function handleChange(value, skipUndoChain, settings) {
setConfig(oldValue => {
- const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos)) : value;
+ const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos, dataPatterns)) : value;
let isArranged = oldValue.isArranged;
if (settings?.isCalledFromArrange) {
isArranged = true;
@@ -122,11 +142,11 @@
});
}
- async function detectAutoArrange(config: PerspectiveConfig, dbInfos, root) {
+ async function detectAutoArrange(config: PerspectiveConfig, dbInfos, dataPatterns, root) {
if (
root &&
config.nodes.find(x => !x.position) &&
- perspectiveNodesHaveStructure(config, dbInfos, conid, database) &&
+ perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database) &&
config.nodes.every(x => root?.findNodeByDesignerId(x.designerId))
) {
await tick();
@@ -134,7 +154,7 @@
}
}
- $: detectAutoArrange(config, dbInfos, root);
+ $: detectAutoArrange(config, dbInfos, dataPatterns, root);
// $: console.log('DESIGNER ROOT', root);
@@ -221,6 +241,14 @@
const orderIndex = sort.length > 1 ? _.findIndex(sort, x => x.columnName == columnName) : -1;
return { order, orderIndex };
},
+ getColumnIconOverride: (designerId, columnName) => {
+ const pattern = dataPatterns?.[designerId];
+ const column = pattern?.columns.find(x => x.name == columnName);
+ if (column?.types?.includes('json')) {
+ return 'img json';
+ }
+ return null;
+ },
isColumnFiltered: (designerId, columnName) => {
return !!config.nodes.find(x => x.designerId == designerId)?.filters?.[columnName];
},
@@ -277,6 +305,6 @@
onClickTableHeader,
}}
referenceComponent={QueryDesignerReference}
- value={createDesignerModel(config, dbInfos)}
+ value={createDesignerModel(config, dbInfos, dataPatterns)}
onChange={handleChange}
/>
diff --git a/packages/web/src/perspectives/PerspectiveTable.svelte b/packages/web/src/perspectives/PerspectiveTable.svelte
index 4f7990c4a..09277e76d 100644
--- a/packages/web/src/perspectives/PerspectiveTable.svelte
+++ b/packages/web/src/perspectives/PerspectiveTable.svelte
@@ -16,6 +16,7 @@
ChangePerspectiveConfigFunc,
PerspectiveConfig,
PerspectiveDisplay,
+ PerspectivePatternColumnNode,
PerspectiveTableColumnNode,
PerspectiveTreeNode,
PERSPECTIVE_PAGE_SIZE,
@@ -41,6 +42,24 @@
import { getFilterValueExpression } from 'dbgate-filterparser';
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
+ const TABS_BY_FIELD = {
+ tables: {
+ text: 'table',
+ tabComponent: 'TableDataTab',
+ icon: 'img table',
+ },
+ views: {
+ text: 'view',
+ tabComponent: 'ViewDataTab',
+ icon: 'img view',
+ },
+ collections: {
+ text: 'collection',
+ tabComponent: 'CollectionDataTab',
+ icon: 'img collection',
+ },
+ };
+
const dbg = debug('dbgate:PerspectiveTable');
export const activator = createActivator('PerspectiveTable', true, ['Designer']);
@@ -57,6 +76,7 @@
let errorMessage;
let rowCount;
let isLoading = false;
+ let isLoadQueued = false;
const lastVisibleRowIndexRef = createRef(0);
const disableLoadNextRef = createRef(false);
@@ -121,6 +141,12 @@
}
async function loadData(node: PerspectiveTreeNode, counts) {
+ if (isLoading) {
+ isLoadQueued = true;
+ return;
+ } else {
+ isLoadQueued = false;
+ }
// console.log('LOADING', node);
if (!node) return;
const rows = [];
@@ -147,6 +173,10 @@
// loadProps.push(child.getNodeLoadProps());
// }
// }
+
+ if (isLoadQueued) {
+ loadData(root, $loadedCounts);
+ }
}
export function openJson() {
@@ -199,24 +229,28 @@
const tableNode = root?.findNodeByDesignerId(tableNodeDesignerId);
if (tableNode?.headerTableAttributes) {
- const { pureName, schemaName, conid, database } = tableNode?.headerTableAttributes;
- res.push({
- text: `Open table ${pureName}`,
- onClick: () => {
- openNewTab({
- title: pureName,
- icon: 'img table',
- tabComponent: 'TableDataTab',
- props: {
- schemaName,
- pureName,
- conid: conid,
- database: database,
- objectTypeField: 'tables',
- },
- });
- },
- });
+ const { pureName, schemaName, conid, database, objectTypeField } = tableNode?.headerTableAttributes;
+ console.log('objectTypeField', objectTypeField);
+ const tab = TABS_BY_FIELD[objectTypeField];
+ if (tab) {
+ res.push({
+ text: `Open ${tab.text} ${pureName}`,
+ onClick: () => {
+ openNewTab({
+ title: pureName,
+ icon: tab.icon,
+ tabComponent: tab.tabComponent,
+ props: {
+ schemaName,
+ pureName,
+ conid: conid,
+ database: database,
+ objectTypeField,
+ },
+ });
+ },
+ });
+ }
}
const setColumnDisplay = type => {
@@ -280,42 +314,39 @@
const value = display.rows[rowIndex].rowData[columnIndex];
const { dataNode } = column;
- if (dataNode instanceof PerspectiveTableColumnNode) {
+ if (
+ dataNode.filterInfo &&
+ (dataNode instanceof PerspectiveTableColumnNode || dataNode instanceof PerspectivePatternColumnNode)
+ ) {
const { table } = dataNode;
- let tabComponent = null;
- let icon = null;
- let objectTypeField = null;
- if (dataNode.isTable) {
- tabComponent = 'TableDataTab';
- icon = 'img table';
- objectTypeField = 'tables';
- }
- if (dataNode.isView) {
- tabComponent = 'ViewDataTab';
- icon = 'img view';
- objectTypeField = 'views';
- }
- if (tabComponent) {
+
+ const tab = TABS_BY_FIELD[table.objectTypeField];
+ const filterExpression = getFilterValueExpression(
+ value,
+ dataNode instanceof PerspectiveTableColumnNode ? dataNode.column.dataType : null
+ );
+
+ if (tab) {
res.push({
- text: 'Open filtered table',
+ text: 'Open filtered grid',
onClick: () => {
openNewTab(
{
title: table.pureName,
- icon,
- tabComponent,
+ icon: tab.icon,
+ tabComponent: tab.tabComponent,
props: {
schemaName: table.schemaName,
pureName: table.pureName,
conid,
database,
- objectTypeField,
+ objectTypeField: table.objectTypeField,
},
},
{
grid: {
filters: {
- [dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType),
+ [dataNode.columnName]: filterExpression,
},
// isFormView: true,
},
@@ -339,7 +370,7 @@
...n,
filters: {
...n.filters,
- [dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType),
+ [dataNode.columnName]: filterExpression,
},
}
: n
diff --git a/packages/web/src/perspectives/PerspectiveView.svelte b/packages/web/src/perspectives/PerspectiveView.svelte
index e5d644404..a260f296b 100644
--- a/packages/web/src/perspectives/PerspectiveView.svelte
+++ b/packages/web/src/perspectives/PerspectiveView.svelte
@@ -65,6 +65,7 @@
import { sleep } from '../utility/common';
import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
+ import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
const dbg = debug('dbgate:PerspectiveView');
@@ -128,17 +129,21 @@
}
$: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases);
+ $: loader = new PerspectiveDataLoader(apiCall);
+ $: dataPatterns = usePerspectiveDataPatterns({ conid, database }, config, cache, $dbInfos, loader);
$: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId);
$: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null;
$: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
+ $: collectionInfo = rootDb?.collections.find(
+ x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName
+ );
- $: loader = new PerspectiveDataLoader(apiCall);
- $: dataProvider = new PerspectiveDataProvider(cache, loader);
+ $: dataProvider = new PerspectiveDataProvider(cache, loader, $dataPatterns);
$: root =
- tableInfo || viewInfo
+ tableInfo || viewInfo || collectionInfo
? new PerspectiveTableNode(
- tableInfo || viewInfo,
+ tableInfo || viewInfo || collectionInfo,
$dbInfos,
config,
setConfig,
@@ -151,13 +156,14 @@
$: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId);
$: {
- if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, conid, database)) {
- setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, conid, database));
+ if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, $dataPatterns, conid, database)) {
+ setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, $dataPatterns, conid, database));
}
}
// $: console.log('PERSPECTIVE', config);
// $: console.log('VIEW ROOT', root);
+ // $: console.log('dataPatterns', $dataPatterns);
@@ -205,6 +211,7 @@
{database}
{setConfig}
dbInfos={$dbInfos}
+ dataPatterns={$dataPatterns}
{root}
onClickTableHeader={designerId => {
sleep(100).then(() => {
diff --git a/packages/web/src/query/AceEditor.svelte b/packages/web/src/query/AceEditor.svelte
index 8544f3b33..2968ec365 100644
--- a/packages/web/src/query/AceEditor.svelte
+++ b/packages/web/src/query/AceEditor.svelte
@@ -155,6 +155,8 @@
export let readOnly = false;
export let splitterOptions = null;
export let onKeyDown = null;
+ export let onExecuteFragment = null;
+ export let errorMessages = null;
const tabVisible: any = getContext('tabVisible');
@@ -166,7 +168,8 @@
let queryParts = [];
let currentPart = null;
- let currentPartMarker = null;
+ let currentPartLines = [];
+ // let currentPartMarker = null;
let queryParserWorker;
@@ -183,16 +186,26 @@
return editor;
}
- export function getCurrentCommandText(): string {
- if (currentPart != null) return currentPart.text;
- if (!editor) return '';
- const selectedText = editor.getSelectedText();
- if (selectedText) return selectedText;
- if (editor.getHighlightActiveLine()) {
- const line = editor.getSelectionRange().start.row;
- return editor.session.getLine(line);
+ export function getCurrentCommandText(): { text: string; line?: number } {
+ if (currentPart != null) {
+ return {
+ text: currentPart.text,
+ line: currentPart.trimStart.line,
+ };
}
- return '';
+ if (!editor) return { text: '' };
+ const selectedText = editor.getSelectedText();
+ if (selectedText) {
+ return {
+ text: selectedText,
+ line: editor.getSelectionRange().start.row,
+ };
+ }
+ const line = editor.getSelectionRange().start.row;
+ return {
+ text: editor.session.getLine(line),
+ line,
+ };
}
export function getCodeCompletionCommandText() {
@@ -285,8 +298,37 @@
function processParserResult(data) {
queryParts = data;
- editor.setHighlightActiveLine(queryParts.length <= 1);
+ // editor.setHighlightActiveLine(queryParts.length <= 1);
changedCurrentQueryPart();
+ updateAnnotations();
+ }
+
+ function updateAnnotations() {
+ if (!mode?.includes('sql')) return;
+
+ // console.log('UPDATING ANNOTATIONS');
+
+ editor?.session?.setAnnotations([
+ ...(queryParts || [])
+ .filter(part => !(errorMessages || []).find(err => err.line == part.trimStart.line))
+ .map(part => ({
+ row: part.trimStart.line,
+ text: part.text,
+ className: currentPartLines.includes(part.trimStart.line)
+ ? 'ace-gutter-sql-run ace-gutter-current-part'
+ : 'ace-gutter-sql-run', // className: 'ace-gutter-sql-run',
+ })),
+ ...(errorMessages || []).map(error => ({
+ row: error.line,
+ text: error.message,
+ type: 'error',
+ })),
+ ]);
+ }
+
+ $: {
+ errorMessages;
+ updateAnnotations();
}
const handleContextMenu = e => {
@@ -304,8 +346,6 @@
function changedQueryParts() {
const editor = getEditor();
if (splitterOptions && editor && queryParserWorker) {
- const editor = getEditor();
-
const message = {
text: editor.getValue(),
options: {
@@ -329,19 +369,20 @@
function changedCurrentQueryPart() {
if (queryParts.length <= 1) {
removeCurrentPartMarker();
+ updateAnnotations();
return;
}
const selectionRange = editor.getSelectionRange();
- if (
- selectionRange.start.row != selectionRange.end.row ||
- selectionRange.start.column != selectionRange.end.column
- ) {
- removeCurrentPartMarker();
- currentPart = null;
- return;
- }
+ // if (
+ // selectionRange.start.row != selectionRange.end.row ||
+ // selectionRange.start.column != selectionRange.end.column
+ // ) {
+ // removeCurrentPartMarker();
+ // currentPart = null;
+ // return;
+ // }
const cursor = selectionRange.start;
const part = queryParts.find(
@@ -350,25 +391,39 @@
((cursor.row == x.end.line && cursor.column <= x.end.column) || cursor.row < x.end.line)
);
- if (part?.text != currentPart?.text || part?.start?.position != currentPart?.start?.position) {
+ if (
+ part?.text != currentPart?.text ||
+ part?.start?.position != currentPart?.start?.position ||
+ part?.end?.position != currentPart?.end?.position
+ ) {
removeCurrentPartMarker();
currentPart = part;
if (currentPart) {
const start = currentPart.trimStart || currentPart.start;
const end = currentPart.trimEnd || currentPart.end;
- currentPartMarker = editor
- .getSession()
- .addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text');
+ if (start && end) {
+ currentPartLines = _.range(start.line, end.line + 1);
+ for (const row of currentPartLines) {
+ if ((queryParts || []).find(part => part.trimStart.line == row)) {
+ continue;
+ }
+ editor.getSession().addGutterDecoration(row, 'ace-gutter-current-part');
+ }
+ }
+ // currentPartMarker = editor
+ // .getSession()
+ // .addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text');
}
+ updateAnnotations();
}
}
function removeCurrentPartMarker() {
- if (currentPartMarker != null) {
- editor.getSession().removeMarker(currentPartMarker);
- currentPartMarker = null;
+ for (const row of currentPartLines) {
+ editor.getSession().removeGutterDecoration(row, 'ace-gutter-current-part');
}
+ currentPartLines = [];
}
onMount(() => {
@@ -380,6 +435,7 @@
editor.getSession().setMode('ace/mode/' + mode);
editor.setTheme('ace/theme/' + theme);
editor.setValue(value, 1);
+ editor.setHighlightActiveLine(false);
contentBackup = value;
setEventCallBacks();
if (options) {
@@ -392,6 +448,77 @@
editor.container.addEventListener('contextmenu', handleContextMenu);
editor.keyBinding.addKeyboardHandler(handleKeyDown);
changedQueryParts();
+
+ // editor.session.addGutterDecoration(0, 'ace-gutter-sql-run');
+
+ // editor.session.setAnnotations([
+ // {
+ // row: 1,
+ // text: 'SQL SCRIPT 0',
+ // type: 'gutter',
+ // },
+ // ]);
+
+ // editor.getSession().setAnnotations([
+ // {
+ // row: 0,
+ // // column: 0,
+ // text: 'Error Message', // Or the Json reply from the parser
+ // type: 'error', // also "warning" and "information"
+ // },
+ // {
+ // row: 1,
+ // // column: 0,
+ // text: 'SELECT * FROM \n22222', // Or the Json reply from the parser
+ // // type: 'info', // also "warning" and "information"
+ // className: 'ace-gutter-sql-run',
+ // },
+ // ]);
+
+ // editor.on('guttermousemove', e => console.log('MOVE', e.target), true);
+ // editor.on('guttermouseout', e => console.log('OUT', e.target), true);
+ // editor.on('guttermouseleave', e => console.log('LEAVE', e.target), true);
+ // editor.session.setBreakpoint(0);
+
+ // editor.on(
+ // 'gutterclick',
+ // e => {
+ // const row = e.getDocumentPosition().row;
+
+ // const part = (queryParts || []).find(part => part.trimStart.line == row);
+ // if (part && onExecuteFragment) {
+ // onExecuteFragment(part.text);
+ // e.stop();
+ // }
+ // },
+ // true
+ // );
+
+ editor.on(
+ 'guttermousedown',
+ e => {
+ const row = e.getDocumentPosition().row;
+
+ const part = (queryParts || []).find(part => part.trimStart.line == row);
+ if (part && onExecuteFragment) {
+ onExecuteFragment(part.text, part.trimStart.line);
+ e.stop();
+ editor.moveCursorTo(part.trimStart.line, 0);
+ editor.selection.clearSelection();
+ }
+ },
+ true
+ );
+
+ // editor.session.gutterRenderer = {
+ // getWidth: function (session, lastLineNumber, config) {
+ // return lastLineNumber.toString().length * config.characterWidth;
+ // },
+ // getText: function (session, row) {
+ // return (row + 1).toString();
+ // // return String.fromCharCode(row + 65);
+ // },
+ // };
});
onDestroy(() => {
@@ -427,6 +554,7 @@
editor.on('focus', () => dispatch('focus'));
editor.setReadOnly(readOnly);
+
editor.on('change', () => {
const content = editor.getValue();
value = content;
diff --git a/packages/web/src/query/MessageView.svelte b/packages/web/src/query/MessageView.svelte
index 99d47cc78..20d0a6e08 100644
--- a/packages/web/src/query/MessageView.svelte
+++ b/packages/web/src/query/MessageView.svelte
@@ -18,6 +18,7 @@
export let items: any[];
export let showProcedure = false;
export let showLine = false;
+ export let startLine = 0;
$: time0 = items[0] && new Date(items[0].time).getTime();
@@ -58,7 +59,7 @@
| {row.procedure || ''} |
{/if}
{#if showLine}
- {row.line || ''} |
+ {row.line == null ? '' : row.line + 1 + startLine} |
{/if}
{/each}
diff --git a/packages/web/src/query/SocketMessageView.svelte b/packages/web/src/query/SocketMessageView.svelte
index cc48ffcfa..5e1bddf41 100644
--- a/packages/web/src/query/SocketMessageView.svelte
+++ b/packages/web/src/query/SocketMessageView.svelte
@@ -13,8 +13,11 @@
export let eventName;
export let executeNumber;
export let showNoMessagesAlert = false;
+ export let startLine = 0;
+ export let onChangeErrors = null;
const cachedMessagesRef = createRef([]);
+ const lastErrorMessageCountRef = createRef(0);
let displayedMessages = [];
@@ -44,11 +47,26 @@
}
}
+ $: {
+ if (onChangeErrors) {
+ const errors = displayedMessages.filter(x => x.severity == 'error' && x.line != null);
+ if (lastErrorMessageCountRef.get() != errors.length) {
+ onChangeErrors(
+ errors.map(err => ({
+ ...err,
+ line: err.line == null ? null : err.line + startLine,
+ }))
+ );
+ lastErrorMessageCountRef.set(errors.length);
+ }
+ }
+ }
+
$: $effect;
{#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)}
{:else}
-
+
{/if}
diff --git a/packages/web/src/query/SqlEditor.svelte b/packages/web/src/query/SqlEditor.svelte
index 68fc206f3..b7870756f 100644
--- a/packages/web/src/query/SqlEditor.svelte
+++ b/packages/web/src/query/SqlEditor.svelte
@@ -32,7 +32,7 @@
return domEditor.getEditor();
}
- export function getCurrentCommandText(): string {
+ export function getCurrentCommandText(): { text: string; line?: number } {
return domEditor.getCurrentCommandText();
}
diff --git a/packages/web/src/query/codeCompletion.ts b/packages/web/src/query/codeCompletion.ts
index e59503b59..ddb662098 100644
--- a/packages/web/src/query/codeCompletion.ts
+++ b/packages/web/src/query/codeCompletion.ts
@@ -2,6 +2,7 @@ import _ from 'lodash';
import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
import { getDatabaseInfo } from '../utility/metadataLoaders';
import analyseQuerySources from './analyseQuerySources';
+import { getStringSettingsValue } from '../settings/settingsTools';
const COMMON_KEYWORDS = [
'select',
@@ -78,13 +79,21 @@ export function mountCodeCompletion({ conid, database, editor, getText }) {
const line = session.getLine(cursor.row).slice(0, cursor.column);
const dbinfo = await getDatabaseInfo({ conid, database });
- let list = COMMON_KEYWORDS.map(word => ({
- name: word,
- value: word,
- caption: word,
- meta: 'keyword',
- score: 800,
- }));
+ const convertUpper = getStringSettingsValue('sqlEditor.sqlCommandsCase', 'upperCase') == 'upperCase';
+
+ let list = COMMON_KEYWORDS.map(word => {
+ if (convertUpper) {
+ word = word.toUpperCase();
+ }
+
+ return {
+ name: word,
+ value: word,
+ caption: word,
+ meta: 'keyword',
+ score: 800,
+ };
+ });
if (dbinfo) {
const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte
index 9f9310916..ebe5e5186 100644
--- a/packages/web/src/settings/ConnectionDriverFields.svelte
+++ b/packages/web/src/settings/ConnectionDriverFields.svelte
@@ -173,6 +173,10 @@
/>
{/if}
+{#if driver?.showConnectionField('treeKeySeparator', $values)}
+
+{/if}
+
{#if driver?.showConnectionField('windowsDomain', $values)}
{/if}
diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte
index 99e0ca357..ebf2d9a14 100644
--- a/packages/web/src/settings/SettingsModal.svelte
+++ b/packages/web/src/settings/SettingsModal.svelte
@@ -111,6 +111,19 @@ ORDER BY
defaultValue="30"
disabled={values['connection.autoRefresh'] === false}
/>
+
+ SQL editor
+
+
Application theme
diff --git a/packages/web/src/settings/settingsTools.ts b/packages/web/src/settings/settingsTools.ts
index 7c27ad203..dbf739eee 100644
--- a/packages/web/src/settings/settingsTools.ts
+++ b/packages/web/src/settings/settingsTools.ts
@@ -21,3 +21,10 @@ export function getBoolSettingsValue(name, defaultValue) {
if (res == null) return defaultValue;
return !!res;
}
+
+export function getStringSettingsValue(name, defaultValue) {
+ const settings = getCurrentSettings();
+ const res = settings[name];
+ if (res == null) return defaultValue;
+ return res;
+}
diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte
index 9166fcbaa..c987fd851 100644
--- a/packages/web/src/tabs/QueryTab.svelte
+++ b/packages/web/src/tabs/QueryTab.svelte
@@ -52,7 +52,7 @@
import useEditorData from '../query/useEditorData';
import { extensions } from '../stores';
import applyScriptTemplate from '../utility/applyScriptTemplate';
- import { changeTab } from '../utility/common';
+ import { changeTab, markTabUnsaved } from '../utility/common';
import { getDatabaseInfo, useConnectionInfo } from '../utility/metadataLoaders';
import SocketMessageView from '../query/SocketMessageView.svelte';
import useEffect from '../utility/useEffect';
@@ -86,10 +86,11 @@
let busy = false;
let executeNumber = 0;
+ let executeStartLine = 0;
let visibleResultTabs = false;
let sessionId = null;
let resultCount;
-
+ let errorMessages;
let domEditor;
$: connection = useConnectionInfo({ conid });
@@ -143,13 +144,14 @@
return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions);
}
- async function executeCore(sql) {
+ async function executeCore(sql, startLine = 0) {
if (busy) return;
if (!sql || !sql.trim()) {
showSnackbarError('Skipped executing empty query');
return;
}
+ executeStartLine = startLine;
executeNumber++;
visibleResultTabs = true;
@@ -179,13 +181,14 @@
}
export async function executeCurrent() {
- const sql = domEditor.getCurrentCommandText();
- await executeCore(sql);
+ const cmd = domEditor.getCurrentCommandText();
+ await executeCore(cmd.text, cmd.line);
}
export async function execute() {
const selectedText = domEditor.getEditor().getSelectedText();
- await executeCore(selectedText || $editorValue);
+ const startLine = domEditor.getEditor().getSelectionRange().start.row;
+ await executeCore(selectedText || $editorValue, selectedText ? startLine : 0);
}
export async function kill() {
@@ -257,6 +260,10 @@
: null,
});
+ function handleChangeErrors(errors) {
+ errorMessages = errors;
+ }
+
function createMenu() {
return [
{ command: 'query.execute' },
@@ -276,6 +283,8 @@
}
const quickExportHandlerRef = createQuickExportHandlerRef();
+
+ let isInitialized = false;
@@ -286,21 +295,32 @@
engine={$connection && $connection.engine}
{conid}
{database}
- splitterOptions={driver?.getQuerySplitterOptions('script')}
+ splitterOptions={driver?.getQuerySplitterOptions('editor')}
value={$editorState.value || ''}
menu={createMenu()}
- on:input={e => setEditorData(e.detail)}
+ on:input={e => {
+ setEditorData(e.detail);
+ if (isInitialized) {
+ markTabUnsaved(tabid);
+ }
+ errorMessages = [];
+ }}
on:focus={() => {
activator.activate();
invalidateCommands();
+ setTimeout(() => {
+ isInitialized = true;
+ }, 100);
}}
bind:this={domEditor}
+ onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)}
+ {errorMessages}
/>
{:else}
setEditorData(e.detail)}
on:focus={() => {
@@ -318,8 +338,10 @@
eventName={sessionId ? `session-info-${sessionId}` : null}
on:messageClick={handleMesageClick}
{executeNumber}
+ startLine={executeStartLine}
showProcedure
showLine
+ onChangeErrors={handleChangeErrors}
/>
diff --git a/packages/web/src/utility/SettingsListener.svelte b/packages/web/src/utility/SettingsListener.svelte
new file mode 100644
index 000000000..7dedd6a87
--- /dev/null
+++ b/packages/web/src/utility/SettingsListener.svelte
@@ -0,0 +1,8 @@
+
diff --git a/packages/web/src/utility/common.ts b/packages/web/src/utility/common.ts
index 5fb98c0ff..e5c083cb2 100644
--- a/packages/web/src/utility/common.ts
+++ b/packages/web/src/utility/common.ts
@@ -1,4 +1,4 @@
-import { openedTabs } from '../stores';
+import { getOpenedTabs, openedTabs } from '../stores';
import _ from 'lodash';
import getElectron from './getElectron';
@@ -18,6 +18,16 @@ export function changeTab(tabid, changeFunc) {
openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? changeFunc(tab) : tab)));
}
+export function markTabUnsaved(tabid) {
+ const tab = getOpenedTabs().find(x => x.tabid == tabid);
+ if (tab.unsaved) return;
+ openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: true } : tab)));
+}
+
+export function markTabSaved(tabid) {
+ openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: false } : tab)));
+}
+
export function setSelectedTabFunc(files, tabid) {
return [
...(files || []).filter(x => x.tabid != tabid).map(x => ({ ...x, selected: false })),
diff --git a/packages/web/src/utility/saveTabFile.ts b/packages/web/src/utility/saveTabFile.ts
index 778807fcf..60351ecdb 100644
--- a/packages/web/src/utility/saveTabFile.ts
+++ b/packages/web/src/utility/saveTabFile.ts
@@ -1,7 +1,7 @@
import { derived, get } from 'svelte/store';
import { showModal } from '../modals/modalTools';
import { openedTabs } from '../stores';
-import { changeTab } from './common';
+import { changeTab, markTabSaved } from './common';
import SaveFileModal from '../modals/SaveFileModal.svelte';
import registerCommand from '../commands/registerCommand';
import { apiCall } from './api';
@@ -24,12 +24,14 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
if (savedFilePath) {
await apiCall('files/save-as', { filePath: savedFilePath, data, format });
}
+ markTabSaved(tabid);
};
const onSave = (title, newProps) => {
changeTab(tabid, tab => ({
...tab,
title,
+ unsaved: false,
props: {
...tab.props,
savedFormat: format,
diff --git a/packages/web/src/utility/usePerspectiveDataPatterns.ts b/packages/web/src/utility/usePerspectiveDataPatterns.ts
new file mode 100644
index 000000000..f29c44cea
--- /dev/null
+++ b/packages/web/src/utility/usePerspectiveDataPatterns.ts
@@ -0,0 +1,125 @@
+import {
+ analyseDataPattern,
+ MultipleDatabaseInfo,
+ PerspectiveCache,
+ PerspectiveConfig,
+ PerspectiveDatabaseConfig,
+ PerspectiveDataLoadProps,
+ PerspectiveDataPattern,
+ PerspectiveDataPatternDict,
+} from 'dbgate-datalib';
+import { PerspectiveDataLoader } from 'dbgate-datalib/lib/PerspectiveDataLoader';
+import { writable, Readable } from 'svelte/store';
+
+export function getPerspectiveDataPatternsFromCache(
+ databaseConfig: PerspectiveDatabaseConfig,
+ config: PerspectiveConfig,
+ cache: PerspectiveCache,
+ dbInfos: MultipleDatabaseInfo
+): PerspectiveDataPatternDict {
+ const res = {};
+
+ for (const node of config.nodes) {
+ const conid = node.conid || databaseConfig.conid;
+ const database = node.database || databaseConfig.database;
+ const { schemaName, pureName } = node;
+
+ const cached = cache.dataPatterns.find(
+ x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName
+ );
+ if (cached) {
+ res[node.designerId] = cached;
+ }
+ }
+
+ return res;
+}
+
+export async function getPerspectiveDataPatterns(
+ databaseConfig: PerspectiveDatabaseConfig,
+ config: PerspectiveConfig,
+ cache: PerspectiveCache,
+ dbInfos: MultipleDatabaseInfo,
+ dataLoader: PerspectiveDataLoader
+): Promise {
+ const res = {};
+
+ for (const node of config.nodes) {
+ const conid = node.conid || databaseConfig.conid;
+ const database = node.database || databaseConfig.database;
+ const { schemaName, pureName } = node;
+
+ const cached = cache.dataPatterns.find(
+ x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName
+ );
+ if (cached) {
+ res[node.designerId] = cached;
+ continue;
+ }
+
+ const db = dbInfos?.[conid]?.[database];
+
+ if (!db) continue;
+
+ const table = db.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName);
+ const view = db.views?.find(x => x.pureName == pureName && x.schemaName == schemaName);
+ const collection = db.collections?.find(x => x.pureName == pureName && x.schemaName == schemaName);
+ if (!table && !view && !collection) continue;
+
+ // console.log('LOAD PATTERN FOR', pureName);
+
+ const props: PerspectiveDataLoadProps = {
+ databaseConfig: { conid, database },
+ engineType: collection ? 'docdb' : 'sqldb',
+ schemaName,
+ pureName,
+ orderBy: table?.primaryKey
+ ? table?.primaryKey.columns.map(x => ({ columnName: x.columnName, order: 'ASC' }))
+ : table || view
+ ? [{ columnName: (table || view).columns[0].columnName, order: 'ASC' }]
+ : null,
+ range: {
+ offset: 0,
+ limit: 10,
+ },
+ };
+ // console.log('LOAD PROPS', props);
+ const rows = await dataLoader.loadData(props);
+
+ if (rows.errorMessage) {
+ console.error('Error loading pattern for', pureName, ':', rows.errorMessage);
+ continue;
+ }
+
+ // console.log('PATTERN ROWS', rows);
+
+ const pattern = analyseDataPattern(
+ {
+ conid,
+ database,
+ pureName,
+ schemaName,
+ },
+ rows
+ );
+
+ cache.dataPatterns.push(pattern);
+ res[node.designerId] = pattern;
+ }
+
+ return res;
+}
+
+export function usePerspectiveDataPatterns(
+ databaseConfig: PerspectiveDatabaseConfig,
+ config: PerspectiveConfig,
+ cache: PerspectiveCache,
+ dbInfos: MultipleDatabaseInfo,
+ dataLoader: PerspectiveDataLoader
+): Readable {
+ const cached = getPerspectiveDataPatternsFromCache(databaseConfig, config, cache, dbInfos);
+ const promise = getPerspectiveDataPatterns(databaseConfig, config, cache, dbInfos, dataLoader);
+ const res = writable(cached);
+ promise.then(value => res.set(value));
+ return res;
+}
diff --git a/packages/web/src/widgets/TabsPanel.svelte b/packages/web/src/widgets/TabsPanel.svelte
index 7244a70c9..fe9ad5742 100644
--- a/packages/web/src/widgets/TabsPanel.svelte
+++ b/packages/web/src/widgets/TabsPanel.svelte
@@ -1,5 +1,23 @@
|