Merge branch 'develop'

This commit is contained in:
Jan Prochazka
2022-01-26 17:46:36 +01:00
34 changed files with 2006 additions and 230 deletions

View File

@@ -42,7 +42,6 @@ jobs:
if: matrix.os == 'ubuntu-18.04' if: matrix.os == 'ubuntu-18.04'
uses: samuelmeuli/action-snapcraft@v1 uses: samuelmeuli/action-snapcraft@v1
- name: Publish - name: Publish
if: matrix.os != 'macOS-10.15'
run: | run: |
yarn run build:app yarn run build:app
env: env:
@@ -50,13 +49,6 @@ jobs:
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }} WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
- name: Publish Mac
if: matrix.os == 'macOS-10.15'
run: |
yarn run build:app:mac
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
- name: Save snap login - name: Save snap login
if: matrix.os == 'ubuntu-18.04' if: matrix.os == 'ubuntu-18.04'
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login' run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
@@ -81,7 +73,7 @@ jobs:
cp app/dist/*win*.exe artifacts/dbgate-beta.exe || true cp app/dist/*win*.exe artifacts/dbgate-beta.exe || true
cp app/dist/*win_x64.zip artifacts/dbgate-windows-beta.zip || true cp app/dist/*win_x64.zip artifacts/dbgate-windows-beta.zip || true
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-beta-arm64.zip || true cp app/dist/*win_arm64.zip artifacts/dbgate-windows-beta-arm64.zip || true
cp app/dist/*-mac.dmg artifacts/dbgate-beta.dmg || true cp app/dist/*-mac_x64.dmg artifacts/dbgate-beta.dmg || true
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true
mv app/dist/*.exe artifacts/ || true mv app/dist/*.exe artifacts/ || true

View File

@@ -81,7 +81,7 @@ jobs:
cp app/dist/*.exe artifacts/dbgate-latest.exe || true cp app/dist/*.exe artifacts/dbgate-latest.exe || true
cp app/dist/*win_x64.zip artifacts/dbgate-windows-latest.zip || true cp app/dist/*win_x64.zip artifacts/dbgate-windows-latest.zip || true
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-latest-arm64.zip || true cp app/dist/*win_arm64.zip artifacts/dbgate-windows-latest-arm64.zip || true
cp app/dist/*-mac.dmg artifacts/dbgate-latest.dmg || true cp app/dist/*-mac_x64.dmg artifacts/dbgate-latest.dmg || true
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true
mv app/dist/*.exe artifacts/ || true mv app/dist/*.exe artifacts/ || true

View File

@@ -92,7 +92,6 @@
"start:local": "cross-env electron .", "start:local": "cross-env electron .",
"dist": "electron-builder", "dist": "electron-builder",
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist", "build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
"build:mac": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && node setMacPlatform x64 && yarn dist && node setMacPlatform arm64 && yarn dist",
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist", "build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
"postinstall": "yarn rebuild && patch-package", "postinstall": "yarn rebuild && patch-package",
"rebuild": "electron-builder install-app-deps", "rebuild": "electron-builder install-app-deps",

View File

@@ -1,8 +0,0 @@
const fs = require('fs');
const text = fs.readFileSync('package.json', { encoding: 'utf-8' });
const json = JSON.parse(text);
json.build.mac.target.arch = process.argv[2];
fs.writeFileSync('package.json', JSON.stringify(json, null, 2), { encoding: 'utf-8' });

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "4.5.1", "version": "4.5.2-beta.8",
"name": "dbgate-all", "name": "dbgate-all",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
@@ -25,7 +25,6 @@
"build:tools": "yarn workspace dbgate-tools build", "build:tools": "yarn workspace dbgate-tools build",
"build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib", "build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build", "build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
"build:app:mac": "yarn plugins:copydist && cd app && yarn install && yarn build:mac",
"build:api": "yarn workspace dbgate-api build", "build:api": "yarn workspace dbgate-api build",
"build:web:docker": "yarn workspace dbgate-web build", "build:web:docker": "yarn workspace dbgate-web build",
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend", "build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",

View File

@@ -6,6 +6,7 @@ const getChartExport = require('../utility/getChartExport');
const hasPermission = require('../utility/hasPermission'); const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
const scheduler = require('./scheduler'); const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport');
function serialize(format, data) { function serialize(format, data) {
if (format == 'text') return data; if (format == 'text') return data;
@@ -86,8 +87,9 @@ module.exports = {
const dir = resolveArchiveFolder(folder.substring('archive:'.length)); const dir = resolveArchiveFolder(folder.substring('archive:'.length));
await fs.writeFile(path.join(dir, file), serialize(format, data)); await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`); socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
return true;
} else { } else {
if (!hasPermission(`files/${folder}/write`)) return; if (!hasPermission(`files/${folder}/write`)) return false;
const dir = path.join(filesdir(), folder); const dir = path.join(filesdir(), folder);
if (!(await fs.exists(dir))) { if (!(await fs.exists(dir))) {
await fs.mkdir(dir); await fs.mkdir(dir);
@@ -98,6 +100,7 @@ module.exports = {
if (folder == 'shell') { if (folder == 'shell') {
scheduler.reload(); scheduler.reload();
} }
return true;
} }
}, },
@@ -150,4 +153,10 @@ module.exports = {
} }
return true; return true;
}, },
exportDiagram_meta: true,
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
return true;
},
}; };

View File

@@ -0,0 +1,25 @@
const getDiagramExport = (html, css, themeType, themeClassName) => {
return `<html>
<meta charset='utf-8'>
<head>
<style>
${css}
body {
background: var(--theme-bg-1);
color: var(--theme-font-1);
}
</style>
<link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
</head>
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
${html}
</body>
</html>`;
};
module.exports = getDiagramExport;

View File

@@ -55,6 +55,7 @@
"uuid": "^3.4.0" "uuid": "^3.4.0"
}, },
"dependencies": { "dependencies": {
"chartjs-plugin-zoom": "^1.2.0" "chartjs-plugin-zoom": "^1.2.0",
"interval-operations": "^1.0.7"
} }
} }

View File

@@ -45,6 +45,9 @@ body {
.nowrap { .nowrap {
white-space: nowrap; white-space: nowrap;
} }
.noselect {
user-select: none;
}
.bold { .bold {
font-weight: bold; font-weight: bold;
} }

View File

@@ -27,7 +27,7 @@
</script> </script>
<div <div
class={`${$currentTheme} ${currentThemeType} root`} class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
use:dragDropFileTarget use:dragDropFileTarget
on:contextmenu={e => e.preventDefault()} on:contextmenu={e => e.preventDefault()}
> >

View File

@@ -85,6 +85,34 @@
}); });
}; };
const handleShowDiagram = async () => {
const db = await getDatabaseInfo({
conid: connection._id,
database: name,
});
openNewTab(
{
title: 'Diagram #',
icon: 'img diagram',
tabComponent: 'DiagramTab',
props: {
conid: connection._id,
database: name,
},
},
{
editor: {
tables: db.tables.map(table => ({
...table,
designerId: `${table.pureName}-${uuidv1()}`,
})),
references: [],
autoLayout: true,
},
}
);
};
const handleDisconnect = () => { const handleDisconnect = () => {
const electron = getElectron(); const electron = getElectron();
if (electron) { if (electron) {
@@ -138,6 +166,7 @@
{ divider: true }, { divider: true },
{ onClick: handleImport, text: 'Import' }, { onClick: handleImport, text: 'Import' },
{ onClick: handleExport, text: 'Export' }, { onClick: handleExport, text: 'Export' },
{ onClick: handleShowDiagram, text: 'Show diagram' },
{ onClick: handleSqlGenerator, text: 'SQL Generator' }, { onClick: handleSqlGenerator, text: 'SQL Generator' },
{ onClick: handleOpenJsonModel, text: 'Open model as JSON' }, { onClick: handleOpenJsonModel, text: 'Open model as JSON' },
{ onClick: handleExportModel, text: 'Export DB model - experimental' }, { onClick: handleExportModel, text: 'Export DB model - experimental' },
@@ -157,6 +186,7 @@
<script lang="ts"> <script lang="ts">
import getConnectionLabel from '../utility/getConnectionLabel'; import getConnectionLabel from '../utility/getConnectionLabel';
import uuidv1 from 'uuid/v1';
import _, { find } from 'lodash'; import _, { find } from 'lodash';
import ImportExportModal from '../modals/ImportExportModal.svelte'; import ImportExportModal from '../modals/ImportExportModal.svelte';

View File

@@ -53,6 +53,10 @@
label: 'Query designer', label: 'Query designer',
isQueryDesigner: true, isQueryDesigner: true,
}, },
{
label: 'Show diagram',
isDiagram: true,
},
{ {
divider: true, divider: true,
}, },
@@ -388,45 +392,15 @@
a.schemaName == b.schemaName a.schemaName == b.schemaName
); );
} }
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, openedConnections, pinnedTables } from '../stores';
import openNewTab from '../utility/openNewTab';
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import { findEngineDriver } from 'dbgate-tools';
import uuidv1 from 'uuid/v1';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import getElectron from '../utility/getElectron';
import { exportElectronFile } from '../utility/exportElectronFile';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { apiCall } from '../utility/api';
export let data;
export let passProps;
function handleClick(forceNewTab = false) {
handleDatabaseObjectClick(data, forceNewTab);
}
export function createDatabaseObjectMenu(data) {
const getDriver = async () => { const getDriver = async () => {
const conn = await getConnectionInfo(data); const conn = await getConnectionInfo(data);
if (!conn) return; if (!conn) return;
const driver = findEngineDriver(conn, $extensions); const driver = findEngineDriver(conn, getExtensions());
return driver; return driver;
}; };
function createMenu() {
const { objectTypeField } = data; const { objectTypeField } = data;
return menus[objectTypeField] return menus[objectTypeField]
.filter(x => x) .filter(x => x)
@@ -434,7 +408,7 @@
if (menu.divider) return menu; if (menu.divider) return menu;
if (menu.isQuickExport) { if (menu.isQuickExport) {
return createQuickExportMenu($extensions, fmt => async () => { return createQuickExportMenu(getExtensions(), fmt => async () => {
const coninfo = await getConnectionInfo(data); const coninfo = await getConnectionInfo(data);
exportElectronFile( exportElectronFile(
data.pureName, data.pureName,
@@ -531,6 +505,31 @@
}, },
} }
); );
} else if (menu.isDiagram) {
openNewTab(
{
title: 'Diagram #',
icon: 'img diagram',
tabComponent: 'DiagramTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: `${data.pureName}-${uuidv1()}`,
autoAddReferences: true,
},
],
references: [],
autoLayout: true,
},
}
);
} else if (menu.sqlGeneratorProps) { } else if (menu.sqlGeneratorProps) {
showModal(SqlGeneratorModal, { showModal(SqlGeneratorModal, {
initialObjects: [data], initialObjects: [data],
@@ -580,6 +579,40 @@
}; };
}); });
} }
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, getExtensions, openedConnections, pinnedTables } from '../stores';
import openNewTab from '../utility/openNewTab';
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import { findEngineDriver } from 'dbgate-tools';
import uuidv1 from 'uuid/v1';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import getElectron from '../utility/getElectron';
import { exportElectronFile } from '../utility/exportElectronFile';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { apiCall } from '../utility/api';
export let data;
export let passProps;
function handleClick(forceNewTab = false) {
handleDatabaseObjectClick(data, forceNewTab);
}
function createMenu() {
return createDatabaseObjectMenu(data);
}
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x)); $: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
</script> </script>

View File

@@ -55,6 +55,14 @@
currentConnection: true, currentConnection: true,
}; };
const diagrams: FileTypeHandler = {
icon: 'img diagram',
format: 'json',
tabComponent: 'DiagramTab',
folder: 'diagrams',
currentConnection: true,
};
const HANDLERS = { const HANDLERS = {
sql, sql,
shell, shell,
@@ -62,6 +70,7 @@
charts, charts,
query, query,
sqlite, sqlite,
diagrams,
}; };
export const extractKey = data => data.file; export const extractKey = data => data.file;
@@ -74,7 +83,7 @@
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import { currentDatabase } from '../stores'; import { currentDatabase } from '../stores';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import getConnectionLabel from '../utility/getConnectionLabel'; import getConnectionLabel from '../utility/getConnectionLabel';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';

View File

@@ -17,6 +17,7 @@
export let onCreateReference; export let onCreateReference;
export let onAddReferenceByColumn; export let onAddReferenceByColumn;
export let onSelectColumn; export let onSelectColumn;
export let settings;
$: designerColumn = (designer.columns || []).find( $: designerColumn = (designer.columns || []).find(
x => x.designerId == designerId && x.columnName == column.columnName x => x.designerId == designerId && x.columnName == column.columnName
@@ -38,9 +39,11 @@
}; };
return [ return [
settings?.allowColumnOperations && [
{ text: 'Sort ascending', onClick: () => setSortOrder(1) }, { text: 'Sort ascending', onClick: () => setSortOrder(1) },
{ text: 'Sort descending', onClick: () => setSortOrder(-1) }, { text: 'Sort descending', onClick: () => setSortOrder(-1) },
{ text: 'Unsort', onClick: () => setSortOrder(0) }, { text: 'Unsort', onClick: () => setSortOrder(0) },
],
foreignKey && { text: 'Add reference', onClick: addReference }, foreignKey && { text: 'Add reference', onClick: addReference },
]; ];
} }
@@ -48,9 +51,12 @@
<div <div
class="line" class="line"
class:canSelectColumns={settings?.canSelectColumns}
bind:this={domLine} bind:this={domLine}
draggable={true} draggable={!!settings?.allowCreateRefByDrag}
on:dragstart={e => { on:dragstart={e => {
if (!settings?.allowCreateRefByDrag) return;
const dragData = { const dragData = {
...column, ...column,
designerId, designerId,
@@ -90,8 +96,9 @@
...column, ...column,
designerId, designerId,
})} })}
use:contextMenu={createMenu} use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
> >
{#if settings?.allowColumnOperations}
<CheckboxField <CheckboxField
checked={!!(designer.columns || []).find( checked={!!(designer.columns || []).find(
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
@@ -116,6 +123,7 @@
} }
}} }}
/> />
{/if}
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon /> <ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
{#if designerColumn?.filter} {#if designerColumn?.filter}
<FontIcon icon="img filter" /> <FontIcon icon="img filter" />
@@ -129,16 +137,36 @@
{#if designerColumn?.isGrouped} {#if designerColumn?.isGrouped}
<FontIcon icon="img group" /> <FontIcon icon="img group" />
{/if} {/if}
{#if designer?.style?.showNullability || designer?.style?.showDataType}
<div class="space" />
{#if designer?.style?.showDataType && column?.dataType}
<div class="ml-2">
{column?.dataType.toLowerCase()}
</div>
{/if}
{#if designer?.style?.showNullability}
<div class="ml-2">
{column?.notNull ? 'NOT NULL' : 'NULL'}
</div>
{/if}
{/if}
</div> </div>
<style> <style>
.line:hover { :global(.dbgate-screen) .line.canSelectColumns:hover {
background: var(--theme-bg-1); background: var(--theme-bg-1);
} }
.line.isDragSource { :global(.dbgate-screen) .line.isDragSource {
background: var(--theme-bg-gold); background: var(--theme-bg-gold);
} }
.line.isDragTarget { :global(.dbgate-screen) .line.isDragTarget {
background: var(--theme-bg-gold); background: var(--theme-bg-gold);
} }
.line {
display: flex;
}
.space {
flex-grow: 1;
}
</style> </style>

View File

@@ -1,32 +1,78 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('Designer');
registerCommand({
id: 'designer.arrange',
category: 'Designer',
icon: 'icon arrange',
name: 'Arrange',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.canArrange(),
onClick: () => getCurrentEditor().arrange(),
});
registerCommand({
id: 'diagram.export',
category: 'Designer',
toolbarName: 'Export diagram',
name: 'Export diagram',
icon: 'icon report',
toolbar: true,
isRelatedToTab: true,
onClick: () => getCurrentEditor().exportDiagram(),
testEnabled: () => getCurrentEditor()?.canExport(),
});
</script> </script>
<script lang="ts"> <script lang="ts">
import DesignerTable from './DesignerTable.svelte'; import DesignerTable from './DesignerTable.svelte';
import { isConnectedByReference } from './designerTools'; import { isConnectedByReference } from './designerTools';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import { getTableInfo } from '../utility/metadataLoaders'; import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import cleanupDesignColumns from './cleanupDesignColumns'; import cleanupDesignColumns from './cleanupDesignColumns';
import _ from 'lodash'; import _ from 'lodash';
import createRef from '../utility/createRef';
import DesignerReference from './DesignerReference.svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { tick } from 'svelte'; import { tick } from 'svelte';
import contextMenu from '../utility/contextMenu'; import contextMenu from '../utility/contextMenu';
import stableStringify from 'json-stable-stringify';
import registerCommand from '../commands/registerCommand';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import { GraphDefinition, GraphLayout } from './GraphLayout';
import { saveFileToDisk } from '../utility/exportElectronFile';
import { apiCall } from '../utility/api';
import moveDrag from '../utility/moveDrag';
import { rectanglesHaveIntersection } from './designerMath';
import { showModal } from '../modals/modalTools';
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
import { currentThemeDefinition } from '../stores';
export let value; export let value;
export let onChange; export let onChange;
export let conid; export let conid;
export let database; export let database;
export let menu; export let menu;
export let settings;
export let referenceComponent;
export const activator = createActivator('Designer', true);
let domCanvas; let domCanvas;
let canvasWidth = 3000;
let canvasHeight = 3000;
let dragStartPoint = null;
let dragCurrentPoint = null;
const sourceDragColumn$ = writable(null); const sourceDragColumn$ = writable(null);
const targetDragColumn$ = writable(null); const targetDragColumn$ = writable(null);
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
$: tables = value?.tables as any[]; $: tables = value?.tables as any[];
$: references = value?.references as any[]; $: references = value?.references as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
const tableRefs = {}; const tableRefs = {};
const referenceRefs = {}; const referenceRefs = {};
@@ -47,6 +93,84 @@
return tables; return tables;
} }
$: {
if (dbInfo) {
updateFromDbInfo($dbInfo);
}
}
$: {
detectSize(tables, domTables);
}
$: {
if (dbInfo && value?.autoLayout) {
performAutoActions($dbInfo);
}
}
function updateFromDbInfo(db = 'auto') {
if (db == 'auto' && dbInfo) db = $dbInfo;
if (!settings?.updateFromDbInfo || !db) return;
onChange(current => {
let newTables = current.tables || [];
for (const table of current.tables || []) {
const dbTable = (db.tables || []).find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
if (
stableStringify(_.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys'])) !=
stableStringify(_.pick(table, ['columns', 'primaryKey', 'foreignKeys']))
) {
newTables = newTables.map(x =>
x == table
? {
...table,
..._.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys']),
}
: x
);
}
}
let references = current?.references;
if (settings?.useDatabaseReferences) {
references = [];
for (const table of newTables) {
for (const fk of table.foreignKeys) {
const dst = newTables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
if (!dst) continue;
references.push({
designerId: uuidv1(),
sourceId: table.designerId,
targetId: dst.designerId,
joinType: '',
columns: fk.columns.map(col => ({
source: col.columnName,
target: col.refColumnName,
})),
});
}
}
}
return {
...current,
tables: newTables,
references,
};
}, true);
}
async function detectSize(tables, domTables) {
await tick();
const rects = _.values(domTables).map(x => x.getRect());
const maxX = _.max(rects.map(x => x.right));
const maxY = _.max(rects.map(x => x.bottom));
canvasWidth = Math.max(3000, maxX + 50);
canvasHeight = Math.max(3000, maxY + 50);
}
function callChange(changeFunc, skipUndoChain = undefined) { function callChange(changeFunc, skipUndoChain = undefined) {
onChange(changeFunc, skipUndoChain); onChange(changeFunc, skipUndoChain);
tick().then(recomputeReferencePositions); tick().then(recomputeReferencePositions);
@@ -69,7 +193,28 @@
); );
}; };
const selectTable = (table, addToSelection) => {
callChange(
current => ({
...current,
tables: (current.tables || []).map(x =>
x.designerId == table.designerId
? { ...x, isSelectedTable: true }
: { ...x, isSelectedTable: addToSelection ? x.isSelectedTable : false }
),
}),
true
);
};
const removeTable = table => { const removeTable = table => {
if (isMultipleTableSelection && settings?.useDatabaseReferences && settings?.canSelectTables) {
callChange(current => ({
...current,
tables: (current.tables || []).filter(x => !x.isSelectedTable),
}));
updateFromDbInfo();
} else {
callChange(current => ({ callChange(current => ({
...current, ...current,
tables: (current.tables || []).filter(x => x.designerId != table.designerId), tables: (current.tables || []).filter(x => x.designerId != table.designerId),
@@ -78,6 +223,7 @@
), ),
columns: (current.columns || []).filter(x => x.designerId != table.designerId), columns: (current.columns || []).filter(x => x.designerId != table.designerId),
})); }));
}
}; };
const changeReference = ref => { const changeReference = ref => {
@@ -150,10 +296,12 @@
pureName: foreignKey.refTableName, pureName: foreignKey.refTableName,
schemaName: foreignKey.refSchemaName, schemaName: foreignKey.refSchemaName,
}); });
const newTableDesignerId = uuidv1(); const newTableDesignerId = `${toTable.pureName}-${uuidv1()}`;
callChange(current => { callChange(current => {
const fromTable = (current.tables || []).find(x => x.designerId == designerId); const fromTable = (current.tables || []).find(x => x.designerId == designerId);
if (!fromTable) return; if (!fromTable) return current;
const alias = getNewTableAlias(toTable, current.tables);
if (alias && !settings?.allowTableAlias) return current;
return { return {
...current, ...current,
tables: [ tables: [
@@ -163,10 +311,11 @@
left: fromTable.left + 300, left: fromTable.left + 300,
top: fromTable.top + 50, top: fromTable.top + 50,
designerId: newTableDesignerId, designerId: newTableDesignerId,
alias: getNewTableAlias(toTable, current.tables), alias,
}, },
], ],
references: [ references: settings?.allowCreateRefByDrag
? [
...(current.references || []), ...(current.references || []),
{ {
designerId: uuidv1(), designerId: uuidv1(),
@@ -178,9 +327,101 @@
target: col.refColumnName, target: col.refColumnName,
})), })),
}, },
], ]
: current.references,
}; };
}); });
updateFromDbInfo();
};
const getTablesWithReferences = (db, table, current) => {
const dbTable = db.tables?.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
if (!dbTable) return;
const newTables = [];
for (const fk of dbTable.foreignKeys || []) {
const existing = [...current.tables, ...newTables].find(
x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName
);
if (!existing) {
const dst = db.tables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
if (dst) newTables.push(dst);
}
}
for (const fk of dbTable.dependencies || []) {
const existing = [...current.tables, ...newTables].find(
x => x.pureName == fk.pureName && x.schemaName == fk.schemaName
);
if (!existing) {
const dst = db.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
if (dst) newTables.push(dst);
}
}
return {
...current,
tables: [
...current.tables,
...newTables.map(x => ({
...x,
designerId: `${x.pureName}-${uuidv1()}`,
needsArrange: true,
})),
],
};
};
const handleAddTableReferences = async table => {
if (!dbInfo) return;
const db = $dbInfo;
if (!db) return;
callChange(current => {
return getTablesWithReferences(db, table, current);
});
updateFromDbInfo();
await tick();
const rect = (domTables[table.designerId] as any)?.getRect();
arrange(true, false, rect ? { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 } : null);
};
const handleChangeTableColor = table => {
showModal(ChooseColorModal, {
onChange: color => {
callChange(current => {
return {
...current,
tables: (current?.tables || []).map(table =>
table.isSelectedTable
? {
...table,
tableColor: color,
}
: table
),
};
});
},
});
};
const performAutoActions = async db => {
if (!db) return;
callChange(current => {
for (const table of current?.tables || []) {
if (table.autoAddReferences) current = getTablesWithReferences(db, table, current);
}
return {
...current,
autoLayout: false,
tables: (current?.tables || []).map(tbl => ({ ...tbl, autoAddReferences: false })),
};
});
updateFromDbInfo();
await tick();
arrange();
}; };
const handleSelectColumn = column => { const handleSelectColumn = column => {
@@ -231,12 +472,14 @@
var json = JSON.parse(data); var json = JSON.parse(data);
const { objectTypeField } = json; const { objectTypeField } = json;
if (objectTypeField != 'tables' && objectTypeField != 'views') return; if (objectTypeField != 'tables' && objectTypeField != 'views') return;
json.designerId = uuidv1(); json.designerId = `${json.pureName}-${uuidv1()}`;
json.left = e.clientX - rect.left; json.left = e.clientX - rect.left;
json.top = e.clientY - rect.top; json.top = e.clientY - rect.top;
callChange(current => { callChange(current => {
const foreignKeys = _.compact([ const foreignKeys = settings?.useDatabaseReferences
? []
: _.compact([
...(json.foreignKeys || []).map(fk => { ...(json.foreignKeys || []).map(fk => {
const tables = ((current || {}).tables || []).filter( const tables = ((current || {}).tables || []).filter(
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
@@ -265,13 +508,16 @@
), ),
]); ]);
const alias = getNewTableAlias(json, current?.tables);
if (alias && !settings?.allowTableAlias) return current;
return { return {
...current, ...current,
tables: [ tables: [
...((current || {}).tables || []), ...((current || {}).tables || []),
{ {
...json, ...json,
alias: getNewTableAlias(json, current?.tables), alias,
}, },
], ],
references: references:
@@ -279,7 +525,7 @@
? [ ? [
...((current || {}).references || []), ...((current || {}).references || []),
{ {
designerId: uuidv1(), designerId: `${current?.pureName}-${uuidv1()}`,
sourceId: foreignKeys[0].sourceId, sourceId: foreignKeys[0].sourceId,
targetId: foreignKeys[0].targetId, targetId: foreignKeys[0].targetId,
joinType: 'INNER JOIN', joinType: 'INNER JOIN',
@@ -292,6 +538,81 @@
: (current || {}).references, : (current || {}).references,
}; };
}); });
updateFromDbInfo();
};
function forEachSelected(op: Function) {
for (const tbl of _.values(tableRefs)) {
const table = tbl as any;
if (!table?.isSelected()) continue;
op(table);
}
}
const tableMoveStart = () => {
forEachSelected(t => t.moveStart());
};
const tableMove = (x, y) => {
forEachSelected(t => t.move(x, y));
tick().then(recomputeReferencePositions);
};
const tableMoveEnd = () => {
const moves = {};
forEachSelected(t => {
moves[t.getDesignerId()] = t.moveEnd();
});
callChange(current => {
return {
...current,
tables: (current?.tables || []).map(table => {
const position = moves[table.designerId];
return position
? {
...table,
left: position.left,
top: position.top,
}
: table;
}),
};
});
tick().then(recomputeReferencePositions);
};
const handleMoveStart = (x, y) => {
dragStartPoint = { x: x / zoomKoef, y: y / zoomKoef };
};
const handleMove = (dx, dy, x, y) => {
dragCurrentPoint = { x: x / zoomKoef, y: y / zoomKoef };
};
const handleMoveEnd = (x, y) => {
if (dragStartPoint && dragCurrentPoint) {
const bounds = {
left: Math.min(dragStartPoint.x, dragCurrentPoint.x),
right: Math.max(dragStartPoint.x, dragCurrentPoint.x),
top: Math.min(dragStartPoint.y, dragCurrentPoint.y),
bottom: Math.max(dragStartPoint.y, dragCurrentPoint.y),
};
callChange(
current => ({
...current,
tables: (current.tables || []).map(x => {
const domTable = domTables[x.designerId] as any;
const rect = domTable.getRect();
return {
...x,
isSelectedTable: rectanglesHaveIntersection(rect, bounds),
};
}),
}),
true
);
}
dragStartPoint = null;
dragCurrentPoint = null;
}; };
function recomputeReferencePositions() { function recomputeReferencePositions() {
@@ -299,22 +620,218 @@
if (ref) ref.recomputePosition(); if (ref) ref.recomputePosition();
} }
} }
export function canArrange() {
return settings?.canArrange;
}
export function canExport() {
return settings?.canExport;
}
export function arrange(skipUndoChain = false, arrangeAll = true, circleMiddle = { x: 0, y: 0 }) {
const graph = new GraphDefinition();
for (const table of value?.tables || []) {
const domTable = domTables[table.designerId] as any;
if (!domTable) continue;
const rect = domTable.getRect();
graph.addNode(
table.designerId,
rect.right - rect.left,
rect.bottom - rect.top,
arrangeAll || table.needsArrange ? null : { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 }
);
}
for (const reference of value?.references) {
graph.addEdge(reference.sourceId, reference.targetId);
}
graph.initialize();
const layout = GraphLayout
// initial circle layout
.createCircle(graph, circleMiddle)
// simulation with Hook's, Coulomb's and gravity law
.springyAlg()
// move nodes to avoid overlaps
.solveOverlaps()
// view box starts with [0,0]
.fixViewBox();
// layout.print();
callChange(current => {
return {
...current,
tables: (current?.tables || []).map(table => {
const node = layout.nodes[table.designerId];
// console.log('POSITION', position);
return node
? {
...table,
needsArrange: false,
left: node.left,
top: node.top,
}
: {
...table,
needsArrange: false,
};
}),
};
}, skipUndoChain);
}
export async function exportDiagram() {
const cssLinks = ['global.css', 'build/bundle.css'];
let css = '';
for (const link of cssLinks) {
const cssResp = await fetch(link);
const cssItem = await cssResp.text();
if (css) css += '\n';
css += cssItem;
}
saveFileToDisk(async filePath => {
await apiCall('files/export-diagram', {
filePath,
html: domCanvas.outerHTML,
css,
themeType: $currentThemeDefinition?.themeType,
themeClassName: $currentThemeDefinition?.className,
});
});
}
const changeStyleFunc = (name, value) => () => {
callChange(current => {
return {
...current,
style: {
...current?.style,
[name]: value,
},
};
});
};
function createMenu() {
return [
menu,
settings?.customizeStyle && [
{ divider: true },
{
text: 'Column properties',
submenu: [
{
text: `Nullability: ${value?.style?.showNullability ? 'YES' : 'NO'}`,
onClick: changeStyleFunc('showNullability', !value?.style?.showNullability),
},
{
text: `Data type: ${value?.style?.showDataType ? 'YES' : 'NO'}`,
onClick: changeStyleFunc('showDataType', !value?.style?.showDataType),
},
],
},
{
text: `Columns - ${_.startCase(value?.style?.filterColumns || 'all')}`,
submenu: [
{
text: 'All',
onClick: changeStyleFunc('filterColumns', null),
},
{
text: 'Primary Key',
onClick: changeStyleFunc('filterColumns', 'primaryKey'),
},
{
text: 'All Keys',
onClick: changeStyleFunc('filterColumns', 'allKeys'),
},
{
text: 'Not Null',
onClick: changeStyleFunc('filterColumns', 'notNull'),
},
{
text: 'Keys And Not Null',
onClick: changeStyleFunc('filterColumns', 'keysAndNotNull'),
},
],
},
{
text: `Zoom - ${(value?.style?.zoomKoef || 1) * 100}%`,
submenu: [
{
text: `10 %`,
onClick: changeStyleFunc('zoomKoef', 0.1),
},
{
text: `15 %`,
onClick: changeStyleFunc('zoomKoef', 0.15),
},
{
text: `20 %`,
onClick: changeStyleFunc('zoomKoef', 0.2),
},
{
text: `40 %`,
onClick: changeStyleFunc('zoomKoef', 0.4),
},
{
text: `60 %`,
onClick: changeStyleFunc('zoomKoef', 0.6),
},
{
text: `80 %`,
onClick: changeStyleFunc('zoomKoef', 0.8),
},
{
text: `100 %`,
onClick: changeStyleFunc('zoomKoef', 1),
},
],
},
],
];
}
</script> </script>
<div class="wrapper" use:contextMenu={menu}> <div class="wrapper noselect" use:contextMenu={createMenu}>
{#if !(tables?.length > 0)} {#if !(tables?.length > 0)}
<div class="empty">Drag &amp; drop tables or views from left panel here</div> <div class="empty">Drag &amp; drop tables or views from left panel here</div>
{/if} {/if}
<div class="canvas" bind:this={domCanvas} on:dragover={e => e.preventDefault()} on:drop={handleDrop}> <div
class="canvas"
bind:this={domCanvas}
on:dragover={e => e.preventDefault()}
on:drop={handleDrop}
style={`width:${canvasWidth}px;height:${canvasHeight}px;
${settings?.customizeStyle && value?.style?.zoomKoef ? `zoom:${value?.style?.zoomKoef};` : ''}
`}
on:mousedown={e => {
if (e.button == 0 && settings?.canSelectTables) {
callChange(
current => ({
...current,
tables: (current.tables || []).map(x => ({ ...x, isSelectedTable: false })),
}),
true
);
}
}}
use:moveDrag={settings?.canSelectTables ? [handleMoveStart, handleMove, handleMoveEnd] : null}
>
{#each references || [] as ref (ref.designerId)} {#each references || [] as ref (ref.designerId)}
<DesignerReference <svelte:component
this={referenceComponent}
bind:this={referenceRefs[ref.designerId]} bind:this={referenceRefs[ref.designerId]}
{domTables} {domTables}
reference={ref} reference={ref}
onChangeReference={changeReference} onChangeReference={changeReference}
onRemoveReference={removeReference} onRemoveReference={removeReference}
designer={value} designer={value}
{settings}
/> />
{/each} {/each}
<!-- <!--
@@ -335,17 +852,42 @@
onSelectColumn={handleSelectColumn} onSelectColumn={handleSelectColumn}
onChangeColumn={handleChangeColumn} onChangeColumn={handleChangeColumn}
onAddReferenceByColumn={handleAddReferenceByColumn} onAddReferenceByColumn={handleAddReferenceByColumn}
onAddAllReferences={handleAddTableReferences}
onChangeTableColor={handleChangeTableColor}
onMoveReferences={recomputeReferencePositions} onMoveReferences={recomputeReferencePositions}
{table} {table}
{conid}
{database}
{zoomKoef}
{isMultipleTableSelection}
onChangeTable={changeTable} onChangeTable={changeTable}
onBringToFront={bringToFront} onBringToFront={bringToFront}
onSelectTable={selectTable}
onRemoveTable={removeTable} onRemoveTable={removeTable}
onMoveStart={tableMoveStart}
onMove={tableMove}
onMoveEnd={tableMoveEnd}
{domCanvas} {domCanvas}
designer={value} designer={value}
{sourceDragColumn$} {sourceDragColumn$}
{targetDragColumn$} {targetDragColumn$}
{settings}
/> />
{/each} {/each}
{#if dragStartPoint && dragCurrentPoint}
<svg class="drag-rect">
<polyline
points={`
${dragStartPoint.x},${dragStartPoint.y}
${dragStartPoint.x},${dragCurrentPoint.y}
${dragCurrentPoint.x},${dragCurrentPoint.y}
${dragCurrentPoint.x},${dragStartPoint.y}
${dragStartPoint.x},${dragStartPoint.y}
`}
/>
</svg>
{/if}
</div> </div>
</div> </div>
@@ -360,8 +902,26 @@
font-size: 20px; font-size: 20px;
} }
.canvas { .canvas {
width: 3000px;
height: 3000px;
position: relative; position: relative;
} }
svg.drag-rect {
visibility: hidden;
pointer-events: none;
}
:global(.dbgate-screen) svg.drag-rect {
visibility: visible;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
:global(.dbgate-screen) svg.drag-rect polyline {
fill: none;
stroke: var(--theme-bg-4);
stroke-width: 2;
}
</style> </style>

View File

@@ -1,22 +1,38 @@
<script lang="ts"> <script lang="ts">
import { presetDarkPalettes, presetPalettes } from '@ant-design/colors';
import { computeDbDiffRows } from 'dbgate-tools';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { createDatabaseObjectMenu } from '../appobj/DatabaseObjectAppObject.svelte';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import InputTextModal from '../modals/InputTextModal.svelte'; import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import { currentThemeDefinition } from '../stores';
import contextMenu from '../utility/contextMenu'; import contextMenu from '../utility/contextMenu';
import moveDrag from '../utility/moveDrag'; import moveDrag from '../utility/moveDrag';
import ColumnLine from './ColumnLine.svelte'; import ColumnLine from './ColumnLine.svelte';
import DomTableRef from './DomTableRef'; import DomTableRef from './DomTableRef';
export let conid;
export let database;
export let table; export let table;
export let zoomKoef;
export let onChangeTable; export let onChangeTable;
export let onBringToFront; export let onBringToFront;
export let onSelectTable;
export let onRemoveTable; export let onRemoveTable;
export let onAddAllReferences;
export let onCreateReference; export let onCreateReference;
export let onAddReferenceByColumn; export let onAddReferenceByColumn;
export let onSelectColumn; export let onSelectColumn;
export let onChangeColumn; export let onChangeColumn;
export let onChangeTableColor;
export let isMultipleTableSelection;
export let onMoveStart;
export let onMove;
export let onMoveEnd;
// export let sourceDragColumn; // export let sourceDragColumn;
// export let setSourceDragColumn; // export let setSourceDragColumn;
@@ -31,6 +47,7 @@
// export let domTablesRef; // export let domTablesRef;
export let designer; export let designer;
export let onMoveReferences; export let onMoveReferences;
export let settings;
let movingPosition = null; let movingPosition = null;
let domWrapper; let domWrapper;
@@ -39,29 +56,95 @@
$: pureName = table?.pureName; $: pureName = table?.pureName;
$: alias = table?.alias; $: alias = table?.alias;
$: columns = table?.columns as any[]; $: columns = (table?.columns as any[]).filter(x => shouldShowColumn(table, x, designer?.style));
$: designerId = table?.designerId; $: designerId = table?.designerId;
$: objectTypeField = table?.objectTypeField; $: objectTypeField = table?.objectTypeField;
$: left = table?.left; $: left = table?.left;
$: top = table?.top; $: top = table?.top;
function handleMoveStart() { export function isSelected() {
return table?.isSelectedTable;
}
export function getDesignerId() {
return designerId;
}
export function moveStart() {
movingPosition = { left, top }; movingPosition = { left, top };
} }
export function move(x, y) {
movingPosition.left += x / zoomKoef;
movingPosition.top += y / zoomKoef;
}
export function moveEnd() {
const res = movingPosition;
movingPosition = null;
return res;
}
function shouldShowColumn(table, column, style) {
if (!settings?.customizeStyle) {
return true;
}
switch (style?.filterColumns || 'all') {
case 'primaryKey':
return table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName);
case 'allKeys':
return (
table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName) ||
table?.foreignKeys?.find(fk => fk.columns.find(x => x.columnName == column?.columnName))
);
case 'keysAndNotNull':
return (
column?.notNull ||
table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName) ||
table?.foreignKeys?.find(fk => fk.columns.find(x => x.columnName == column?.columnName))
);
case 'notNull':
return column?.notNull;
}
return true;
}
function handleMoveStart() {
if (settings?.canSelectTables) {
onMoveStart();
} else {
moveStart();
}
}
function handleMove(x, y) { function handleMove(x, y) {
movingPosition.left += x; if (settings?.canSelectTables) {
movingPosition.top += y; onMove(x, y);
} else {
move(x, y);
tick().then(onMoveReferences); tick().then(onMoveReferences);
} }
}
function handleMoveEnd() { function handleMoveEnd() {
if (settings?.canSelectTables) {
onMoveEnd();
} else {
const position = moveEnd();
onChangeTable({ onChangeTable({
...table, ...table,
left: movingPosition.left, left: position.left,
top: movingPosition.top, top: position.top,
}); });
movingPosition = null;
tick().then(onMoveReferences); tick().then(onMoveReferences);
} }
}
function getTableColorStyle(themeDef, table, colorIndex = 3) {
if (!table?.tableColor) return null;
const palettes = themeDef?.themeType == 'dark' ? presetDarkPalettes : presetPalettes;
const palette = palettes[table?.tableColor];
if (!palette) return null;
return `background: ${palette[colorIndex]}`;
}
export function getDomTable() { export function getDomTable() {
const domRefs = { ...columnRefs }; const domRefs = { ...columnRefs };
@@ -87,6 +170,8 @@
return [ return [
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) }, { text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
{ divider: true }, { divider: true },
settings?.allowTableAlias &&
!isMultipleTableSelection && [
{ text: 'Set table alias', onClick: handleSetTableAlias }, { text: 'Set table alias', onClick: handleSetTableAlias },
alias && { alias && {
text: 'Remove table alias', text: 'Remove table alias',
@@ -96,29 +181,50 @@
alias: null, alias: null,
}), }),
}, },
],
settings?.allowAddAllReferences &&
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
settings?.appendTableSystemMenu &&
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
]; ];
} }
</script> </script>
<div <div
class="wrapper" class="wrapper"
class:canSelectColumns={settings?.canSelectColumns}
class:isSelectedTable={table?.isSelectedTable}
style={`left: ${movingPosition ? movingPosition.left : left}px; top:${movingPosition ? movingPosition.top : top}px`} style={`left: ${movingPosition ? movingPosition.left : left}px; top:${movingPosition ? movingPosition.top : top}px`}
bind:this={domWrapper} bind:this={domWrapper}
on:mousedown={() => onBringToFront(table)} on:mousedown={e => {
if (e.button == 0) {
e.stopPropagation();
onBringToFront(table);
if (settings?.canSelectTables && !table?.isSelectedTable) {
onSelectTable(table, e.ctrlKey);
}
}
}}
use:contextMenu={settings?.canSelectColumns ? '__no_menu' : createMenu}
use:moveDrag={settings?.canSelectColumns ? null : [handleMoveStart, handleMove, handleMoveEnd]}
> >
<div <div
class="header" class="header"
class:isTable={objectTypeField == 'tables'} class:isTable={objectTypeField == 'tables'}
class:isView={objectTypeField == 'views'} class:isView={objectTypeField == 'views'}
use:moveDrag={[handleMoveStart, handleMove, handleMoveEnd]} use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null}
use:contextMenu={createMenu} use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
style={getTableColorStyle($currentThemeDefinition, table)}
> >
<div>{alias || pureName}</div> <div>{alias || pureName}</div>
{#if settings?.showTableCloseButton}
<div class="close" on:click={() => onRemoveTable(table)}> <div class="close" on:click={() => onRemoveTable(table)}>
<FontIcon icon="icon close" /> <FontIcon icon="icon close" />
</div> </div>
{/if}
</div> </div>
<div class="columns" on:scroll={() => tick().then(onMoveReferences)}> <div class="columns" on:scroll={() => tick().then(onMoveReferences)} class:scroll={settings?.allowScrollColumns}>
{#each columns || [] as column} {#each columns || [] as column}
<ColumnLine <ColumnLine
{column} {column}
@@ -131,10 +237,17 @@
{targetDragColumn$} {targetDragColumn$}
{onCreateReference} {onCreateReference}
{onAddReferenceByColumn} {onAddReferenceByColumn}
{settings}
bind:domLine={columnRefs[column.columnName]} bind:domLine={columnRefs[column.columnName]}
/> />
{/each} {/each}
</div> </div>
{#if table?.isSelectedTable}
<div class="selection-marker lt" />
<div class="selection-marker rt" />
<div class="selection-marker lb" />
<div class="selection-marker rb" />
{/if}
</div> </div>
<style> <style>
@@ -143,16 +256,51 @@
background-color: var(--theme-bg-0); background-color: var(--theme-bg-0);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
} }
/* :global(.dbgate-screen) .isSelectedTable {
border: 3px solid var(--theme-border);
} */
.selection-marker {
display: none;
position: absolute;
width: 6px;
height: 6px;
background: var(--theme-font-1);
}
.selection-marker.lt {
left: -3px;
top: -3px;
}
.selection-marker.rt {
right: -3px;
top: -3px;
}
.selection-marker.lb {
left: -3px;
bottom: -3px;
}
.selection-marker.rb {
right: -3px;
bottom: -3px;
}
:global(.dbgate-screen) .selection-marker {
display: block;
}
:global(.dbgate-screen) .wrapper:not(.canSelectColumns) {
cursor: pointer;
}
.header { .header {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
padding: 2px; padding: 2px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
:global(.dbgate-screen) .header {
cursor: pointer;
}
.header.isTable { .header.isTable {
background: var(--theme-bg-blue); background: var(--theme-bg-blue);
} }
@@ -169,9 +317,11 @@
background: var(--theme-bg-3); background: var(--theme-bg-3);
} }
.columns { .columns {
max-height: 400px;
overflow-y: auto;
width: calc(100% - 10px); width: calc(100% - 10px);
padding: 5px; padding: 5px;
} }
.columns.scroll {
max-height: 400px;
overflow-y: auto;
}
</style> </style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import Designer from './Designer.svelte';
import DiagramDesignerReference from './DiagramDesignerReference.svelte';
</script>
<Designer
{...$$props}
settings={{
showTableCloseButton: false,
allowColumnOperations: false,
allowCreateRefByDrag: false,
allowTableAlias: false,
updateFromDbInfo: true,
useDatabaseReferences: true,
allowScrollColumns: false,
allowAddAllReferences: true,
canArrange: true,
canExport: true,
canSelectColumns: false,
canSelectTables: true,
allowChangeColor: true,
appendTableSystemMenu: true,
customizeStyle: true,
}}
referenceComponent={DiagramDesignerReference}
/>

View File

@@ -0,0 +1,98 @@
<script lang="ts">
import _ from 'lodash';
import { intersectLineBox } from './designerMath';
export let reference;
export let onRemoveReference;
export let onChangeReference;
export let designer;
export let domTables;
export let settings;
let src = null;
let dst = null;
let arrowPt = null;
let arrowAngle = 0;
const arwi = 12;
const arhi = 12;
const arpad = 3;
export function recomputePosition() {
const { designerId, sourceId, targetId, columns, joinType } = reference;
/** @type {DomTableRef} */
const sourceTable = domTables[sourceId];
/** @type {DomTableRef} */
const targetTable = domTables[targetId];
if (!sourceTable || !targetTable) return null;
const sourceRect = sourceTable.getRect();
const targetRect = targetTable.getRect();
if (!sourceRect || !targetRect) return null;
src = {
x: (sourceRect.left + sourceRect.right) / 2,
y: (sourceRect.top + sourceRect.bottom) / 2,
};
dst = {
x: (targetRect.left + targetRect.right) / 2,
y: (targetRect.top + targetRect.bottom) / 2,
};
arrowPt = intersectLineBox(src, dst, targetRect)[0];
arrowAngle = Math.atan2(dst.y - src.y, dst.x - src.x);
}
$: {
domTables;
recomputePosition();
}
</script>
<svg>
{#if src && dst}
<polyline
points={`
${src.x},${src.y}
${dst.x},${dst.y}
`}
/>
{/if}
{#if arrowPt}
<g transform={`translate(${arrowPt.x} ${arrowPt.y})`}>
<polygon
transform={`rotate(${180 + (arrowAngle * 180) / Math.PI})`}
points={`
0,0
${arwi},${arhi / 2}
${arwi},${-arhi / 2}
`}
/>
</g>
{/if}
</svg>
<!-- -->
<style>
svg {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
polyline {
fill: none;
stroke: var(--theme-bg-4);
stroke-width: 2;
}
polygon {
fill: var(--theme-font-1);
}
</style>

View File

@@ -0,0 +1,484 @@
import _ from 'lodash';
import {
IBoxBounds,
IPoint,
rectangleDistance,
rectangleIntersectArea,
solveOverlapsInIntervalArray,
Vector2D,
} from './designerMath';
import { union, intersection } from 'interval-operations';
const MIN_NODE_DISTANCE = 50;
const SPRING_LENGTH = 100;
const SPRINGY_STEPS = 50;
const GRAVITY_X = 0.005;
const GRAVITY_Y = 0.01;
// const REPULSION = 500_000;
const REPULSION = 1000;
const MAX_FORCE_SIZE = 100;
const NODE_MARGIN = 30;
const GRAVITY_EXPONENT = 1.05;
// const MOVE_STEP = 20;
// const MOVE_BIG_STEP = 50;
// const MOVE_STEP_COUNT = 100;
// const MINIMAL_SCORE_BENEFIT = 1;
// const SCORE_ASPECT_RATIO = 1.6;
class GraphNode {
neightboors: GraphNode[] = [];
radius: number;
constructor(
public graph: GraphDefinition,
public designerId: string,
public width: number,
public height: number,
public fixedPosition: IPoint
) {}
initialize() {
this.radius = Math.sqrt((this.width * this.width) / 4 + (this.height * this.height) / 4);
}
}
class GraphEdge {
constructor(public graph: GraphDefinition, public source: GraphNode, public target: GraphNode) {}
}
// function initialCompareNodes(a: GraphNode, b: GraphNode) {
// if (a.neightboors.length < b.neightboors.length) return -1;
// if (a.neightboors.length > b.neightboors.length) return 1;
// if (a.height < b.height) return -1;
// if (a.height > b.height) return 1;
// return;
// }
export class GraphDefinition {
nodes: { [designerId: string]: GraphNode } = {};
edges: GraphEdge[] = [];
addNode(designerId: string, width: number, height: number, fixedPosition: IPoint) {
this.nodes[designerId] = new GraphNode(this, designerId, width, height, fixedPosition);
}
addEdge(sourceId: string, targetId: string) {
const source = this.nodes[sourceId];
const target = this.nodes[targetId];
if (
source &&
target &&
!this.edges.find(x => (x.source == source && x.target == target) || (x.target == source && x.source == target))
) {
this.edges.push(new GraphEdge(this, source, target));
}
}
initialize() {
for (const node of Object.values(this.nodes)) {
for (const edge of this.edges) {
if (edge.source == node && !node.neightboors.includes(edge.target)) node.neightboors.push(edge.target);
if (edge.target == node && !node.neightboors.includes(edge.source)) node.neightboors.push(edge.source);
}
node.initialize();
}
}
detectCentreNode(): GraphNode {
if (_.values(this.nodes).find(x => x.fixedPosition)) {
return null;
}
const res: GraphNode[] = [];
for (const n1 of _.values(this.nodes)) {
let candidate = true;
for (const n2 of _.values(this.nodes)) {
if (n1 == n2) {
continue;
}
if (!n1.neightboors.includes(n2)) {
candidate = false;
break;
}
}
if (candidate) {
res.push(n1);
}
}
if (res.length == 1) return res[0];
return null;
}
}
class LayoutNode {
position: Vector2D;
left: number;
right: number;
top: number;
bottom: number;
// paddedRect: IBoxBounds;
rangeXPadded: [number, number];
rangeYPadded: [number, number];
constructor(public node: GraphNode, public x: number, public y: number) {
this.left = x - node.width / 2;
this.top = y - node.height / 2;
this.right = x + node.width / 2;
this.bottom = y + node.height / 2;
this.position = new Vector2D(x, y);
this.rangeXPadded = [this.left - NODE_MARGIN, this.right + NODE_MARGIN];
this.rangeYPadded = [this.top - NODE_MARGIN, this.bottom + NODE_MARGIN];
// this.paddedRect = {
// left: this.left - NODE_MARGIN,
// top: this.top - NODE_MARGIN,
// right: this.right + NODE_MARGIN,
// bottom: this.bottom + NODE_MARGIN,
// };
}
translate(dx: number, dy: number, forceMoveFixed = false) {
if (this.node.fixedPosition && !forceMoveFixed) return this;
return new LayoutNode(this.node, this.x + dx, this.y + dy);
}
distanceTo(node: LayoutNode) {
return rectangleDistance(this, node);
}
// intersectArea(node: LayoutNode) {
// return rectangleIntersectArea(this.paddedRect, node.paddedRect);
// }
hasPaddedIntersect(node: LayoutNode) {
const xIntersection = intersection(this.rangeXPadded, node.rangeXPadded) as [number, number];
const yIntersection = intersection(this.rangeYPadded, node.rangeYPadded) as [number, number];
return (
xIntersection &&
xIntersection[1] - xIntersection[0] > 0.1 &&
yIntersection &&
yIntersection[1] - yIntersection[0] > 0.1
);
}
}
class ForceAlgorithmStep {
nodeForces: { [designerId: string]: Vector2D } = {};
constructor(public layout: GraphLayout) {}
applyForce(node: LayoutNode, force: Vector2D) {
const size = force.magnitude();
if (size > MAX_FORCE_SIZE) {
force = force.normalise().multiply(MAX_FORCE_SIZE);
}
if (node.node.designerId in this.nodeForces) {
this.nodeForces[node.node.designerId] = this.nodeForces[node.node.designerId].add(force);
} else {
this.nodeForces[node.node.designerId] = force;
}
}
applyCoulombsLaw() {
for (const n1 of _.values(this.layout.nodes)) {
for (const n2 of _.values(this.layout.nodes)) {
if (n1.node.designerId == n2.node.designerId) {
continue;
}
const d = n1.position.subtract(n2.position);
const direction = d.normalise();
const distance = n1.distanceTo(n2) + MIN_NODE_DISTANCE;
this.applyForce(n1, direction.multiply((+0.5 * REPULSION) / (distance * distance)));
this.applyForce(n2, direction.multiply((-0.5 * REPULSION) / (distance * distance)));
}
}
}
applyHooksLaw() {
for (const edge of this.layout.edges) {
const d = edge.target.position.subtract(edge.source.position); // the direction of the spring
const displacement = SPRING_LENGTH - edge.length;
var direction = d.normalise();
// apply force to each end point
this.applyForce(edge.source, direction.multiply(displacement * -0.5));
this.applyForce(edge.target, direction.multiply(displacement * +0.5));
}
}
applyGravity() {
for (const node of _.values(this.layout.nodes)) {
this.applyForce(
node,
// new Vector2D(-node.position.x * GRAVITY_X, -node.position.y * GRAVITY_Y)
new Vector2D(
-Math.pow(Math.abs(node.position.x), GRAVITY_EXPONENT) * Math.sign(node.position.x) * GRAVITY_X,
-Math.pow(Math.abs(node.position.y), GRAVITY_EXPONENT) * Math.sign(node.position.y) * GRAVITY_Y
)
);
}
}
moveNode(node: LayoutNode): LayoutNode {
const force = this.nodeForces[node.node.designerId];
if (force) {
return node.translate(force.x, force.y);
}
return node;
}
}
class LayoutEdge {
edge: GraphEdge;
length: number;
source: LayoutNode;
target: LayoutNode;
}
function addNodeNeighboors(nodes: GraphNode[], res: GraphNode[], addedNodes: Set<string>) {
const nodesSorted = _.sortBy(nodes, [x => x.neightboors.length, x => x.height, x => x.designerId]);
for (const node of nodesSorted) {
if (addedNodes.has(node.designerId)) continue;
addedNodes.add(node.designerId);
res.push(node);
addNodeNeighboors(node.neightboors, res, addedNodes);
}
return res;
}
export class GraphLayout {
nodes: { [designerId: string]: LayoutNode } = {};
edges: LayoutEdge[] = [];
constructor(public graph: GraphDefinition) {}
static createCircle(graph: GraphDefinition, middle: IPoint = { x: 0, y: 0 }): GraphLayout {
const res = new GraphLayout(graph);
if (_.isEmpty(graph.nodes)) return res;
const addedNodes = new Set<string>();
const circleSortedNodes: GraphNode[] = [];
const centreNode = graph.detectCentreNode();
addNodeNeighboors(
_.values(graph.nodes).filter(x => x != centreNode && !x.fixedPosition),
circleSortedNodes,
addedNodes
);
const nodeRadius = _.max(circleSortedNodes.map(x => x.radius));
const nodeCount = circleSortedNodes.length;
const radius = (nodeCount * nodeRadius) / (2 * Math.PI) + nodeRadius;
let angle = 0;
const dangle = (2 * Math.PI) / circleSortedNodes.length;
for (const node of circleSortedNodes) {
res.nodes[node.designerId] = new LayoutNode(
node,
middle.x + Math.sin(angle) * radius,
middle.y + Math.cos(angle) * radius
);
angle += dangle;
}
for (const node of _.values(graph.nodes).filter(x => x.fixedPosition)) {
res.nodes[node.designerId] = new LayoutNode(node, node.fixedPosition.x, node.fixedPosition.y);
}
if (centreNode) {
res.nodes[centreNode.designerId] = new LayoutNode(centreNode, middle.x, middle.y);
}
res.fillEdges();
return res;
}
fillEdges() {
this.edges = this.graph.edges.map(edge => {
const res = new LayoutEdge();
res.edge = edge;
const n1 = this.nodes[edge.source.designerId];
const n2 = this.nodes[edge.target.designerId];
res.length = n1.distanceTo(n2);
res.source = n1;
res.target = n2;
return res;
});
}
changePositions(nodeFunc: (node: LayoutNode) => LayoutNode, callFillEdges = true): GraphLayout {
const res = new GraphLayout(this.graph);
res.nodes = _.mapValues(this.nodes, nodeFunc);
if (callFillEdges) res.fillEdges();
return res;
}
fixViewBox() {
const minX = _.min(_.values(this.nodes).map(n => n.left));
const minY = _.min(_.values(this.nodes).map(n => n.top));
return this.changePositions(n => n.translate(-minX + 50, -minY + 50, true));
}
springyStep() {
const step = new ForceAlgorithmStep(this);
step.applyHooksLaw();
step.applyCoulombsLaw();
step.applyGravity();
return this.changePositions(node => step.moveNode(node));
}
springyAlg() {
let res: GraphLayout = this;
for (let step = 0; step < SPRINGY_STEPS; step++) {
res = res.springyStep();
}
return res;
}
// score() {
// let res = 0;
// for (const n1 of _.values(this.nodes)) {
// for (const n2 of _.values(this.nodes)) {
// if (n1.node.designerId == n2.node.designerId) {
// continue;
// }
// res += n1.intersectArea(n2);
// }
// }
// const minX = _.min(_.values(this.nodes).map(n => n.left));
// const minY = _.min(_.values(this.nodes).map(n => n.top));
// const maxX = _.max(_.values(this.nodes).map(n => n.right));
// const maxY = _.max(_.values(this.nodes).map(n => n.bottom));
// res += maxX - minX;
// res += (maxY - minY) * SCORE_ASPECT_RATIO;
// return res;
// }
// tryMoveNode(node: LayoutNode): GraphLayout[] {
// if (node.node.fixedPosition) return [];
// return [
// this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x), false),
// this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x), false),
// this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x), false),
// this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x), false),
// this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
// this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
// this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
// this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
// ];
// }
// tryMoveElement() {
// let res = null;
// let resScore = null;
// for (const node of _.values(this.nodes)) {
// for (const item of this.tryMoveNode(node)) {
// const score = item.score();
// if (resScore == null || score < resScore) {
// res = item;
// resScore = score;
// }
// }
// }
// return res;
// }
// doMoveSteps() {
// let res: GraphLayout = this;
// let score = res.score();
// const start = new Date().getTime();
// for (let step = 0; step < MOVE_STEP_COUNT; step++) {
// const lastRes = res;
// res = res.tryMoveElement();
// if (!res) {
// lastRes.fillEdges();
// return lastRes;
// }
// const newScore = res.score();
// // console.log('STEP, SCORE, NEW SCORE', step, score, newScore);
// if (score - newScore < MINIMAL_SCORE_BENEFIT || new Date().getTime() - start > 1000) {
// lastRes.fillEdges();
// return lastRes;
// }
// score = newScore;
// }
// res.fillEdges();
// return res;
// }
solveOverlaps(): GraphLayout {
const nodes = _.sortBy(_.values(this.nodes), x => x.position.magnitude());
const res = new GraphLayout(this.graph);
for (const node of nodes) {
const placedNodes = _.values(res.nodes);
if (placedNodes.find(x => x.hasPaddedIntersect(node))) {
// if (node.node.designerId.startsWith('ProtocolWinPriceAllocation')) {
// console.log('PLACING NODE', node);
// console.log('PLACED NODES', placedNodes);
// }
// intersection found, must perform moving algorithm
const xIntervalArray = union(
...placedNodes.filter(x => intersection(x.rangeYPadded, node.rangeYPadded)).map(x => x.rangeXPadded)
);
const yIntervalArray = union(
...placedNodes.filter(x => intersection(x.rangeXPadded, node.rangeXPadded)).map(x => x.rangeYPadded)
);
// if (node.node.designerId.startsWith('ProtocolWinPriceAllocation')) {
// console.log('xIntervalArray', xIntervalArray);
// console.log('yIntervalArray', yIntervalArray);
// }
const newX = solveOverlapsInIntervalArray(node.x, node.node.width + NODE_MARGIN * 2, xIntervalArray as any);
const newY = solveOverlapsInIntervalArray(node.y, node.node.height + NODE_MARGIN * 2, yIntervalArray as any);
// if (node.node.designerId.startsWith('ProtocolWinPriceAllocation')) {
// console.log('NEWXY', newX, newY);
// }
const newNode =
Math.abs(newX - node.x) < Math.abs(newY - node.y)
? new LayoutNode(node.node, newX, node.y)
: new LayoutNode(node.node, node.x, newY);
res.nodes[node.node.designerId] = newNode;
// if (placedNodes.find(x => x.hasPaddedIntersect(newNode))) {
// console.log('!!!!! LOGICAL ERROR WHEN PLACING', newNode);
// }
} else {
res.nodes[node.node.designerId] = node;
}
}
res.fillEdges();
return res;
}
print() {
for (const node of _.values(this.nodes)) {
console.log({
designerId: node.node.designerId,
left: node.left,
top: node.top,
right: node.right,
bottom: node.bottom,
});
}
}
}

View File

@@ -1,5 +1,26 @@
<script lang="ts"> <script lang="ts">
import Designer from './Designer.svelte'; import Designer from './Designer.svelte';
import QueryDesignerReference from './QueryDesignerReference.svelte';
</script> </script>
<Designer {...$$props} /> <Designer
{...$$props}
settings={{
showTableCloseButton: true,
allowColumnOperations: true,
allowCreateRefByDrag: true,
allowTableAlias: true,
updateFromDbInfo: false,
useDatabaseReferences: false,
allowScrollColumns: true,
allowAddAllReferences: false,
canArrange: false,
canExport: false,
canSelectColumns: true,
canSelectTables: false,
allowChangeColor: false,
appendTableSystemMenu: false,
customizeStyle: false,
}}
referenceComponent={QueryDesignerReference}
/>

View File

@@ -8,6 +8,7 @@
export let onChangeReference; export let onChangeReference;
export let designer; export let designer;
export let domTables; export let domTables;
export let settings;
let src = null; let src = null;
let dst = null; let dst = null;

View File

@@ -0,0 +1,170 @@
import { intersection, arrayDifference } from 'interval-operations';
import _ from 'lodash';
export interface IPoint {
x: number;
y: number;
}
export interface IBoxBounds {
left: number;
top: number;
right: number;
bottom: number;
}
// helpers for figuring out where to draw arrows
export function intersectLineLine(p1: IPoint, p2: IPoint, p3: IPoint, p4: IPoint): IPoint {
const denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
// lines are parallel
if (denom === 0) {
return null;
}
const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom;
const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom;
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return null;
}
return {
x: p1.x + ua * (p2.x - p1.x),
y: p1.y + ua * (p2.y - p1.y),
};
}
export function intersectLineBox(p1: IPoint, p2: IPoint, box: IBoxBounds): IPoint[] {
const tl = { x: box.left, y: box.top };
const tr = { x: box.right, y: box.top };
const bl = { x: box.left, y: box.bottom };
const br = { x: box.right, y: box.bottom };
const res = [];
let item;
if ((item = intersectLineLine(p1, p2, tl, tr))) {
res.push(item);
} // top
if ((item = intersectLineLine(p1, p2, tr, br))) {
res.push(item);
} // right
if ((item = intersectLineLine(p1, p2, br, bl))) {
res.push(item);
} // bottom
if ((item = intersectLineLine(p1, p2, bl, tl))) {
res.push(item);
} // left
return res;
}
export function rectangleDistance(r1: IBoxBounds, r2: IBoxBounds) {
function dist(x1: number, y1: number, x2: number, y2: number) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
const x1 = r1.left;
const y1 = r1.top;
const x1b = r1.right;
const y1b = r1.bottom;
const x2 = r2.left;
const y2 = r2.top;
const x2b = r2.right;
const y2b = r2.bottom;
const left = x2b < x1;
const right = x1b < x2;
const bottom = y2b < y1;
const top = y1b < y2;
if (top && left) return dist(x1, y1b, x2b, y2);
else if (left && bottom) return dist(x1, y1, x2b, y2b);
else if (bottom && right) return dist(x1b, y1, x2, y2b);
else if (right && top) return dist(x1b, y1b, x2, y2);
else if (left) return x1 - x2b;
else if (right) return x2 - x1b;
else if (bottom) return y1 - y2b;
else if (top) return y2 - y1b;
// rectangles intersect
else return 0;
}
export function rectangleIntersectArea(rect1: IBoxBounds, rect2: IBoxBounds) {
const x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
const y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
// console.log('rectangleIntersectArea', rect1, rect2, x_overlap * y_overlap);
// if (rect1.left < 100 && rect2.left < 100) {
// console.log('rectangleIntersectArea', rect1, rect2, x_overlap * y_overlap);
// }
return x_overlap * y_overlap;
}
export function rectanglesHaveIntersection(rect1: IBoxBounds, rect2: IBoxBounds) {
const xIntersection = intersection([rect1.left, rect1.right], [rect2.left, rect2.right]);
const yIntersection = intersection([rect1.top, rect1.bottom], [rect2.top, rect2.bottom]);
return !!xIntersection && !!yIntersection;
}
export class Vector2D {
constructor(public x: number, public y: number) {}
static random() {
return new Vector2D(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5));
}
add(v2: Vector2D) {
return new Vector2D(this.x + v2.x, this.y + v2.y);
}
subtract(v2: Vector2D) {
return new Vector2D(this.x - v2.x, this.y - v2.y);
}
multiply(n: number) {
return new Vector2D(this.x * n, this.y * n);
}
divide(n: number) {
return new Vector2D(this.x / n || 0, this.y / n || 0); // Avoid divide by zero errors..
}
magnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
normal(n: number) {
return new Vector2D(-this.y, this.x);
}
normalise() {
return this.divide(this.magnitude());
}
}
export function solveOverlapsInIntervalArray(position: number, size: number, usedIntervals: [number, number][]) {
const freeIntervals = arrayDifference([[-Infinity, Infinity]], usedIntervals) as [number, number][];
const candidates = [];
for (const interval of freeIntervals) {
const intervalSize = interval[1] - interval[0];
if (intervalSize < size) continue;
if (interval[1] < position) {
candidates.push(interval[1] - size / 2);
} else if (interval[0] > position) {
candidates.push(interval[0] + size / 2);
} else {
// position is in interval
let candidate = position;
if (candidate - size / 2 < interval[0]) candidate = interval[0] + size / 2;
if (candidate + size / 2 > interval[1]) candidate = interval[1] - size / 2;
candidates.push(candidate);
}
}
return _.minBy(candidates, x => Math.abs(x - position));
}

View File

@@ -25,6 +25,7 @@
'icon settings': 'mdi mdi-cog', 'icon settings': 'mdi mdi-cog',
'icon version': 'mdi mdi-ticket-confirmation', 'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin', 'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
'icon columns': 'mdi mdi-view-column', 'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline', 'icon columns-outline': 'mdi mdi-view-column-outline',
@@ -122,6 +123,7 @@
'img preview': 'mdi mdi-file-find color-icon-red', 'img preview': 'mdi mdi-file-find color-icon-red',
'img favorite': 'mdi mdi-star color-icon-yellow', 'img favorite': 'mdi mdi-star color-icon-yellow',
'img query-design': 'mdi mdi-vector-polyline-edit color-icon-red', 'img query-design': 'mdi mdi-vector-polyline-edit color-icon-red',
'img diagram': 'mdi mdi-graph color-icon-blue',
'img yaml': 'mdi mdi-code-brackets color-icon-red', 'img yaml': 'mdi mdi-code-brackets color-icon-red',
'img compare': 'mdi mdi-compare color-icon-red', 'img compare': 'mdi mdi-compare color-icon-red',

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import ColorSelector from '../forms/ColorSelector.svelte';
import ModalBase from './ModalBase.svelte';
export let color;
export let header;
export let text;
export let onChange;
$: value = color;
</script>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">{header}</svelte:fragment>
<div class="m-2">
{text}
</div>
<ColorSelector
{value}
on:change={e => {
value = e.detail;
onChange(value);
}}
/>
</ModalBase>

View File

@@ -0,0 +1,93 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('DiagramTab');
registerFileCommands({
idPrefix: 'diagram',
category: 'Diagram',
getCurrentEditor,
folder: 'diagrams',
format: 'json',
fileExtension: 'diagram',
undoRedo: true,
});
</script>
<script lang="ts">
import useEditorData from '../query/useEditorData';
import { extensions } from '../stores';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { registerFileCommands } from '../commands/stdCommands';
import createUndoReducer from '../utility/createUndoReducer';
import _ from 'lodash';
import { findEngineDriver } from 'dbgate-tools';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import DiagramDesigner from '../designer/DiagramDesigner.svelte';
export let tabid;
export let conid;
export let database;
export let initialArgs;
export const activator = createActivator('DiagramTab', true);
$: setEditorData($modelState.value);
export function getTabId() {
return tabid;
}
export function getData() {
return $editorState.value || '';
}
export function canUndo() {
return $modelState.canUndo;
}
export function undo() {
dispatchModel({ type: 'undo' });
}
export function canRedo() {
return $modelState.canRedo;
}
export function redo() {
dispatchModel({ type: 'redo' });
}
const handleChange = (value, skipUndoChain) =>
// @ts-ignore
dispatchModel({
type: 'compute',
useMerge: skipUndoChain,
compute: v => (_.isFunction(value) ? value(v) : value),
});
const { editorState, editorValue, setEditorData } = useEditorData({
tabid,
onInitialData: value => {
dispatchModel({ type: 'reset', value });
},
});
const [modelState, dispatchModel] = createUndoReducer({
tables: [],
references: [],
});
function createMenu() {
return [
{ command: 'diagram.save' },
{ command: 'diagram.saveAs' },
{ command: 'designer.arrange' },
{ command: 'diagram.export' },
{ divider: true },
{ command: 'diagram.undo' },
{ command: 'diagram.redo' },
];
}
</script>
<DiagramDesigner value={$modelState.value || {}} {conid} {database} onChange={handleChange} menu={createMenu} />

View File

@@ -18,6 +18,7 @@ import * as YamlEditorTab from './YamlEditorTab.svelte';
import * as CompareModelTab from './CompareModelTab.svelte'; import * as CompareModelTab from './CompareModelTab.svelte';
import * as JsonTab from './JsonTab.svelte'; import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte'; import * as ChangelogTab from './ChangelogTab.svelte';
import * as DiagramTab from './DiagramTab.svelte';
export default { export default {
TableDataTab, TableDataTab,
@@ -40,4 +41,5 @@ export default {
CompareModelTab, CompareModelTab,
JsonTab, JsonTab,
ChangelogTab, ChangelogTab,
DiagramTab,
}; };

View File

@@ -9,7 +9,7 @@ export function registerMenu(...items) {
setContext('componentContextMenu', [parentMenu, ...items]); setContext('componentContextMenu', [parentMenu, ...items]);
} }
export default function contextMenu(node, items = []) { export default function contextMenu(node, items: any = []) {
const handleContextMenu = async e => { const handleContextMenu = async e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -23,6 +23,8 @@ export default function contextMenu(node, items = []) {
} }
}; };
if (items == '__no_menu') return;
node.addEventListener('contextmenu', handleContextMenu); node.addEventListener('contextmenu', handleContextMenu);
return { return {

View File

@@ -1,32 +1,49 @@
export default function moveDrag(node, [onStart, onMove, onEnd]) { export default function moveDrag(node, dragEvents) {
if (!dragEvents) return;
const [onStart, onMove, onEnd] = dragEvents;
let startX = null; let startX = null;
let startY = null; let startY = null;
let clientX = null;
let clientY = null;
const handleMoveDown = e => { const handleMoveDown = e => {
if (e.button != 0) return; if (e.button != 0) return;
const zoomKoef = window.getComputedStyle(node)['zoom'];
const clientRect = node.getBoundingClientRect();
clientX = clientRect.left * zoomKoef;
clientY = clientRect.top * zoomKoef;
startX = e.clientX; startX = e.clientX;
startY = e.clientY; startY = e.clientY;
document.addEventListener('mousemove', handleMoveMove, true); document.addEventListener('mousemove', handleMoveMove, true);
document.addEventListener('mouseup', handleMoveEnd, true); document.addEventListener('mouseup', handleMoveEnd, true);
onStart(); onStart(e.clientX - clientX, e.clientY - clientY);
}; };
const handleMoveMove = e => { const handleMoveMove = e => {
const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault(); e.preventDefault();
const diffX = e.clientX - startX; const diffX = e.clientX - startX;
startX = e.clientX; startX = e.clientX;
const diffY = e.clientY - startY; const diffY = e.clientY - startY;
startY = e.clientY; startY = e.clientY;
onMove(diffX, diffY); onMove(diffX, diffY, e.clientX - clientX, e.clientY - clientY);
}; };
const handleMoveEnd = e => { const handleMoveEnd = e => {
const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault(); e.preventDefault();
startX = null; startX = null;
startY = null; startY = null;
document.removeEventListener('mousemove', handleMoveMove, true); document.removeEventListener('mousemove', handleMoveMove, true);
document.removeEventListener('mouseup', handleMoveEnd, true); document.removeEventListener('mouseup', handleMoveEnd, true);
onEnd(); onEnd(e.clientX - clientX, e.clientY - clientY);
}; };
node.addEventListener('mousedown', handleMoveDown); node.addEventListener('mousedown', handleMoveDown);

View File

@@ -0,0 +1,28 @@
import { writable } from "svelte/store";
export const statusBarTabInfo = writable({});
// export function updateStatuBarInfo(tabid, info) {
// statusBarTabInfo.update(x => ({
// ...x,
// [tabid]: info,
// }));
// }
export function updateStatuBarInfoItem(tabid, key, item) {
statusBarTabInfo.update(tabs => {
const items = tabs[tabid] || [];
let newItems;
if (item == null) {
newItems = items.filter(x => x.key != key);
} else if (items.find(x => x.key == key)) {
newItems = items.map(x => (x.key == key ? { ...item, key } : x));
} else {
newItems = [...items, { ...item, key }];
}
return {
...tabs,
[tabid]: newItems,
};
});
}

View File

@@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { getContext, onDestroy } from 'svelte'; import { getContext, onDestroy } from 'svelte';
import { updateStatuBarInfoItem } from '../widgets/StatusBar.svelte'; import { updateStatuBarInfoItem } from './statusBarStore';
function formatSeconds(duration) { function formatSeconds(duration) {
if (duration == null) return ''; if (duration == null) return '';

View File

@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte'; import AppObjectList from '../appobj/AppObjectList.svelte';
import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte'; import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte';
import { useFiles } from '../utility/metadataLoaders'; import { useFiles } from '../utility/metadataLoaders';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
const sqlFiles = useFiles({ folder: 'sql' }); const sqlFiles = useFiles({ folder: 'sql' });
@@ -13,6 +11,7 @@ import { useFiles } from '../utility/metadataLoaders';
const chartFiles = useFiles({ folder: 'charts' }); const chartFiles = useFiles({ folder: 'charts' });
const queryFiles = useFiles({ folder: 'query' }); const queryFiles = useFiles({ folder: 'query' });
const sqliteFiles = useFiles({ folder: 'sqlite' }); const sqliteFiles = useFiles({ folder: 'sqlite' });
const diagramFiles = useFiles({ folder: 'diagrams' });
$: files = [ $: files = [
...($sqlFiles || []), ...($sqlFiles || []),
@@ -21,8 +20,8 @@ import { useFiles } from '../utility/metadataLoaders';
...($chartFiles || []), ...($chartFiles || []),
...($queryFiles || []), ...($queryFiles || []),
...($sqliteFiles || []), ...($sqliteFiles || []),
...($diagramFiles || []),
]; ];
</script> </script>
<WidgetsInnerContainer> <WidgetsInnerContainer>

View File

@@ -1,35 +1,5 @@
<script lang="ts" context="module">
const statusBarTabInfo = writable({});
// export function updateStatuBarInfo(tabid, info) {
// statusBarTabInfo.update(x => ({
// ...x,
// [tabid]: info,
// }));
// }
export function updateStatuBarInfoItem(tabid, key, item) {
statusBarTabInfo.update(tabs => {
const items = tabs[tabid] || [];
let newItems;
if (item == null) {
newItems = items.filter(x => x.key != key);
} else if (items.find(x => x.key == key)) {
newItems = items.map(x => (x.key == key ? { ...item, key } : x));
} else {
newItems = [...items, { ...item, key }];
}
return {
...tabs,
[tabid]: newItems,
};
});
}
</script>
<script lang="ts"> <script lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import { writable } from 'svelte/store';
import moment from 'moment'; import moment from 'moment';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte'; import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
@@ -42,6 +12,7 @@
import { findCommand } from '../commands/runCommand'; import { findCommand } from '../commands/runCommand';
import { useConnectionColor } from '../utility/useConnectionColor'; import { useConnectionColor } from '../utility/useConnectionColor';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import { statusBarTabInfo } from '../utility/statusBarStore';
$: databaseName = $currentDatabase && $currentDatabase.name; $: databaseName = $currentDatabase && $currentDatabase.name;
$: connection = $currentDatabase && $currentDatabase.connection; $: connection = $currentDatabase && $currentDatabase.connection;

View File

@@ -2,7 +2,7 @@
import { getContext, onDestroy, onMount } from 'svelte'; import { getContext, onDestroy, onMount } from 'svelte';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import { updateStatuBarInfoItem } from './StatusBar.svelte'; import { updateStatuBarInfoItem } from '../utility/statusBarStore';
export let text; export let text;
export let clickable = false; export let clickable = false;

View File

@@ -5177,6 +5177,11 @@ interpret@1.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
interval-operations@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/interval-operations/-/interval-operations-1.0.7.tgz#c935dfed6bb040064d488f542b9a2c0004d7a333"
integrity sha512-VBxXaK+DxTt7Hwr8Rhg03bgdyv5OZ0OfH3hotg9fT2jqufF3VOtvkX33n3t6jNjS8tx1jTKBt7fs++SqNd1OQg==
invariant@^2.2.4: invariant@^2.2.4:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"