mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-26 05:15:59 +00:00
Merge branch 'develop'
This commit is contained in:
10
.github/workflows/build-app-beta.yaml
vendored
10
.github/workflows/build-app-beta.yaml
vendored
@@ -42,7 +42,6 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
- name: Publish
|
||||
if: matrix.os != 'macOS-10.15'
|
||||
run: |
|
||||
yarn run build:app
|
||||
env:
|
||||
@@ -50,13 +49,6 @@ jobs:
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
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
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
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_x64.zip artifacts/dbgate-windows-beta.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
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
|
||||
2
.github/workflows/build-app.yaml
vendored
2
.github/workflows/build-app.yaml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
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_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
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"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",
|
||||
"postinstall": "yarn rebuild && patch-package",
|
||||
"rebuild": "electron-builder install-app-deps",
|
||||
|
||||
@@ -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' });
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.5.1",
|
||||
"version": "4.5.2-beta.8",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -25,7 +25,6 @@
|
||||
"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: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:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
||||
|
||||
@@ -6,6 +6,7 @@ const getChartExport = require('../utility/getChartExport');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
|
||||
function serialize(format, data) {
|
||||
if (format == 'text') return data;
|
||||
@@ -86,8 +87,9 @@ module.exports = {
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
||||
return true;
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/write`)) return;
|
||||
if (!hasPermission(`files/${folder}/write`)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
@@ -98,6 +100,7 @@ module.exports = {
|
||||
if (folder == 'shell') {
|
||||
scheduler.reload();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -150,4 +153,10 @@ module.exports = {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
exportDiagram_meta: true,
|
||||
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
||||
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
25
packages/api/src/utility/getDiagramExport.js
Normal file
25
packages/api/src/utility/getDiagramExport.js
Normal 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;
|
||||
@@ -55,6 +55,7 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chartjs-plugin-zoom": "^1.2.0"
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"interval-operations": "^1.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ body {
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.noselect {
|
||||
user-select: none;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={`${$currentTheme} ${currentThemeType} root`}
|
||||
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
||||
use:dragDropFileTarget
|
||||
on:contextmenu={e => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -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 electron = getElectron();
|
||||
if (electron) {
|
||||
@@ -138,6 +166,7 @@
|
||||
{ divider: true },
|
||||
{ onClick: handleImport, text: 'Import' },
|
||||
{ onClick: handleExport, text: 'Export' },
|
||||
{ onClick: handleShowDiagram, text: 'Show diagram' },
|
||||
{ onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||
{ onClick: handleOpenJsonModel, text: 'Open model as JSON' },
|
||||
{ onClick: handleExportModel, text: 'Export DB model - experimental' },
|
||||
@@ -157,6 +186,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
import _, { find } from 'lodash';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
{
|
||||
label: 'Show diagram',
|
||||
isDiagram: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
@@ -388,45 +392,15 @@
|
||||
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 function createDatabaseObjectMenu(data) {
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, getExtensions());
|
||||
return driver;
|
||||
};
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
|
||||
function handleClick(forceNewTab = false) {
|
||||
handleDatabaseObjectClick(data, forceNewTab);
|
||||
}
|
||||
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, $extensions);
|
||||
return driver;
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
const { objectTypeField } = data;
|
||||
return menus[objectTypeField]
|
||||
.filter(x => x)
|
||||
@@ -434,7 +408,7 @@
|
||||
if (menu.divider) return menu;
|
||||
|
||||
if (menu.isQuickExport) {
|
||||
return createQuickExportMenu($extensions, fmt => async () => {
|
||||
return createQuickExportMenu(getExtensions(), fmt => async () => {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
exportElectronFile(
|
||||
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) {
|
||||
showModal(SqlGeneratorModal, {
|
||||
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));
|
||||
</script>
|
||||
|
||||
@@ -55,6 +55,14 @@
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const diagrams: FileTypeHandler = {
|
||||
icon: 'img diagram',
|
||||
format: 'json',
|
||||
tabComponent: 'DiagramTab',
|
||||
folder: 'diagrams',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -62,6 +70,7 @@
|
||||
charts,
|
||||
query,
|
||||
sqlite,
|
||||
diagrams,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
@@ -74,7 +83,7 @@
|
||||
import { showModal } from '../modals/modalTools';
|
||||
|
||||
import { currentDatabase } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
export let onCreateReference;
|
||||
export let onAddReferenceByColumn;
|
||||
export let onSelectColumn;
|
||||
export let settings;
|
||||
|
||||
$: designerColumn = (designer.columns || []).find(
|
||||
x => x.designerId == designerId && x.columnName == column.columnName
|
||||
@@ -38,9 +39,11 @@
|
||||
};
|
||||
|
||||
return [
|
||||
{ text: 'Sort ascending', onClick: () => setSortOrder(1) },
|
||||
{ text: 'Sort descending', onClick: () => setSortOrder(-1) },
|
||||
{ text: 'Unsort', onClick: () => setSortOrder(0) },
|
||||
settings?.allowColumnOperations && [
|
||||
{ text: 'Sort ascending', onClick: () => setSortOrder(1) },
|
||||
{ text: 'Sort descending', onClick: () => setSortOrder(-1) },
|
||||
{ text: 'Unsort', onClick: () => setSortOrder(0) },
|
||||
],
|
||||
foreignKey && { text: 'Add reference', onClick: addReference },
|
||||
];
|
||||
}
|
||||
@@ -48,9 +51,12 @@
|
||||
|
||||
<div
|
||||
class="line"
|
||||
class:canSelectColumns={settings?.canSelectColumns}
|
||||
bind:this={domLine}
|
||||
draggable={true}
|
||||
draggable={!!settings?.allowCreateRefByDrag}
|
||||
on:dragstart={e => {
|
||||
if (!settings?.allowCreateRefByDrag) return;
|
||||
|
||||
const dragData = {
|
||||
...column,
|
||||
designerId,
|
||||
@@ -90,32 +96,34 @@
|
||||
...column,
|
||||
designerId,
|
||||
})}
|
||||
use:contextMenu={createMenu}
|
||||
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={!!(designer.columns || []).find(
|
||||
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
||||
)}
|
||||
on:change={e => {
|
||||
if (e.target.checked) {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: true })
|
||||
);
|
||||
} else {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: false })
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if settings?.allowColumnOperations}
|
||||
<CheckboxField
|
||||
checked={!!(designer.columns || []).find(
|
||||
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
||||
)}
|
||||
on:change={e => {
|
||||
if (e.target.checked) {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: true })
|
||||
);
|
||||
} else {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: false })
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
|
||||
{#if designerColumn?.filter}
|
||||
<FontIcon icon="img filter" />
|
||||
@@ -129,16 +137,36 @@
|
||||
{#if designerColumn?.isGrouped}
|
||||
<FontIcon icon="img group" />
|
||||
{/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>
|
||||
|
||||
<style>
|
||||
.line:hover {
|
||||
:global(.dbgate-screen) .line.canSelectColumns:hover {
|
||||
background: var(--theme-bg-1);
|
||||
}
|
||||
.line.isDragSource {
|
||||
:global(.dbgate-screen) .line.isDragSource {
|
||||
background: var(--theme-bg-gold);
|
||||
}
|
||||
.line.isDragTarget {
|
||||
:global(.dbgate-screen) .line.isDragTarget {
|
||||
background: var(--theme-bg-gold);
|
||||
}
|
||||
.line {
|
||||
display: flex;
|
||||
}
|
||||
.space {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,78 @@
|
||||
<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 lang="ts">
|
||||
import DesignerTable from './DesignerTable.svelte';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { getTableInfo } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import _ from 'lodash';
|
||||
import createRef from '../utility/createRef';
|
||||
import DesignerReference from './DesignerReference.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { tick } from 'svelte';
|
||||
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 onChange;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let menu;
|
||||
export let settings;
|
||||
export let referenceComponent;
|
||||
|
||||
export const activator = createActivator('Designer', true);
|
||||
|
||||
let domCanvas;
|
||||
let canvasWidth = 3000;
|
||||
let canvasHeight = 3000;
|
||||
let dragStartPoint = null;
|
||||
let dragCurrentPoint = null;
|
||||
|
||||
const sourceDragColumn$ = writable(null);
|
||||
const targetDragColumn$ = writable(null);
|
||||
|
||||
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
||||
|
||||
$: tables = value?.tables 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 referenceRefs = {};
|
||||
@@ -47,6 +93,84 @@
|
||||
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) {
|
||||
onChange(changeFunc, skipUndoChain);
|
||||
tick().then(recomputeReferencePositions);
|
||||
@@ -69,15 +193,37 @@
|
||||
);
|
||||
};
|
||||
|
||||
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 => {
|
||||
callChange(current => ({
|
||||
...current,
|
||||
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
||||
references: (current.references || []).filter(
|
||||
x => x.sourceId != table.designerId && x.targetId != table.designerId
|
||||
),
|
||||
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
||||
}));
|
||||
if (isMultipleTableSelection && settings?.useDatabaseReferences && settings?.canSelectTables) {
|
||||
callChange(current => ({
|
||||
...current,
|
||||
tables: (current.tables || []).filter(x => !x.isSelectedTable),
|
||||
}));
|
||||
updateFromDbInfo();
|
||||
} else {
|
||||
callChange(current => ({
|
||||
...current,
|
||||
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
||||
references: (current.references || []).filter(
|
||||
x => x.sourceId != table.designerId && x.targetId != table.designerId
|
||||
),
|
||||
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const changeReference = ref => {
|
||||
@@ -150,10 +296,12 @@
|
||||
pureName: foreignKey.refTableName,
|
||||
schemaName: foreignKey.refSchemaName,
|
||||
});
|
||||
const newTableDesignerId = uuidv1();
|
||||
const newTableDesignerId = `${toTable.pureName}-${uuidv1()}`;
|
||||
callChange(current => {
|
||||
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 {
|
||||
...current,
|
||||
tables: [
|
||||
@@ -163,24 +311,117 @@
|
||||
left: fromTable.left + 300,
|
||||
top: fromTable.top + 50,
|
||||
designerId: newTableDesignerId,
|
||||
alias: getNewTableAlias(toTable, current.tables),
|
||||
},
|
||||
],
|
||||
references: [
|
||||
...(current.references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
sourceId: fromTable.designerId,
|
||||
targetId: newTableDesignerId,
|
||||
joinType: 'INNER JOIN',
|
||||
columns: foreignKey.columns.map(col => ({
|
||||
source: col.columnName,
|
||||
target: col.refColumnName,
|
||||
})),
|
||||
alias,
|
||||
},
|
||||
],
|
||||
references: settings?.allowCreateRefByDrag
|
||||
? [
|
||||
...(current.references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
sourceId: fromTable.designerId,
|
||||
targetId: newTableDesignerId,
|
||||
joinType: 'INNER JOIN',
|
||||
columns: foreignKey.columns.map(col => ({
|
||||
source: col.columnName,
|
||||
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 => {
|
||||
@@ -231,39 +472,44 @@
|
||||
var json = JSON.parse(data);
|
||||
const { objectTypeField } = json;
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
||||
json.designerId = uuidv1();
|
||||
json.designerId = `${json.pureName}-${uuidv1()}`;
|
||||
json.left = e.clientX - rect.left;
|
||||
json.top = e.clientY - rect.top;
|
||||
|
||||
callChange(current => {
|
||||
const foreignKeys = _.compact([
|
||||
...(json.foreignKeys || []).map(fk => {
|
||||
const tables = ((current || {}).tables || []).filter(
|
||||
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
||||
);
|
||||
if (tables.length == 1)
|
||||
return {
|
||||
...fk,
|
||||
sourceId: json.designerId,
|
||||
targetId: tables[0].designerId,
|
||||
};
|
||||
return null;
|
||||
}),
|
||||
..._.flatten(
|
||||
((current || {}).tables || []).map(tbl =>
|
||||
(tbl.foreignKeys || []).map(fk => {
|
||||
if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) {
|
||||
const foreignKeys = settings?.useDatabaseReferences
|
||||
? []
|
||||
: _.compact([
|
||||
...(json.foreignKeys || []).map(fk => {
|
||||
const tables = ((current || {}).tables || []).filter(
|
||||
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
||||
);
|
||||
if (tables.length == 1)
|
||||
return {
|
||||
...fk,
|
||||
sourceId: tbl.designerId,
|
||||
targetId: json.designerId,
|
||||
sourceId: json.designerId,
|
||||
targetId: tables[0].designerId,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
),
|
||||
]);
|
||||
}),
|
||||
..._.flatten(
|
||||
((current || {}).tables || []).map(tbl =>
|
||||
(tbl.foreignKeys || []).map(fk => {
|
||||
if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) {
|
||||
return {
|
||||
...fk,
|
||||
sourceId: tbl.designerId,
|
||||
targetId: json.designerId,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
const alias = getNewTableAlias(json, current?.tables);
|
||||
if (alias && !settings?.allowTableAlias) return current;
|
||||
|
||||
return {
|
||||
...current,
|
||||
@@ -271,7 +517,7 @@
|
||||
...((current || {}).tables || []),
|
||||
{
|
||||
...json,
|
||||
alias: getNewTableAlias(json, current?.tables),
|
||||
alias,
|
||||
},
|
||||
],
|
||||
references:
|
||||
@@ -279,7 +525,7 @@
|
||||
? [
|
||||
...((current || {}).references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
designerId: `${current?.pureName}-${uuidv1()}`,
|
||||
sourceId: foreignKeys[0].sourceId,
|
||||
targetId: foreignKeys[0].targetId,
|
||||
joinType: 'INNER JOIN',
|
||||
@@ -292,6 +538,81 @@
|
||||
: (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() {
|
||||
@@ -299,22 +620,218 @@
|
||||
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>
|
||||
|
||||
<div class="wrapper" use:contextMenu={menu}>
|
||||
<div class="wrapper noselect" use:contextMenu={createMenu}>
|
||||
{#if !(tables?.length > 0)}
|
||||
<div class="empty">Drag & drop tables or views from left panel here</div>
|
||||
{/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)}
|
||||
<DesignerReference
|
||||
<svelte:component
|
||||
this={referenceComponent}
|
||||
bind:this={referenceRefs[ref.designerId]}
|
||||
{domTables}
|
||||
reference={ref}
|
||||
onChangeReference={changeReference}
|
||||
onRemoveReference={removeReference}
|
||||
designer={value}
|
||||
{settings}
|
||||
/>
|
||||
{/each}
|
||||
<!--
|
||||
@@ -335,17 +852,42 @@
|
||||
onSelectColumn={handleSelectColumn}
|
||||
onChangeColumn={handleChangeColumn}
|
||||
onAddReferenceByColumn={handleAddReferenceByColumn}
|
||||
onAddAllReferences={handleAddTableReferences}
|
||||
onChangeTableColor={handleChangeTableColor}
|
||||
onMoveReferences={recomputeReferencePositions}
|
||||
{table}
|
||||
{conid}
|
||||
{database}
|
||||
{zoomKoef}
|
||||
{isMultipleTableSelection}
|
||||
onChangeTable={changeTable}
|
||||
onBringToFront={bringToFront}
|
||||
onSelectTable={selectTable}
|
||||
onRemoveTable={removeTable}
|
||||
onMoveStart={tableMoveStart}
|
||||
onMove={tableMove}
|
||||
onMoveEnd={tableMoveEnd}
|
||||
{domCanvas}
|
||||
designer={value}
|
||||
{sourceDragColumn$}
|
||||
{targetDragColumn$}
|
||||
{settings}
|
||||
/>
|
||||
{/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>
|
||||
|
||||
@@ -360,8 +902,26 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
.canvas {
|
||||
width: 3000px;
|
||||
height: 3000px;
|
||||
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>
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { presetDarkPalettes, presetPalettes } from '@ant-design/colors';
|
||||
import { computeDbDiffRows } from 'dbgate-tools';
|
||||
|
||||
import { tick } from 'svelte';
|
||||
import { createDatabaseObjectMenu } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { currentThemeDefinition } from '../stores';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import moveDrag from '../utility/moveDrag';
|
||||
import ColumnLine from './ColumnLine.svelte';
|
||||
import DomTableRef from './DomTableRef';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
export let table;
|
||||
export let zoomKoef;
|
||||
export let onChangeTable;
|
||||
export let onBringToFront;
|
||||
export let onSelectTable;
|
||||
export let onRemoveTable;
|
||||
export let onAddAllReferences;
|
||||
export let onCreateReference;
|
||||
export let onAddReferenceByColumn;
|
||||
export let onSelectColumn;
|
||||
export let onChangeColumn;
|
||||
export let onChangeTableColor;
|
||||
export let isMultipleTableSelection;
|
||||
|
||||
export let onMoveStart;
|
||||
export let onMove;
|
||||
export let onMoveEnd;
|
||||
|
||||
// export let sourceDragColumn;
|
||||
// export let setSourceDragColumn;
|
||||
@@ -31,6 +47,7 @@
|
||||
// export let domTablesRef;
|
||||
export let designer;
|
||||
export let onMoveReferences;
|
||||
export let settings;
|
||||
|
||||
let movingPosition = null;
|
||||
let domWrapper;
|
||||
@@ -39,28 +56,94 @@
|
||||
|
||||
$: pureName = table?.pureName;
|
||||
$: alias = table?.alias;
|
||||
$: columns = table?.columns as any[];
|
||||
$: columns = (table?.columns as any[]).filter(x => shouldShowColumn(table, x, designer?.style));
|
||||
$: designerId = table?.designerId;
|
||||
$: objectTypeField = table?.objectTypeField;
|
||||
$: left = table?.left;
|
||||
$: top = table?.top;
|
||||
|
||||
function handleMoveStart() {
|
||||
export function isSelected() {
|
||||
return table?.isSelectedTable;
|
||||
}
|
||||
|
||||
export function getDesignerId() {
|
||||
return designerId;
|
||||
}
|
||||
|
||||
export function moveStart() {
|
||||
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) {
|
||||
movingPosition.left += x;
|
||||
movingPosition.top += y;
|
||||
tick().then(onMoveReferences);
|
||||
if (settings?.canSelectTables) {
|
||||
onMove(x, y);
|
||||
} else {
|
||||
move(x, y);
|
||||
tick().then(onMoveReferences);
|
||||
}
|
||||
}
|
||||
function handleMoveEnd() {
|
||||
onChangeTable({
|
||||
...table,
|
||||
left: movingPosition.left,
|
||||
top: movingPosition.top,
|
||||
});
|
||||
movingPosition = null;
|
||||
tick().then(onMoveReferences);
|
||||
if (settings?.canSelectTables) {
|
||||
onMoveEnd();
|
||||
} else {
|
||||
const position = moveEnd();
|
||||
onChangeTable({
|
||||
...table,
|
||||
left: position.left,
|
||||
top: position.top,
|
||||
});
|
||||
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() {
|
||||
@@ -87,38 +170,61 @@
|
||||
return [
|
||||
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
||||
{ divider: true },
|
||||
{ text: 'Set table alias', onClick: handleSetTableAlias },
|
||||
alias && {
|
||||
text: 'Remove table alias',
|
||||
onClick: () =>
|
||||
onChangeTable({
|
||||
...table,
|
||||
alias: null,
|
||||
}),
|
||||
},
|
||||
settings?.allowTableAlias &&
|
||||
!isMultipleTableSelection && [
|
||||
{ text: 'Set table alias', onClick: handleSetTableAlias },
|
||||
alias && {
|
||||
text: 'Remove table alias',
|
||||
onClick: () =>
|
||||
onChangeTable({
|
||||
...table,
|
||||
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>
|
||||
|
||||
<div
|
||||
class="wrapper"
|
||||
class:canSelectColumns={settings?.canSelectColumns}
|
||||
class:isSelectedTable={table?.isSelectedTable}
|
||||
style={`left: ${movingPosition ? movingPosition.left : left}px; top:${movingPosition ? movingPosition.top : top}px`}
|
||||
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
|
||||
class="header"
|
||||
class:isTable={objectTypeField == 'tables'}
|
||||
class:isView={objectTypeField == 'views'}
|
||||
use:moveDrag={[handleMoveStart, handleMove, handleMoveEnd]}
|
||||
use:contextMenu={createMenu}
|
||||
use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null}
|
||||
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
|
||||
style={getTableColorStyle($currentThemeDefinition, table)}
|
||||
>
|
||||
<div>{alias || pureName}</div>
|
||||
<div class="close" on:click={() => onRemoveTable(table)}>
|
||||
<FontIcon icon="icon close" />
|
||||
</div>
|
||||
{#if settings?.showTableCloseButton}
|
||||
<div class="close" on:click={() => onRemoveTable(table)}>
|
||||
<FontIcon icon="icon close" />
|
||||
</div>
|
||||
{/if}
|
||||
</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}
|
||||
<ColumnLine
|
||||
{column}
|
||||
@@ -131,10 +237,17 @@
|
||||
{targetDragColumn$}
|
||||
{onCreateReference}
|
||||
{onAddReferenceByColumn}
|
||||
{settings}
|
||||
bind:domLine={columnRefs[column.columnName]}
|
||||
/>
|
||||
{/each}
|
||||
</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>
|
||||
|
||||
<style>
|
||||
@@ -143,16 +256,51 @@
|
||||
background-color: var(--theme-bg-0);
|
||||
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 {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
:global(.dbgate-screen) .header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header.isTable {
|
||||
background: var(--theme-bg-blue);
|
||||
}
|
||||
@@ -169,9 +317,11 @@
|
||||
background: var(--theme-bg-3);
|
||||
}
|
||||
.columns {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
width: calc(100% - 10px);
|
||||
padding: 5px;
|
||||
}
|
||||
.columns.scroll {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
26
packages/web/src/designer/DiagramDesigner.svelte
Normal file
26
packages/web/src/designer/DiagramDesigner.svelte
Normal 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}
|
||||
/>
|
||||
98
packages/web/src/designer/DiagramDesignerReference.svelte
Normal file
98
packages/web/src/designer/DiagramDesignerReference.svelte
Normal 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>
|
||||
484
packages/web/src/designer/GraphLayout.ts
Normal file
484
packages/web/src/designer/GraphLayout.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Designer from './Designer.svelte';
|
||||
import QueryDesignerReference from './QueryDesignerReference.svelte';
|
||||
</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}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let onChangeReference;
|
||||
export let designer;
|
||||
export let domTables;
|
||||
export let settings;
|
||||
|
||||
let src = null;
|
||||
let dst = null;
|
||||
170
packages/web/src/designer/designerMath.ts
Normal file
170
packages/web/src/designer/designerMath.ts
Normal 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));
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
'icon settings': 'mdi mdi-cog',
|
||||
'icon version': 'mdi mdi-ticket-confirmation',
|
||||
'icon pin': 'mdi mdi-pin',
|
||||
'icon arrange': 'mdi mdi-arrange-send-to-back',
|
||||
|
||||
'icon columns': 'mdi mdi-view-column',
|
||||
'icon columns-outline': 'mdi mdi-view-column-outline',
|
||||
@@ -122,6 +123,7 @@
|
||||
'img preview': 'mdi mdi-file-find color-icon-red',
|
||||
'img favorite': 'mdi mdi-star color-icon-yellow',
|
||||
'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 compare': 'mdi mdi-compare color-icon-red',
|
||||
|
||||
|
||||
27
packages/web/src/modals/ChooseColorModal.svelte
Normal file
27
packages/web/src/modals/ChooseColorModal.svelte
Normal 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>
|
||||
93
packages/web/src/tabs/DiagramTab.svelte
Normal file
93
packages/web/src/tabs/DiagramTab.svelte
Normal 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} />
|
||||
@@ -18,6 +18,7 @@ import * as YamlEditorTab from './YamlEditorTab.svelte';
|
||||
import * as CompareModelTab from './CompareModelTab.svelte';
|
||||
import * as JsonTab from './JsonTab.svelte';
|
||||
import * as ChangelogTab from './ChangelogTab.svelte';
|
||||
import * as DiagramTab from './DiagramTab.svelte';
|
||||
|
||||
export default {
|
||||
TableDataTab,
|
||||
@@ -40,4 +41,5 @@ export default {
|
||||
CompareModelTab,
|
||||
JsonTab,
|
||||
ChangelogTab,
|
||||
DiagramTab,
|
||||
};
|
||||
|
||||
@@ -9,13 +9,13 @@ export function registerMenu(...items) {
|
||||
setContext('componentContextMenu', [parentMenu, ...items]);
|
||||
}
|
||||
|
||||
export default function contextMenu(node, items = []) {
|
||||
export default function contextMenu(node, items: any = []) {
|
||||
const handleContextMenu = async e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
await invalidateCommands();
|
||||
|
||||
|
||||
if (items) {
|
||||
const left = e.pageX;
|
||||
const top = e.pageY;
|
||||
@@ -23,6 +23,8 @@ export default function contextMenu(node, items = []) {
|
||||
}
|
||||
};
|
||||
|
||||
if (items == '__no_menu') return;
|
||||
|
||||
node.addEventListener('contextmenu', handleContextMenu);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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 startY = null;
|
||||
|
||||
let clientX = null;
|
||||
let clientY = null;
|
||||
|
||||
const handleMoveDown = e => {
|
||||
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;
|
||||
startY = e.clientY;
|
||||
document.addEventListener('mousemove', handleMoveMove, true);
|
||||
document.addEventListener('mouseup', handleMoveEnd, true);
|
||||
onStart();
|
||||
onStart(e.clientX - clientX, e.clientY - clientY);
|
||||
};
|
||||
|
||||
const handleMoveMove = e => {
|
||||
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
|
||||
e.preventDefault();
|
||||
const diffX = e.clientX - startX;
|
||||
startX = e.clientX;
|
||||
const diffY = e.clientY - startY;
|
||||
startY = e.clientY;
|
||||
|
||||
onMove(diffX, diffY);
|
||||
onMove(diffX, diffY, e.clientX - clientX, e.clientY - clientY);
|
||||
};
|
||||
const handleMoveEnd = e => {
|
||||
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
|
||||
e.preventDefault();
|
||||
startX = null;
|
||||
startY = null;
|
||||
document.removeEventListener('mousemove', handleMoveMove, true);
|
||||
document.removeEventListener('mouseup', handleMoveEnd, true);
|
||||
onEnd();
|
||||
onEnd(e.clientX - clientX, e.clientY - clientY);
|
||||
};
|
||||
|
||||
node.addEventListener('mousedown', handleMoveDown);
|
||||
|
||||
28
packages/web/src/utility/statusBarStore.ts
Normal file
28
packages/web/src/utility/statusBarStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { updateStatuBarInfoItem } from '../widgets/StatusBar.svelte';
|
||||
import { updateStatuBarInfoItem } from './statusBarStore';
|
||||
|
||||
function formatSeconds(duration) {
|
||||
if (duration == null) return '';
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
import _ from 'lodash';
|
||||
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||
import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte';
|
||||
import { useFiles } from '../utility/metadataLoaders';
|
||||
|
||||
import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte';
|
||||
import { useFiles } from '../utility/metadataLoaders';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
|
||||
const sqlFiles = useFiles({ folder: 'sql' });
|
||||
@@ -13,6 +11,7 @@ import { useFiles } from '../utility/metadataLoaders';
|
||||
const chartFiles = useFiles({ folder: 'charts' });
|
||||
const queryFiles = useFiles({ folder: 'query' });
|
||||
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
||||
const diagramFiles = useFiles({ folder: 'diagrams' });
|
||||
|
||||
$: files = [
|
||||
...($sqlFiles || []),
|
||||
@@ -21,8 +20,8 @@ import { useFiles } from '../utility/metadataLoaders';
|
||||
...($chartFiles || []),
|
||||
...($queryFiles || []),
|
||||
...($sqliteFiles || []),
|
||||
...($diagramFiles || []),
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<WidgetsInnerContainer>
|
||||
|
||||
@@ -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">
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
import moment from 'moment';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
|
||||
@@ -42,6 +12,7 @@
|
||||
import { findCommand } from '../commands/runCommand';
|
||||
import { useConnectionColor } from '../utility/useConnectionColor';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { statusBarTabInfo } from '../utility/statusBarStore';
|
||||
|
||||
$: databaseName = $currentDatabase && $currentDatabase.name;
|
||||
$: connection = $currentDatabase && $currentDatabase.connection;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext, onDestroy, onMount } from 'svelte';
|
||||
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { updateStatuBarInfoItem } from './StatusBar.svelte';
|
||||
import { updateStatuBarInfoItem } from '../utility/statusBarStore';
|
||||
|
||||
export let text;
|
||||
export let clickable = false;
|
||||
|
||||
@@ -5177,6 +5177,11 @@ interpret@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||
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:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
|
||||
Reference in New Issue
Block a user