diff --git a/docs/en_US/images/preferences_erd_options.png b/docs/en_US/images/preferences_erd_options.png index e942ff15c..ec5e9515d 100644 Binary files a/docs/en_US/images/preferences_erd_options.png and b/docs/en_US/images/preferences_erd_options.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index ce0f672cf..4fe0d336d 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -196,6 +196,10 @@ Use the fields on the *Options* panel to manage ERD preferences. generated by the ERD Tool will add DROP table DDL before each CREATE table DDL. +* *Table Relation Depth* is useful when generating an ERD for a table. + It allows to set the limit on the depth level pgAdmin should traverse + to find the relations. Use -1 to set no limit. + The Graphs Node *************** diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js index dbb88f835..f47bd7d25 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js @@ -121,7 +121,11 @@ define('pgadmin.node.table', [ applies: ['object', 'context'], callback: 'count_table_rows', category: 'Count', priority: 2, label: gettext('Count Rows'), enable: true, - }, + },{ + name: 'generate_erd', node: 'table', module: this, + applies: ['object', 'context'], callback: 'generate_erd', + category: 'erd', priority: 5, label: gettext('ERD For Table'), + } ]); pgBrowser.Events.on( 'pgadmin:browser:node:table:updated', this.onTableUpdated, this @@ -289,6 +293,14 @@ define('pgadmin.node.table', [ t.unload(i); }); }, + /* Generate the ERD */ + generate_erd: function(args) { + let input = args || {}, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i ? t.itemData(i) : undefined; + pgAdmin.Tools.ERD.showErdTool(d, i, true); + }, }, getSchema: function(treeNodeInfo, itemNodeData) { return getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/properties.sql index 610100093..06834fe25 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/properties.sql @@ -13,6 +13,7 @@ SELECT ct.oid, confrelid, nl.nspname as fknsp, cl.relname as fktab, + nr.oid as refnspoid, nr.nspname as refnsp, cr.relname as reftab, description as comment, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/fk_ref_tables.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/fk_ref_tables.sql new file mode 100644 index 000000000..d4ad8c9e0 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/fk_ref_tables.sql @@ -0,0 +1,10 @@ +SELECT + co.conrelid as confrelid, co.conname, nl.oid as refnspoid +FROM pg_catalog.pg_depend dep + JOIN pg_catalog.pg_constraint co ON dep.objid=co.oid + JOIN pg_catalog.pg_class cl ON cl.oid=co.conrelid + JOIN pg_catalog.pg_namespace nl ON nl.oid=cl.relnamespace + WHERE dep.refobjid={{oid}}::OID + AND deptype = 'n' + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py index 07dc5b1a2..d00de4855 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py @@ -395,6 +395,22 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings): status=200 ) + def get_fk_ref_tables(self, tid): + """ + This function get the depending tables of the current table. + The tables depending on tid table using FK relation. + Args: + tid: Table ID + """ + sql = render_template("/".join([self.table_template_path, + 'fk_ref_tables.sql']), oid=tid) + + status, res = self.conn.execute_dict(sql) + if not status: + return status, res + + return status, res['rows'] + def get_table_statistics(self, scid, tid): """ Statistics diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index d525cffc7..7df2d1a00 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -98,7 +98,7 @@ define('pgadmin.node.database', [ },{ name: 'generate_erd', node: 'database', module: this, applies: ['object', 'context'], callback: 'generate_erd', - category: 'erd', priority: 5, label: gettext('Generate ERD'), + category: 'erd', priority: 5, label: gettext('ERD For Database'), enable: (node) => { return node.allowConn; } diff --git a/web/pgadmin/static/js/socket_instance.js b/web/pgadmin/static/js/socket_instance.js index ee5338682..24dde5da6 100644 --- a/web/pgadmin/static/js/socket_instance.js +++ b/web/pgadmin/static/js/socket_instance.js @@ -7,6 +7,7 @@ // ////////////////////////////////////////////////////////////// import { io } from 'socketio'; +import gettext from 'sources/gettext'; export function openSocket(namespace, options) { return new Promise((resolve, reject)=>{ @@ -41,5 +42,8 @@ export function socketApiGet(socket, endpoint, params) { socket.on(`${endpoint}_failed`, (data)=>{ reject(data); }); + socket.on('disconnect', ()=>{ + reject(gettext('Connection to pgAdmin server has been lost')); + }); }); } diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index 5100a39f9..25005b276 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -386,6 +386,20 @@ class ERDModule(PgAdminModule): ) ) + self.preference.register( + 'options', + 'table_relation_depth', + gettext('Table Relation Depth'), + 'integer', + -1, + category_label=PREF_LABEL_OPTIONS, + help_str=gettext( + 'The maximum depth pgAdmin should traverse to find ' + 'related tables when generating an ERD for a table. ' + 'Use -1 for no limit.' + ) + ) + blueprint = ERDModule(MODULE_NAME, __name__, static_url_path='/static') @@ -621,7 +635,9 @@ def tables(params): try: helper = ERDHelper(params['trans_id'], params['sid'], params['did']) _get_connection(params['sid'], params['did'], params['trans_id']) - status, tables = helper.get_all_tables() + + status, tables = helper.get_all_tables(params.get('scid', None), + params.get('tid', None)) if not status: socketio.emit('tables_failed', tables, diff --git a/web/pgadmin/tools/erd/static/js/ERDModule.js b/web/pgadmin/tools/erd/static/js/ERDModule.js index ad2d05832..a2d10c6e4 100644 --- a/web/pgadmin/tools/erd/static/js/ERDModule.js +++ b/web/pgadmin/tools/erd/static/js/ERDModule.js @@ -193,6 +193,11 @@ export default class ERDModule { +`&did=${parentData.database._id}` +`&gen=${gen}`; + if(parentData.table) { + openUrl += `&scid=${parentData.schema._id}` + +`&tid=${parentData.table._id}`; + } + return openUrl; } diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx index 4571d10c4..399cecb9f 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx @@ -355,7 +355,6 @@ class ERDTool extends React.Component { } else { window.removeEventListener('beforeunload', this.onBeforeUnload); } - } componentWillUnmount() { @@ -660,7 +659,7 @@ class ERDTool extends React.Component { } onSQLClick(sqlWithDrop=false) { - let scriptHeader = gettext('-- This script was generated by a beta version of the ERD tool in pgAdmin 4.\n'); + let scriptHeader = gettext('-- This script was generated by the ERD tool in pgAdmin 4.\n'); scriptHeader += gettext('-- Please log an issue at https://redmine.postgresql.org/projects/pgadmin4/issues/new if you find any bugs, including reproduction steps.\n'); let url = url_for('erd.sql', { @@ -926,12 +925,18 @@ class ERDTool extends React.Component { sgid: parseInt(this.props.params.sgid), sid: parseInt(this.props.params.sid), did: parseInt(this.props.params.did), + scid: this.props.params.scid ? parseInt(this.props.params.scid) : undefined, + tid: this.props.params.tid ? parseInt(this.props.params.tid) : undefined, }); } catch (error) { this.handleAxiosCatch(error); } socket?.disconnect(); - this.diagram.deserializeData(resData); + try { + this.diagram.deserializeData(resData); + } catch (error) { + this.handleAxiosCatch(error); + } this.setLoading(null); } @@ -964,6 +969,8 @@ ERDTool.propTypes = { sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + scid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + tid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), server_type: PropTypes.string.isRequired, title: PropTypes.string.isRequired, bgcolor: PropTypes.string, diff --git a/web/pgadmin/tools/erd/utils.py b/web/pgadmin/tools/erd/utils.py index ebe29e425..9145914e2 100644 --- a/web/pgadmin/tools/erd/utils.py +++ b/web/pgadmin/tools/erd/utils.py @@ -13,6 +13,7 @@ from pgadmin.browser.server_groups.servers.databases.schemas.utils \ import get_schemas from pgadmin.browser.server_groups.servers.databases.schemas.utils \ import DataTypeReader +from pgadmin.utils.preferences import Preferences class ERDTableView(BaseTableView, DataTypeReader): @@ -31,7 +32,7 @@ class ERDTableView(BaseTableView, DataTypeReader): return DataTypeReader.get_types(self, self.conn, condition, True) @BaseTableView.check_precondition - def fetch_all_tables(self, conn_id=None, did=None, sid=None): + def fetch_all_tables(self, did=None, sid=None): status, schemas = get_schemas(self.conn, show_system_objects=False) if not status: return status, schemas @@ -47,6 +48,44 @@ class ERDTableView(BaseTableView, DataTypeReader): return True, all_tables + @BaseTableView.check_precondition + def traverse_related_tables(self, did=None, sid=None, scid=None, + tid=None, related={}, maxdepth=0, currdepth=0): + + status, res = \ + BaseTableView.fetch_tables(self, sid, did, scid, tid=tid) + + if not status: + return status, res + + related[tid] = res + # Max depth limit reached + if currdepth == maxdepth: + new_fks = [] + for fk in related[tid].pop('foreign_key', []): + if fk['confrelid'] in related: + new_fks.append(fk) + + related[tid]['foreign_key'] = new_fks + return True, None + + status, depending_res = BaseTableView.get_fk_ref_tables( + self, tid) + + if not status: + return status, depending_res + + for fk in [*res.get('foreign_key', []), *depending_res]: + if fk['confrelid'] in related: + continue + status, res = self.traverse_related_tables( + did=did, sid=sid, scid=fk['refnspoid'], tid=fk['confrelid'], + related=related, maxdepth=maxdepth, currdepth=currdepth + 1) + if not status: + return status, res + + return True, None + class ERDHelper: def __init__(self, conn_id, sid, did): @@ -66,8 +105,18 @@ class ERDHelper: data=data, with_drop=with_drop) return SQL - def get_all_tables(self): - status, res = self.table_view.fetch_all_tables( - conn_id=self.conn_id, did=self.did, sid=self.sid) - + def get_all_tables(self, scid, tid): + if tid is None and scid is None: + status, res = self.table_view.fetch_all_tables( + did=self.did, sid=self.sid) + else: + prefs = Preferences.module('erd') + table_relation_depth = prefs.preference('table_relation_depth') + related = {} + status, res = self.table_view.traverse_related_tables( + did=self.did, sid=self.sid, scid=scid, tid=tid, + related=related, maxdepth=table_relation_depth.get() + ) + if status: + res = list(related.values()) return status, res diff --git a/web/regression/javascript/erd/ui_components/ERDTool.spec.js b/web/regression/javascript/erd/ui_components/ERDTool.spec.js index d2d8d0b73..16dee160c 100644 --- a/web/regression/javascript/erd/ui_components/ERDTool.spec.js +++ b/web/regression/javascript/erd/ui_components/ERDTool.spec.js @@ -393,7 +393,7 @@ describe('ERDTool', ()=>{ bodyInstance.onSQLClick(); setTimeout(()=>{ - let sql = '-- This script was generated by a beta version of the ERD tool in pgAdmin 4.\n' + let sql = '-- This script was generated by the ERD tool in pgAdmin 4.\n' + '-- Please log an issue at https://redmine.postgresql.org/projects/pgadmin4/issues/new if you find any bugs, including reproduction steps.\n' + 'BEGIN;\nSELECT 1;\nEND;';