Enhancements to the ERD when selecting a relationship. #4088

pull/5608/head
Aditya Toshniwal 2022-12-05 10:47:05 +05:30 committed by GitHub
parent 0e6d8ed030
commit 4ab06b4a2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 86 deletions

View File

@ -15,7 +15,7 @@ import { ZoomCanvasAction } from '@projectstorm/react-canvas-core';
import _ from 'lodash';
import {TableNodeFactory, TableNodeModel } from './nodes/TableNode';
import {OneToManyLinkFactory, OneToManyLinkModel } from './links/OneToManyLink';
import {OneToManyLinkFactory, OneToManyLinkModel, POINTER_SIZE } from './links/OneToManyLink';
import { OneToManyPortFactory } from './ports/OneToManyPort';
import ERDModel from './ERDModel';
import ForeignKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui';
@ -81,6 +81,7 @@ export default class ERDCore {
if(!this.node_position_updating) {
this.node_position_updating = true;
this.fireEvent({}, 'nodesUpdated', true);
this.optimizePortsPosition(node);
setTimeout(()=>{
this.node_position_updating = false;
}, 500);
@ -193,15 +194,44 @@ export default class ERDCore {
});
}
getNewPort(type, initData, initOptions) {
return this.getEngine().getPortFactories().getFactory(type).generateModel({
getNewPort(portName, alignment) {
return this.getEngine().getPortFactories().getFactory('onetomany').generateModel({
initialConfig: {
data:initData,
options:initOptions,
data: null,
options: {
name: portName,
alignment: alignment
},
},
});
}
getLeftRightPorts(node, attnum) {
const leftPort = node.getPort(node.getPortName(attnum, PortModelAlignment.LEFT))
?? node.addPort(this.getNewPort(node.getPortName(attnum, PortModelAlignment.LEFT), PortModelAlignment.LEFT));
const rightPort = node.getPort(node.getPortName(attnum, PortModelAlignment.RIGHT))
?? node.addPort(this.getNewPort(node.getPortName(attnum, PortModelAlignment.RIGHT), PortModelAlignment.RIGHT));
return [leftPort, rightPort];
}
optimizePortsPosition(node) {
Object.values(node.getLinks()).forEach((link)=>{
const sourcePort = link.getSourcePort();
const targetPort = link.getTargetPort();
const [newSourcePort, newTargetPort] = this.getOptimumPorts(
sourcePort.getNode(),
sourcePort.getNode().getPortAttnum(sourcePort.getName()),
targetPort.getNode(),
targetPort.getNode().getPortAttnum(targetPort.getName())
);
sourcePort != newSourcePort && link.setSourcePort(newSourcePort);
targetPort != newTargetPort && link.setTargetPort(newTargetPort);
});
}
addNode(data, position=[50, 50], metadata={}) {
let newNode = this.getNewNode(data);
this.clearSelection();
@ -231,24 +261,45 @@ export default class ERDCore {
}).length > 0;
}
getOptimumPorts(sourceNode, sourceAttnum, targetNode, targetAttnum) {
const [sourceLeftPort, sourceRightPort] = this.getLeftRightPorts(sourceNode, sourceAttnum);
const [targetLeftPort, targetRightPort] = this.getLeftRightPorts(targetNode, targetAttnum);
/* Lets use right as default */
let sourcePort = sourceRightPort;
let targetPort = targetRightPort;
const sourceNodePos = sourceNode.getBoundingBox();
const targetNodePos = targetNode.getBoundingBox();
const sourceLeftX = sourceNodePos.getBottomLeft().x;
const sourceRightX = sourceNodePos.getBottomRight().x;
const targetLeftX = targetNodePos.getBottomLeft().x;
const targetRightX = targetNodePos.getBottomRight().x;
const OFFSET = POINTER_SIZE*2+10;
if(targetLeftX - sourceRightX >= OFFSET) {
sourcePort = sourceRightPort;
targetPort = targetLeftPort;
} else if(sourceLeftX - targetRightX >= OFFSET) {
sourcePort = sourceLeftPort;
targetPort = targetRightPort;
} else if(targetLeftX - sourceRightX < OFFSET || sourceLeftX - targetRightX < OFFSET) {
if(sourcePort.getAlignment() == PortModelAlignment.RIGHT) {
targetPort = targetRightPort;
} else {
targetPort = targetLeftPort;
}
}
return [sourcePort, targetPort];
}
addLink(data, type) {
let tableNodesDict = this.getModel().getNodesDict();
let sourceNode = tableNodesDict[data.referenced_table_uid];
let targetNode = tableNodesDict[data.local_table_uid];
let portName = sourceNode.getPortName(data.referenced_column_attnum);
let sourcePort = sourceNode.getPort(portName);
/* Create the port if not there */
if(!sourcePort) {
sourcePort = sourceNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'one', alignment:PortModelAlignment.RIGHT}));
}
portName = targetNode.getPortName(data.local_column_attnum);
let targetPort = targetNode.getPort(portName);
/* Create the port if not there */
if(!targetPort) {
targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'many', alignment:PortModelAlignment.RIGHT}));
}
const [sourcePort, targetPort] = this.getOptimumPorts(
sourceNode, data.referenced_column_attnum, targetNode, data.local_column_attnum);
/* Link the ports */
let newLink = this.getNewLink(type, data);
@ -297,30 +348,30 @@ export default class ERDCore {
}
let tableData = tableNode.getData();
/* Sync the name changes in references FK */
Object.values(tableNode.getPorts()).forEach((port)=>{
if(port.getSubtype() != 'one') {
Object.values(tableNode.getLinks()).forEach((link)=>{
if(link.getSourcePort().getNode() != tableNode) {
/* SourcePort is the referred table */
/* If the link doesn't refer this table, skip it */
return;
}
Object.values(port.getLinks()).forEach((link)=>{
let linkData = link.getData();
let fkTableNode = this.getModel().getNodesDict()[linkData.local_table_uid];
let linkData = link.getData();
let fkTableNode = this.getModel().getNodesDict()[linkData.local_table_uid];
let newForeingKeys = [];
/* Update the FK table with new references */
fkTableNode.getData().foreign_key?.forEach((theFkRow)=>{
for(let fkColumn of theFkRow.columns) {
if(fkColumn.references == tableNode.getID()) {
let attnum = _.find(oldTableData.columns, (c)=>c.name==fkColumn.referenced).attnum;
fkColumn.referenced = _.find(tableData.columns, (colm)=>colm.attnum==attnum).name;
fkColumn.references_table_name = tableData.name;
}
let newForeingKeys = [];
/* Update the FK table with new references */
fkTableNode.getData().foreign_key?.forEach((theFkRow)=>{
for(let fkColumn of theFkRow.columns) {
if(fkColumn.references == tableNode.getID()) {
let attnum = _.find(oldTableData.columns, (c)=>c.name==fkColumn.referenced).attnum;
fkColumn.referenced = _.find(tableData.columns, (colm)=>colm.attnum==attnum).name;
fkColumn.references_table_name = tableData.name;
}
newForeingKeys.push(theFkRow);
});
fkTableNode.setData({
...fkTableNode.getData(),
foreign_key: newForeingKeys,
});
}
newForeingKeys.push(theFkRow);
});
fkTableNode.setData({
...fkTableNode.getData(),
foreign_key: newForeingKeys,
});
});
}
@ -352,12 +403,20 @@ export default class ERDCore {
const removeLink = (theFk)=>{
if(!theFk) return;
let attnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
let existPort = tableNode.getPort(tableNode.getPortName(attnum));
if(existPort && existPort.getSubtype() == 'many') {
existPort.removeAllLinks();
tableNode.removePort(existPort);
}
let tableNodesDict = this.getModel().getNodesDict();
let sourceNode = tableNodesDict[theFk.references];
let localAttnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
let refAttnum = _.find(sourceNode.getColumns(), (col)=>col.name==theFk.referenced).attnum;
const fkLink = Object.values(tableNode.getLinks()).find((link)=>{
const ldata = link.getData();
return ldata.local_column_attnum == localAttnum
&& ldata.local_table_uid == tableNode.getID()
&& ldata.referenced_column_attnum == refAttnum
&& ldata.referenced_table_uid == theFk.references;
});
fkLink?.remove();
};
const changeDiff = diffArray(

View File

@ -22,6 +22,8 @@ import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
export const POINTER_SIZE = 30;
export const OneToManyModel = {
local_table_uid: undefined,
local_column_attnum: undefined,
@ -141,32 +143,31 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
super(props);
}
endPointTranslation(alignment, offset) {
endPointTranslation(alignment) {
let degree = 0;
let tx = 0, ty = 0;
switch(alignment) {
case PortModelAlignment.BOTTOM:
ty = -offset;
ty = -POINTER_SIZE;
break;
case PortModelAlignment.LEFT:
degree = 90;
tx = offset;
tx = POINTER_SIZE;
break;
case PortModelAlignment.TOP:
degree = 180;
ty = offset;
ty = POINTER_SIZE;
break;
case PortModelAlignment.RIGHT:
degree = -90;
tx = -offset;
tx = -POINTER_SIZE;
break;
}
return [degree, tx, ty];
}
addCustomWidgetPoint(type, endpoint, point) {
let offset = 30;
const [rotation, tx, ty] = this.endPointTranslation(endpoint.options.alignment, offset);
const [rotation, tx, ty] = this.endPointTranslation(endpoint.options.alignment);
if(!point) {
point = this.props.link.point(
endpoint.getX()-tx, endpoint.getY()-ty, {'one': 1, 'many': 2}[type]

View File

@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import React from 'react';
import { DefaultNodeModel, DiagramEngine, PortWidget } from '@projectstorm/react-diagrams';
import { DefaultNodeModel, DiagramEngine, PortModelAlignment, PortWidget } from '@projectstorm/react-diagrams';
import { AbstractReactFactory } from '@projectstorm/react-canvas-core';
import _ from 'lodash';
import SchemaIcon from 'top/browser/server_groups/servers/databases/schemas/static/img/schema.svg';
@ -29,6 +29,7 @@ import { Box } from '@material-ui/core';
const TYPE = 'table';
const TABLE_WIDTH = 175;
export class TableNodeModel extends DefaultNodeModel {
constructor({otherInfo, ...options}) {
@ -36,6 +37,7 @@ export class TableNodeModel extends DefaultNodeModel {
...options,
type: TYPE,
});
this.width = TABLE_WIDTH;
this._note = otherInfo.note || '';
this._metadata = {
@ -72,8 +74,15 @@ export class TableNodeModel extends DefaultNodeModel {
}
}
getPortName(attnum) {
return `coll-port-${attnum}`;
getPortName(attnum, alignment) {
if(alignment) {
return `coll-port-${attnum}-${alignment}`;
}
return `coll-port-${attnum}-right`;
}
getPortAttnum(portName) {
return portName.split('-')[2];
}
setNote(note) {
@ -95,6 +104,18 @@ export class TableNodeModel extends DefaultNodeModel {
};
}
getLinks() {
let links = {};
this.getPorts();
Object.values(this.getPorts()).forEach((port)=>{
links = {
...links,
...port.getLinks(),
};
});
return links;
}
addColumn(col) {
this._data.columns.push(col);
}
@ -166,7 +187,7 @@ const styles = (theme)=>({
...theme.mixins.panelBorder.all,
borderRadius: theme.shape.borderRadius,
position: 'relative',
width: '175px',
width: `${TABLE_WIDTH}px`,
fontSize: '0.8em',
'& div:last-child': {
borderBottomLeftRadius: 'inherit',
@ -227,7 +248,9 @@ class TableNodeWidgetRaw extends React.Component {
}
generateColumn(col, localFkCols, localUkCols) {
let port = this.props.node.getPort(this.props.node.getPortName(col.attnum));
let leftPort = this.props.node.getPort(this.props.node.getPortName(col.attnum, PortModelAlignment.LEFT));
let rightPort = this.props.node.getPort(this.props.node.getPortName(col.attnum, PortModelAlignment.RIGHT));
let icon = ColumnIcon;
/* Less priority */
if(localUkCols.indexOf(col.name) > -1) {
@ -247,6 +270,9 @@ class TableNodeWidgetRaw extends React.Component {
const {classes} = this.props;
return (
<div className={classes.tableSection} key={col.attnum} data-test="column-row">
<Box marginRight="auto" padding="0" minHeight="0" display="flex" alignItems="center">
{this.generatePort(leftPort)}
</Box>
<Box display="flex" width="100%" style={{wordBreak: 'break-all'}}>
<RowIcon icon={icon} />
<Box margin="auto 0">
@ -256,13 +282,13 @@ class TableNodeWidgetRaw extends React.Component {
</Box>
</Box>
<Box marginLeft="auto" padding="0" minHeight="0" display="flex" alignItems="center">
{this.generatePort(port)}
{this.generatePort(rightPort)}
</Box>
</div>
);
}
generatePort = port => {
generatePort = (port) => {
if(port) {
return (
<PortWidget engine={this.props.engine} port={port} key={port.getID()} className={'port-' + port.options.alignment} />

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import { PortModel } from '@projectstorm/react-diagrams-core';
import { PortModel, PortModelAlignment } from '@projectstorm/react-diagrams-core';
import {OneToManyLinkModel} from '../links/OneToManyLink';
import { AbstractModelFactory } from '@projectstorm/react-canvas-core';
@ -16,7 +16,6 @@ const TYPE = 'onetomany';
export default class OneToManyPortModel extends PortModel {
constructor({options}) {
super({
subtype: 'notset',
...options,
type: TYPE,
});
@ -32,21 +31,24 @@ export default class OneToManyPortModel extends PortModel {
return new OneToManyLinkModel({});
}
getSubtype() {
return this.options.subtype;
}
deserialize(event) {
/* Make it backward compatible */
const alignment = event.data?.name?.split('-').slice(-1)[0];
if(event.data?.name && ![PortModelAlignment.LEFT, PortModelAlignment.RIGHT].includes(alignment)) {
event.data.name += '-' + PortModelAlignment.RIGHT;
}
super.deserialize(event);
this.options.subtype = event.data.subtype || 'notset';
}
serialize() {
return {
...super.serialize(),
subtype: this.options.subtype,
};
}
getAlignment() {
return this.options.alignment;
}
}
export class OneToManyPortFactory extends AbstractModelFactory {

View File

@ -10,6 +10,7 @@ import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore';
import * as createEngineLib from '@projectstorm/react-diagrams';
import TEST_TABLES_DATA from './test_tables';
import { FakeLink, FakeNode } from './fake_item';
import { PortModelAlignment } from '@projectstorm/react-diagrams';
describe('ERDCore', ()=>{
let eleFactory = jasmine.createSpyObj('nodeFactories', {
@ -120,15 +121,16 @@ describe('ERDCore', ()=>{
});
it('getNewPort', ()=>{
let data = {name: 'link1'};
let options = {opt1: 'val1'};
erdCoreObj.getNewPort('porttype', data, options);
expect(erdEngine.getPortFactories().getFactory).toHaveBeenCalledWith('porttype');
erdEngine.getPortFactories().getFactory().generateModel.calls.reset();
erdCoreObj.getNewPort('port1', PortModelAlignment.LEFT);
expect(erdEngine.getPortFactories().getFactory).toHaveBeenCalledWith('onetomany');
expect(erdEngine.getPortFactories().getFactory().generateModel).toHaveBeenCalledWith({
initialConfig: {
data:data,
options:options,
data: null,
options: {
name: 'port1',
alignment: PortModelAlignment.LEFT
},
},
});
});
@ -156,8 +158,7 @@ describe('ERDCore', ()=>{
it('addLink', ()=>{
let node1 = new FakeNode({'name': 'table1'}, 'id1');
let node2 = new FakeNode({'name': 'table2'}, 'id2');
spyOn(node1, 'addPort').and.callThrough();
spyOn(node2, 'addPort').and.callThrough();
spyOn(erdCoreObj, 'getOptimumPorts').and.returnValue([{name: 'port-1'}, {name: 'port-3'}]);
let nodesDict = {
'id1': node1,
'id2': node2,
@ -169,11 +170,6 @@ describe('ERDCore', ()=>{
spyOn(erdCoreObj, 'getNewLink').and.callFake(function() {
return link;
});
spyOn(erdCoreObj, 'getNewPort').and.callFake(function(type, initData, options) {
return {
name: options.name,
};
});
erdCoreObj.addLink({
'referenced_column_attnum': 1,
@ -182,8 +178,6 @@ describe('ERDCore', ()=>{
'local_table_uid': 'id2',
}, 'onetomany');
expect(nodesDict['id1'].addPort).toHaveBeenCalledWith({name: 'port-1'});
expect(nodesDict['id2'].addPort).toHaveBeenCalledWith({name: 'port-3'});
expect(link.setSourcePort).toHaveBeenCalledWith({name: 'port-1'});
expect(link.setTargetPort).toHaveBeenCalledWith({name: 'port-3'});
});

View File

@ -37,7 +37,7 @@ describe('ERD TableNodeModel', ()=>{
});
it('getPortName', ()=>{
expect(modelObj.getPortName(2)).toBe('coll-port-2');
expect(modelObj.getPortName(2)).toBe('coll-port-2-right');
});
it('setNote', ()=>{
@ -66,7 +66,6 @@ describe('ERD TableNodeModel', ()=>{
describe('setData', ()=>{
let existPort = jasmine.createSpyObj('port', {
'removeAllLinks': jasmine.createSpy('removeAllLinks'),
'getSubtype': 'notset',
});
beforeEach(()=>{
@ -87,7 +86,6 @@ describe('ERD TableNodeModel', ()=>{
});
it('add columns', ()=>{
spyOn(existPort, 'getSubtype').and.returnValue('many');
existPort.removeAllLinks.calls.reset();
modelObj.setData({
name: 'noname',
@ -113,7 +111,6 @@ describe('ERD TableNodeModel', ()=>{
});
it('update columns', ()=>{
spyOn(existPort, 'getSubtype').and.returnValue('many');
existPort.removeAllLinks.calls.reset();
modelObj.setData({
name: 'noname',