mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-26 19:06:02 +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'
|
if: matrix.os == 'ubuntu-18.04'
|
||||||
uses: samuelmeuli/action-snapcraft@v1
|
uses: samuelmeuli/action-snapcraft@v1
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: matrix.os != 'macOS-10.15'
|
|
||||||
run: |
|
run: |
|
||||||
yarn run build:app
|
yarn run build:app
|
||||||
env:
|
env:
|
||||||
@@ -50,13 +49,6 @@ jobs:
|
|||||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||||
|
|
||||||
- name: Publish Mac
|
|
||||||
if: matrix.os == 'macOS-10.15'
|
|
||||||
run: |
|
|
||||||
yarn run build:app:mac
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
|
||||||
|
|
||||||
- name: Save snap login
|
- name: Save snap login
|
||||||
if: matrix.os == 'ubuntu-18.04'
|
if: matrix.os == 'ubuntu-18.04'
|
||||||
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
|
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
|
||||||
@@ -81,7 +73,7 @@ jobs:
|
|||||||
cp app/dist/*win*.exe artifacts/dbgate-beta.exe || true
|
cp app/dist/*win*.exe artifacts/dbgate-beta.exe || true
|
||||||
cp app/dist/*win_x64.zip artifacts/dbgate-windows-beta.zip || true
|
cp app/dist/*win_x64.zip artifacts/dbgate-windows-beta.zip || true
|
||||||
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-beta-arm64.zip || true
|
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-beta-arm64.zip || true
|
||||||
cp app/dist/*-mac.dmg artifacts/dbgate-beta.dmg || true
|
cp app/dist/*-mac_x64.dmg artifacts/dbgate-beta.dmg || true
|
||||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true
|
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true
|
||||||
|
|
||||||
mv app/dist/*.exe artifacts/ || true
|
mv app/dist/*.exe artifacts/ || true
|
||||||
|
|||||||
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/*.exe artifacts/dbgate-latest.exe || true
|
||||||
cp app/dist/*win_x64.zip artifacts/dbgate-windows-latest.zip || true
|
cp app/dist/*win_x64.zip artifacts/dbgate-windows-latest.zip || true
|
||||||
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-latest-arm64.zip || true
|
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-latest-arm64.zip || true
|
||||||
cp app/dist/*-mac.dmg artifacts/dbgate-latest.dmg || true
|
cp app/dist/*-mac_x64.dmg artifacts/dbgate-latest.dmg || true
|
||||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true
|
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true
|
||||||
|
|
||||||
mv app/dist/*.exe artifacts/ || true
|
mv app/dist/*.exe artifacts/ || true
|
||||||
|
|||||||
@@ -92,7 +92,6 @@
|
|||||||
"start:local": "cross-env electron .",
|
"start:local": "cross-env electron .",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||||
"build:mac": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && node setMacPlatform x64 && yarn dist && node setMacPlatform arm64 && yarn dist",
|
|
||||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||||
"postinstall": "yarn rebuild && patch-package",
|
"postinstall": "yarn rebuild && patch-package",
|
||||||
"rebuild": "electron-builder install-app-deps",
|
"rebuild": "electron-builder install-app-deps",
|
||||||
|
|||||||
@@ -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,
|
"private": true,
|
||||||
"version": "4.5.1",
|
"version": "4.5.2-beta.8",
|
||||||
"name": "dbgate-all",
|
"name": "dbgate-all",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
"build:tools": "yarn workspace dbgate-tools build",
|
"build:tools": "yarn workspace dbgate-tools build",
|
||||||
"build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
"build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||||
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
|
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
|
||||||
"build:app:mac": "yarn plugins:copydist && cd app && yarn install && yarn build:mac",
|
|
||||||
"build:api": "yarn workspace dbgate-api build",
|
"build:api": "yarn workspace dbgate-api build",
|
||||||
"build:web:docker": "yarn workspace dbgate-web build",
|
"build:web:docker": "yarn workspace dbgate-web build",
|
||||||
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const getChartExport = require('../utility/getChartExport');
|
|||||||
const hasPermission = require('../utility/hasPermission');
|
const hasPermission = require('../utility/hasPermission');
|
||||||
const socket = require('../utility/socket');
|
const socket = require('../utility/socket');
|
||||||
const scheduler = require('./scheduler');
|
const scheduler = require('./scheduler');
|
||||||
|
const getDiagramExport = require('../utility/getDiagramExport');
|
||||||
|
|
||||||
function serialize(format, data) {
|
function serialize(format, data) {
|
||||||
if (format == 'text') return data;
|
if (format == 'text') return data;
|
||||||
@@ -86,8 +87,9 @@ module.exports = {
|
|||||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||||
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (!hasPermission(`files/${folder}/write`)) return;
|
if (!hasPermission(`files/${folder}/write`)) return false;
|
||||||
const dir = path.join(filesdir(), folder);
|
const dir = path.join(filesdir(), folder);
|
||||||
if (!(await fs.exists(dir))) {
|
if (!(await fs.exists(dir))) {
|
||||||
await fs.mkdir(dir);
|
await fs.mkdir(dir);
|
||||||
@@ -98,6 +100,7 @@ module.exports = {
|
|||||||
if (folder == 'shell') {
|
if (folder == 'shell') {
|
||||||
scheduler.reload();
|
scheduler.reload();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,4 +153,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
exportDiagram_meta: true,
|
||||||
|
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
||||||
|
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
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"
|
"uuid": "^3.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chartjs-plugin-zoom": "^1.2.0"
|
"chartjs-plugin-zoom": "^1.2.0",
|
||||||
|
"interval-operations": "^1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ body {
|
|||||||
.nowrap {
|
.nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.noselect {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={`${$currentTheme} ${currentThemeType} root`}
|
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
||||||
use:dragDropFileTarget
|
use:dragDropFileTarget
|
||||||
on:contextmenu={e => e.preventDefault()}
|
on:contextmenu={e => e.preventDefault()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -85,6 +85,34 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowDiagram = async () => {
|
||||||
|
const db = await getDatabaseInfo({
|
||||||
|
conid: connection._id,
|
||||||
|
database: name,
|
||||||
|
});
|
||||||
|
openNewTab(
|
||||||
|
{
|
||||||
|
title: 'Diagram #',
|
||||||
|
icon: 'img diagram',
|
||||||
|
tabComponent: 'DiagramTab',
|
||||||
|
props: {
|
||||||
|
conid: connection._id,
|
||||||
|
database: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editor: {
|
||||||
|
tables: db.tables.map(table => ({
|
||||||
|
...table,
|
||||||
|
designerId: `${table.pureName}-${uuidv1()}`,
|
||||||
|
})),
|
||||||
|
references: [],
|
||||||
|
autoLayout: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => {
|
||||||
const electron = getElectron();
|
const electron = getElectron();
|
||||||
if (electron) {
|
if (electron) {
|
||||||
@@ -138,6 +166,7 @@
|
|||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ onClick: handleImport, text: 'Import' },
|
{ onClick: handleImport, text: 'Import' },
|
||||||
{ onClick: handleExport, text: 'Export' },
|
{ onClick: handleExport, text: 'Export' },
|
||||||
|
{ onClick: handleShowDiagram, text: 'Show diagram' },
|
||||||
{ onClick: handleSqlGenerator, text: 'SQL Generator' },
|
{ onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||||
{ onClick: handleOpenJsonModel, text: 'Open model as JSON' },
|
{ onClick: handleOpenJsonModel, text: 'Open model as JSON' },
|
||||||
{ onClick: handleExportModel, text: 'Export DB model - experimental' },
|
{ onClick: handleExportModel, text: 'Export DB model - experimental' },
|
||||||
@@ -157,6 +186,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||||
|
import uuidv1 from 'uuid/v1';
|
||||||
|
|
||||||
import _, { find } from 'lodash';
|
import _, { find } from 'lodash';
|
||||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||||
|
|||||||
@@ -53,6 +53,10 @@
|
|||||||
label: 'Query designer',
|
label: 'Query designer',
|
||||||
isQueryDesigner: true,
|
isQueryDesigner: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Show diagram',
|
||||||
|
isDiagram: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
divider: true,
|
||||||
},
|
},
|
||||||
@@ -388,45 +392,15 @@
|
|||||||
a.schemaName == b.schemaName
|
a.schemaName == b.schemaName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import _ from 'lodash';
|
|
||||||
import AppObjectCore from './AppObjectCore.svelte';
|
|
||||||
import { currentDatabase, extensions, openedConnections, pinnedTables } from '../stores';
|
|
||||||
import openNewTab from '../utility/openNewTab';
|
|
||||||
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
|
|
||||||
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
|
||||||
import fullDisplayName from '../utility/fullDisplayName';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
|
||||||
import { showModal } from '../modals/modalTools';
|
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
|
||||||
import uuidv1 from 'uuid/v1';
|
|
||||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
|
||||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
|
||||||
import getElectron from '../utility/getElectron';
|
|
||||||
import { exportElectronFile } from '../utility/exportElectronFile';
|
|
||||||
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
|
||||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
|
||||||
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
|
|
||||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
|
||||||
import { apiCall } from '../utility/api';
|
|
||||||
|
|
||||||
export let data;
|
|
||||||
export let passProps;
|
|
||||||
|
|
||||||
function handleClick(forceNewTab = false) {
|
|
||||||
handleDatabaseObjectClick(data, forceNewTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function createDatabaseObjectMenu(data) {
|
||||||
const getDriver = async () => {
|
const getDriver = async () => {
|
||||||
const conn = await getConnectionInfo(data);
|
const conn = await getConnectionInfo(data);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
const driver = findEngineDriver(conn, $extensions);
|
const driver = findEngineDriver(conn, getExtensions());
|
||||||
return driver;
|
return driver;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createMenu() {
|
|
||||||
const { objectTypeField } = data;
|
const { objectTypeField } = data;
|
||||||
return menus[objectTypeField]
|
return menus[objectTypeField]
|
||||||
.filter(x => x)
|
.filter(x => x)
|
||||||
@@ -434,7 +408,7 @@
|
|||||||
if (menu.divider) return menu;
|
if (menu.divider) return menu;
|
||||||
|
|
||||||
if (menu.isQuickExport) {
|
if (menu.isQuickExport) {
|
||||||
return createQuickExportMenu($extensions, fmt => async () => {
|
return createQuickExportMenu(getExtensions(), fmt => async () => {
|
||||||
const coninfo = await getConnectionInfo(data);
|
const coninfo = await getConnectionInfo(data);
|
||||||
exportElectronFile(
|
exportElectronFile(
|
||||||
data.pureName,
|
data.pureName,
|
||||||
@@ -531,6 +505,31 @@
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} else if (menu.isDiagram) {
|
||||||
|
openNewTab(
|
||||||
|
{
|
||||||
|
title: 'Diagram #',
|
||||||
|
icon: 'img diagram',
|
||||||
|
tabComponent: 'DiagramTab',
|
||||||
|
props: {
|
||||||
|
conid: data.conid,
|
||||||
|
database: data.database,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editor: {
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
designerId: `${data.pureName}-${uuidv1()}`,
|
||||||
|
autoAddReferences: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: [],
|
||||||
|
autoLayout: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (menu.sqlGeneratorProps) {
|
} else if (menu.sqlGeneratorProps) {
|
||||||
showModal(SqlGeneratorModal, {
|
showModal(SqlGeneratorModal, {
|
||||||
initialObjects: [data],
|
initialObjects: [data],
|
||||||
@@ -580,6 +579,40 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import AppObjectCore from './AppObjectCore.svelte';
|
||||||
|
import { currentDatabase, extensions, getExtensions, openedConnections, pinnedTables } from '../stores';
|
||||||
|
import openNewTab from '../utility/openNewTab';
|
||||||
|
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
|
||||||
|
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
|
import fullDisplayName from '../utility/fullDisplayName';
|
||||||
|
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
|
import uuidv1 from 'uuid/v1';
|
||||||
|
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||||
|
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||||
|
import getElectron from '../utility/getElectron';
|
||||||
|
import { exportElectronFile } from '../utility/exportElectronFile';
|
||||||
|
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
||||||
|
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||||
|
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
|
||||||
|
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
export let passProps;
|
||||||
|
|
||||||
|
function handleClick(forceNewTab = false) {
|
||||||
|
handleDatabaseObjectClick(data, forceNewTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return createDatabaseObjectMenu(data);
|
||||||
|
}
|
||||||
|
|
||||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -55,6 +55,14 @@
|
|||||||
currentConnection: true,
|
currentConnection: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const diagrams: FileTypeHandler = {
|
||||||
|
icon: 'img diagram',
|
||||||
|
format: 'json',
|
||||||
|
tabComponent: 'DiagramTab',
|
||||||
|
folder: 'diagrams',
|
||||||
|
currentConnection: true,
|
||||||
|
};
|
||||||
|
|
||||||
const HANDLERS = {
|
const HANDLERS = {
|
||||||
sql,
|
sql,
|
||||||
shell,
|
shell,
|
||||||
@@ -62,6 +70,7 @@
|
|||||||
charts,
|
charts,
|
||||||
query,
|
query,
|
||||||
sqlite,
|
sqlite,
|
||||||
|
diagrams,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractKey = data => data.file;
|
export const extractKey = data => data.file;
|
||||||
@@ -74,7 +83,7 @@
|
|||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
|
|
||||||
import { currentDatabase } from '../stores';
|
import { currentDatabase } from '../stores';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||||
import hasPermission from '../utility/hasPermission';
|
import hasPermission from '../utility/hasPermission';
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
export let onCreateReference;
|
export let onCreateReference;
|
||||||
export let onAddReferenceByColumn;
|
export let onAddReferenceByColumn;
|
||||||
export let onSelectColumn;
|
export let onSelectColumn;
|
||||||
|
export let settings;
|
||||||
|
|
||||||
$: designerColumn = (designer.columns || []).find(
|
$: designerColumn = (designer.columns || []).find(
|
||||||
x => x.designerId == designerId && x.columnName == column.columnName
|
x => x.designerId == designerId && x.columnName == column.columnName
|
||||||
@@ -38,9 +39,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
settings?.allowColumnOperations && [
|
||||||
{ text: 'Sort ascending', onClick: () => setSortOrder(1) },
|
{ text: 'Sort ascending', onClick: () => setSortOrder(1) },
|
||||||
{ text: 'Sort descending', onClick: () => setSortOrder(-1) },
|
{ text: 'Sort descending', onClick: () => setSortOrder(-1) },
|
||||||
{ text: 'Unsort', onClick: () => setSortOrder(0) },
|
{ text: 'Unsort', onClick: () => setSortOrder(0) },
|
||||||
|
],
|
||||||
foreignKey && { text: 'Add reference', onClick: addReference },
|
foreignKey && { text: 'Add reference', onClick: addReference },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -48,9 +51,12 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="line"
|
class="line"
|
||||||
|
class:canSelectColumns={settings?.canSelectColumns}
|
||||||
bind:this={domLine}
|
bind:this={domLine}
|
||||||
draggable={true}
|
draggable={!!settings?.allowCreateRefByDrag}
|
||||||
on:dragstart={e => {
|
on:dragstart={e => {
|
||||||
|
if (!settings?.allowCreateRefByDrag) return;
|
||||||
|
|
||||||
const dragData = {
|
const dragData = {
|
||||||
...column,
|
...column,
|
||||||
designerId,
|
designerId,
|
||||||
@@ -90,8 +96,9 @@
|
|||||||
...column,
|
...column,
|
||||||
designerId,
|
designerId,
|
||||||
})}
|
})}
|
||||||
use:contextMenu={createMenu}
|
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
|
||||||
>
|
>
|
||||||
|
{#if settings?.allowColumnOperations}
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
checked={!!(designer.columns || []).find(
|
checked={!!(designer.columns || []).find(
|
||||||
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
||||||
@@ -116,6 +123,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
|
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
|
||||||
{#if designerColumn?.filter}
|
{#if designerColumn?.filter}
|
||||||
<FontIcon icon="img filter" />
|
<FontIcon icon="img filter" />
|
||||||
@@ -129,16 +137,36 @@
|
|||||||
{#if designerColumn?.isGrouped}
|
{#if designerColumn?.isGrouped}
|
||||||
<FontIcon icon="img group" />
|
<FontIcon icon="img group" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if designer?.style?.showNullability || designer?.style?.showDataType}
|
||||||
|
<div class="space" />
|
||||||
|
{#if designer?.style?.showDataType && column?.dataType}
|
||||||
|
<div class="ml-2">
|
||||||
|
{column?.dataType.toLowerCase()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if designer?.style?.showNullability}
|
||||||
|
<div class="ml-2">
|
||||||
|
{column?.notNull ? 'NOT NULL' : 'NULL'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.line:hover {
|
:global(.dbgate-screen) .line.canSelectColumns:hover {
|
||||||
background: var(--theme-bg-1);
|
background: var(--theme-bg-1);
|
||||||
}
|
}
|
||||||
.line.isDragSource {
|
:global(.dbgate-screen) .line.isDragSource {
|
||||||
background: var(--theme-bg-gold);
|
background: var(--theme-bg-gold);
|
||||||
}
|
}
|
||||||
.line.isDragTarget {
|
:global(.dbgate-screen) .line.isDragTarget {
|
||||||
background: var(--theme-bg-gold);
|
background: var(--theme-bg-gold);
|
||||||
}
|
}
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.space {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,78 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
|
const getCurrentEditor = () => getActiveComponent('Designer');
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'designer.arrange',
|
||||||
|
category: 'Designer',
|
||||||
|
icon: 'icon arrange',
|
||||||
|
name: 'Arrange',
|
||||||
|
toolbar: true,
|
||||||
|
isRelatedToTab: true,
|
||||||
|
testEnabled: () => getCurrentEditor()?.canArrange(),
|
||||||
|
onClick: () => getCurrentEditor().arrange(),
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'diagram.export',
|
||||||
|
category: 'Designer',
|
||||||
|
toolbarName: 'Export diagram',
|
||||||
|
name: 'Export diagram',
|
||||||
|
icon: 'icon report',
|
||||||
|
toolbar: true,
|
||||||
|
isRelatedToTab: true,
|
||||||
|
onClick: () => getCurrentEditor().exportDiagram(),
|
||||||
|
testEnabled: () => getCurrentEditor()?.canExport(),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DesignerTable from './DesignerTable.svelte';
|
import DesignerTable from './DesignerTable.svelte';
|
||||||
import { isConnectedByReference } from './designerTools';
|
import { isConnectedByReference } from './designerTools';
|
||||||
import uuidv1 from 'uuid/v1';
|
import uuidv1 from 'uuid/v1';
|
||||||
import { getTableInfo } from '../utility/metadataLoaders';
|
import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import createRef from '../utility/createRef';
|
|
||||||
import DesignerReference from './DesignerReference.svelte';
|
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import contextMenu from '../utility/contextMenu';
|
import contextMenu from '../utility/contextMenu';
|
||||||
|
import stableStringify from 'json-stable-stringify';
|
||||||
|
import registerCommand from '../commands/registerCommand';
|
||||||
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
|
import { GraphDefinition, GraphLayout } from './GraphLayout';
|
||||||
|
import { saveFileToDisk } from '../utility/exportElectronFile';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import moveDrag from '../utility/moveDrag';
|
||||||
|
import { rectanglesHaveIntersection } from './designerMath';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
|
||||||
|
import { currentThemeDefinition } from '../stores';
|
||||||
|
|
||||||
export let value;
|
export let value;
|
||||||
export let onChange;
|
export let onChange;
|
||||||
export let conid;
|
export let conid;
|
||||||
export let database;
|
export let database;
|
||||||
export let menu;
|
export let menu;
|
||||||
|
export let settings;
|
||||||
|
export let referenceComponent;
|
||||||
|
|
||||||
|
export const activator = createActivator('Designer', true);
|
||||||
|
|
||||||
let domCanvas;
|
let domCanvas;
|
||||||
|
let canvasWidth = 3000;
|
||||||
|
let canvasHeight = 3000;
|
||||||
|
let dragStartPoint = null;
|
||||||
|
let dragCurrentPoint = null;
|
||||||
|
|
||||||
const sourceDragColumn$ = writable(null);
|
const sourceDragColumn$ = writable(null);
|
||||||
const targetDragColumn$ = writable(null);
|
const targetDragColumn$ = writable(null);
|
||||||
|
|
||||||
|
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
||||||
|
|
||||||
$: tables = value?.tables as any[];
|
$: tables = value?.tables as any[];
|
||||||
$: references = value?.references as any[];
|
$: references = value?.references as any[];
|
||||||
|
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||||
|
|
||||||
|
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||||
|
|
||||||
const tableRefs = {};
|
const tableRefs = {};
|
||||||
const referenceRefs = {};
|
const referenceRefs = {};
|
||||||
@@ -47,6 +93,84 @@
|
|||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (dbInfo) {
|
||||||
|
updateFromDbInfo($dbInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
detectSize(tables, domTables);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (dbInfo && value?.autoLayout) {
|
||||||
|
performAutoActions($dbInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromDbInfo(db = 'auto') {
|
||||||
|
if (db == 'auto' && dbInfo) db = $dbInfo;
|
||||||
|
if (!settings?.updateFromDbInfo || !db) return;
|
||||||
|
|
||||||
|
onChange(current => {
|
||||||
|
let newTables = current.tables || [];
|
||||||
|
for (const table of current.tables || []) {
|
||||||
|
const dbTable = (db.tables || []).find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
|
||||||
|
if (
|
||||||
|
stableStringify(_.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys'])) !=
|
||||||
|
stableStringify(_.pick(table, ['columns', 'primaryKey', 'foreignKeys']))
|
||||||
|
) {
|
||||||
|
newTables = newTables.map(x =>
|
||||||
|
x == table
|
||||||
|
? {
|
||||||
|
...table,
|
||||||
|
..._.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys']),
|
||||||
|
}
|
||||||
|
: x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let references = current?.references;
|
||||||
|
if (settings?.useDatabaseReferences) {
|
||||||
|
references = [];
|
||||||
|
for (const table of newTables) {
|
||||||
|
for (const fk of table.foreignKeys) {
|
||||||
|
const dst = newTables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
|
||||||
|
if (!dst) continue;
|
||||||
|
references.push({
|
||||||
|
designerId: uuidv1(),
|
||||||
|
sourceId: table.designerId,
|
||||||
|
targetId: dst.designerId,
|
||||||
|
joinType: '',
|
||||||
|
columns: fk.columns.map(col => ({
|
||||||
|
source: col.columnName,
|
||||||
|
target: col.refColumnName,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
tables: newTables,
|
||||||
|
references,
|
||||||
|
};
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectSize(tables, domTables) {
|
||||||
|
await tick();
|
||||||
|
const rects = _.values(domTables).map(x => x.getRect());
|
||||||
|
const maxX = _.max(rects.map(x => x.right));
|
||||||
|
const maxY = _.max(rects.map(x => x.bottom));
|
||||||
|
|
||||||
|
canvasWidth = Math.max(3000, maxX + 50);
|
||||||
|
canvasHeight = Math.max(3000, maxY + 50);
|
||||||
|
}
|
||||||
|
|
||||||
function callChange(changeFunc, skipUndoChain = undefined) {
|
function callChange(changeFunc, skipUndoChain = undefined) {
|
||||||
onChange(changeFunc, skipUndoChain);
|
onChange(changeFunc, skipUndoChain);
|
||||||
tick().then(recomputeReferencePositions);
|
tick().then(recomputeReferencePositions);
|
||||||
@@ -69,7 +193,28 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectTable = (table, addToSelection) => {
|
||||||
|
callChange(
|
||||||
|
current => ({
|
||||||
|
...current,
|
||||||
|
tables: (current.tables || []).map(x =>
|
||||||
|
x.designerId == table.designerId
|
||||||
|
? { ...x, isSelectedTable: true }
|
||||||
|
: { ...x, isSelectedTable: addToSelection ? x.isSelectedTable : false }
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const removeTable = table => {
|
const removeTable = table => {
|
||||||
|
if (isMultipleTableSelection && settings?.useDatabaseReferences && settings?.canSelectTables) {
|
||||||
|
callChange(current => ({
|
||||||
|
...current,
|
||||||
|
tables: (current.tables || []).filter(x => !x.isSelectedTable),
|
||||||
|
}));
|
||||||
|
updateFromDbInfo();
|
||||||
|
} else {
|
||||||
callChange(current => ({
|
callChange(current => ({
|
||||||
...current,
|
...current,
|
||||||
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
||||||
@@ -78,6 +223,7 @@
|
|||||||
),
|
),
|
||||||
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeReference = ref => {
|
const changeReference = ref => {
|
||||||
@@ -150,10 +296,12 @@
|
|||||||
pureName: foreignKey.refTableName,
|
pureName: foreignKey.refTableName,
|
||||||
schemaName: foreignKey.refSchemaName,
|
schemaName: foreignKey.refSchemaName,
|
||||||
});
|
});
|
||||||
const newTableDesignerId = uuidv1();
|
const newTableDesignerId = `${toTable.pureName}-${uuidv1()}`;
|
||||||
callChange(current => {
|
callChange(current => {
|
||||||
const fromTable = (current.tables || []).find(x => x.designerId == designerId);
|
const fromTable = (current.tables || []).find(x => x.designerId == designerId);
|
||||||
if (!fromTable) return;
|
if (!fromTable) return current;
|
||||||
|
const alias = getNewTableAlias(toTable, current.tables);
|
||||||
|
if (alias && !settings?.allowTableAlias) return current;
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
tables: [
|
tables: [
|
||||||
@@ -163,10 +311,11 @@
|
|||||||
left: fromTable.left + 300,
|
left: fromTable.left + 300,
|
||||||
top: fromTable.top + 50,
|
top: fromTable.top + 50,
|
||||||
designerId: newTableDesignerId,
|
designerId: newTableDesignerId,
|
||||||
alias: getNewTableAlias(toTable, current.tables),
|
alias,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
references: [
|
references: settings?.allowCreateRefByDrag
|
||||||
|
? [
|
||||||
...(current.references || []),
|
...(current.references || []),
|
||||||
{
|
{
|
||||||
designerId: uuidv1(),
|
designerId: uuidv1(),
|
||||||
@@ -178,9 +327,101 @@
|
|||||||
target: col.refColumnName,
|
target: col.refColumnName,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
|
: current.references,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
updateFromDbInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTablesWithReferences = (db, table, current) => {
|
||||||
|
const dbTable = db.tables?.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
|
||||||
|
if (!dbTable) return;
|
||||||
|
|
||||||
|
const newTables = [];
|
||||||
|
for (const fk of dbTable.foreignKeys || []) {
|
||||||
|
const existing = [...current.tables, ...newTables].find(
|
||||||
|
x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
const dst = db.tables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
|
||||||
|
if (dst) newTables.push(dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fk of dbTable.dependencies || []) {
|
||||||
|
const existing = [...current.tables, ...newTables].find(
|
||||||
|
x => x.pureName == fk.pureName && x.schemaName == fk.schemaName
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
const dst = db.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
|
||||||
|
if (dst) newTables.push(dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
tables: [
|
||||||
|
...current.tables,
|
||||||
|
...newTables.map(x => ({
|
||||||
|
...x,
|
||||||
|
designerId: `${x.pureName}-${uuidv1()}`,
|
||||||
|
needsArrange: true,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTableReferences = async table => {
|
||||||
|
if (!dbInfo) return;
|
||||||
|
const db = $dbInfo;
|
||||||
|
if (!db) return;
|
||||||
|
callChange(current => {
|
||||||
|
return getTablesWithReferences(db, table, current);
|
||||||
|
});
|
||||||
|
updateFromDbInfo();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const rect = (domTables[table.designerId] as any)?.getRect();
|
||||||
|
arrange(true, false, rect ? { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 } : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeTableColor = table => {
|
||||||
|
showModal(ChooseColorModal, {
|
||||||
|
onChange: color => {
|
||||||
|
callChange(current => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
tables: (current?.tables || []).map(table =>
|
||||||
|
table.isSelectedTable
|
||||||
|
? {
|
||||||
|
...table,
|
||||||
|
tableColor: color,
|
||||||
|
}
|
||||||
|
: table
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const performAutoActions = async db => {
|
||||||
|
if (!db) return;
|
||||||
|
|
||||||
|
callChange(current => {
|
||||||
|
for (const table of current?.tables || []) {
|
||||||
|
if (table.autoAddReferences) current = getTablesWithReferences(db, table, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
autoLayout: false,
|
||||||
|
tables: (current?.tables || []).map(tbl => ({ ...tbl, autoAddReferences: false })),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
updateFromDbInfo();
|
||||||
|
await tick();
|
||||||
|
arrange();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectColumn = column => {
|
const handleSelectColumn = column => {
|
||||||
@@ -231,12 +472,14 @@
|
|||||||
var json = JSON.parse(data);
|
var json = JSON.parse(data);
|
||||||
const { objectTypeField } = json;
|
const { objectTypeField } = json;
|
||||||
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
||||||
json.designerId = uuidv1();
|
json.designerId = `${json.pureName}-${uuidv1()}`;
|
||||||
json.left = e.clientX - rect.left;
|
json.left = e.clientX - rect.left;
|
||||||
json.top = e.clientY - rect.top;
|
json.top = e.clientY - rect.top;
|
||||||
|
|
||||||
callChange(current => {
|
callChange(current => {
|
||||||
const foreignKeys = _.compact([
|
const foreignKeys = settings?.useDatabaseReferences
|
||||||
|
? []
|
||||||
|
: _.compact([
|
||||||
...(json.foreignKeys || []).map(fk => {
|
...(json.foreignKeys || []).map(fk => {
|
||||||
const tables = ((current || {}).tables || []).filter(
|
const tables = ((current || {}).tables || []).filter(
|
||||||
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
||||||
@@ -265,13 +508,16 @@
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const alias = getNewTableAlias(json, current?.tables);
|
||||||
|
if (alias && !settings?.allowTableAlias) return current;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
tables: [
|
tables: [
|
||||||
...((current || {}).tables || []),
|
...((current || {}).tables || []),
|
||||||
{
|
{
|
||||||
...json,
|
...json,
|
||||||
alias: getNewTableAlias(json, current?.tables),
|
alias,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
references:
|
references:
|
||||||
@@ -279,7 +525,7 @@
|
|||||||
? [
|
? [
|
||||||
...((current || {}).references || []),
|
...((current || {}).references || []),
|
||||||
{
|
{
|
||||||
designerId: uuidv1(),
|
designerId: `${current?.pureName}-${uuidv1()}`,
|
||||||
sourceId: foreignKeys[0].sourceId,
|
sourceId: foreignKeys[0].sourceId,
|
||||||
targetId: foreignKeys[0].targetId,
|
targetId: foreignKeys[0].targetId,
|
||||||
joinType: 'INNER JOIN',
|
joinType: 'INNER JOIN',
|
||||||
@@ -292,6 +538,81 @@
|
|||||||
: (current || {}).references,
|
: (current || {}).references,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
updateFromDbInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
function forEachSelected(op: Function) {
|
||||||
|
for (const tbl of _.values(tableRefs)) {
|
||||||
|
const table = tbl as any;
|
||||||
|
if (!table?.isSelected()) continue;
|
||||||
|
op(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableMoveStart = () => {
|
||||||
|
forEachSelected(t => t.moveStart());
|
||||||
|
};
|
||||||
|
const tableMove = (x, y) => {
|
||||||
|
forEachSelected(t => t.move(x, y));
|
||||||
|
tick().then(recomputeReferencePositions);
|
||||||
|
};
|
||||||
|
const tableMoveEnd = () => {
|
||||||
|
const moves = {};
|
||||||
|
forEachSelected(t => {
|
||||||
|
moves[t.getDesignerId()] = t.moveEnd();
|
||||||
|
});
|
||||||
|
callChange(current => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
tables: (current?.tables || []).map(table => {
|
||||||
|
const position = moves[table.designerId];
|
||||||
|
return position
|
||||||
|
? {
|
||||||
|
...table,
|
||||||
|
left: position.left,
|
||||||
|
top: position.top,
|
||||||
|
}
|
||||||
|
: table;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
tick().then(recomputeReferencePositions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveStart = (x, y) => {
|
||||||
|
dragStartPoint = { x: x / zoomKoef, y: y / zoomKoef };
|
||||||
|
};
|
||||||
|
const handleMove = (dx, dy, x, y) => {
|
||||||
|
dragCurrentPoint = { x: x / zoomKoef, y: y / zoomKoef };
|
||||||
|
};
|
||||||
|
const handleMoveEnd = (x, y) => {
|
||||||
|
if (dragStartPoint && dragCurrentPoint) {
|
||||||
|
const bounds = {
|
||||||
|
left: Math.min(dragStartPoint.x, dragCurrentPoint.x),
|
||||||
|
right: Math.max(dragStartPoint.x, dragCurrentPoint.x),
|
||||||
|
top: Math.min(dragStartPoint.y, dragCurrentPoint.y),
|
||||||
|
bottom: Math.max(dragStartPoint.y, dragCurrentPoint.y),
|
||||||
|
};
|
||||||
|
|
||||||
|
callChange(
|
||||||
|
current => ({
|
||||||
|
...current,
|
||||||
|
tables: (current.tables || []).map(x => {
|
||||||
|
const domTable = domTables[x.designerId] as any;
|
||||||
|
const rect = domTable.getRect();
|
||||||
|
return {
|
||||||
|
...x,
|
||||||
|
isSelectedTable: rectanglesHaveIntersection(rect, bounds),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStartPoint = null;
|
||||||
|
dragCurrentPoint = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function recomputeReferencePositions() {
|
function recomputeReferencePositions() {
|
||||||
@@ -299,22 +620,218 @@
|
|||||||
if (ref) ref.recomputePosition();
|
if (ref) ref.recomputePosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canArrange() {
|
||||||
|
return settings?.canArrange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExport() {
|
||||||
|
return settings?.canExport;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrange(skipUndoChain = false, arrangeAll = true, circleMiddle = { x: 0, y: 0 }) {
|
||||||
|
const graph = new GraphDefinition();
|
||||||
|
for (const table of value?.tables || []) {
|
||||||
|
const domTable = domTables[table.designerId] as any;
|
||||||
|
if (!domTable) continue;
|
||||||
|
const rect = domTable.getRect();
|
||||||
|
graph.addNode(
|
||||||
|
table.designerId,
|
||||||
|
rect.right - rect.left,
|
||||||
|
rect.bottom - rect.top,
|
||||||
|
arrangeAll || table.needsArrange ? null : { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reference of value?.references) {
|
||||||
|
graph.addEdge(reference.sourceId, reference.targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.initialize();
|
||||||
|
|
||||||
|
const layout = GraphLayout
|
||||||
|
// initial circle layout
|
||||||
|
.createCircle(graph, circleMiddle)
|
||||||
|
// simulation with Hook's, Coulomb's and gravity law
|
||||||
|
.springyAlg()
|
||||||
|
// move nodes to avoid overlaps
|
||||||
|
.solveOverlaps()
|
||||||
|
// view box starts with [0,0]
|
||||||
|
.fixViewBox();
|
||||||
|
|
||||||
|
// layout.print();
|
||||||
|
|
||||||
|
callChange(current => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
tables: (current?.tables || []).map(table => {
|
||||||
|
const node = layout.nodes[table.designerId];
|
||||||
|
// console.log('POSITION', position);
|
||||||
|
return node
|
||||||
|
? {
|
||||||
|
...table,
|
||||||
|
needsArrange: false,
|
||||||
|
left: node.left,
|
||||||
|
top: node.top,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...table,
|
||||||
|
needsArrange: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, skipUndoChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportDiagram() {
|
||||||
|
const cssLinks = ['global.css', 'build/bundle.css'];
|
||||||
|
let css = '';
|
||||||
|
for (const link of cssLinks) {
|
||||||
|
const cssResp = await fetch(link);
|
||||||
|
const cssItem = await cssResp.text();
|
||||||
|
if (css) css += '\n';
|
||||||
|
css += cssItem;
|
||||||
|
}
|
||||||
|
saveFileToDisk(async filePath => {
|
||||||
|
await apiCall('files/export-diagram', {
|
||||||
|
filePath,
|
||||||
|
html: domCanvas.outerHTML,
|
||||||
|
css,
|
||||||
|
themeType: $currentThemeDefinition?.themeType,
|
||||||
|
themeClassName: $currentThemeDefinition?.className,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeStyleFunc = (name, value) => () => {
|
||||||
|
callChange(current => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
style: {
|
||||||
|
...current?.style,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return [
|
||||||
|
menu,
|
||||||
|
settings?.customizeStyle && [
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
text: 'Column properties',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
text: `Nullability: ${value?.style?.showNullability ? 'YES' : 'NO'}`,
|
||||||
|
onClick: changeStyleFunc('showNullability', !value?.style?.showNullability),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Data type: ${value?.style?.showDataType ? 'YES' : 'NO'}`,
|
||||||
|
onClick: changeStyleFunc('showDataType', !value?.style?.showDataType),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Columns - ${_.startCase(value?.style?.filterColumns || 'all')}`,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
text: 'All',
|
||||||
|
onClick: changeStyleFunc('filterColumns', null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Primary Key',
|
||||||
|
onClick: changeStyleFunc('filterColumns', 'primaryKey'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'All Keys',
|
||||||
|
onClick: changeStyleFunc('filterColumns', 'allKeys'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Not Null',
|
||||||
|
onClick: changeStyleFunc('filterColumns', 'notNull'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Keys And Not Null',
|
||||||
|
onClick: changeStyleFunc('filterColumns', 'keysAndNotNull'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Zoom - ${(value?.style?.zoomKoef || 1) * 100}%`,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
text: `10 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `15 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.15),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `20 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `40 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `60 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.6),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `80 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 0.8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `100 %`,
|
||||||
|
onClick: changeStyleFunc('zoomKoef', 1),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" use:contextMenu={menu}>
|
<div class="wrapper noselect" use:contextMenu={createMenu}>
|
||||||
{#if !(tables?.length > 0)}
|
{#if !(tables?.length > 0)}
|
||||||
<div class="empty">Drag & drop tables or views from left panel here</div>
|
<div class="empty">Drag & drop tables or views from left panel here</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="canvas" bind:this={domCanvas} on:dragover={e => e.preventDefault()} on:drop={handleDrop}>
|
<div
|
||||||
|
class="canvas"
|
||||||
|
bind:this={domCanvas}
|
||||||
|
on:dragover={e => e.preventDefault()}
|
||||||
|
on:drop={handleDrop}
|
||||||
|
style={`width:${canvasWidth}px;height:${canvasHeight}px;
|
||||||
|
${settings?.customizeStyle && value?.style?.zoomKoef ? `zoom:${value?.style?.zoomKoef};` : ''}
|
||||||
|
`}
|
||||||
|
on:mousedown={e => {
|
||||||
|
if (e.button == 0 && settings?.canSelectTables) {
|
||||||
|
callChange(
|
||||||
|
current => ({
|
||||||
|
...current,
|
||||||
|
tables: (current.tables || []).map(x => ({ ...x, isSelectedTable: false })),
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
use:moveDrag={settings?.canSelectTables ? [handleMoveStart, handleMove, handleMoveEnd] : null}
|
||||||
|
>
|
||||||
{#each references || [] as ref (ref.designerId)}
|
{#each references || [] as ref (ref.designerId)}
|
||||||
<DesignerReference
|
<svelte:component
|
||||||
|
this={referenceComponent}
|
||||||
bind:this={referenceRefs[ref.designerId]}
|
bind:this={referenceRefs[ref.designerId]}
|
||||||
{domTables}
|
{domTables}
|
||||||
reference={ref}
|
reference={ref}
|
||||||
onChangeReference={changeReference}
|
onChangeReference={changeReference}
|
||||||
onRemoveReference={removeReference}
|
onRemoveReference={removeReference}
|
||||||
designer={value}
|
designer={value}
|
||||||
|
{settings}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<!--
|
<!--
|
||||||
@@ -335,17 +852,42 @@
|
|||||||
onSelectColumn={handleSelectColumn}
|
onSelectColumn={handleSelectColumn}
|
||||||
onChangeColumn={handleChangeColumn}
|
onChangeColumn={handleChangeColumn}
|
||||||
onAddReferenceByColumn={handleAddReferenceByColumn}
|
onAddReferenceByColumn={handleAddReferenceByColumn}
|
||||||
|
onAddAllReferences={handleAddTableReferences}
|
||||||
|
onChangeTableColor={handleChangeTableColor}
|
||||||
onMoveReferences={recomputeReferencePositions}
|
onMoveReferences={recomputeReferencePositions}
|
||||||
{table}
|
{table}
|
||||||
|
{conid}
|
||||||
|
{database}
|
||||||
|
{zoomKoef}
|
||||||
|
{isMultipleTableSelection}
|
||||||
onChangeTable={changeTable}
|
onChangeTable={changeTable}
|
||||||
onBringToFront={bringToFront}
|
onBringToFront={bringToFront}
|
||||||
|
onSelectTable={selectTable}
|
||||||
onRemoveTable={removeTable}
|
onRemoveTable={removeTable}
|
||||||
|
onMoveStart={tableMoveStart}
|
||||||
|
onMove={tableMove}
|
||||||
|
onMoveEnd={tableMoveEnd}
|
||||||
{domCanvas}
|
{domCanvas}
|
||||||
designer={value}
|
designer={value}
|
||||||
{sourceDragColumn$}
|
{sourceDragColumn$}
|
||||||
{targetDragColumn$}
|
{targetDragColumn$}
|
||||||
|
{settings}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#if dragStartPoint && dragCurrentPoint}
|
||||||
|
<svg class="drag-rect">
|
||||||
|
<polyline
|
||||||
|
points={`
|
||||||
|
${dragStartPoint.x},${dragStartPoint.y}
|
||||||
|
${dragStartPoint.x},${dragCurrentPoint.y}
|
||||||
|
${dragCurrentPoint.x},${dragCurrentPoint.y}
|
||||||
|
${dragCurrentPoint.x},${dragStartPoint.y}
|
||||||
|
${dragStartPoint.x},${dragStartPoint.y}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -360,8 +902,26 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
.canvas {
|
.canvas {
|
||||||
width: 3000px;
|
|
||||||
height: 3000px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg.drag-rect {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
:global(.dbgate-screen) svg.drag-rect {
|
||||||
|
visibility: visible;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:global(.dbgate-screen) svg.drag-rect polyline {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--theme-bg-4);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { presetDarkPalettes, presetPalettes } from '@ant-design/colors';
|
||||||
|
import { computeDbDiffRows } from 'dbgate-tools';
|
||||||
|
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
import { createDatabaseObjectMenu } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||||
|
|
||||||
import FontIcon from '../icons/FontIcon.svelte';
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import { currentThemeDefinition } from '../stores';
|
||||||
import contextMenu from '../utility/contextMenu';
|
import contextMenu from '../utility/contextMenu';
|
||||||
import moveDrag from '../utility/moveDrag';
|
import moveDrag from '../utility/moveDrag';
|
||||||
import ColumnLine from './ColumnLine.svelte';
|
import ColumnLine from './ColumnLine.svelte';
|
||||||
import DomTableRef from './DomTableRef';
|
import DomTableRef from './DomTableRef';
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
export let table;
|
export let table;
|
||||||
|
export let zoomKoef;
|
||||||
export let onChangeTable;
|
export let onChangeTable;
|
||||||
export let onBringToFront;
|
export let onBringToFront;
|
||||||
|
export let onSelectTable;
|
||||||
export let onRemoveTable;
|
export let onRemoveTable;
|
||||||
|
export let onAddAllReferences;
|
||||||
export let onCreateReference;
|
export let onCreateReference;
|
||||||
export let onAddReferenceByColumn;
|
export let onAddReferenceByColumn;
|
||||||
export let onSelectColumn;
|
export let onSelectColumn;
|
||||||
export let onChangeColumn;
|
export let onChangeColumn;
|
||||||
|
export let onChangeTableColor;
|
||||||
|
export let isMultipleTableSelection;
|
||||||
|
|
||||||
|
export let onMoveStart;
|
||||||
|
export let onMove;
|
||||||
|
export let onMoveEnd;
|
||||||
|
|
||||||
// export let sourceDragColumn;
|
// export let sourceDragColumn;
|
||||||
// export let setSourceDragColumn;
|
// export let setSourceDragColumn;
|
||||||
@@ -31,6 +47,7 @@
|
|||||||
// export let domTablesRef;
|
// export let domTablesRef;
|
||||||
export let designer;
|
export let designer;
|
||||||
export let onMoveReferences;
|
export let onMoveReferences;
|
||||||
|
export let settings;
|
||||||
|
|
||||||
let movingPosition = null;
|
let movingPosition = null;
|
||||||
let domWrapper;
|
let domWrapper;
|
||||||
@@ -39,29 +56,95 @@
|
|||||||
|
|
||||||
$: pureName = table?.pureName;
|
$: pureName = table?.pureName;
|
||||||
$: alias = table?.alias;
|
$: alias = table?.alias;
|
||||||
$: columns = table?.columns as any[];
|
$: columns = (table?.columns as any[]).filter(x => shouldShowColumn(table, x, designer?.style));
|
||||||
$: designerId = table?.designerId;
|
$: designerId = table?.designerId;
|
||||||
$: objectTypeField = table?.objectTypeField;
|
$: objectTypeField = table?.objectTypeField;
|
||||||
$: left = table?.left;
|
$: left = table?.left;
|
||||||
$: top = table?.top;
|
$: top = table?.top;
|
||||||
|
|
||||||
function handleMoveStart() {
|
export function isSelected() {
|
||||||
|
return table?.isSelectedTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDesignerId() {
|
||||||
|
return designerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveStart() {
|
||||||
movingPosition = { left, top };
|
movingPosition = { left, top };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function move(x, y) {
|
||||||
|
movingPosition.left += x / zoomKoef;
|
||||||
|
movingPosition.top += y / zoomKoef;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveEnd() {
|
||||||
|
const res = movingPosition;
|
||||||
|
movingPosition = null;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowColumn(table, column, style) {
|
||||||
|
if (!settings?.customizeStyle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (style?.filterColumns || 'all') {
|
||||||
|
case 'primaryKey':
|
||||||
|
return table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName);
|
||||||
|
case 'allKeys':
|
||||||
|
return (
|
||||||
|
table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName) ||
|
||||||
|
table?.foreignKeys?.find(fk => fk.columns.find(x => x.columnName == column?.columnName))
|
||||||
|
);
|
||||||
|
case 'keysAndNotNull':
|
||||||
|
return (
|
||||||
|
column?.notNull ||
|
||||||
|
table?.primaryKey?.columns?.find(x => x.columnName == column?.columnName) ||
|
||||||
|
table?.foreignKeys?.find(fk => fk.columns.find(x => x.columnName == column?.columnName))
|
||||||
|
);
|
||||||
|
case 'notNull':
|
||||||
|
return column?.notNull;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveStart() {
|
||||||
|
if (settings?.canSelectTables) {
|
||||||
|
onMoveStart();
|
||||||
|
} else {
|
||||||
|
moveStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
function handleMove(x, y) {
|
function handleMove(x, y) {
|
||||||
movingPosition.left += x;
|
if (settings?.canSelectTables) {
|
||||||
movingPosition.top += y;
|
onMove(x, y);
|
||||||
|
} else {
|
||||||
|
move(x, y);
|
||||||
tick().then(onMoveReferences);
|
tick().then(onMoveReferences);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function handleMoveEnd() {
|
function handleMoveEnd() {
|
||||||
|
if (settings?.canSelectTables) {
|
||||||
|
onMoveEnd();
|
||||||
|
} else {
|
||||||
|
const position = moveEnd();
|
||||||
onChangeTable({
|
onChangeTable({
|
||||||
...table,
|
...table,
|
||||||
left: movingPosition.left,
|
left: position.left,
|
||||||
top: movingPosition.top,
|
top: position.top,
|
||||||
});
|
});
|
||||||
movingPosition = null;
|
|
||||||
tick().then(onMoveReferences);
|
tick().then(onMoveReferences);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableColorStyle(themeDef, table, colorIndex = 3) {
|
||||||
|
if (!table?.tableColor) return null;
|
||||||
|
const palettes = themeDef?.themeType == 'dark' ? presetDarkPalettes : presetPalettes;
|
||||||
|
const palette = palettes[table?.tableColor];
|
||||||
|
if (!palette) return null;
|
||||||
|
return `background: ${palette[colorIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDomTable() {
|
export function getDomTable() {
|
||||||
const domRefs = { ...columnRefs };
|
const domRefs = { ...columnRefs };
|
||||||
@@ -87,6 +170,8 @@
|
|||||||
return [
|
return [
|
||||||
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
settings?.allowTableAlias &&
|
||||||
|
!isMultipleTableSelection && [
|
||||||
{ text: 'Set table alias', onClick: handleSetTableAlias },
|
{ text: 'Set table alias', onClick: handleSetTableAlias },
|
||||||
alias && {
|
alias && {
|
||||||
text: 'Remove table alias',
|
text: 'Remove table alias',
|
||||||
@@ -96,29 +181,50 @@
|
|||||||
alias: null,
|
alias: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
settings?.allowAddAllReferences &&
|
||||||
|
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
|
||||||
|
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
|
||||||
|
settings?.appendTableSystemMenu &&
|
||||||
|
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
|
class:canSelectColumns={settings?.canSelectColumns}
|
||||||
|
class:isSelectedTable={table?.isSelectedTable}
|
||||||
style={`left: ${movingPosition ? movingPosition.left : left}px; top:${movingPosition ? movingPosition.top : top}px`}
|
style={`left: ${movingPosition ? movingPosition.left : left}px; top:${movingPosition ? movingPosition.top : top}px`}
|
||||||
bind:this={domWrapper}
|
bind:this={domWrapper}
|
||||||
on:mousedown={() => onBringToFront(table)}
|
on:mousedown={e => {
|
||||||
|
if (e.button == 0) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onBringToFront(table);
|
||||||
|
if (settings?.canSelectTables && !table?.isSelectedTable) {
|
||||||
|
onSelectTable(table, e.ctrlKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
use:contextMenu={settings?.canSelectColumns ? '__no_menu' : createMenu}
|
||||||
|
use:moveDrag={settings?.canSelectColumns ? null : [handleMoveStart, handleMove, handleMoveEnd]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header"
|
class="header"
|
||||||
class:isTable={objectTypeField == 'tables'}
|
class:isTable={objectTypeField == 'tables'}
|
||||||
class:isView={objectTypeField == 'views'}
|
class:isView={objectTypeField == 'views'}
|
||||||
use:moveDrag={[handleMoveStart, handleMove, handleMoveEnd]}
|
use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null}
|
||||||
use:contextMenu={createMenu}
|
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
|
||||||
|
style={getTableColorStyle($currentThemeDefinition, table)}
|
||||||
>
|
>
|
||||||
<div>{alias || pureName}</div>
|
<div>{alias || pureName}</div>
|
||||||
|
{#if settings?.showTableCloseButton}
|
||||||
<div class="close" on:click={() => onRemoveTable(table)}>
|
<div class="close" on:click={() => onRemoveTable(table)}>
|
||||||
<FontIcon icon="icon close" />
|
<FontIcon icon="icon close" />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="columns" on:scroll={() => tick().then(onMoveReferences)}>
|
<div class="columns" on:scroll={() => tick().then(onMoveReferences)} class:scroll={settings?.allowScrollColumns}>
|
||||||
{#each columns || [] as column}
|
{#each columns || [] as column}
|
||||||
<ColumnLine
|
<ColumnLine
|
||||||
{column}
|
{column}
|
||||||
@@ -131,10 +237,17 @@
|
|||||||
{targetDragColumn$}
|
{targetDragColumn$}
|
||||||
{onCreateReference}
|
{onCreateReference}
|
||||||
{onAddReferenceByColumn}
|
{onAddReferenceByColumn}
|
||||||
|
{settings}
|
||||||
bind:domLine={columnRefs[column.columnName]}
|
bind:domLine={columnRefs[column.columnName]}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if table?.isSelectedTable}
|
||||||
|
<div class="selection-marker lt" />
|
||||||
|
<div class="selection-marker rt" />
|
||||||
|
<div class="selection-marker lb" />
|
||||||
|
<div class="selection-marker rb" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -143,16 +256,51 @@
|
|||||||
background-color: var(--theme-bg-0);
|
background-color: var(--theme-bg-0);
|
||||||
border: 1px solid var(--theme-border);
|
border: 1px solid var(--theme-border);
|
||||||
}
|
}
|
||||||
|
/* :global(.dbgate-screen) .isSelectedTable {
|
||||||
|
border: 3px solid var(--theme-border);
|
||||||
|
} */
|
||||||
|
.selection-marker {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--theme-font-1);
|
||||||
|
}
|
||||||
|
.selection-marker.lt {
|
||||||
|
left: -3px;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
.selection-marker.rt {
|
||||||
|
right: -3px;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
.selection-marker.lb {
|
||||||
|
left: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
}
|
||||||
|
.selection-marker.rb {
|
||||||
|
right: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
}
|
||||||
|
:global(.dbgate-screen) .selection-marker {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
:global(.dbgate-screen) .wrapper:not(.canSelectColumns) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-bottom: 1px solid var(--theme-border);
|
border-bottom: 1px solid var(--theme-border);
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
:global(.dbgate-screen) .header {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.header.isTable {
|
.header.isTable {
|
||||||
background: var(--theme-bg-blue);
|
background: var(--theme-bg-blue);
|
||||||
}
|
}
|
||||||
@@ -169,9 +317,11 @@
|
|||||||
background: var(--theme-bg-3);
|
background: var(--theme-bg-3);
|
||||||
}
|
}
|
||||||
.columns {
|
.columns {
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
width: calc(100% - 10px);
|
width: calc(100% - 10px);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
.columns.scroll {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
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">
|
<script lang="ts">
|
||||||
import Designer from './Designer.svelte';
|
import Designer from './Designer.svelte';
|
||||||
|
import QueryDesignerReference from './QueryDesignerReference.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Designer {...$$props} />
|
<Designer
|
||||||
|
{...$$props}
|
||||||
|
settings={{
|
||||||
|
showTableCloseButton: true,
|
||||||
|
allowColumnOperations: true,
|
||||||
|
allowCreateRefByDrag: true,
|
||||||
|
allowTableAlias: true,
|
||||||
|
updateFromDbInfo: false,
|
||||||
|
useDatabaseReferences: false,
|
||||||
|
allowScrollColumns: true,
|
||||||
|
allowAddAllReferences: false,
|
||||||
|
canArrange: false,
|
||||||
|
canExport: false,
|
||||||
|
canSelectColumns: true,
|
||||||
|
canSelectTables: false,
|
||||||
|
allowChangeColor: false,
|
||||||
|
appendTableSystemMenu: false,
|
||||||
|
customizeStyle: false,
|
||||||
|
}}
|
||||||
|
referenceComponent={QueryDesignerReference}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
export let onChangeReference;
|
export let onChangeReference;
|
||||||
export let designer;
|
export let designer;
|
||||||
export let domTables;
|
export let domTables;
|
||||||
|
export let settings;
|
||||||
|
|
||||||
let src = null;
|
let src = null;
|
||||||
let dst = null;
|
let dst = null;
|
||||||
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 settings': 'mdi mdi-cog',
|
||||||
'icon version': 'mdi mdi-ticket-confirmation',
|
'icon version': 'mdi mdi-ticket-confirmation',
|
||||||
'icon pin': 'mdi mdi-pin',
|
'icon pin': 'mdi mdi-pin',
|
||||||
|
'icon arrange': 'mdi mdi-arrange-send-to-back',
|
||||||
|
|
||||||
'icon columns': 'mdi mdi-view-column',
|
'icon columns': 'mdi mdi-view-column',
|
||||||
'icon columns-outline': 'mdi mdi-view-column-outline',
|
'icon columns-outline': 'mdi mdi-view-column-outline',
|
||||||
@@ -122,6 +123,7 @@
|
|||||||
'img preview': 'mdi mdi-file-find color-icon-red',
|
'img preview': 'mdi mdi-file-find color-icon-red',
|
||||||
'img favorite': 'mdi mdi-star color-icon-yellow',
|
'img favorite': 'mdi mdi-star color-icon-yellow',
|
||||||
'img query-design': 'mdi mdi-vector-polyline-edit color-icon-red',
|
'img query-design': 'mdi mdi-vector-polyline-edit color-icon-red',
|
||||||
|
'img diagram': 'mdi mdi-graph color-icon-blue',
|
||||||
'img yaml': 'mdi mdi-code-brackets color-icon-red',
|
'img yaml': 'mdi mdi-code-brackets color-icon-red',
|
||||||
'img compare': 'mdi mdi-compare color-icon-red',
|
'img compare': 'mdi mdi-compare color-icon-red',
|
||||||
|
|
||||||
|
|||||||
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 CompareModelTab from './CompareModelTab.svelte';
|
||||||
import * as JsonTab from './JsonTab.svelte';
|
import * as JsonTab from './JsonTab.svelte';
|
||||||
import * as ChangelogTab from './ChangelogTab.svelte';
|
import * as ChangelogTab from './ChangelogTab.svelte';
|
||||||
|
import * as DiagramTab from './DiagramTab.svelte';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
TableDataTab,
|
TableDataTab,
|
||||||
@@ -40,4 +41,5 @@ export default {
|
|||||||
CompareModelTab,
|
CompareModelTab,
|
||||||
JsonTab,
|
JsonTab,
|
||||||
ChangelogTab,
|
ChangelogTab,
|
||||||
|
DiagramTab,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function registerMenu(...items) {
|
|||||||
setContext('componentContextMenu', [parentMenu, ...items]);
|
setContext('componentContextMenu', [parentMenu, ...items]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function contextMenu(node, items = []) {
|
export default function contextMenu(node, items: any = []) {
|
||||||
const handleContextMenu = async e => {
|
const handleContextMenu = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -23,6 +23,8 @@ export default function contextMenu(node, items = []) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (items == '__no_menu') return;
|
||||||
|
|
||||||
node.addEventListener('contextmenu', handleContextMenu);
|
node.addEventListener('contextmenu', handleContextMenu);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,32 +1,49 @@
|
|||||||
export default function moveDrag(node, [onStart, onMove, onEnd]) {
|
export default function moveDrag(node, dragEvents) {
|
||||||
|
if (!dragEvents) return;
|
||||||
|
|
||||||
|
const [onStart, onMove, onEnd] = dragEvents;
|
||||||
let startX = null;
|
let startX = null;
|
||||||
let startY = null;
|
let startY = null;
|
||||||
|
|
||||||
|
let clientX = null;
|
||||||
|
let clientY = null;
|
||||||
|
|
||||||
const handleMoveDown = e => {
|
const handleMoveDown = e => {
|
||||||
if (e.button != 0) return;
|
if (e.button != 0) return;
|
||||||
|
|
||||||
|
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||||
|
|
||||||
|
const clientRect = node.getBoundingClientRect();
|
||||||
|
clientX = clientRect.left * zoomKoef;
|
||||||
|
clientY = clientRect.top * zoomKoef;
|
||||||
|
|
||||||
startX = e.clientX;
|
startX = e.clientX;
|
||||||
startY = e.clientY;
|
startY = e.clientY;
|
||||||
document.addEventListener('mousemove', handleMoveMove, true);
|
document.addEventListener('mousemove', handleMoveMove, true);
|
||||||
document.addEventListener('mouseup', handleMoveEnd, true);
|
document.addEventListener('mouseup', handleMoveEnd, true);
|
||||||
onStart();
|
onStart(e.clientX - clientX, e.clientY - clientY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveMove = e => {
|
const handleMoveMove = e => {
|
||||||
|
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const diffX = e.clientX - startX;
|
const diffX = e.clientX - startX;
|
||||||
startX = e.clientX;
|
startX = e.clientX;
|
||||||
const diffY = e.clientY - startY;
|
const diffY = e.clientY - startY;
|
||||||
startY = e.clientY;
|
startY = e.clientY;
|
||||||
|
|
||||||
onMove(diffX, diffY);
|
onMove(diffX, diffY, e.clientX - clientX, e.clientY - clientY);
|
||||||
};
|
};
|
||||||
const handleMoveEnd = e => {
|
const handleMoveEnd = e => {
|
||||||
|
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
startX = null;
|
startX = null;
|
||||||
startY = null;
|
startY = null;
|
||||||
document.removeEventListener('mousemove', handleMoveMove, true);
|
document.removeEventListener('mousemove', handleMoveMove, true);
|
||||||
document.removeEventListener('mouseup', handleMoveEnd, true);
|
document.removeEventListener('mouseup', handleMoveEnd, true);
|
||||||
onEnd();
|
onEnd(e.clientX - clientX, e.clientY - clientY);
|
||||||
};
|
};
|
||||||
|
|
||||||
node.addEventListener('mousedown', handleMoveDown);
|
node.addEventListener('mousedown', handleMoveDown);
|
||||||
|
|||||||
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 _ from 'lodash';
|
||||||
import { getContext, onDestroy } from 'svelte';
|
import { getContext, onDestroy } from 'svelte';
|
||||||
import { updateStatuBarInfoItem } from '../widgets/StatusBar.svelte';
|
import { updateStatuBarInfoItem } from './statusBarStore';
|
||||||
|
|
||||||
function formatSeconds(duration) {
|
function formatSeconds(duration) {
|
||||||
if (duration == null) return '';
|
if (duration == null) return '';
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import AppObjectList from '../appobj/AppObjectList.svelte';
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte';
|
import * as savedFileAppObject from '../appobj/SavedFileAppObject.svelte';
|
||||||
import { useFiles } from '../utility/metadataLoaders';
|
import { useFiles } from '../utility/metadataLoaders';
|
||||||
|
|
||||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
|
||||||
const sqlFiles = useFiles({ folder: 'sql' });
|
const sqlFiles = useFiles({ folder: 'sql' });
|
||||||
@@ -13,6 +11,7 @@ import { useFiles } from '../utility/metadataLoaders';
|
|||||||
const chartFiles = useFiles({ folder: 'charts' });
|
const chartFiles = useFiles({ folder: 'charts' });
|
||||||
const queryFiles = useFiles({ folder: 'query' });
|
const queryFiles = useFiles({ folder: 'query' });
|
||||||
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
||||||
|
const diagramFiles = useFiles({ folder: 'diagrams' });
|
||||||
|
|
||||||
$: files = [
|
$: files = [
|
||||||
...($sqlFiles || []),
|
...($sqlFiles || []),
|
||||||
@@ -21,8 +20,8 @@ import { useFiles } from '../utility/metadataLoaders';
|
|||||||
...($chartFiles || []),
|
...($chartFiles || []),
|
||||||
...($queryFiles || []),
|
...($queryFiles || []),
|
||||||
...($sqliteFiles || []),
|
...($sqliteFiles || []),
|
||||||
|
...($diagramFiles || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WidgetsInnerContainer>
|
<WidgetsInnerContainer>
|
||||||
|
|||||||
@@ -1,35 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
const statusBarTabInfo = writable({});
|
|
||||||
|
|
||||||
// export function updateStatuBarInfo(tabid, info) {
|
|
||||||
// statusBarTabInfo.update(x => ({
|
|
||||||
// ...x,
|
|
||||||
// [tabid]: info,
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
|
|
||||||
export function updateStatuBarInfoItem(tabid, key, item) {
|
|
||||||
statusBarTabInfo.update(tabs => {
|
|
||||||
const items = tabs[tabid] || [];
|
|
||||||
let newItems;
|
|
||||||
if (item == null) {
|
|
||||||
newItems = items.filter(x => x.key != key);
|
|
||||||
} else if (items.find(x => x.key == key)) {
|
|
||||||
newItems = items.map(x => (x.key == key ? { ...item, key } : x));
|
|
||||||
} else {
|
|
||||||
newItems = [...items, { ...item, key }];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...tabs,
|
|
||||||
[tabid]: newItems,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
|
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
|
||||||
@@ -42,6 +12,7 @@
|
|||||||
import { findCommand } from '../commands/runCommand';
|
import { findCommand } from '../commands/runCommand';
|
||||||
import { useConnectionColor } from '../utility/useConnectionColor';
|
import { useConnectionColor } from '../utility/useConnectionColor';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
import { statusBarTabInfo } from '../utility/statusBarStore';
|
||||||
|
|
||||||
$: databaseName = $currentDatabase && $currentDatabase.name;
|
$: databaseName = $currentDatabase && $currentDatabase.name;
|
||||||
$: connection = $currentDatabase && $currentDatabase.connection;
|
$: connection = $currentDatabase && $currentDatabase.connection;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext, onDestroy, onMount } from 'svelte';
|
import { getContext, onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
import uuidv1 from 'uuid/v1';
|
import uuidv1 from 'uuid/v1';
|
||||||
import { updateStatuBarInfoItem } from './StatusBar.svelte';
|
import { updateStatuBarInfoItem } from '../utility/statusBarStore';
|
||||||
|
|
||||||
export let text;
|
export let text;
|
||||||
export let clickable = false;
|
export let clickable = false;
|
||||||
|
|||||||
@@ -5177,6 +5177,11 @@ interpret@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||||
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
|
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
|
||||||
|
|
||||||
|
interval-operations@^1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/interval-operations/-/interval-operations-1.0.7.tgz#c935dfed6bb040064d488f542b9a2c0004d7a333"
|
||||||
|
integrity sha512-VBxXaK+DxTt7Hwr8Rhg03bgdyv5OZ0OfH3hotg9fT2jqufF3VOtvkX33n3t6jNjS8tx1jTKBt7fs++SqNd1OQg==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
|
|||||||
Reference in New Issue
Block a user