Enhancements to the ERD when selecting a relationship. #4088

This commit is contained in:
Aditya Toshniwal
2022-12-05 10:47:05 +05:30
committed by GitHub
parent 0e6d8ed030
commit 4ab06b4a2a
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 _ from 'lodash';
import {TableNodeFactory, TableNodeModel } from './nodes/TableNode'; 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 { OneToManyPortFactory } from './ports/OneToManyPort';
import ERDModel from './ERDModel'; import ERDModel from './ERDModel';
import ForeignKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui'; 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) { if(!this.node_position_updating) {
this.node_position_updating = true; this.node_position_updating = true;
this.fireEvent({}, 'nodesUpdated', true); this.fireEvent({}, 'nodesUpdated', true);
this.optimizePortsPosition(node);
setTimeout(()=>{ setTimeout(()=>{
this.node_position_updating = false; this.node_position_updating = false;
}, 500); }, 500);
@@ -193,15 +194,44 @@ export default class ERDCore {
}); });
} }
getNewPort(type, initData, initOptions) { getNewPort(portName, alignment) {
return this.getEngine().getPortFactories().getFactory(type).generateModel({ return this.getEngine().getPortFactories().getFactory('onetomany').generateModel({
initialConfig: { initialConfig: {
data:initData, data: null,
options:initOptions, 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={}) { addNode(data, position=[50, 50], metadata={}) {
let newNode = this.getNewNode(data); let newNode = this.getNewNode(data);
this.clearSelection(); this.clearSelection();
@@ -231,24 +261,45 @@ export default class ERDCore {
}).length > 0; }).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) { addLink(data, type) {
let tableNodesDict = this.getModel().getNodesDict(); let tableNodesDict = this.getModel().getNodesDict();
let sourceNode = tableNodesDict[data.referenced_table_uid]; let sourceNode = tableNodesDict[data.referenced_table_uid];
let targetNode = tableNodesDict[data.local_table_uid]; let targetNode = tableNodesDict[data.local_table_uid];
let portName = sourceNode.getPortName(data.referenced_column_attnum); const [sourcePort, targetPort] = this.getOptimumPorts(
let sourcePort = sourceNode.getPort(portName); sourceNode, data.referenced_column_attnum, targetNode, data.local_column_attnum);
/* 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}));
}
/* Link the ports */ /* Link the ports */
let newLink = this.getNewLink(type, data); let newLink = this.getNewLink(type, data);
@@ -297,30 +348,30 @@ export default class ERDCore {
} }
let tableData = tableNode.getData(); let tableData = tableNode.getData();
/* Sync the name changes in references FK */ /* Sync the name changes in references FK */
Object.values(tableNode.getPorts()).forEach((port)=>{ Object.values(tableNode.getLinks()).forEach((link)=>{
if(port.getSubtype() != 'one') { if(link.getSourcePort().getNode() != tableNode) {
/* SourcePort is the referred table */
/* If the link doesn't refer this table, skip it */
return; return;
} }
Object.values(port.getLinks()).forEach((link)=>{ let linkData = link.getData();
let linkData = link.getData(); let fkTableNode = this.getModel().getNodesDict()[linkData.local_table_uid];
let fkTableNode = this.getModel().getNodesDict()[linkData.local_table_uid];
let newForeingKeys = []; let newForeingKeys = [];
/* Update the FK table with new references */ /* Update the FK table with new references */
fkTableNode.getData().foreign_key?.forEach((theFkRow)=>{ fkTableNode.getData().foreign_key?.forEach((theFkRow)=>{
for(let fkColumn of theFkRow.columns) { for(let fkColumn of theFkRow.columns) {
if(fkColumn.references == tableNode.getID()) { if(fkColumn.references == tableNode.getID()) {
let attnum = _.find(oldTableData.columns, (c)=>c.name==fkColumn.referenced).attnum; let attnum = _.find(oldTableData.columns, (c)=>c.name==fkColumn.referenced).attnum;
fkColumn.referenced = _.find(tableData.columns, (colm)=>colm.attnum==attnum).name; fkColumn.referenced = _.find(tableData.columns, (colm)=>colm.attnum==attnum).name;
fkColumn.references_table_name = tableData.name; fkColumn.references_table_name = tableData.name;
}
} }
newForeingKeys.push(theFkRow); }
}); newForeingKeys.push(theFkRow);
fkTableNode.setData({ });
...fkTableNode.getData(), fkTableNode.setData({
foreign_key: newForeingKeys, ...fkTableNode.getData(),
}); foreign_key: newForeingKeys,
}); });
}); });
} }
@@ -352,12 +403,20 @@ export default class ERDCore {
const removeLink = (theFk)=>{ const removeLink = (theFk)=>{
if(!theFk) return; if(!theFk) return;
let attnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
let existPort = tableNode.getPort(tableNode.getPortName(attnum)); let tableNodesDict = this.getModel().getNodesDict();
if(existPort && existPort.getSubtype() == 'many') { let sourceNode = tableNodesDict[theFk.references];
existPort.removeAllLinks();
tableNode.removePort(existPort); 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( const changeDiff = diffArray(

View File

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

View File

@@ -8,7 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; 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 { AbstractReactFactory } from '@projectstorm/react-canvas-core';
import _ from 'lodash'; import _ from 'lodash';
import SchemaIcon from 'top/browser/server_groups/servers/databases/schemas/static/img/schema.svg'; 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 TYPE = 'table';
const TABLE_WIDTH = 175;
export class TableNodeModel extends DefaultNodeModel { export class TableNodeModel extends DefaultNodeModel {
constructor({otherInfo, ...options}) { constructor({otherInfo, ...options}) {
@@ -36,6 +37,7 @@ export class TableNodeModel extends DefaultNodeModel {
...options, ...options,
type: TYPE, type: TYPE,
}); });
this.width = TABLE_WIDTH;
this._note = otherInfo.note || ''; this._note = otherInfo.note || '';
this._metadata = { this._metadata = {
@@ -72,8 +74,15 @@ export class TableNodeModel extends DefaultNodeModel {
} }
} }
getPortName(attnum) { getPortName(attnum, alignment) {
return `coll-port-${attnum}`; if(alignment) {
return `coll-port-${attnum}-${alignment}`;
}
return `coll-port-${attnum}-right`;
}
getPortAttnum(portName) {
return portName.split('-')[2];
} }
setNote(note) { 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) { addColumn(col) {
this._data.columns.push(col); this._data.columns.push(col);
} }
@@ -166,7 +187,7 @@ const styles = (theme)=>({
...theme.mixins.panelBorder.all, ...theme.mixins.panelBorder.all,
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
position: 'relative', position: 'relative',
width: '175px', width: `${TABLE_WIDTH}px`,
fontSize: '0.8em', fontSize: '0.8em',
'& div:last-child': { '& div:last-child': {
borderBottomLeftRadius: 'inherit', borderBottomLeftRadius: 'inherit',
@@ -227,7 +248,9 @@ class TableNodeWidgetRaw extends React.Component {
} }
generateColumn(col, localFkCols, localUkCols) { 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; let icon = ColumnIcon;
/* Less priority */ /* Less priority */
if(localUkCols.indexOf(col.name) > -1) { if(localUkCols.indexOf(col.name) > -1) {
@@ -247,6 +270,9 @@ class TableNodeWidgetRaw extends React.Component {
const {classes} = this.props; const {classes} = this.props;
return ( return (
<div className={classes.tableSection} key={col.attnum} data-test="column-row"> <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'}}> <Box display="flex" width="100%" style={{wordBreak: 'break-all'}}>
<RowIcon icon={icon} /> <RowIcon icon={icon} />
<Box margin="auto 0"> <Box margin="auto 0">
@@ -256,13 +282,13 @@ class TableNodeWidgetRaw extends React.Component {
</Box> </Box>
</Box> </Box>
<Box marginLeft="auto" padding="0" minHeight="0" display="flex" alignItems="center"> <Box marginLeft="auto" padding="0" minHeight="0" display="flex" alignItems="center">
{this.generatePort(port)} {this.generatePort(rightPort)}
</Box> </Box>
</div> </div>
); );
} }
generatePort = port => { generatePort = (port) => {
if(port) { if(port) {
return ( return (
<PortWidget engine={this.props.engine} port={port} key={port.getID()} className={'port-' + port.options.alignment} /> <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 {OneToManyLinkModel} from '../links/OneToManyLink';
import { AbstractModelFactory } from '@projectstorm/react-canvas-core'; import { AbstractModelFactory } from '@projectstorm/react-canvas-core';
@@ -16,7 +16,6 @@ const TYPE = 'onetomany';
export default class OneToManyPortModel extends PortModel { export default class OneToManyPortModel extends PortModel {
constructor({options}) { constructor({options}) {
super({ super({
subtype: 'notset',
...options, ...options,
type: TYPE, type: TYPE,
}); });
@@ -32,21 +31,24 @@ export default class OneToManyPortModel extends PortModel {
return new OneToManyLinkModel({}); return new OneToManyLinkModel({});
} }
getSubtype() {
return this.options.subtype;
}
deserialize(event) { 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); super.deserialize(event);
this.options.subtype = event.data.subtype || 'notset';
} }
serialize() { serialize() {
return { return {
...super.serialize(), ...super.serialize(),
subtype: this.options.subtype,
}; };
} }
getAlignment() {
return this.options.alignment;
}
} }
export class OneToManyPortFactory extends AbstractModelFactory { 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 * as createEngineLib from '@projectstorm/react-diagrams';
import TEST_TABLES_DATA from './test_tables'; import TEST_TABLES_DATA from './test_tables';
import { FakeLink, FakeNode } from './fake_item'; import { FakeLink, FakeNode } from './fake_item';
import { PortModelAlignment } from '@projectstorm/react-diagrams';
describe('ERDCore', ()=>{ describe('ERDCore', ()=>{
let eleFactory = jasmine.createSpyObj('nodeFactories', { let eleFactory = jasmine.createSpyObj('nodeFactories', {
@@ -120,15 +121,16 @@ describe('ERDCore', ()=>{
}); });
it('getNewPort', ()=>{ it('getNewPort', ()=>{
let data = {name: 'link1'}; erdEngine.getPortFactories().getFactory().generateModel.calls.reset();
let options = {opt1: 'val1'}; erdCoreObj.getNewPort('port1', PortModelAlignment.LEFT);
erdCoreObj.getNewPort('porttype', data, options); expect(erdEngine.getPortFactories().getFactory).toHaveBeenCalledWith('onetomany');
expect(erdEngine.getPortFactories().getFactory).toHaveBeenCalledWith('porttype');
expect(erdEngine.getPortFactories().getFactory().generateModel).toHaveBeenCalledWith({ expect(erdEngine.getPortFactories().getFactory().generateModel).toHaveBeenCalledWith({
initialConfig: { initialConfig: {
data:data, data: null,
options:options, options: {
name: 'port1',
alignment: PortModelAlignment.LEFT
},
}, },
}); });
}); });
@@ -156,8 +158,7 @@ describe('ERDCore', ()=>{
it('addLink', ()=>{ it('addLink', ()=>{
let node1 = new FakeNode({'name': 'table1'}, 'id1'); let node1 = new FakeNode({'name': 'table1'}, 'id1');
let node2 = new FakeNode({'name': 'table2'}, 'id2'); let node2 = new FakeNode({'name': 'table2'}, 'id2');
spyOn(node1, 'addPort').and.callThrough(); spyOn(erdCoreObj, 'getOptimumPorts').and.returnValue([{name: 'port-1'}, {name: 'port-3'}]);
spyOn(node2, 'addPort').and.callThrough();
let nodesDict = { let nodesDict = {
'id1': node1, 'id1': node1,
'id2': node2, 'id2': node2,
@@ -169,11 +170,6 @@ describe('ERDCore', ()=>{
spyOn(erdCoreObj, 'getNewLink').and.callFake(function() { spyOn(erdCoreObj, 'getNewLink').and.callFake(function() {
return link; return link;
}); });
spyOn(erdCoreObj, 'getNewPort').and.callFake(function(type, initData, options) {
return {
name: options.name,
};
});
erdCoreObj.addLink({ erdCoreObj.addLink({
'referenced_column_attnum': 1, 'referenced_column_attnum': 1,
@@ -182,8 +178,6 @@ describe('ERDCore', ()=>{
'local_table_uid': 'id2', 'local_table_uid': 'id2',
}, 'onetomany'); }, '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.setSourcePort).toHaveBeenCalledWith({name: 'port-1'});
expect(link.setTargetPort).toHaveBeenCalledWith({name: 'port-3'}); expect(link.setTargetPort).toHaveBeenCalledWith({name: 'port-3'});
}); });

View File

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