diff --git a/docs/en_US/erd_tool.rst b/docs/en_US/erd_tool.rst index e8493fc9a..e828b9978 100644 --- a/docs/en_US/erd_tool.rst +++ b/docs/en_US/erd_tool.rst @@ -12,6 +12,7 @@ The Entity-Relationship Diagram (ERD) tool is a database design tool that provid * Save the diagram and open it later to continue working on it. * Generate ready to run SQL from the database design. * Generate the database diagram for an existing database. +* Drag and drop tables from browser tree to the diagram. .. image:: images/erd_tool.png :alt: ERD tool window diff --git a/docs/en_US/images/erd_1m_dialog.png b/docs/en_US/images/erd_1m_dialog.png index 3c341b16a..df6629a7b 100644 Binary files a/docs/en_US/images/erd_1m_dialog.png and b/docs/en_US/images/erd_1m_dialog.png differ diff --git a/docs/en_US/images/erd_mm_dialog.png b/docs/en_US/images/erd_mm_dialog.png index 3465d1f14..25621bde7 100644 Binary files a/docs/en_US/images/erd_mm_dialog.png and b/docs/en_US/images/erd_mm_dialog.png differ diff --git a/docs/en_US/images/erd_table_dialog.png b/docs/en_US/images/erd_table_dialog.png index 3b6b1f263..8eec7f07c 100644 Binary files a/docs/en_US/images/erd_table_dialog.png and b/docs/en_US/images/erd_table_dialog.png differ diff --git a/docs/en_US/release_notes_6_1.rst b/docs/en_US/release_notes_6_1.rst index 4f362d8d0..3c61c0218 100644 --- a/docs/en_US/release_notes_6_1.rst +++ b/docs/en_US/release_notes_6_1.rst @@ -11,6 +11,7 @@ New features | `Issue #4596 `_ - Added support for indent guides in the browser tree. | `Issue #6081 `_ - Added support for advanced table fields like the foreign key, primary key in the ERD tool. +| `Issue #6241 `_ - Added support to allow tables to be dragged to ERD Tool. | `Issue #6529 `_ - Added index creation when generating SQL in the ERD tool. | `Issue #6657 `_ - Added support for authentication via the webserver (REMOTE_USER). | `Issue #6794 `_ - Added support to enable/disable rules. diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 108dc4d86..f88fb6584 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -7,6 +7,8 @@ // ////////////////////////////////////////////////////////////// +import { generateNodeUrl } from './node_ajax'; + define('pgadmin.browser', [ 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', @@ -61,8 +63,16 @@ define('pgadmin.browser', [ function(b) { InitTree.initBrowserTree(b).then(() => { b.tree.registerDraggableType({ - 'collation domain domain_constraints fts_configuration fts_dictionary fts_parser fts_template synonym table partition type sequence package view mview foreign_table edbvar' : (data, item)=>{ - return pgadminUtils.fully_qualify(b, data, item); + 'collation domain domain_constraints fts_configuration fts_dictionary fts_parser fts_template synonym table partition type sequence package view mview foreign_table edbvar' : (data, item, treeNodeInfo)=>{ + let text = pgadminUtils.fully_qualify(b, data, item); + return { + text: text, + objUrl: generateNodeUrl.call(pgBrowser.Nodes[data._type], treeNodeInfo, 'properties', data, true), + cur: { + from: text.length, + to: text.length, + }, + }; }, 'schema column database cast event_trigger extension language foreign_data_wrapper foreign_server user_mapping compound_trigger index index_constraint primary_key unique_constraint check_constraint exclusion_constraint foreign_key rule' : (data)=>{ return pgadminUtils.quote_ident(data._label); diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js index 18c82517c..05582cee9 100644 --- a/web/pgadmin/static/js/tree/tree.js +++ b/web/pgadmin/static/js/tree/tree.js @@ -467,7 +467,7 @@ export class Tree { * overrides the dragstart event set using element.on('dragstart') * This will avoid conflict. */ - let dropDetails = dropDetailsFunc(data, item); + let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item)); if(typeof dropDetails == 'string') { dropDetails = { diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js index 60dbf4e34..44ffca5e0 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js @@ -175,11 +175,12 @@ export default class ERDCore { getModel() {return this.getEngine().getModel();} - getNewNode(initData) { + getNewNode(initData, dataUrl=null) { return this.getEngine().getNodeFactories().getFactory('table').generateModel({ initialConfig: { otherInfo: { data:initData, + dataUrl: dataUrl, }, }, }); @@ -404,6 +405,21 @@ export default class ERDCore { this.repaint(); } + cloneTableData(tableData, name) { + const SKIP_CLONE_KEYS = ['foreign_key']; + + if(!tableData) { + return tableData; + } + let newData = { + ..._.pickBy(tableData, (_v, k)=>(SKIP_CLONE_KEYS.indexOf(k) == -1)), + }; + if(name) { + newData['name'] = name; + } + return newData; + } + serialize(version) { return { version: version||0, @@ -422,7 +438,10 @@ export default class ERDCore { let nodesDict = this.getModel().getNodesDict(); Object.keys(nodesDict).forEach((id)=>{ - nodes[id] = nodesDict[id].serializeData(); + let nodeData = nodesDict[id].serializeData(); + if(nodeData) { + nodes[id] = nodeData; + } }); return { diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx index fb43fa768..b72f96d9b 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx @@ -18,6 +18,7 @@ import PrimaryKeyIcon from 'top/browser/server_groups/servers/databases/schemas/ import ForeignKeyIcon from 'top/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/img/foreign_key.svg'; import ColumnIcon from 'top/browser/server_groups/servers/databases/schemas/tables/columns/static/img/column.svg'; import PropTypes from 'prop-types'; +import gettext from 'sources/gettext'; const TYPE = 'table'; @@ -29,11 +30,35 @@ export class TableNodeModel extends DefaultNodeModel { }); this._note = otherInfo.note || ''; - - this._data = { - columns: [], - ...otherInfo.data, + this._metadata = { + data_failed: false, + ...otherInfo.metadata, + is_promise: Boolean(otherInfo.data?.then || (otherInfo.metadata?.data_failed && !otherInfo.data)), }; + this._data = null; + if(otherInfo.data?.then) { + otherInfo.data.then((data)=>{ + /* Once the data is available, it is no more a promise */ + this._data = data; + this._metadata = { + data_failed: false, + is_promise: false, + }; + this.fireEvent(this._metadata, 'dataAvaiable'); + this.fireEvent({}, 'nodeUpdated'); + }).catch(()=>{ + this._metadata = { + data_failed: true, + is_promise: true, + }; + this.fireEvent(this._metadata, 'dataAvaiable'); + }); + } else { + this._data = { + columns: [], + ...otherInfo.data, + }; + } } getPortName(attnum) { @@ -48,6 +73,10 @@ export class TableNodeModel extends DefaultNodeModel { return this._note; } + getMetadata() { + return this._metadata; + } + addColumn(col) { this._data.columns.push(col); } @@ -64,17 +93,6 @@ export class TableNodeModel extends DefaultNodeModel { this._data['name'] = name; } - cloneData(name) { - const SKIP_CLONE_KEYS = ['foreign_key']; - let newData = { - ..._.pickBy(this.getData(), (_v, k)=>(SKIP_CLONE_KEYS.indexOf(k) == -1)), - }; - if(name) { - newData['name'] = name; - } - return newData; - } - setData(data) { let self = this; /* Remove the links if column dropped or primary key removed */ @@ -116,6 +134,7 @@ export class TableNodeModel extends DefaultNodeModel { otherInfo: { data: this.getData(), note: this.getNote(), + metadata: this.getMetadata(), }, }; } @@ -146,6 +165,10 @@ export class TableNodeWidget extends React.Component { toggleDetails: (event) => { this.setState({show_details: event.show_details}); }, + dataAvaiable: ()=>{ + /* Just re-render */ + this.setState({}); + } }); } @@ -192,26 +215,37 @@ export class TableNodeWidget extends React.Component { render() { let tableData = this.props.node.getData(); + let tableMetaData = this.props.node.getMetadata(); return (
{this.props.node.fireEvent({}, 'editTable');}}>
- {e.stopPropagation();}} /> + {e.stopPropagation();}} + disabled={tableMetaData.is_promise} /> {this.props.node.getNote() && { this.props.node.fireEvent({}, 'showNote'); - }} title="Check note" />} -
-
- -
{tableData.schema}
-
-
- -
{tableData.name}
-
-
- {_.map(tableData.columns, (col)=>this.generateColumn(col, tableData))} + }} title="Check note"/>}
+ {tableMetaData.is_promise && <> +
+ {!tableMetaData.data_failed &&
{gettext('Fetching...')}
} + {tableMetaData.data_failed &&
{gettext('Failed to get data. Please delete this table.')}
} +
+ } + {!tableMetaData.is_promise && <> +
+ +
{tableData.schema}
+
+
+ +
{tableData.name}
+
+
+ {_.map(tableData.columns, (col)=>this.generateColumn(col, tableData))} +
+ }
); } diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx index 4ff8825c1..106ba4989 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx @@ -26,6 +26,7 @@ import url_for from 'sources/url_for'; import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool'; import 'wcdocker'; import Theme from '../../../../../../static/js/Theme'; +import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui'; /* Custom react-diagram action for keyboard events */ export class KeyboardShortcutAction extends Action { @@ -94,7 +95,7 @@ export default class BodyWidget extends React.Component { _.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSaveAsDiagram', 'onSQLClick', 'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick', 'onNoteClose', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle', - 'onDetailsToggle', 'onHelpClick' + 'onDetailsToggle', 'onHelpClick', 'onDropNode', ]); this.diagram.zoomToFit = this.diagram.zoomToFit.bind(this.diagram); @@ -114,8 +115,15 @@ export default class BodyWidget extends React.Component { this.realignGrid({backgroundSize: `${bgSize*3}px ${bgSize*3}px`}); }, 'nodesSelectionChanged': ()=>{ + let singleNodeSelected = false; + if(this.diagram.getSelectedNodes().length == 1) { + let metadata = this.diagram.getSelectedNodes()[0].getMetadata(); + if(!metadata.is_promise) { + singleNodeSelected = true; + } + } this.setState({ - single_node_selected: this.diagram.getSelectedNodes().length == 1, + single_node_selected: singleNodeSelected, any_item_selected: this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0, }); }, @@ -361,6 +369,29 @@ export default class BodyWidget extends React.Component { } } + onDropNode(e) { + let nodeDropData = JSON.parse(e.dataTransfer.getData('text')); + if(nodeDropData.objUrl) { + let matchUrl = `/${this.props.params.sgid}/${this.props.params.sid}/${this.props.params.did}/`; + if(nodeDropData.objUrl.indexOf(matchUrl) == -1) { + this.props.alertify.error(gettext('Cannot drop table from outside of the current database.')); + } else { + let dataPromise = new Promise((resolve, reject)=>{ + axios.get(nodeDropData.objUrl) + .then((res)=>{ + resolve(this.diagram.cloneTableData(TableSchema.getErdSupportedData(res.data))); + }) + .catch((err)=>{ + console.error(err); + reject(); + }); + }); + const {x, y} = this.diagram.getEngine().getRelativeMousePoint(e); + this.diagram.addNode(dataPromise, [x, y]).setSelected(true); + } + } + } + onEditTable() { const selected = this.diagram.getSelectedNodes(); if(selected.length == 1) { @@ -375,10 +406,12 @@ export default class BodyWidget extends React.Component { onCloneNode() { const selected = this.diagram.getSelectedNodes(); if(selected.length == 1) { - let newData = selected[0].cloneData(this.diagram.getNextTableName()); - let {x, y} = selected[0].getPosition(); - let newNode = this.diagram.addNode(newData, [x+20, y+20]); - newNode.setSelected(true); + let newData = this.diagram.cloneTableData(selected[0].getData(), this.diagram.getNextTableName()); + if(newData) { + let {x, y} = selected[0].getPosition(); + let newNode = this.diagram.addNode(newData, [x+20, y+20]); + newNode.setSelected(true); + } } } @@ -825,7 +858,7 @@ export default class BodyWidget extends React.Component { fgcolor={this.props.params.fgcolor} title={this.props.params.title}/> -
+
{e.preventDefault();}}> {this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
diff --git a/web/pgadmin/tools/erd/static/scss/_erd.scss b/web/pgadmin/tools/erd/static/scss/_erd.scss index 150ef6778..6b6ccdf9d 100644 --- a/web/pgadmin/tools/erd/static/scss/_erd.scss +++ b/web/pgadmin/tools/erd/static/scss/_erd.scss @@ -135,6 +135,10 @@ font-weight: bold; word-break: break-all; } + + & .fetch-error { + color: $color-danger; + } } .table-cols { diff --git a/web/regression/javascript/erd/erd_core_spec.js b/web/regression/javascript/erd/erd_core_spec.js index 635d488d4..1f84dee46 100644 --- a/web/regression/javascript/erd/erd_core_spec.js +++ b/web/regression/javascript/erd/erd_core_spec.js @@ -101,6 +101,7 @@ describe('ERDCore', ()=>{ initialConfig: { otherInfo: { data:data, + dataUrl: null, }, }, }); diff --git a/web/regression/javascript/erd/fake_item.js b/web/regression/javascript/erd/fake_item.js index 498ef1db1..2ef2bff2f 100644 --- a/web/regression/javascript/erd/fake_item.js +++ b/web/regression/javascript/erd/fake_item.js @@ -24,6 +24,11 @@ export class FakeNode { retVal.name = tabName; return retVal; } + getMetadata() { + return { + is_promise: false, + }; + } } export class FakeLink { diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js index 37d45e99b..43e45efb7 100644 --- a/web/regression/javascript/erd/table_node_spec.js +++ b/web/regression/javascript/erd/table_node_spec.js @@ -63,15 +63,6 @@ describe('ERD TableNodeModel', ()=>{ expect(modelObj.getData().name).toBe('changedName'); }); - it('cloneData', ()=>{ - modelObj.addColumn({name: 'col1', not_null:false, attnum: 0}); - expect(modelObj.cloneData('clonedNode')).toEqual({ - name: 'clonedNode', - schema: 'erd', - columns: [{name: 'col1', not_null:false, attnum: 0}], - }); - }); - describe('setData', ()=>{ let existPort = jasmine.createSpyObj('port', { 'removeAllLinks': jasmine.createSpy('removeAllLinks'), @@ -196,6 +187,9 @@ describe('ERD TableNodeModel', ()=>{ schema: 'erd', }, note: 'some note', + metadata: { + data_failed: false, is_promise: false + } }, }); }); diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js index ca30f63ff..3ecc4a8ff 100644 --- a/web/regression/javascript/erd/ui_components/body_widget_spec.js +++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js @@ -178,7 +178,7 @@ describe('ERD BodyWidget', ()=>{ }); it('event nodesSelectionChanged', (done)=>{ - spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([{key:'value'}]); + spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([new FakeNode({key:'value'})]); bodyInstance.diagram.fireEvent({}, 'nodesSelectionChanged', true); setTimeout(()=>{ expect(body.state().single_node_selected).toBe(true);