mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added ERD Diagram support with basic table fields, primary key, foreign key, and DDL SQL generation. Fixes #1802
This commit is contained in:
parent
065bda37b4
commit
0c8226ff39
docs/en_US
web
.eslintrc.jspackage.jsonwebpack.config.jswebpack.shim.jswebpack.test.config.jsyarn.lock
pgadmin
browser
__init__.pyregister_browser_preferences.py
server_groups/servers/databases
schemas
static/js
templates/browser/js
static
bundle
js
scss
tools
datagrid/static/js
erd
__init__.py
static
js
erd_module.js
erd_tool
erd_tool_hook.jsindex.jsscss
templates/erd
tests
__init__.py
utils.pysql
test_close.pytest_initialize.pytest_panel.pytest_prequisite.pytest_sql.pytest_sql_input_data.jsontest_tables.pysqleditor/static/js
utils
regression/javascript
erd
erd_core_spec.jserd_model_spec.jskeyboard_shortcut_action_spec.jsonetomany_link_spec.jsonetomany_port_spec.jstable_node_spec.jstest_tables.js
fake_endpoints.jsui_components
@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #1802 <https://redmine.postgresql.org/issues/1802>`_ - Added ERD Diagram support with basic table fields, primary key, foreign key, and DDL SQL generation.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
@ -18,6 +18,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
],
|
||||
'parser': 'babel-eslint',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
'ecmaFeatures': {
|
||||
|
@ -7,12 +7,15 @@
|
||||
],
|
||||
"license": "PostgreSQL",
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.6.0",
|
||||
"@babel/preset-env": "~7.6.0",
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@emotion/core": "^10.0.14",
|
||||
"@emotion/styled": "^10.0.14",
|
||||
"autoprefixer": "^9.6.4",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"babel-loader": "~8.0.5",
|
||||
"babel-plugin-transform-object-rest-spread": "^7.0.0-beta.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"copy-webpack-plugin": "^5.1.0",
|
||||
"core-js": "^3.2.1",
|
||||
"cross-env": "^5.2.0",
|
||||
@ -41,7 +44,8 @@
|
||||
"popper.js": "^1.14.7",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"raw-loader": "^1.0.0",
|
||||
"raw-loader": "^3.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.24.4",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
@ -50,7 +54,7 @@
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.5.1",
|
||||
"webpack-cli": "^3.2.3",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-require-from": "^1.8.0",
|
||||
"yarn-audit-html": "^1.1.0"
|
||||
},
|
||||
@ -58,12 +62,12 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@projectstorm/react-diagrams": "^6.3.0",
|
||||
"@simonwep/pickr": "^1.5.1",
|
||||
"@tippyjs/react": "^4.2.0",
|
||||
"acitree": "git+https://github.com/imsurinder90/jquery-aciTree.git#rc.7",
|
||||
"alertifyjs": "git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263",
|
||||
"axios": "^0.18.1",
|
||||
"babel-plugin-transform-es2015-modules-amd": "^6.24.1",
|
||||
"babel-preset-es2015-without-strict": "~0.0.4",
|
||||
"babelify": "~10.0.0",
|
||||
"backbone": "1.4.0",
|
||||
"backform": "^0.2.0",
|
||||
@ -75,12 +79,17 @@
|
||||
"bootstrap4-toggle": "3.4.0",
|
||||
"bowser": "2.1.2",
|
||||
"browserify": "~16.2.3",
|
||||
"canvg": "^3.0.7",
|
||||
"chart.js": "^2.9.3",
|
||||
"closest": "^0.0.1",
|
||||
"codemirror": "^5.54.0",
|
||||
"css-loader": "2.1.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dagre": "^0.8.4",
|
||||
"dropzone": "^5.5.1",
|
||||
"exports-loader": "~0.7.0",
|
||||
"html-to-image": "^0.1.1",
|
||||
"html2canvas": "^1.0.0-rc.7",
|
||||
"immutability-helper": "^3.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"ip-address": "^5.8.9",
|
||||
@ -91,11 +100,17 @@
|
||||
"json-bignumber": "^1.0.1",
|
||||
"karma-coverage": "^2.0.3",
|
||||
"leaflet": "^1.5.1",
|
||||
"lodash": "4.*",
|
||||
"ml-matrix": "^6.5.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.23",
|
||||
"mousetrap": "^1.6.3",
|
||||
"pathfinding": "^0.4.18",
|
||||
"paths-js": "^0.4.9",
|
||||
"raf": "^3.4.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-to-print": "^2.10.3",
|
||||
"requirejs": "~2.3.6",
|
||||
"select2": "^4.0.6-rc.1",
|
||||
"shim-loader": "^1.0.1",
|
||||
|
@ -767,6 +767,7 @@ def utils():
|
||||
editor_insert_pair_brackets=insert_pair_brackets,
|
||||
editor_indent_with_tabs=editor_indent_with_tabs,
|
||||
app_name=config.APP_NAME,
|
||||
app_version_int=config.APP_VERSION_INT,
|
||||
pg_libpq_version=pg_libpq_version,
|
||||
support_ssh_tunnel=config.SUPPORT_SSH_TUNNEL,
|
||||
logout_url=_get_logout_url()
|
||||
|
@ -497,7 +497,8 @@ def register_browser_preferences(self):
|
||||
category_label=PREF_LABEL_OPTIONS,
|
||||
options=[{'label': gettext('Query Tool'), 'value': 'qt'},
|
||||
{'label': gettext('Debugger'), 'value': 'debugger'},
|
||||
{'label': gettext('Schema Diff'), 'value': 'schema_diff'}],
|
||||
{'label': gettext('Schema Diff'), 'value': 'schema_diff'},
|
||||
{'label': gettext('ERD Tool'), 'value': 'erd_tool'}],
|
||||
help_str=gettext('Select Query Tool, Debugger, or Schema Diff from '
|
||||
'the drop-down to set open in new browser tab for '
|
||||
'that particular module.'),
|
||||
|
@ -21,7 +21,6 @@ from pgadmin.browser.server_groups.servers.utils import parse_priv_to_db
|
||||
from pgadmin.utils.ajax import make_json_response, internal_server_error, \
|
||||
make_response as ajax_response, gone
|
||||
from .utils import BaseTableView
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tables.\
|
||||
constraints.foreign_key import utils as fkey_utils
|
||||
@ -134,8 +133,7 @@ class TableModule(SchemaChildModule):
|
||||
blueprint = TableModule(__name__)
|
||||
|
||||
|
||||
class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
SchemaDiffTableCompare):
|
||||
class TableView(BaseTableView, DataTypeReader, SchemaDiffTableCompare):
|
||||
"""
|
||||
This class is responsible for generating routes for Table node
|
||||
|
||||
@ -589,7 +587,7 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
Returns:
|
||||
JSON of selected table node
|
||||
"""
|
||||
status, res = self._fetch_properties(did, scid, tid)
|
||||
status, res = self._fetch_table_properties(did, scid, tid)
|
||||
if not status:
|
||||
return res
|
||||
if not res['rows']:
|
||||
@ -599,86 +597,6 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
gid, sid, did, scid, tid, res=res
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_rlspolicy_support(res):
|
||||
"""
|
||||
This function is used to check whether 'rlspolicy' in response
|
||||
as it supported for version 9.5 and above
|
||||
:param res:
|
||||
:return:
|
||||
"""
|
||||
if 'rlspolicy' in res['rows'][0]:
|
||||
# Set the value of rls policy
|
||||
if res['rows'][0]['rlspolicy'] == "true":
|
||||
res['rows'][0]['rlspolicy'] = True
|
||||
|
||||
# Set the value of force rls policy for table owner
|
||||
if res['rows'][0]['forcerlspolicy'] == "true":
|
||||
res['rows'][0]['forcerlspolicy'] = True
|
||||
|
||||
def _fetch_properties(self, did, scid, tid):
|
||||
"""
|
||||
This function is used to fetch the properties of the specified object
|
||||
:param did:
|
||||
:param scid:
|
||||
:param tid:
|
||||
:return:
|
||||
"""
|
||||
sql = render_template(
|
||||
"/".join([self.table_template_path, self._PROPERTIES_SQL]),
|
||||
did=did, scid=scid, tid=tid,
|
||||
datlastsysoid=self.datlastsysoid
|
||||
)
|
||||
status, res = self.conn.execute_dict(sql)
|
||||
if not status:
|
||||
return False, internal_server_error(errormsg=res)
|
||||
|
||||
elif len(res['rows']) == 0:
|
||||
return False, gone(
|
||||
gettext(self.not_found_error_msg()))
|
||||
|
||||
# Update autovacuum properties
|
||||
self.update_autovacuum_properties(res['rows'][0])
|
||||
|
||||
# We will check the threshold set by user before executing
|
||||
# the query because that can cause performance issues
|
||||
# with large result set
|
||||
pref = Preferences.module('browser')
|
||||
table_row_count_pref = pref.preference('table_row_count_threshold')
|
||||
table_row_count_threshold = table_row_count_pref.get()
|
||||
estimated_row_count = int(res['rows'][0].get('reltuples', 0))
|
||||
|
||||
# Check whether 'rlspolicy' in response as it supported for
|
||||
# version 9.5 and above
|
||||
TableView._check_rlspolicy_support(res)
|
||||
|
||||
# If estimated rows are greater than threshold then
|
||||
if estimated_row_count and \
|
||||
estimated_row_count > table_row_count_threshold:
|
||||
res['rows'][0]['rows_cnt'] = str(table_row_count_threshold) + '+'
|
||||
|
||||
# If estimated rows is lower than threshold then calculate the count
|
||||
elif estimated_row_count and \
|
||||
table_row_count_threshold >= estimated_row_count:
|
||||
sql = render_template(
|
||||
"/".join(
|
||||
[self.table_template_path, 'get_table_row_count.sql']
|
||||
), data=res['rows'][0]
|
||||
)
|
||||
|
||||
status, count = self.conn.execute_scalar(sql)
|
||||
|
||||
if not status:
|
||||
return False, internal_server_error(errormsg=count)
|
||||
|
||||
res['rows'][0]['rows_cnt'] = count
|
||||
|
||||
# If estimated_row_count is zero then set the row count with same
|
||||
elif not estimated_row_count:
|
||||
res['rows'][0]['rows_cnt'] = estimated_row_count
|
||||
|
||||
return True, res
|
||||
|
||||
@BaseTableView.check_precondition
|
||||
def types(self, gid, sid, did, scid, tid=None, clid=None):
|
||||
"""
|
||||
@ -686,12 +604,8 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
This function will return list of types available for column node
|
||||
for node-ajax-control
|
||||
"""
|
||||
condition = render_template(
|
||||
"/".join([
|
||||
self.table_template_path, 'get_types_where_condition.sql'
|
||||
]),
|
||||
show_system_objects=self.blueprint.show_system_objects
|
||||
)
|
||||
condition = self.get_types_condition_sql(
|
||||
self.blueprint.show_system_objects)
|
||||
|
||||
status, types = self.get_types(self.conn, condition, True, sid)
|
||||
|
||||
@ -1073,7 +987,7 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
data[k] = v
|
||||
|
||||
try:
|
||||
status, res = self._fetch_properties(did, scid, tid)
|
||||
status, res = self._fetch_table_properties(did, scid, tid)
|
||||
if not status:
|
||||
return res
|
||||
|
||||
@ -1314,7 +1228,7 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
res = None
|
||||
|
||||
if tid is not None:
|
||||
status, res = self._fetch_properties(did, scid, tid)
|
||||
status, res = self._fetch_table_properties(did, scid, tid)
|
||||
if not status:
|
||||
return res
|
||||
|
||||
@ -1378,7 +1292,7 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
"""
|
||||
main_sql = []
|
||||
|
||||
status, res = self._fetch_properties(did, scid, tid)
|
||||
status, res = self._fetch_table_properties(did, scid, tid)
|
||||
if not status:
|
||||
return res
|
||||
|
||||
@ -1665,57 +1579,12 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
:return: Table dataset
|
||||
"""
|
||||
|
||||
if tid:
|
||||
status, data = self._fetch_properties(did, scid, tid)
|
||||
status, res = BaseTableView.fetch_tables(self, sid, did, scid, tid)
|
||||
if not status:
|
||||
current_app.logger.error(res)
|
||||
return False
|
||||
|
||||
if not status:
|
||||
current_app.logger.error(data)
|
||||
return False
|
||||
|
||||
data = super(TableView, self).properties(
|
||||
0, sid, did, scid, tid, res=data, return_ajax_response=False
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
else:
|
||||
res = dict()
|
||||
sql = render_template("/".join([self.table_template_path,
|
||||
self._NODES_SQL]), scid=scid)
|
||||
status, tables = self.conn.execute_2darray(sql)
|
||||
if not status:
|
||||
current_app.logger.error(tables)
|
||||
return False
|
||||
|
||||
for row in tables['rows']:
|
||||
status, data = self._fetch_properties(did, scid, row['oid'])
|
||||
|
||||
if status:
|
||||
data = super(TableView, self).properties(
|
||||
0, sid, did, scid, row['oid'], res=data,
|
||||
return_ajax_response=False
|
||||
)
|
||||
|
||||
# Get sub module data of a specified table for object
|
||||
# comparison
|
||||
self._get_sub_module_data_for_compare(sid, did, scid, data,
|
||||
row)
|
||||
res[row['name']] = data
|
||||
|
||||
return res
|
||||
|
||||
def _get_sub_module_data_for_compare(self, sid, did, scid, data, row):
|
||||
# Get sub module data of a specified table for object
|
||||
# comparison
|
||||
for module in self.tables_sub_modules:
|
||||
module_view = SchemaDiffRegistry.get_node_view(module)
|
||||
if module_view.blueprint.server_type is None or \
|
||||
self.manager.server_type in \
|
||||
module_view.blueprint.server_type:
|
||||
sub_data = module_view.fetch_objects_to_compare(
|
||||
sid=sid, did=did, scid=scid, tid=row['oid'],
|
||||
oid=None)
|
||||
data[module] = sub_data
|
||||
return res
|
||||
|
||||
def get_submodule_template_path(self, module_name):
|
||||
"""
|
||||
|
@ -318,22 +318,23 @@ def _get_sql_for_create_fk_const(data, conn, template_path):
|
||||
len(data['columns']) < 1):
|
||||
return True, '-- definition incomplete', name, ''
|
||||
|
||||
if data['autoindex'] and \
|
||||
if data.get('autoindex', False) and \
|
||||
('coveringindex' not in data or data['coveringindex'] == ''):
|
||||
return True, '-- definition incomplete', name, ''
|
||||
|
||||
# Get the parent schema and table.
|
||||
schema, table = get_parent(conn,
|
||||
data['columns'][0]['references'])
|
||||
if 'references' in data['columns'][0]:
|
||||
# Get the parent schema and table.
|
||||
schema, table = get_parent(conn,
|
||||
data['columns'][0]['references'])
|
||||
|
||||
# Below handling will be used in Schema diff in case
|
||||
# of different database comparison
|
||||
_checks_for_schema_diff(table, schema, data)
|
||||
# Below handling will be used in Schema diff in case
|
||||
# of different database comparison
|
||||
_checks_for_schema_diff(table, schema, data)
|
||||
|
||||
sql = render_template("/".join([template_path, 'create.sql']),
|
||||
data=data, conn=conn)
|
||||
|
||||
if data['autoindex']:
|
||||
if data.get('autoindex', False):
|
||||
sql += render_template(
|
||||
"/".join([template_path, 'create_index.sql']),
|
||||
data=data, conn=conn)
|
||||
|
@ -175,8 +175,7 @@ class PartitionsModule(CollectionNodeModule):
|
||||
blueprint = PartitionsModule(__name__)
|
||||
|
||||
|
||||
class PartitionsView(BaseTableView, DataTypeReader, VacuumSettings,
|
||||
SchemaDiffObjectCompare):
|
||||
class PartitionsView(BaseTableView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
"""
|
||||
This class is responsible for generating routes for Partition node
|
||||
|
||||
|
@ -2,21 +2,21 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
|
||||
ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} FOREIGN KEY ({% for columnobj in data.columns %}{% if loop.index != 1 %}
|
||||
, {% endif %}{{ conn|qtIdent(columnobj.local_column)}}{% endfor %})
|
||||
REFERENCES {{ conn|qtIdent(data.remote_schema, data.remote_table) }} ({% for columnobj in data.columns %}{% if loop.index != 1 %}
|
||||
, {% endif %}{{ conn|qtIdent(columnobj.referenced)}}{% endfor %}) {% if data.confmatchtype %}MATCH FULL{% else %}MATCH SIMPLE{% endif%}
|
||||
, {% endif %}{{ conn|qtIdent(columnobj.referenced)}}{% endfor %}){% if data.confmatchtype is defined %} {% if data.confmatchtype %}MATCH FULL{% else %}MATCH SIMPLE{% endif%}{% endif%}{% if data.confupdtype is defined %}
|
||||
|
||||
ON UPDATE{% if data.confupdtype == 'a' %}
|
||||
NO ACTION{% elif data.confupdtype == 'r' %}
|
||||
RESTRICT{% elif data.confupdtype == 'c' %}
|
||||
CASCADE{% elif data.confupdtype == 'n' %}
|
||||
SET NULL{% elif data.confupdtype == 'd' %}
|
||||
SET DEFAULT{% endif %}
|
||||
SET DEFAULT{% endif %}{% endif %}{% if data.confdeltype is defined %}
|
||||
|
||||
ON DELETE{% if data.confdeltype == 'a' %}
|
||||
NO ACTION{% elif data.confdeltype == 'r' %}
|
||||
RESTRICT{% elif data.confdeltype == 'c' %}
|
||||
CASCADE{% elif data.confdeltype == 'n' %}
|
||||
SET NULL{% elif data.confdeltype == 'd' %}
|
||||
SET DEFAULT{% endif %}
|
||||
SET DEFAULT{% endif %}{% endif %}
|
||||
{% if data.condeferrable %}
|
||||
|
||||
DEFERRABLE{% if data.condeferred %}
|
||||
|
@ -45,9 +45,13 @@ from pgadmin.browser.server_groups.servers.databases.schemas.tables.\
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas. \
|
||||
tables.row_security_policies import \
|
||||
utils as row_security_policies_utils
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
|
||||
import VacuumSettings
|
||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
|
||||
|
||||
class BaseTableView(PGChildNodeView, BasePartitionTable):
|
||||
class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
"""
|
||||
This class is base class for tables and partitioned tables.
|
||||
|
||||
@ -101,7 +105,11 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
did = kwargs['did']
|
||||
self.manager = driver.connection_manager(kwargs['sid'])
|
||||
self.conn = self.manager.connection(did=kwargs['did'])
|
||||
if "conn_id" in kwargs:
|
||||
self.conn = self.manager.connection(
|
||||
did=kwargs['did'], conn_id=kwargs['conn_id'])
|
||||
else:
|
||||
self.conn = self.manager.connection(did=kwargs['did'])
|
||||
self.qtIdent = driver.qtIdent
|
||||
self.qtTypeIdent = driver.qtTypeIdent
|
||||
# We need datlastsysoid to check if current table is system table
|
||||
@ -442,6 +450,161 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
|
||||
status=200
|
||||
)
|
||||
|
||||
def get_types_condition_sql(self, show_system_objects):
|
||||
condition = render_template(
|
||||
"/".join([
|
||||
self.table_template_path, 'get_types_where_condition.sql'
|
||||
]),
|
||||
show_system_objects=show_system_objects
|
||||
)
|
||||
|
||||
return condition
|
||||
|
||||
def fetch_tables(self, sid, did, scid, tid=None):
|
||||
"""
|
||||
This function will fetch the list of all the tables
|
||||
and will be used by schema diff.
|
||||
|
||||
:param sid: Server Id
|
||||
:param did: Database Id
|
||||
:param scid: Schema Id
|
||||
:param tid: Table Id
|
||||
:return: Table dataset
|
||||
"""
|
||||
|
||||
if tid:
|
||||
status, data = self._fetch_table_properties(did, scid, tid)
|
||||
|
||||
if not status:
|
||||
return False, data
|
||||
|
||||
data = BaseTableView.properties(
|
||||
self, 0, sid, did, scid, tid, res=data,
|
||||
return_ajax_response=False
|
||||
)
|
||||
|
||||
return True, data
|
||||
|
||||
else:
|
||||
res = dict()
|
||||
sql = render_template("/".join([self.table_template_path,
|
||||
self._NODES_SQL]), scid=scid)
|
||||
status, tables = self.conn.execute_2darray(sql)
|
||||
if not status:
|
||||
return False, tables
|
||||
|
||||
for row in tables['rows']:
|
||||
status, data = \
|
||||
self._fetch_table_properties(did, scid, row['oid'])
|
||||
|
||||
if status:
|
||||
data = BaseTableView.properties(
|
||||
self, 0, sid, did, scid, row['oid'], res=data,
|
||||
return_ajax_response=False
|
||||
)
|
||||
|
||||
# Get sub module data of a specified table for object
|
||||
# comparison
|
||||
BaseTableView._get_sub_module_data_for_compare(
|
||||
self, sid, did, scid, data, row)
|
||||
res[row['name']] = data
|
||||
res[row['name']] = data
|
||||
|
||||
return True, res
|
||||
|
||||
def _get_sub_module_data_for_compare(self, sid, did, scid, data, row):
|
||||
# Get sub module data of a specified table for object
|
||||
# comparison
|
||||
for module in self.tables_sub_modules:
|
||||
module_view = SchemaDiffRegistry.get_node_view(module)
|
||||
if module_view.blueprint.server_type is None or \
|
||||
self.manager.server_type in \
|
||||
module_view.blueprint.server_type:
|
||||
sub_data = module_view.fetch_objects_to_compare(
|
||||
sid=sid, did=did, scid=scid, tid=row['oid'],
|
||||
oid=None)
|
||||
data[module] = sub_data
|
||||
|
||||
@staticmethod
|
||||
def _check_rlspolicy_support(res):
|
||||
"""
|
||||
This function is used to check whether 'rlspolicy' in response
|
||||
as it supported for version 9.5 and above
|
||||
:param res:
|
||||
:return:
|
||||
"""
|
||||
if 'rlspolicy' in res['rows'][0]:
|
||||
# Set the value of rls policy
|
||||
if res['rows'][0]['rlspolicy'] == "true":
|
||||
res['rows'][0]['rlspolicy'] = True
|
||||
|
||||
# Set the value of force rls policy for table owner
|
||||
if res['rows'][0]['forcerlspolicy'] == "true":
|
||||
res['rows'][0]['forcerlspolicy'] = True
|
||||
|
||||
def _fetch_table_properties(self, did, scid, tid):
|
||||
"""
|
||||
This function is used to fetch the properties of the specified object
|
||||
:param did:
|
||||
:param scid:
|
||||
:param tid:
|
||||
:return:
|
||||
"""
|
||||
sql = render_template(
|
||||
"/".join([self.table_template_path, self._PROPERTIES_SQL]),
|
||||
did=did, scid=scid, tid=tid,
|
||||
datlastsysoid=self.datlastsysoid
|
||||
)
|
||||
status, res = self.conn.execute_dict(sql)
|
||||
if not status:
|
||||
return False, internal_server_error(errormsg=res)
|
||||
|
||||
elif len(res['rows']) == 0:
|
||||
return False, gone(
|
||||
gettext(self.not_found_error_msg()))
|
||||
|
||||
# Update autovacuum properties
|
||||
self.update_autovacuum_properties(res['rows'][0])
|
||||
|
||||
# We will check the threshold set by user before executing
|
||||
# the query because that can cause performance issues
|
||||
# with large result set
|
||||
pref = Preferences.module('browser')
|
||||
table_row_count_pref = pref.preference('table_row_count_threshold')
|
||||
table_row_count_threshold = table_row_count_pref.get()
|
||||
estimated_row_count = int(res['rows'][0].get('reltuples', 0))
|
||||
|
||||
# Check whether 'rlspolicy' in response as it supported for
|
||||
# version 9.5 and above
|
||||
BaseTableView._check_rlspolicy_support(res)
|
||||
|
||||
# If estimated rows are greater than threshold then
|
||||
if estimated_row_count and \
|
||||
estimated_row_count > table_row_count_threshold:
|
||||
res['rows'][0]['rows_cnt'] = str(table_row_count_threshold) + '+'
|
||||
|
||||
# If estimated rows is lower than threshold then calculate the count
|
||||
elif estimated_row_count and \
|
||||
table_row_count_threshold >= estimated_row_count:
|
||||
sql = render_template(
|
||||
"/".join(
|
||||
[self.table_template_path, 'get_table_row_count.sql']
|
||||
), data=res['rows'][0]
|
||||
)
|
||||
|
||||
status, count = self.conn.execute_scalar(sql)
|
||||
|
||||
if not status:
|
||||
return False, internal_server_error(errormsg=count)
|
||||
|
||||
res['rows'][0]['rows_cnt'] = count
|
||||
|
||||
# If estimated_row_count is zero then set the row count with same
|
||||
elif not estimated_row_count:
|
||||
res['rows'][0]['rows_cnt'] = estimated_row_count
|
||||
|
||||
return True, res
|
||||
|
||||
def _format_column_list(self, data):
|
||||
# Now we have all lis of columns which we need
|
||||
# to include in our create definition, Let's format them
|
||||
|
@ -110,13 +110,15 @@ class DataTypeReader:
|
||||
"""
|
||||
# Check if template path is already set or not
|
||||
# if not then we will set the template path here
|
||||
manager = conn.manager if not hasattr(self, 'manager') \
|
||||
else self.manager
|
||||
if not hasattr(self, 'data_type_template_path'):
|
||||
self.data_type_template_path = 'datatype/sql/' + (
|
||||
'#{0}#{1}#'.format(
|
||||
self.manager.server_type,
|
||||
self.manager.version
|
||||
) if self.manager.server_type == 'gpdb' else
|
||||
'#{0}#'.format(self.manager.version)
|
||||
manager.server_type,
|
||||
manager.version
|
||||
) if manager.server_type == 'gpdb' else
|
||||
'#{0}#'.format(manager.version)
|
||||
)
|
||||
sql = render_template(
|
||||
"/".join([self.data_type_template_path, 'get_types.sql']),
|
||||
@ -701,3 +703,23 @@ def get_schema(sid, did, scid):
|
||||
)
|
||||
|
||||
return status, schema_name
|
||||
|
||||
|
||||
def get_schemas(conn, show_system_objects=False):
|
||||
"""
|
||||
This function will return the schemas.
|
||||
"""
|
||||
|
||||
ver = conn.manager.version
|
||||
server_type = conn.manager.server_type
|
||||
|
||||
SQL = render_template(
|
||||
"/".join(['schemas',
|
||||
'{0}/#{1}#'.format(server_type, ver),
|
||||
'sql/nodes.sql']),
|
||||
show_sysobj=show_system_objects,
|
||||
schema_restrictions=None
|
||||
)
|
||||
|
||||
status, rset = conn.execute_2darray(SQL)
|
||||
return status, rset
|
||||
|
@ -82,6 +82,10 @@ define('pgadmin.node.database', [
|
||||
applies: ['object', 'context'], callback: 'disconnect_database',
|
||||
category: 'drop', priority: 5, label: gettext('Disconnect Database...'),
|
||||
icon: 'fa fa-unlink', enable : 'is_connected',
|
||||
},{
|
||||
name: 'generate_erd', node: 'database', module: this,
|
||||
applies: ['object', 'context'], callback: 'generate_erd',
|
||||
category: 'erd', priority: 5, label: gettext('Generate ERD(Beta)...'),
|
||||
}]);
|
||||
|
||||
_.bindAll(this, 'connection_lost');
|
||||
@ -236,6 +240,15 @@ define('pgadmin.node.database', [
|
||||
return false;
|
||||
},
|
||||
|
||||
/* Generate the ERD */
|
||||
generate_erd: function(args) {
|
||||
var input = args || {},
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
pgBrowser.erd.showErdTool(d, i, true);
|
||||
},
|
||||
|
||||
/* Connect the database (if not connected), before opening this node */
|
||||
beforeopen: function(item, data) {
|
||||
if(!data || data._type != 'database' || data.label == 'template0') {
|
||||
|
@ -68,6 +68,7 @@ define('pgadmin.browser.utils',
|
||||
braceMatching: '{{ editor_brace_matching }}' == 'True',
|
||||
is_indent_with_tabs: '{{ editor_indent_with_tabs }}' == 'True',
|
||||
app_name: '{{ app_name }}',
|
||||
app_version_int: '{{ app_version_int}}',
|
||||
pg_libpq_version: {{pg_libpq_version|e}},
|
||||
support_ssh_tunnel: '{{ support_ssh_tunnel }}' == 'True',
|
||||
logout_url: '{{logout_url}}',
|
||||
|
@ -10,6 +10,7 @@
|
||||
define('bundled_browser',[
|
||||
'pgadmin.browser',
|
||||
'sources/browser/index',
|
||||
'top/tools/erd/static/js/index',
|
||||
], function(pgBrowser) {
|
||||
pgBrowser.init();
|
||||
});
|
||||
|
@ -1465,6 +1465,10 @@ define([
|
||||
collection: collection,
|
||||
node_info: self.model.node_info,
|
||||
});
|
||||
|
||||
if(data.beforeAdd) {
|
||||
m = data.beforeAdd.apply(self, [m]);
|
||||
}
|
||||
collection.add(m);
|
||||
|
||||
var idx = collection.indexOf(m),
|
||||
|
@ -10,10 +10,10 @@
|
||||
define([
|
||||
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
|
||||
'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
|
||||
'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
|
||||
'sources/window', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
|
||||
], function(
|
||||
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror,
|
||||
commonUtils, keyboardShortcuts, configure_show_on_scroll
|
||||
commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow
|
||||
) {
|
||||
/*
|
||||
* Add mechanism in backgrid to render different types of cells in
|
||||
@ -43,7 +43,7 @@ define([
|
||||
// bind shortcut in cell edit mode
|
||||
_.extend(Backgrid.InputCellEditor.prototype.events, {
|
||||
'keydown': function(e) {
|
||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser');
|
||||
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
|
||||
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||
} else {
|
||||
@ -323,7 +323,7 @@ define([
|
||||
},
|
||||
events: {
|
||||
'keydown': function (event) {
|
||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser');
|
||||
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
|
||||
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||
}
|
||||
@ -764,7 +764,7 @@ define([
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser');
|
||||
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
|
||||
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||
}
|
||||
|
10
web/pgadmin/static/js/custom_prop_types.js
Normal file
10
web/pgadmin/static/js/custom_prop_types.js
Normal file
@ -0,0 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CustomPropTypes = {
|
||||
ref: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default CustomPropTypes;
|
@ -181,6 +181,15 @@ legend {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@include button-variant($color-warning, $color-warning-fg);
|
||||
border-color: $color-warning;
|
||||
@include hover() {
|
||||
color: $color-warning-fg !important;
|
||||
border-color: $color-warning !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-group fieldset {
|
||||
background-color: $color-gray-lighter;
|
||||
@ -371,6 +380,11 @@ td.switch-cell > div.toggle {
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
@extend .btn-sm;
|
||||
padding: 0.125rem 0.25rem !important;
|
||||
}
|
||||
|
||||
.btn-toolbar {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
@ -886,6 +886,12 @@ table.table-noouter-border {
|
||||
border-bottom: $table-border-width solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.table-noheader {
|
||||
& > tbody tr:first-of-type td {
|
||||
border-top: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table-bottom-border {
|
||||
|
9
web/pgadmin/static/scss/_tippy.overrides.scss
Normal file
9
web/pgadmin/static/scss/_tippy.overrides.scss
Normal file
@ -0,0 +1,9 @@
|
||||
@import "~node_modules/tippy.js/dist/tippy.css";
|
||||
|
||||
.tippy-box {
|
||||
background-color: $popover-bg;
|
||||
color: $popover-body-color;
|
||||
.tippy-arrow {
|
||||
color: $popover-bg;
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ $theme-colors: (
|
||||
}
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
@import 'webcabin.pgadmin';
|
||||
@import 'bootstrap.overrides';
|
||||
@import 'backgrid.overrides';
|
||||
@ -27,3 +26,4 @@ $theme-colors: (
|
||||
@import 'pgadmin.style';
|
||||
@import 'bootstrap4-toggle.overrides';
|
||||
@import 'pickr.overrides';
|
||||
@import 'tippy.overrides';
|
||||
|
@ -111,8 +111,8 @@ $dropdown-spacer: .125rem; //no-change
|
||||
$dropdown-link-disabled-color: $text-muted;
|
||||
$nav-divider-margin-y: .25rem;
|
||||
|
||||
$popover-bg: $color-gray-dark !default;
|
||||
$popover-body-color: $white !default;
|
||||
$popover-bg: $color-fg !default;
|
||||
$popover-body-color: $color-bg !default;
|
||||
$popover-border-color: $dropdown-border-color;
|
||||
$popover-box-shadow: $dropdown-box-shadow;
|
||||
|
||||
@ -349,3 +349,16 @@ $grid-hover-fg-color: $color-fg !default;
|
||||
|
||||
$btn-copied-color-fg: $active-color !default;
|
||||
|
||||
/** ERD **/
|
||||
$erd-row-padding: 0.25rem;
|
||||
$erd-node-border-color: $border-color !default;
|
||||
$erd-canvas-bg: $color-bg !default;
|
||||
$erd-canvas-grid: $color-gray !default;
|
||||
$erd-link-color: $color-fg !default;
|
||||
$erd-link-selected-color: $color-fg !default;
|
||||
|
||||
|
||||
@function url-friendly-colour($colour) {
|
||||
@return '%23' + str-slice('#{$colour}', 2, -1)
|
||||
}
|
||||
$erd-bg-grid: url("data:image/svg+xml, %3Csvg width='100%25' viewBox='0 0 45 45' style='background-color:#{url-friendly-colour($erd-canvas-bg)}' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='15' height='15' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 15 0 L 0 0 0 15' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='0.5'/%3E%3C/pattern%3E%3Cpattern id='grid' width='45' height='45' patternUnits='userSpaceOnUse'%3E%3Crect width='100' height='100' fill='url(%23smallGrid)'/%3E%3Cpath d='M 100 0 L 0 0 0 100' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='1'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)' /%3E%3C/svg%3E%0A");
|
||||
|
@ -121,3 +121,11 @@ $color-success-hover-fg: $color-fg;
|
||||
$datagrid-selected-color: $color-primary-fg;
|
||||
|
||||
$select2-placeholder: #999;
|
||||
|
||||
/* ERD */
|
||||
$erd-row-padding: 0.25rem;
|
||||
$erd-node-border-color: $border-color;
|
||||
$erd-canvas-bg: $color-gray-light;
|
||||
$erd-canvas-grid: #444952;
|
||||
$erd-link-color: $color-fg;
|
||||
$erd-link-selected-color: $color-fg;
|
||||
|
@ -20,16 +20,17 @@ function isServerInformationAvailable(parentData) {
|
||||
return parentData.server === undefined;
|
||||
}
|
||||
|
||||
export function getPanelTitle(pgBrowser, selected_item=null, custom_title=null) {
|
||||
export function getPanelTitle(pgBrowser, selected_item=null, custom_title=null, parentData=null) {
|
||||
var preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
if(selected_item == null) {
|
||||
if(selected_item == null && parentData == null) {
|
||||
selected_item = pgBrowser.treeMenu.selected();
|
||||
}
|
||||
|
||||
const parentData = getTreeNodeHierarchyFromIdentifier
|
||||
.call(pgBrowser, selected_item);
|
||||
if (isServerInformationAvailable(parentData)) {
|
||||
return;
|
||||
if(parentData == null) {
|
||||
parentData = getTreeNodeHierarchyFromIdentifier.call(pgBrowser, selected_item);
|
||||
if (isServerInformationAvailable(parentData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const db_label = getDatabaseLabel(parentData);
|
||||
|
@ -18,7 +18,7 @@ function hasDatabaseInformation(parentData) {
|
||||
return parentData.database;
|
||||
}
|
||||
|
||||
function generateUrl(trans_id, title, parentData) {
|
||||
function generateUrl(trans_id, title, parentData, sqlId) {
|
||||
let url_endpoint = url_for('datagrid.panel', {
|
||||
'trans_id': trans_id,
|
||||
});
|
||||
@ -32,6 +32,10 @@ function generateUrl(trans_id, title, parentData) {
|
||||
url_endpoint += `&did=${parentData.database._id}`;
|
||||
}
|
||||
|
||||
if(sqlId) {
|
||||
url_endpoint += `&sql_id=${sqlId}`;
|
||||
}
|
||||
|
||||
return url_endpoint;
|
||||
}
|
||||
|
||||
@ -85,6 +89,25 @@ export function generateScript(parentData, datagrid, alertify) {
|
||||
launchDataGrid(datagrid, transId, url_endpoint, queryToolTitle, '', alertify);
|
||||
}
|
||||
|
||||
export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, datagrid, alertify) {
|
||||
const transId = getRandomInt(1, 9999999);
|
||||
parentData = {
|
||||
server_group: {
|
||||
_id: parentData.sgid,
|
||||
},
|
||||
server: {
|
||||
_id: parentData.sid,
|
||||
server_type: parentData.stype,
|
||||
},
|
||||
database: {
|
||||
_id: parentData.did,
|
||||
},
|
||||
};
|
||||
|
||||
const gridUrl = generateUrl(transId, queryToolTitle, parentData, erdSqlId);
|
||||
launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, '', alertify);
|
||||
}
|
||||
|
||||
export function launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, sURL, alertify) {
|
||||
let retVal = datagrid.launch_grid(transId, gridUrl, true, queryToolTitle, sURL);
|
||||
|
||||
|
553
web/pgadmin/tools/erd/__init__.py
Normal file
553
web/pgadmin/tools/erd/__init__.py
Normal file
@ -0,0 +1,553 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""A blueprint module implementing the erd tool."""
|
||||
import simplejson as json
|
||||
|
||||
from flask import url_for, request
|
||||
from flask import render_template, current_app as app
|
||||
from flask_security import login_required
|
||||
from flask_babelex import gettext
|
||||
from werkzeug.useragents import UserAgent
|
||||
from pgadmin.utils import PgAdminModule, \
|
||||
SHORTCUT_FIELDS as shortcut_fields
|
||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
||||
internal_server_error
|
||||
from pgadmin.model import Server
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.browser.utils import underscore_unescape
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
|
||||
import get_schemas
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tables. \
|
||||
constraints.foreign_key import utils as fkey_utils
|
||||
from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \
|
||||
PREF_LABEL_DISPLAY
|
||||
from .utils import ERDHelper
|
||||
from pgadmin.utils.exception import ConnectionLost
|
||||
|
||||
MODULE_NAME = 'erd'
|
||||
|
||||
|
||||
class ERDModule(PgAdminModule):
|
||||
"""
|
||||
class ERDModule(PgAdminModule)
|
||||
|
||||
A module class for ERD derived from PgAdminModule.
|
||||
"""
|
||||
|
||||
LABEL = gettext("ERD tool")
|
||||
|
||||
def get_own_menuitems(self):
|
||||
return {}
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [{
|
||||
'name': 'pgadmin.erd',
|
||||
'path': url_for('erd.index') + "erd",
|
||||
'when': None
|
||||
}]
|
||||
|
||||
def get_panels(self):
|
||||
return []
|
||||
|
||||
def get_exposed_url_endpoints(self):
|
||||
"""
|
||||
Returns:
|
||||
list: URL endpoints
|
||||
"""
|
||||
return [
|
||||
'erd.panel',
|
||||
'erd.initialize',
|
||||
'erd.prequisite',
|
||||
'erd.sql',
|
||||
'erd.tables',
|
||||
'erd.close'
|
||||
]
|
||||
|
||||
def register_preferences(self):
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'open_project',
|
||||
gettext('Open project'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': False,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 79,
|
||||
'char': 'o'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'save_project',
|
||||
gettext('Save project'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': False,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'save_project_as',
|
||||
gettext('Save project as'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': False,
|
||||
'shift': True,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'generate_sql',
|
||||
gettext('Generate SQL'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'add_table',
|
||||
gettext('Add table'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 65,
|
||||
'char': 'a'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'edit_table',
|
||||
gettext('Edit table'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 69,
|
||||
'char': 'e'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'clone_table',
|
||||
gettext('Clone table'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 67,
|
||||
'char': 'c'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'drop_table',
|
||||
gettext('Drop table'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 68,
|
||||
'char': 'd'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'add_edit_note',
|
||||
gettext('Add/Edit note'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 78,
|
||||
'char': 'n'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'one_to_many',
|
||||
gettext('One to many link'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 79,
|
||||
'char': 'o'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'many_to_many',
|
||||
gettext('Many to many link'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 77,
|
||||
'char': 'm'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'auto_align',
|
||||
gettext('Auto align'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': False,
|
||||
'control': True,
|
||||
'key': {
|
||||
'key_code': 76,
|
||||
'char': 'l'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'zoom_to_fit',
|
||||
gettext('Zoom to fit'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': True,
|
||||
'control': False,
|
||||
'key': {
|
||||
'key_code': 70,
|
||||
'char': 'f'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'zoom_in',
|
||||
gettext('Zoom in'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': True,
|
||||
'control': False,
|
||||
'key': {
|
||||
'key_code': 187,
|
||||
'char': '+'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
self.preference.register(
|
||||
'keyboard_shortcuts',
|
||||
'zoom_out',
|
||||
gettext('Zoom out'),
|
||||
'keyboardshortcut',
|
||||
{
|
||||
'alt': True,
|
||||
'shift': True,
|
||||
'control': False,
|
||||
'key': {
|
||||
'key_code': 189,
|
||||
'char': '-'
|
||||
}
|
||||
},
|
||||
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||
fields=shortcut_fields
|
||||
)
|
||||
|
||||
|
||||
blueprint = ERDModule(MODULE_NAME, __name__, static_url_path='/static')
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/panel/<int:trans_id>',
|
||||
methods=["POST"],
|
||||
endpoint='panel'
|
||||
)
|
||||
@login_required
|
||||
def panel(trans_id):
|
||||
"""
|
||||
This method calls index.html to render the erd tool.
|
||||
|
||||
Args:
|
||||
panel_title: Title of the panel
|
||||
"""
|
||||
|
||||
params = {
|
||||
'trans_id': trans_id,
|
||||
'title': request.form['title']
|
||||
}
|
||||
if request.args:
|
||||
params.update({k: v for k, v in request.args.items()})
|
||||
|
||||
if 'gen' in params:
|
||||
params['gen'] = True if params['gen'] == 'true' else False
|
||||
|
||||
close_url = request.form['close_url']
|
||||
|
||||
# We need client OS information to render correct Keyboard shortcuts
|
||||
user_agent = UserAgent(request.headers.get('User-Agent'))
|
||||
|
||||
"""
|
||||
Animations and transitions are not automatically GPU accelerated and by
|
||||
default use browser's slow rendering engine. We need to set 'translate3d'
|
||||
value of '-webkit-transform' property in order to use GPU. After applying
|
||||
this property under linux, Webkit calculates wrong position of the
|
||||
elements so panel contents are not visible. To make it work, we need to
|
||||
explicitly set '-webkit-transform' property to 'none' for .ajs-notifier,
|
||||
.ajs-message, .ajs-modal classes.
|
||||
|
||||
This issue is only with linux runtime application and observed in Query
|
||||
tool and debugger. When we open 'Open File' dialog then whole Query tool
|
||||
panel content is not visible though it contains HTML element in back end.
|
||||
|
||||
The port number should have already been set by the runtime if we're
|
||||
running in desktop mode.
|
||||
"""
|
||||
is_linux_platform = False
|
||||
|
||||
from sys import platform as _platform
|
||||
if "linux" in _platform:
|
||||
is_linux_platform = True
|
||||
|
||||
s = Server.query.filter_by(id=params['sid']).first()
|
||||
|
||||
params.update({
|
||||
'bgcolor': s.bgcolor,
|
||||
'fgcolor': s.fgcolor,
|
||||
'client_platform': user_agent.platform,
|
||||
'is_desktop_mode': app.PGADMIN_RUNTIME,
|
||||
'is_linux': is_linux_platform
|
||||
})
|
||||
|
||||
return render_template(
|
||||
"erd/index.html",
|
||||
title=underscore_unescape(params['title']),
|
||||
close_url=close_url,
|
||||
params=json.dumps(params),
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/initialize/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
methods=["POST"], endpoint='initialize'
|
||||
)
|
||||
@login_required
|
||||
def initialize_erd(trans_id, sgid, sid, did):
|
||||
"""
|
||||
This method is responsible for instantiating and initializing
|
||||
the erd tool object. It will also create a unique
|
||||
transaction id and store the information into session variable.
|
||||
|
||||
Args:
|
||||
sgid: Server group Id
|
||||
sid: Server Id
|
||||
did: Database Id
|
||||
"""
|
||||
# Read the data if present. Skipping read may cause connection
|
||||
# reset error if data is sent from the client
|
||||
if request.data:
|
||||
_ = request.data
|
||||
|
||||
conn = _get_connection(sid, did, trans_id)
|
||||
|
||||
return make_json_response(
|
||||
data={
|
||||
'connId': str(trans_id),
|
||||
'serverVersion': conn.manager.version,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_connection(sid, did, trans_id):
|
||||
"""
|
||||
Get the connection object of ERD.
|
||||
:param sid:
|
||||
:param did:
|
||||
:param trans_id:
|
||||
:return:
|
||||
"""
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
try:
|
||||
conn = manager.connection(did=did, conn_id=trans_id,
|
||||
auto_reconnect=True,
|
||||
use_binary_placeholder=True)
|
||||
status, msg = conn.connect()
|
||||
if not status:
|
||||
app.logger.error(msg)
|
||||
raise ConnectionLost(sid, conn.db, trans_id)
|
||||
|
||||
return conn
|
||||
except Exception as e:
|
||||
app.logger.error(e)
|
||||
raise
|
||||
|
||||
|
||||
@blueprint.route('/prequisite/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
methods=["GET"],
|
||||
endpoint='prequisite')
|
||||
@login_required
|
||||
def prequisite(trans_id, sgid, sid, did):
|
||||
conn = _get_connection(sid, did, trans_id)
|
||||
helper = ERDHelper(trans_id, sid, did)
|
||||
status, col_types = helper.get_types()
|
||||
|
||||
if not status:
|
||||
return internal_server_error(errormsg=col_types)
|
||||
|
||||
status, schemas = get_schemas(conn, show_system_objects=False)
|
||||
|
||||
if not status:
|
||||
return internal_server_error(errormsg=schemas)
|
||||
|
||||
return make_json_response(
|
||||
data={
|
||||
'col_types': col_types,
|
||||
'schemas': schemas['rows']
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route('/sql/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
methods=["POST"],
|
||||
endpoint='sql')
|
||||
@login_required
|
||||
def sql(trans_id, sgid, sid, did):
|
||||
data = json.loads(request.data, encoding='utf-8')
|
||||
helper = ERDHelper(trans_id, sid, did)
|
||||
conn = _get_connection(sid, did, trans_id)
|
||||
|
||||
sql = ''
|
||||
for tab_key, tab_data in data.get('nodes', {}).items():
|
||||
sql += '\n\n' + helper.get_table_sql(tab_data)
|
||||
|
||||
for link_key, link_data in data.get('links', {}).items():
|
||||
link_sql, name = fkey_utils.get_sql(conn, link_data, None)
|
||||
sql += '\n\n' + link_sql
|
||||
|
||||
return make_json_response(
|
||||
data=sql,
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route('/tables/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
methods=["GET"],
|
||||
endpoint='tables')
|
||||
@login_required
|
||||
def tables(trans_id, sgid, sid, did):
|
||||
helper = ERDHelper(trans_id, sid, did)
|
||||
status, tables = helper.get_all_tables()
|
||||
|
||||
if not status:
|
||||
return internal_server_error(errormsg=tables)
|
||||
|
||||
return make_json_response(
|
||||
data=tables,
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route('/close/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
methods=["DELETE"],
|
||||
endpoint='close')
|
||||
@login_required
|
||||
def close(trans_id, sgid, sid, did):
|
||||
manager = get_driver(
|
||||
PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
if manager is not None:
|
||||
conn = manager.connection(did=did, conn_id=trans_id)
|
||||
|
||||
# Release the connection
|
||||
if conn.connected():
|
||||
conn.cancel_transaction(trans_id, did=did)
|
||||
manager.release(did=did, conn_id=trans_id)
|
||||
return make_json_response(data={'status': True})
|
215
web/pgadmin/tools/erd/static/js/erd_module.js
Normal file
215
web/pgadmin/tools/erd/static/js/erd_module.js
Normal file
@ -0,0 +1,215 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
|
||||
import {getPanelTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
|
||||
import {getRandomInt} from 'sources/utils';
|
||||
|
||||
|
||||
export function setPanelTitle(erdToolPanel, panelTitle) {
|
||||
erdToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
|
||||
}
|
||||
|
||||
export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser, wcDocker) {
|
||||
/* Return back, this has been called more than once */
|
||||
if (pgBrowser.erd)
|
||||
return pgBrowser.erd;
|
||||
|
||||
pgBrowser.erd = {
|
||||
init: function() {
|
||||
if (this.initialized)
|
||||
return;
|
||||
|
||||
this.initialized = true;
|
||||
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
||||
|
||||
|
||||
// Define the nodes on which the menus to be appear
|
||||
var menus = [{
|
||||
name: 'erd',
|
||||
module: this,
|
||||
applies: ['tools'],
|
||||
callback: 'showErdTool',
|
||||
priority: 1,
|
||||
label: gettext('New ERD project(Beta)'),
|
||||
enable: this.erdToolEnabled,
|
||||
}];
|
||||
|
||||
pgBrowser.add_menus(menus);
|
||||
|
||||
// Creating a new pgBrowser frame to show the data.
|
||||
var erdFrameType = new pgBrowser.Frame({
|
||||
name: 'frm_erdtool',
|
||||
showTitle: true,
|
||||
isCloseable: true,
|
||||
isPrivate: true,
|
||||
url: 'about:blank',
|
||||
});
|
||||
|
||||
let self = this;
|
||||
/* Cache may take time to load for the first time
|
||||
* Keep trying till available
|
||||
*/
|
||||
let cacheIntervalId = setInterval(function() {
|
||||
if(pgBrowser.preference_version() > 0) {
|
||||
self.preferences = pgBrowser.get_preferences_for_module('erd');
|
||||
clearInterval(cacheIntervalId);
|
||||
}
|
||||
},0);
|
||||
|
||||
pgBrowser.onPreferencesChange('erd', function() {
|
||||
self.preferences = pgBrowser.get_preferences_for_module('erd');
|
||||
});
|
||||
|
||||
// Load the newly created frame
|
||||
erdFrameType.load(pgBrowser.docker);
|
||||
return this;
|
||||
},
|
||||
|
||||
erdToolEnabled: function(obj) {
|
||||
/* Same as query tool */
|
||||
var isEnabled = (() => {
|
||||
if (!_.isUndefined(obj) && !_.isNull(obj)) {
|
||||
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
|
||||
if (obj._type == 'database' && obj.allowConn) {
|
||||
return true;
|
||||
} else if (obj._type != 'database') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
return isEnabled;
|
||||
},
|
||||
|
||||
// Callback to draw schema diff for objects
|
||||
showErdTool: function(data, aciTreeIdentifier, gen=false) {
|
||||
const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
|
||||
if (node === undefined || !node.getData()) {
|
||||
Alertify.alert(
|
||||
gettext('ERD Error'),
|
||||
gettext('No object selected.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentData = getTreeNodeHierarchyFromIdentifier.call(
|
||||
pgBrowser,
|
||||
aciTreeIdentifier
|
||||
);
|
||||
|
||||
if(_.isUndefined(parentData.database)) {
|
||||
Alertify.alert(
|
||||
gettext('ERD Error'),
|
||||
gettext('Please select a database/database object.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const transId = getRandomInt(1, 9999999);
|
||||
const panelTitle = getPanelTitle(pgBrowser, aciTreeIdentifier);
|
||||
const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
|
||||
|
||||
let erdToolForm = `
|
||||
<form id="erdToolForm" action="${panelUrl}" method="post">
|
||||
<input id="title" name="title" hidden />
|
||||
<input name="close_url" value="${panelCloseUrl}" hidden />
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById("title").value = "${_.escape(panelTitle)}";
|
||||
document.getElementById("erdToolForm").submit();
|
||||
</script>
|
||||
`;
|
||||
|
||||
var open_new_tab = pgBrowser.get_preferences_for_module('browser').new_browser_tab_open;
|
||||
if (open_new_tab && open_new_tab.includes('erd_tool')) {
|
||||
var newWin = window.open('', '_blank');
|
||||
newWin.document.write(erdToolForm);
|
||||
newWin.document.title = panelTitle;
|
||||
} else {
|
||||
/* On successfully initialization find the dashboard panel,
|
||||
* create new panel and add it to the dashboard panel.
|
||||
*/
|
||||
var propertiesPanel = pgBrowser.docker.findPanels('properties');
|
||||
var erdToolPanel = pgBrowser.docker.addPanel('frm_erdtool', wcDocker.DOCK.STACKED, propertiesPanel[0]);
|
||||
|
||||
// Set panel title and icon
|
||||
setPanelTitle(erdToolPanel, 'Untitled');
|
||||
erdToolPanel.icon('fa fa-sitemap');
|
||||
erdToolPanel.focus();
|
||||
|
||||
// Listen on the panel closed event.
|
||||
erdToolPanel.on(wcDocker.EVENT.CLOSED, function() {
|
||||
$.ajax({
|
||||
url: panelCloseUrl,
|
||||
method: 'DELETE',
|
||||
});
|
||||
});
|
||||
|
||||
var openErdToolURL = function(j) {
|
||||
// add spinner element
|
||||
let $spinner_el =
|
||||
$(`<div class="pg-sp-container">
|
||||
<div class="pg-sp-content">
|
||||
<div class="row">
|
||||
<div class="col-12 pg-sp-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).appendTo($(j).data('embeddedFrame').$container);
|
||||
|
||||
let init_poller_id = setInterval(function() {
|
||||
var frameInitialized = $(j).data('frameInitialized');
|
||||
if (frameInitialized) {
|
||||
clearInterval(init_poller_id);
|
||||
var frame = $(j).data('embeddedFrame');
|
||||
if (frame) {
|
||||
frame.onLoaded(()=>{
|
||||
$spinner_el.remove();
|
||||
});
|
||||
frame.openHTML(erdToolForm);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
openErdToolURL(erdToolPanel);
|
||||
}
|
||||
},
|
||||
|
||||
getPanelUrls: function(transId, panelTitle, parentData, gen) {
|
||||
let openUrl = url_for('erd.panel', {
|
||||
trans_id: transId,
|
||||
});
|
||||
|
||||
openUrl += `?sgid=${parentData.server_group._id}`
|
||||
+`&sid=${parentData.server._id}`
|
||||
+`&server_type=${parentData.server.server_type}`
|
||||
+`&did=${parentData.database._id}`
|
||||
+`&gen=${gen}`;
|
||||
|
||||
let closeUrl = url_for('erd.close', {
|
||||
trans_id: transId,
|
||||
sgid: parentData.server_group._id,
|
||||
sid: parentData.server._id,
|
||||
did: parentData.database._id,
|
||||
});
|
||||
|
||||
return [openUrl, closeUrl];
|
||||
},
|
||||
};
|
||||
|
||||
return pgBrowser.erd;
|
||||
}
|
395
web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
Normal file
395
web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
Normal file
@ -0,0 +1,395 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
* The ERDCore is the middleware between the canvas engine and the UI DOM.
|
||||
*/
|
||||
import createEngine from '@projectstorm/react-diagrams';
|
||||
import {DagreEngine, PathFindingLinkFactory, PortModelAlignment} from '@projectstorm/react-diagrams';
|
||||
import { ZoomCanvasAction } from '@projectstorm/react-canvas-core';
|
||||
|
||||
import {TableNodeFactory, TableNodeModel } from './nodes/TableNode';
|
||||
import {OneToManyLinkFactory, OneToManyLinkModel } from './links/OneToManyLink';
|
||||
import { OneToManyPortFactory } from './ports/OneToManyPort';
|
||||
import ERDModel from './ERDModel';
|
||||
|
||||
export default class ERDCore {
|
||||
constructor() {
|
||||
this._cache = {};
|
||||
this.table_counter = 1;
|
||||
this.node_position_updating = false;
|
||||
this.link_position_updating = false;
|
||||
this.initializeEngine();
|
||||
this.initializeModel();
|
||||
this.computeTableCounter();
|
||||
}
|
||||
|
||||
initializeEngine() {
|
||||
this.engine = createEngine({
|
||||
registerDefaultDeleteItemsAction: false,
|
||||
registerDefaultZoomCanvasAction: false,
|
||||
});
|
||||
this.dagre_engine = new DagreEngine({
|
||||
graph: {
|
||||
marginx: 5,
|
||||
marginy: 5,
|
||||
},
|
||||
includeLinks: true,
|
||||
});
|
||||
|
||||
this.engine.getNodeFactories().registerFactory(new TableNodeFactory());
|
||||
this.engine.getLinkFactories().registerFactory(new OneToManyLinkFactory());
|
||||
this.engine.getPortFactories().registerFactory(new OneToManyPortFactory());
|
||||
this.registerKeyAction(new ZoomCanvasAction({inverseZoom: true}));
|
||||
}
|
||||
|
||||
initializeModel(data, callback=()=>{}) {
|
||||
let model = new ERDModel();
|
||||
if(data) {
|
||||
model.deserializeModel(data, this.engine);
|
||||
}
|
||||
|
||||
const registerNodeEvents = (node) => {
|
||||
node.registerListener({
|
||||
eventDidFire: (e) => {
|
||||
if(e.function === 'selectionChanged') {
|
||||
this.fireEvent({}, 'nodesSelectionChanged', true);
|
||||
}
|
||||
else if(e.function === 'showNote') {
|
||||
this.fireEvent({node: e.entity}, 'showNote', true);
|
||||
}
|
||||
else if(e.function === 'editNode') {
|
||||
this.fireEvent({node: e.entity}, 'editNode', true);
|
||||
}
|
||||
else if(e.function === 'nodeUpdated') {
|
||||
this.fireEvent({}, 'nodesUpdated', true);
|
||||
}
|
||||
else if(e.function === 'positionChanged') {
|
||||
/* Eat up the excessive positionChanged events if node is dragged continuosly */
|
||||
if(!this.node_position_updating) {
|
||||
this.node_position_updating = true;
|
||||
this.fireEvent({}, 'nodesUpdated', true);
|
||||
setTimeout(()=>{
|
||||
this.node_position_updating = false;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const registerLinkEvents = (link) => {
|
||||
link.registerListener({
|
||||
eventDidFire: (e) => {
|
||||
if(e.function === 'selectionChanged') {
|
||||
this.fireEvent({}, 'linksSelectionChanged', true);
|
||||
}
|
||||
else if(e.function === 'positionChanged') {
|
||||
/* positionChanged is triggered manually in Link */
|
||||
/* Eat up the excessive positionChanged events if link is dragged continuosly */
|
||||
if(!this.link_position_updating) {
|
||||
this.link_position_updating = true;
|
||||
this.fireEvent({}, 'linksUpdated', true);
|
||||
setTimeout(()=>{
|
||||
this.link_position_updating = false;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/* Register events for deserialized data */
|
||||
model.getNodes().forEach(node => {
|
||||
registerNodeEvents(node);
|
||||
});
|
||||
model.getLinks().forEach(link => {
|
||||
registerLinkEvents(link);
|
||||
});
|
||||
|
||||
/* Listen and register events for new data */
|
||||
model.registerListener({
|
||||
'nodesUpdated': (e)=>{
|
||||
if(e.isCreated) {
|
||||
registerNodeEvents(e.node);
|
||||
}
|
||||
},
|
||||
'linksUpdated': (e)=>{
|
||||
if(e.isCreated) {
|
||||
registerLinkEvents(e.link);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
model.setGridSize(15);
|
||||
this.engine.setModel(model);
|
||||
callback();
|
||||
}
|
||||
|
||||
computeTableCounter() {
|
||||
/* Some inteligence can be added to set the counter */
|
||||
this.table_counter = 1;
|
||||
}
|
||||
|
||||
setCache(data, value) {
|
||||
if(typeof(data) == 'string') {
|
||||
this._cache[data] = value;
|
||||
} else {
|
||||
this._cache = {
|
||||
...this._cache,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getCache(key) {
|
||||
return key ? this._cache[key]: this._cache;
|
||||
}
|
||||
|
||||
registerModelEvent(eventName, callback) {
|
||||
this.getModel().registerListener({
|
||||
[eventName]: callback,
|
||||
});
|
||||
}
|
||||
|
||||
getNextTableName() {
|
||||
let newTableName = `newtable${this.table_counter}`;
|
||||
this.table_counter++;
|
||||
return newTableName;
|
||||
}
|
||||
|
||||
getEngine() {return this.engine;}
|
||||
|
||||
getModel() {return this.getEngine().getModel();}
|
||||
|
||||
getNewNode(initData) {
|
||||
return this.getEngine().getNodeFactories().getFactory('table').generateModel({
|
||||
initialConfig: {
|
||||
otherInfo: {
|
||||
data:initData,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getNewLink(type, initData) {
|
||||
return this.getEngine().getLinkFactories().getFactory(type).generateModel({
|
||||
initialConfig: {
|
||||
data:initData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getNewPort(type, initData, initOptions) {
|
||||
return this.getEngine().getPortFactories().getFactory(type).generateModel({
|
||||
initialConfig: {
|
||||
data:initData,
|
||||
options:initOptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addNode(data, position=[50, 50]) {
|
||||
let newNode = this.getNewNode(data);
|
||||
this.clearSelection();
|
||||
newNode.setPosition(position[0], position[1]);
|
||||
this.getModel().addNode(newNode);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
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, 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, alignment:PortModelAlignment.RIGHT}));
|
||||
}
|
||||
|
||||
/* Link the ports */
|
||||
let newLink = this.getNewLink(type, data);
|
||||
newLink.setSourcePort(sourcePort);
|
||||
newLink.setTargetPort(targetPort);
|
||||
this.getModel().addLink(newLink);
|
||||
return newLink;
|
||||
}
|
||||
|
||||
serialize(version) {
|
||||
return {
|
||||
version: version||0,
|
||||
data: this.getModel().serialize(),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(json_data) {
|
||||
if(json_data.version) {
|
||||
this.initializeModel(json_data.data);
|
||||
}
|
||||
}
|
||||
|
||||
serializeData() {
|
||||
let nodes = {}, links = {};
|
||||
let nodesDict = this.getModel().getNodesDict();
|
||||
|
||||
Object.keys(nodesDict).forEach((id)=>{
|
||||
nodes[id] = nodesDict[id].serializeData();
|
||||
});
|
||||
|
||||
this.getModel().getLinks().map((link)=>{
|
||||
links[link.getID()] = link.serializeData(nodesDict);
|
||||
});
|
||||
|
||||
/* Separate the links from nodes so that we don't have any dependancy issues */
|
||||
return {
|
||||
'nodes': nodes,
|
||||
'links': links,
|
||||
};
|
||||
}
|
||||
|
||||
deserializeData(data){
|
||||
let oidUidMap = {};
|
||||
let uidFks = [];
|
||||
data.forEach((node)=>{
|
||||
let newData = {
|
||||
name: node.name,
|
||||
schema: node.schema,
|
||||
description: node.description,
|
||||
columns: node.columns,
|
||||
primary_key: node.primary_key,
|
||||
};
|
||||
let newNode = this.addNode(newData);
|
||||
oidUidMap[node.oid] = newNode.getID();
|
||||
if(node.foreign_key) {
|
||||
node.foreign_key.forEach((a_fk)=>{
|
||||
uidFks.push({
|
||||
uid: newNode.getID(),
|
||||
data: a_fk.columns[0],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* Lets use the oidUidMap for creating the links */
|
||||
uidFks.forEach((fkData)=>{
|
||||
let tableNodesDict = this.getModel().getNodesDict();
|
||||
let newData = {
|
||||
local_table_uid: fkData.uid,
|
||||
local_column_attnum: undefined,
|
||||
referenced_table_uid: oidUidMap[fkData.data.references],
|
||||
referenced_column_attnum: undefined,
|
||||
};
|
||||
|
||||
let sourceNode = tableNodesDict[newData.referenced_table_uid];
|
||||
let targetNode = tableNodesDict[newData.local_table_uid];
|
||||
|
||||
newData.local_column_attnum = _.find(targetNode.getColumns(), (col)=>col.name==fkData.data.local_column).attnum;
|
||||
newData.referenced_column_attnum = _.find(sourceNode.getColumns(), (col)=>col.name==fkData.data.referenced).attnum;
|
||||
|
||||
this.addLink(newData, 'onetomany');
|
||||
});
|
||||
setTimeout(this.dagreDistributeNodes.bind(this), 0);
|
||||
}
|
||||
|
||||
repaint() {
|
||||
this.getEngine().repaintCanvas();
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.getEngine()
|
||||
.getModel()
|
||||
.clearSelection();
|
||||
}
|
||||
|
||||
getNodesData() {
|
||||
return this.getEngine().getModel().getNodes().map((node)=>{
|
||||
return node.getData();
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedNodes() {
|
||||
return this.getEngine()
|
||||
.getModel()
|
||||
.getSelectedEntities()
|
||||
.filter(entity => entity instanceof TableNodeModel);
|
||||
}
|
||||
|
||||
getSelectedLinks() {
|
||||
return this.getEngine()
|
||||
.getModel()
|
||||
.getSelectedEntities()
|
||||
.filter(entity => entity instanceof OneToManyLinkModel);
|
||||
}
|
||||
|
||||
dagreDistributeNodes() {
|
||||
this.dagre_engine.redistribute(this.getModel());
|
||||
this.getEngine()
|
||||
.getLinkFactories()
|
||||
.getFactory(PathFindingLinkFactory.NAME)
|
||||
.calculateRoutingMatrix();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
let model = this.getEngine().getModel();
|
||||
if(model){
|
||||
model.setZoomLevel(model.getZoomLevel() + 25);
|
||||
this.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
zoomOut() {
|
||||
let model = this.getEngine().getModel();
|
||||
if(model) {
|
||||
model.setZoomLevel(model.getZoomLevel() - 25);
|
||||
this.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
zoomToFit() {
|
||||
this.getEngine().zoomToFit();
|
||||
}
|
||||
|
||||
// Sample call: this.fireAction({ type: 'keydown', ctrlKey: true, code: 'KeyN' });
|
||||
fireAction(event) {
|
||||
this.getEngine().getActionEventBus().fireAction({
|
||||
event: {
|
||||
...event,
|
||||
key: '',
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fireEvent(data, eventName, model=false) {
|
||||
if(model) {
|
||||
this.getEngine().getModel().fireEvent(data, eventName);
|
||||
} else {
|
||||
this.getEngine().fireEvent(data, eventName);
|
||||
}
|
||||
}
|
||||
|
||||
registerKeyAction(action) {
|
||||
this.getEngine().getActionEventBus().registerAction(action);
|
||||
}
|
||||
|
||||
deregisterKeyAction(action) {
|
||||
this.getEngine().getActionEventBus().deregisterAction(action);
|
||||
}
|
||||
}
|
21
web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
Normal file
21
web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
Normal file
@ -0,0 +1,21 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { DiagramModel } from '@projectstorm/react-diagrams';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class ERDModel extends DiagramModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
getNodesDict() {
|
||||
return _.fromPairs(this.getNodes().map(node => [node.getID(), node]));
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import * as commonUtils from 'sources/utils';
|
||||
|
||||
export default class DialogWrapper {
|
||||
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
|
||||
jquery, pgBrowser, alertify, backform, backgrid) {
|
||||
|
||||
this.dialogContainerSelector = dialogContainerSelector;
|
||||
this.dialogTitle = dialogTitle;
|
||||
this.jquery = jquery;
|
||||
this.pgBrowser = pgBrowser;
|
||||
this.alertify = alertify;
|
||||
this.backform = backform;
|
||||
this.backgrid = backgrid;
|
||||
this.typeOfDialog = typeOfDialog;
|
||||
}
|
||||
|
||||
main(title, dialogModel, okCallback) {
|
||||
this.set('title', title);
|
||||
this.dialogModel = dialogModel;
|
||||
this.okCallback = okCallback;
|
||||
}
|
||||
|
||||
build() {
|
||||
this.alertify.pgDialogBuild.apply(this);
|
||||
}
|
||||
|
||||
disableOKButton() {
|
||||
this.__internal.buttons[1].element.disabled = true;
|
||||
}
|
||||
|
||||
enableOKButton() {
|
||||
this.__internal.buttons[1].element.disabled = false;
|
||||
}
|
||||
|
||||
focusOnDialog(alertifyDialog) {
|
||||
let backform_tab = this.jquery(alertifyDialog.elements.body).find('.backform-tab');
|
||||
backform_tab.attr('tabindex', -1);
|
||||
this.pgBrowser.keyboardNavigation.getDialogTabNavigator(this.jquery(alertifyDialog.elements.dialog));
|
||||
let container = backform_tab.find('.tab-content:first > .tab-pane.active:first');
|
||||
|
||||
if(container.length === 0 && alertifyDialog.elements.content.innerHTML) {
|
||||
container = this.jquery(alertifyDialog.elements.content);
|
||||
}
|
||||
commonUtils.findAndSetFocus(container);
|
||||
}
|
||||
|
||||
setup() {
|
||||
return {
|
||||
buttons: [{
|
||||
text: gettext('Cancel'),
|
||||
key: 27,
|
||||
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
|
||||
'data-btn-name': 'cancel',
|
||||
}, {
|
||||
text: gettext('OK'),
|
||||
key: 13,
|
||||
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
|
||||
'data-btn-name': 'ok',
|
||||
}],
|
||||
// Set options for dialog
|
||||
options: {
|
||||
title: this.dialogTitle,
|
||||
//disable both padding and overflow control.
|
||||
padding: !1,
|
||||
overflow: !1,
|
||||
model: 0,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
pinnable: false,
|
||||
closableByDimmer: false,
|
||||
modal: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
prepare() {
|
||||
const $container = this.jquery(this.dialogContainerSelector);
|
||||
const dialog = this.createDialog($container);
|
||||
dialog.render();
|
||||
this.elements.content.innerHTML = '';
|
||||
this.elements.content.appendChild($container.get(0));
|
||||
this.jquery(this.elements.body.childNodes[0]).addClass(
|
||||
'alertify_tools_dialog_properties obj_properties'
|
||||
);
|
||||
const statusBar = this.jquery(
|
||||
'<div class=\'pg-prop-status-bar pg-prop-status-bar-absolute pg-el-xs-12 d-none\'>' +
|
||||
' <div class="error-in-footer"> ' +
|
||||
' <div class="d-flex px-2 py-1"> ' +
|
||||
' <div class="pr-2"> ' +
|
||||
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
|
||||
' </div> ' +
|
||||
' <div class="alert-text" role="alert"></div> ' +
|
||||
' <div class="ml-auto close-error-bar"> ' +
|
||||
' <a aria-label="' + gettext('Close error bar') + '" class="close-error fa fa-times text-danger"></a> ' +
|
||||
' </div> ' +
|
||||
' </div> ' +
|
||||
' </div> ' +
|
||||
'</div>').appendTo($container);
|
||||
|
||||
statusBar.find('.close-error').on('click', ()=>{
|
||||
statusBar.addClass('d-none');
|
||||
});
|
||||
|
||||
var onSessionInvalid = (msg) => {
|
||||
statusBar.find('.alert-text').text(msg);
|
||||
statusBar.removeClass('d-none');
|
||||
this.disableOKButton();
|
||||
return true;
|
||||
};
|
||||
|
||||
var onSessionValidated = () => {
|
||||
statusBar.find('.alert-text').text('');
|
||||
statusBar.addClass('d-none');
|
||||
this.enableOKButton();
|
||||
return true;
|
||||
};
|
||||
|
||||
this.dialogModel.on('pgadmin-session:valid', onSessionValidated);
|
||||
this.dialogModel.on('pgadmin-session:invalid', onSessionInvalid);
|
||||
this.dialogModel.startNewSession();
|
||||
this.disableOKButton();
|
||||
this.focusOnDialog(this);
|
||||
}
|
||||
|
||||
callback(event) {
|
||||
if (this.wasOkButtonPressed(event)) {
|
||||
this.okCallback(this.view.model.toJSON(true));
|
||||
}
|
||||
}
|
||||
|
||||
createDialog($container) {
|
||||
let fields = this.backform.generateViewSchema(
|
||||
null, this.dialogModel, 'create', null, null, true, null
|
||||
);
|
||||
|
||||
this.view = new this.backform.Dialog({
|
||||
el: $container,
|
||||
model: this.dialogModel,
|
||||
schema: fields,
|
||||
});
|
||||
|
||||
return this.view;
|
||||
}
|
||||
|
||||
wasOkButtonPressed(event) {
|
||||
return event.button['data-btn-name'] === 'ok';
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import Backform from 'sources/backform.pgadmin';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import $ from 'jquery';
|
||||
|
||||
import DialogWrapper from './DialogWrapper';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class ManyToManyDialog {
|
||||
constructor(pgBrowser) {
|
||||
this.pgBrowser = pgBrowser;
|
||||
}
|
||||
|
||||
dialogName() {
|
||||
return 'manytomany_dialog';
|
||||
}
|
||||
|
||||
getDataModel(attributes, tableNodesDict) {
|
||||
const parseColumns = (columns)=>{
|
||||
return columns.map((col)=>{
|
||||
return {
|
||||
value: col.attnum, label: col.name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
let dialogModel = this.pgBrowser.DataModel.extend({
|
||||
defaults: {
|
||||
left_table_uid: undefined,
|
||||
left_table_column_attnum: undefined,
|
||||
right_table_uid: undefined,
|
||||
right_table_column_attnum: undefined,
|
||||
},
|
||||
schema: [{
|
||||
id: 'left_table_uid', label: gettext('Left Table'),
|
||||
type: 'select2', readonly: true,
|
||||
options: ()=>{
|
||||
let retOpts = [];
|
||||
_.forEach(tableNodesDict, (node, uid)=>{
|
||||
let [schema, name] = node.getSchemaTableName();
|
||||
retOpts.push({value: uid, label: `(${schema}) ${name}`});
|
||||
});
|
||||
return retOpts;
|
||||
},
|
||||
}, {
|
||||
id: 'left_table_column_attnum', label: gettext('Left table Column'),
|
||||
type: 'select2', disabled: false, first_empty: false,
|
||||
editable: true, options: (view)=>{
|
||||
return parseColumns(tableNodesDict[view.model.get('left_table_uid')].getColumns());
|
||||
},
|
||||
},{
|
||||
id: 'right_table_uid', label: gettext('Right Table'),
|
||||
type: 'select2', disabled: false,
|
||||
editable: true, options: (view)=>{
|
||||
let retOpts = [];
|
||||
_.forEach(tableNodesDict, (node, uid)=>{
|
||||
if(uid === view.model.get('left_table_uid')) {
|
||||
return;
|
||||
}
|
||||
let [schema, name] = node.getSchemaTableName();
|
||||
retOpts.push({value: uid, label: `(${schema}) ${name}`});
|
||||
});
|
||||
return retOpts;
|
||||
},
|
||||
},{
|
||||
id: 'right_table_column_attnum', label: gettext('Right table Column'),
|
||||
type: 'select2', disabled: false, deps: ['right_table_uid'],
|
||||
editable: true, options: (view)=>{
|
||||
if(view.model.get('right_table_uid')) {
|
||||
return parseColumns(tableNodesDict[view.model.get('right_table_uid')].getColumns());
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}],
|
||||
validate: function(keys) {
|
||||
var msg = undefined;
|
||||
|
||||
// Nothing to validate
|
||||
if (keys && keys.length == 0) {
|
||||
this.errorModel.clear();
|
||||
return null;
|
||||
} else {
|
||||
this.errorModel.clear();
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('left_table_column_attnum')) || this.get('left_table_column_attnum') == '') {
|
||||
msg = gettext('Select the left table column.');
|
||||
this.errorModel.set('left_table_column_attnum', msg);
|
||||
return msg;
|
||||
}
|
||||
if (_.isUndefined(this.get('right_table_uid')) || this.get('right_table_uid') == '') {
|
||||
msg = gettext('Select the right table.');
|
||||
this.errorModel.set('right_table_uid', msg);
|
||||
return msg;
|
||||
}
|
||||
if (_.isUndefined(this.get('right_table_column_attnum')) || this.get('right_table_column_attnum') == '') {
|
||||
msg = gettext('Select the right table column.');
|
||||
this.errorModel.set('right_table_column_attnum', msg);
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new dialogModel(attributes);
|
||||
}
|
||||
|
||||
createOrGetDialog(title) {
|
||||
const dialogName = this.dialogName();
|
||||
|
||||
if (!Alertify[dialogName]) {
|
||||
Alertify.dialog(dialogName, () => {
|
||||
return new DialogWrapper(
|
||||
`<div class="${dialogName}"></div>`,
|
||||
title,
|
||||
null,
|
||||
$,
|
||||
this.pgBrowser,
|
||||
Alertify,
|
||||
Backform
|
||||
);
|
||||
});
|
||||
}
|
||||
return Alertify[dialogName];
|
||||
}
|
||||
|
||||
show(title, attributes, tablesData, sVersion, callback) {
|
||||
let dialogTitle = title || gettext('Unknown');
|
||||
const dialog = this.createOrGetDialog('manytomany_dialog');
|
||||
dialog(dialogTitle, this.getDataModel(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import Backform from 'sources/backform.pgadmin';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import $ from 'jquery';
|
||||
|
||||
import DialogWrapper from './DialogWrapper';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class OneToManyDialog {
|
||||
constructor(pgBrowser) {
|
||||
this.pgBrowser = pgBrowser;
|
||||
}
|
||||
|
||||
dialogName() {
|
||||
return 'onetomany_dialog';
|
||||
}
|
||||
|
||||
getDataModel(attributes, tableNodesDict) {
|
||||
const parseColumns = (columns)=>{
|
||||
return columns.map((col)=>{
|
||||
return {
|
||||
value: col.attnum, label: col.name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
let dialogModel = this.pgBrowser.DataModel.extend({
|
||||
defaults: {
|
||||
local_table_uid: undefined,
|
||||
local_column_attnum: undefined,
|
||||
referenced_table_uid: undefined,
|
||||
referenced_column_attnum: undefined,
|
||||
},
|
||||
schema: [{
|
||||
id: 'local_table_uid', label: gettext('Local Table'),
|
||||
type: 'select2', readonly: true,
|
||||
options: ()=>{
|
||||
let retOpts = [];
|
||||
_.forEach(tableNodesDict, (node, uid)=>{
|
||||
let [schema, name] = node.getSchemaTableName();
|
||||
retOpts.push({value: uid, label: `(${schema}) ${name}`});
|
||||
});
|
||||
return retOpts;
|
||||
},
|
||||
}, {
|
||||
id: 'local_column_attnum', label: gettext('Local Column'),
|
||||
type: 'select2', disabled: false, first_empty: false,
|
||||
editable: true, options: (view)=>{
|
||||
return parseColumns(tableNodesDict[view.model.get('local_table_uid')].getColumns());
|
||||
},
|
||||
},{
|
||||
id: 'referenced_table_uid', label: gettext('Referenced Table'),
|
||||
type: 'select2', disabled: false,
|
||||
editable: true, options: (view)=>{
|
||||
let retOpts = [];
|
||||
_.forEach(tableNodesDict, (node, uid)=>{
|
||||
if(uid === view.model.get('local_table_uid')) {
|
||||
return;
|
||||
}
|
||||
let [schema, name] = node.getSchemaTableName();
|
||||
retOpts.push({value: uid, label: `(${schema}) ${name}`});
|
||||
});
|
||||
return retOpts;
|
||||
},
|
||||
},{
|
||||
id: 'referenced_column_attnum', label: gettext('Referenced Column'),
|
||||
type: 'select2', disabled: false, deps: ['referenced_table_uid'],
|
||||
editable: true, options: (view)=>{
|
||||
if(view.model.get('referenced_table_uid')) {
|
||||
return parseColumns(tableNodesDict[view.model.get('referenced_table_uid')].getColumns());
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}],
|
||||
validate: function(keys) {
|
||||
var msg = undefined;
|
||||
|
||||
// Nothing to validate
|
||||
if (keys && keys.length == 0) {
|
||||
this.errorModel.clear();
|
||||
return null;
|
||||
} else {
|
||||
this.errorModel.clear();
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('local_column_attnum')) || this.get('local_column_attnum') == '') {
|
||||
msg = gettext('Select the local column.');
|
||||
this.errorModel.set('local_column_attnum', msg);
|
||||
return msg;
|
||||
}
|
||||
if (_.isUndefined(this.get('referenced_table_uid')) || this.get('referenced_table_uid') == '') {
|
||||
msg = gettext('Select the referenced table.');
|
||||
this.errorModel.set('referenced_table_uid', msg);
|
||||
return msg;
|
||||
}
|
||||
if (_.isUndefined(this.get('referenced_column_attnum')) || this.get('referenced_column_attnum') == '') {
|
||||
msg = gettext('Select the referenced table column.');
|
||||
this.errorModel.set('referenced_column_attnum', msg);
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new dialogModel(attributes);
|
||||
}
|
||||
|
||||
createOrGetDialog(title) {
|
||||
const dialogName = this.dialogName();
|
||||
|
||||
if (!Alertify[dialogName]) {
|
||||
Alertify.dialog(dialogName, () => {
|
||||
return new DialogWrapper(
|
||||
`<div class="${dialogName}"></div>`,
|
||||
title,
|
||||
null,
|
||||
$,
|
||||
this.pgBrowser,
|
||||
Alertify,
|
||||
Backform
|
||||
);
|
||||
});
|
||||
}
|
||||
return Alertify[dialogName];
|
||||
}
|
||||
|
||||
show(title, attributes, tablesData, sVersion, callback) {
|
||||
let dialogTitle = title || gettext('Unknown');
|
||||
const dialog = this.createOrGetDialog('onetomany_dialog');
|
||||
dialog(dialogTitle, this.getDataModel(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
|
||||
}
|
||||
}
|
739
web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
Normal file
739
web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
Normal file
@ -0,0 +1,739 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import Backgrid from 'sources/backgrid.pgadmin';
|
||||
import Backform from 'sources/backform.pgadmin';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
import DialogWrapper from './DialogWrapper';
|
||||
|
||||
export function transformToSupported(data) {
|
||||
/* Table fields */
|
||||
data = _.pick(data, ['oid', 'name', 'schema', 'description', 'columns', 'primary_key', 'foreign_key']);
|
||||
|
||||
/* Columns */
|
||||
data['columns'] = data['columns'].map((column)=>{
|
||||
return _.pick(column,[
|
||||
'name','description','attowner','attnum','cltype','min_val_attlen','min_val_attprecision','max_val_attlen',
|
||||
'max_val_attprecision', 'is_primary_key','attnotnull','attlen','attprecision','attidentity','colconstype',
|
||||
'seqincrement','seqstart','seqmin','seqmax','seqcache','seqcycle',
|
||||
]);
|
||||
});
|
||||
|
||||
/* Primary key */
|
||||
data['primary_key'] = data['primary_key'].map((primary_key)=>{
|
||||
primary_key = _.pick(primary_key, ['columns']);
|
||||
primary_key['columns'] = primary_key['columns'].map((column)=>{
|
||||
return _.pick(column, ['column']);
|
||||
});
|
||||
return primary_key;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default class TableDialog {
|
||||
constructor(pgBrowser) {
|
||||
this.pgBrowser = pgBrowser;
|
||||
}
|
||||
|
||||
dialogName() {
|
||||
return 'entity_dialog';
|
||||
}
|
||||
|
||||
getDataModel(attributes, colTypes, schemas, sVersion) {
|
||||
let dialogObj = this;
|
||||
let columnsModel = this.pgBrowser.DataModel.extend({
|
||||
idAttribute: 'attnum',
|
||||
defaults: {
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
attowner: undefined,
|
||||
attnum: undefined,
|
||||
cltype: undefined,
|
||||
min_val_attlen: undefined,
|
||||
min_val_attprecision: undefined,
|
||||
max_val_attlen: undefined,
|
||||
max_val_attprecision: undefined,
|
||||
is_primary_key: false,
|
||||
attnotnull: false,
|
||||
attlen: null,
|
||||
attprecision: null,
|
||||
attidentity: 'a',
|
||||
colconstype: 'n',
|
||||
seqincrement: undefined,
|
||||
seqstart: undefined,
|
||||
seqmin: undefined,
|
||||
seqmax: undefined,
|
||||
seqcache: undefined,
|
||||
seqcycle: undefined,
|
||||
},
|
||||
initialize: function(attrs) {
|
||||
if (_.size(attrs) !== 0) {
|
||||
this.set({
|
||||
'old_attidentity': this.get('attidentity'),
|
||||
}, {silent: true});
|
||||
}
|
||||
dialogObj.pgBrowser.DataModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
if(!this.get('cltype') && colTypes.length > 0) {
|
||||
this.set({
|
||||
'cltype': colTypes[0]['value'],
|
||||
}, {silent: true});
|
||||
}
|
||||
},
|
||||
schema: [{
|
||||
id: 'name', label: gettext('Name'), cell: 'string',
|
||||
type: 'text', disabled: false,
|
||||
cellHeaderClasses: 'width_percent_30',
|
||||
editable: true,
|
||||
}, {
|
||||
// Need to show this field only when creating new table
|
||||
// [in SubNode control]
|
||||
id: 'is_primary_key', label: gettext('Primary key?'),
|
||||
cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
|
||||
deps: ['name'], cellHeaderClasses: 'width_percent_5',
|
||||
options: {
|
||||
onText: gettext('Yes'), offText: gettext('No'),
|
||||
onColor: 'success', offColor: 'ternary',
|
||||
},
|
||||
visible: function () {
|
||||
return true;
|
||||
},
|
||||
disabled: false,
|
||||
editable: true,
|
||||
}, {
|
||||
id: 'description', label: gettext('Comment'), cell: 'string', type: 'multiline',
|
||||
}, {
|
||||
id: 'cltype', label: gettext('Data type'),
|
||||
cell: 'select2',
|
||||
type: 'select2', disabled: false,
|
||||
control: 'select2',
|
||||
cellHeaderClasses: 'width_percent_30',
|
||||
select2: { allowClear: false, first_empty: false }, group: gettext('Definition'),
|
||||
options: function () {
|
||||
return colTypes;
|
||||
},
|
||||
}, {
|
||||
id: 'attlen', label: gettext('Length/Precision'), cell: Backgrid.Extension.IntegerDepCell,
|
||||
deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses: 'width_percent_20',
|
||||
disabled: function (m) {
|
||||
var of_type = m.get('cltype'),
|
||||
flag = true;
|
||||
_.each(colTypes, function (o) {
|
||||
if (of_type == o.value) {
|
||||
if (o.length) {
|
||||
m.set('min_val_attlen', o.min_val, { silent: true });
|
||||
m.set('max_val_attlen', o.max_val, { silent: true });
|
||||
flag = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
flag && setTimeout(function () {
|
||||
if (m.get('attlen')) {
|
||||
m.set('attlen', null);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return flag;
|
||||
},
|
||||
editable: function (m) {
|
||||
var of_type = m.get('cltype'),
|
||||
flag = false;
|
||||
_.each(colTypes, function (o) {
|
||||
if (of_type == o.value) {
|
||||
if (o.length) {
|
||||
m.set('min_val_attlen', o.min_val, { silent: true });
|
||||
m.set('max_val_attlen', o.max_val, { silent: true });
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
!flag && setTimeout(function () {
|
||||
if (m.get('attlen')) {
|
||||
m.set('attlen', null);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return flag;
|
||||
},
|
||||
}, {
|
||||
id: 'attprecision', label: gettext('Scale'), cell: Backgrid.Extension.IntegerDepCell,
|
||||
deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses: 'width_percent_20',
|
||||
disabled: function (m) {
|
||||
var of_type = m.get('cltype'),
|
||||
flag = true;
|
||||
_.each(colTypes, function (o) {
|
||||
if (of_type == o.value) {
|
||||
if (o.precision) {
|
||||
m.set('min_val_attprecision', 0, { silent: true });
|
||||
m.set('max_val_attprecision', o.max_val, { silent: true });
|
||||
flag = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
flag && setTimeout(function () {
|
||||
if (m.get('attprecision')) {
|
||||
m.set('attprecision', null);
|
||||
}
|
||||
}, 10);
|
||||
return flag;
|
||||
},
|
||||
editable: function (m) {
|
||||
if (!colTypes) {
|
||||
// datatypes not loaded yet, may be this call is from CallByNeed from backgrid cell initialize.
|
||||
return true;
|
||||
}
|
||||
|
||||
var of_type = m.get('cltype'),
|
||||
flag = false;
|
||||
_.each(colTypes, function (o) {
|
||||
if (of_type == o.value) {
|
||||
if (o.precision) {
|
||||
m.set('min_val_attprecision', 0, { silent: true });
|
||||
m.set('max_val_attprecision', o.max_val, { silent: true });
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
!flag && setTimeout(function () {
|
||||
if (m.get('attprecision')) {
|
||||
m.set('attprecision', null);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return flag;
|
||||
},
|
||||
}, {
|
||||
id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch',
|
||||
type: 'switch', cellHeaderClasses: 'width_percent_20',
|
||||
group: gettext('Constraints'),
|
||||
options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'ternary' },
|
||||
disabled: function(m) {
|
||||
if (m.get('colconstype') == 'i') {
|
||||
setTimeout(function () {
|
||||
m.set('attnotnull', true);
|
||||
}, 10);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}, {
|
||||
id: 'colconstype',
|
||||
label: gettext('Type'),
|
||||
cell: 'string',
|
||||
type: 'radioModern',
|
||||
controlsClassName: 'pgadmin-controls col-12 col-sm-9',
|
||||
controlLabelClassName: 'control-label col-sm-3 col-12',
|
||||
group: gettext('Constraints'),
|
||||
options: function() {
|
||||
var opt_array = [
|
||||
{'label': gettext('NONE'), 'value': 'n'},
|
||||
{'label': gettext('IDENTITY'), 'value': 'i'},
|
||||
];
|
||||
|
||||
if (sVersion >= 120000) {
|
||||
opt_array.push({
|
||||
'label': gettext('GENERATED'),
|
||||
'value': 'g',
|
||||
});
|
||||
}
|
||||
|
||||
return opt_array;
|
||||
},
|
||||
disabled: false,
|
||||
visible: function() {
|
||||
if (sVersion >= 100000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}, {
|
||||
id: 'attidentity', label: gettext('Identity'), control: 'select2',
|
||||
cell: 'select2',
|
||||
select2: {placeholder: 'Select identity', allowClear: false, width: '100%'},
|
||||
group: gettext('Constraints'),
|
||||
'options': [
|
||||
{label: gettext('ALWAYS'), value: 'a'},
|
||||
{label: gettext('BY DEFAULT'), value: 'd'},
|
||||
],
|
||||
deps: ['colconstype'],
|
||||
visible: function(m) {
|
||||
if (sVersion >= 100000 && m.isTypeIdentity(m)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
disabled: function() {
|
||||
return false;
|
||||
},
|
||||
}, {
|
||||
id: 'seqincrement', label: gettext('Increment'), type: 'int',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
|
||||
visible: 'isTypeIdentity',
|
||||
},{
|
||||
id: 'seqstart', label: gettext('Start'), type: 'int',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
disabled: function(m) {
|
||||
let isIdentity = m.get('attidentity');
|
||||
if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity))
|
||||
return false;
|
||||
return true;
|
||||
}, deps: ['attidentity', 'colconstype'],
|
||||
visible: 'isTypeIdentity',
|
||||
},{
|
||||
id: 'seqmin', label: gettext('Minimum'), type: 'int',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
|
||||
visible: 'isTypeIdentity',
|
||||
},{
|
||||
id: 'seqmax', label: gettext('Maximum'), type: 'int',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
|
||||
visible: 'isTypeIdentity',
|
||||
},{
|
||||
id: 'seqcache', label: gettext('Cache'), type: 'int',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
|
||||
visible: 'isTypeIdentity',
|
||||
},{
|
||||
id: 'seqcycle', label: gettext('Cycled'), type: 'switch',
|
||||
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
|
||||
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
|
||||
visible: 'isTypeIdentity',
|
||||
}],
|
||||
validate: function(keys) {
|
||||
var msg = undefined;
|
||||
|
||||
// Nothing to validate
|
||||
if (keys && keys.length == 0) {
|
||||
this.errorModel.clear();
|
||||
return null;
|
||||
} else {
|
||||
this.errorModel.clear();
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('name'))
|
||||
|| String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Column name cannot be empty.');
|
||||
this.errorModel.set('name', msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('cltype'))
|
||||
|| String(this.get('cltype')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Column type cannot be empty.');
|
||||
this.errorModel.set('cltype', msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(this.get('cltype'))
|
||||
&& !_.isUndefined(this.get('attlen'))
|
||||
&& !_.isNull(this.get('attlen'))
|
||||
&& this.get('attlen') !== '') {
|
||||
// Validation for Length field
|
||||
if (this.get('attlen') < this.get('min_val_attlen'))
|
||||
msg = gettext('Length/Precision should not be less than: ') + this.get('min_val_attlen');
|
||||
if (this.get('attlen') > this.get('max_val_attlen'))
|
||||
msg = gettext('Length/Precision should not be greater than: ') + this.get('max_val_attlen');
|
||||
// If we have any error set then throw it to user
|
||||
if(msg) {
|
||||
this.errorModel.set('attlen', msg);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isUndefined(this.get('cltype'))
|
||||
&& !_.isUndefined(this.get('attprecision'))
|
||||
&& !_.isNull(this.get('attprecision'))
|
||||
&& this.get('attprecision') !== '') {
|
||||
// Validation for precision field
|
||||
if (this.get('attprecision') < this.get('min_val_attprecision'))
|
||||
msg = gettext('Scale should not be less than: ') + this.get('min_val_attprecision');
|
||||
if (this.get('attprecision') > this.get('max_val_attprecision'))
|
||||
msg = gettext('Scale should not be greater than: ') + this.get('max_val_attprecision');
|
||||
// If we have any error set then throw it to user
|
||||
if(msg) {
|
||||
this.errorModel.set('attprecision', msg);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
var minimum = this.get('seqmin'),
|
||||
maximum = this.get('seqmax'),
|
||||
start = this.get('seqstart');
|
||||
|
||||
if (!this.isNew() && this.get('colconstype') == 'i' &&
|
||||
(this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') &&
|
||||
(this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) {
|
||||
if (_.isUndefined(this.get('seqincrement'))
|
||||
|| String(this.get('seqincrement')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Increment value cannot be empty.');
|
||||
this.errorModel.set('seqincrement', msg);
|
||||
return msg;
|
||||
} else {
|
||||
this.errorModel.unset('seqincrement');
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('seqmin'))
|
||||
|| String(this.get('seqmin')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Minimum value cannot be empty.');
|
||||
this.errorModel.set('seqmin', msg);
|
||||
return msg;
|
||||
} else {
|
||||
this.errorModel.unset('seqmin');
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('seqmax'))
|
||||
|| String(this.get('seqmax')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Maximum value cannot be empty.');
|
||||
this.errorModel.set('seqmax', msg);
|
||||
return msg;
|
||||
} else {
|
||||
this.errorModel.unset('seqmax');
|
||||
}
|
||||
|
||||
if (_.isUndefined(this.get('seqcache'))
|
||||
|| String(this.get('seqcache')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Cache value cannot be empty.');
|
||||
this.errorModel.set('seqcache', msg);
|
||||
return msg;
|
||||
} else {
|
||||
this.errorModel.unset('seqcache');
|
||||
}
|
||||
}
|
||||
var min_lt = gettext('Minimum value must be less than maximum value.'),
|
||||
start_lt = gettext('Start value cannot be less than minimum value.'),
|
||||
start_gt = gettext('Start value cannot be greater than maximum value.');
|
||||
|
||||
if (_.isEmpty(minimum) || _.isEmpty(maximum))
|
||||
return null;
|
||||
|
||||
if ((minimum == 0 && maximum == 0) ||
|
||||
(parseInt(minimum, 10) >= parseInt(maximum, 10))) {
|
||||
this.errorModel.set('seqmin', min_lt);
|
||||
return min_lt;
|
||||
} else {
|
||||
this.errorModel.unset('seqmin');
|
||||
}
|
||||
|
||||
if (start && minimum && parseInt(start) < parseInt(minimum)) {
|
||||
this.errorModel.set('seqstart', start_lt);
|
||||
return start_lt;
|
||||
} else {
|
||||
this.errorModel.unset('seqstart');
|
||||
}
|
||||
|
||||
if (start && maximum && parseInt(start) > parseInt(maximum)) {
|
||||
this.errorModel.set('seqstart', start_gt);
|
||||
return start_gt;
|
||||
} else {
|
||||
this.errorModel.unset('seqstart');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
// Check whether the column is identity column or not
|
||||
isIdentityColumn: function(m) {
|
||||
let isIdentity = m.get('attidentity');
|
||||
if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity))
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
// Check whether the column is a identity column
|
||||
isTypeIdentity: function(m) {
|
||||
let colconstype = m.get('colconstype');
|
||||
if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'i') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// Check whether the column is a generated column
|
||||
isTypeGenerated: function(m) {
|
||||
let colconstype = m.get('colconstype');
|
||||
if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'g') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const formatSchemaItem = function(opt) {
|
||||
if (!opt.id) {
|
||||
return opt.text;
|
||||
}
|
||||
|
||||
var optimage = $(opt.element).data('image');
|
||||
|
||||
if (!optimage) {
|
||||
return opt.text;
|
||||
} else {
|
||||
return $('<span></span>').append(
|
||||
$('<span></span>', {
|
||||
class: 'wcTabIcon ' + optimage,
|
||||
})
|
||||
).append($('<span></span>').text(opt.text));
|
||||
}
|
||||
};
|
||||
|
||||
let dialogModel = this.pgBrowser.DataModel.extend({
|
||||
defaults: {
|
||||
name: undefined,
|
||||
schema: undefined,
|
||||
description: undefined,
|
||||
columns: [],
|
||||
primary_key: [],
|
||||
},
|
||||
initialize: function() {
|
||||
dialogObj.pgBrowser.DataModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
if(!this.get('schema') && schemas.length > 0) {
|
||||
this.set({
|
||||
'schema': schemas[0]['name'],
|
||||
}, {silent: true});
|
||||
}
|
||||
},
|
||||
schema: [{
|
||||
id: 'name', label: gettext('Name'), type: 'text', disabled: false,
|
||||
},{
|
||||
id: 'schema', label: gettext('Schema'), type: 'text',
|
||||
control: 'select2', select2: {
|
||||
allowClear: false, first_empty: false,
|
||||
templateResult: formatSchemaItem,
|
||||
templateSelection: formatSchemaItem,
|
||||
},
|
||||
options: function () {
|
||||
return schemas.map((schema)=>{
|
||||
return {
|
||||
'value': schema['name'],
|
||||
'image': 'icon-schema',
|
||||
'label': schema['name'],
|
||||
};
|
||||
});
|
||||
},
|
||||
filter: function(d) {
|
||||
// If schema name start with pg_* then we need to exclude them
|
||||
if(d && d.label.match(/^pg_/))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},{
|
||||
id: 'description', label: gettext('Comment'), type: 'multiline',
|
||||
},{
|
||||
id: 'columns', label: gettext('Columns'), type: 'collection', mode: ['create'],
|
||||
group: gettext('Columns'),
|
||||
model: columnsModel,
|
||||
subnode: columnsModel,
|
||||
disabled: false,
|
||||
uniqueCol : ['name'],
|
||||
columns : ['name' , 'cltype', 'attlen', 'attprecision', 'attnotnull', 'is_primary_key'],
|
||||
control: Backform.UniqueColCollectionControl.extend({
|
||||
initialize: function() {
|
||||
|
||||
Backform.UniqueColCollectionControl.prototype.initialize.apply(this, arguments);
|
||||
var self = this,
|
||||
collection = self.model.get(self.field.get('name'));
|
||||
|
||||
if(collection.isEmpty()) {
|
||||
self.last_attnum = -1;
|
||||
} else {
|
||||
var lastCol = collection.max(function(col) {
|
||||
return col.get('attnum');
|
||||
});
|
||||
self.last_attnum = lastCol.get('attnum');
|
||||
}
|
||||
|
||||
collection.on('change:is_primary_key', function(m) {
|
||||
var primary_key_coll = self.model.get('primary_key'),
|
||||
column_name = m.get('name'),
|
||||
primary_key, primary_key_column_coll;
|
||||
|
||||
if(m.get('is_primary_key')) {
|
||||
// Add column to primary key.
|
||||
if (primary_key_coll.length < 1) {
|
||||
primary_key = new (primary_key_coll.model)({}, {
|
||||
top: self.model,
|
||||
collection: primary_key_coll,
|
||||
handler: primary_key_coll,
|
||||
});
|
||||
primary_key_coll.add(primary_key);
|
||||
} else {
|
||||
primary_key = primary_key_coll.first();
|
||||
}
|
||||
|
||||
primary_key_column_coll = primary_key.get('columns');
|
||||
var primary_key_column_exist = primary_key_column_coll.where({column:column_name});
|
||||
|
||||
if (primary_key_column_exist.length == 0) {
|
||||
var primary_key_column = new (
|
||||
primary_key_column_coll.model
|
||||
)({column: column_name}, {
|
||||
silent: true,
|
||||
top: self.model,
|
||||
collection: primary_key_coll,
|
||||
handler: primary_key_coll,
|
||||
});
|
||||
|
||||
primary_key_column_coll.add(primary_key_column);
|
||||
}
|
||||
|
||||
primary_key_column_coll.trigger(
|
||||
'pgadmin:multicolumn:updated', primary_key_column_coll
|
||||
);
|
||||
} else {
|
||||
// remove column from primary key.
|
||||
if (primary_key_coll.length > 0) {
|
||||
primary_key = primary_key_coll.first();
|
||||
// Do not alter existing primary key columns.
|
||||
if (!_.isUndefined(primary_key.get('oid'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
primary_key_column_coll = primary_key.get('columns');
|
||||
var removedCols = primary_key_column_coll.where({column:column_name});
|
||||
if (removedCols.length > 0) {
|
||||
primary_key_column_coll.remove(removedCols);
|
||||
_.each(removedCols, function(local_model) {
|
||||
local_model.destroy();
|
||||
});
|
||||
if (primary_key_column_coll.length == 0) {
|
||||
/* Ideally above line of code should be "primary_key_coll.reset()".
|
||||
* But our custom DataCollection (extended from Backbone collection in datamodel.js)
|
||||
* does not respond to reset event, it only supports add, remove, change events.
|
||||
* And hence no custom event listeners/validators get called for reset event.
|
||||
*/
|
||||
primary_key_coll.remove(primary_key_coll.first());
|
||||
}
|
||||
}
|
||||
primary_key_column_coll.trigger('pgadmin:multicolumn:updated', primary_key_column_coll);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collection.on('change:name', function(m) {
|
||||
let primary_key = self.model.get('primary_key').first();
|
||||
if(primary_key) {
|
||||
let updatedCols = primary_key.get('columns').where(
|
||||
{column: m.previous('name')}
|
||||
);
|
||||
if (updatedCols.length > 0) {
|
||||
/*
|
||||
* Table column name has changed so update
|
||||
* column name in primary key as well.
|
||||
*/
|
||||
updatedCols[0].set(
|
||||
{'column': m.get('name')},
|
||||
{silent: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collection.on('remove', function(m) {
|
||||
let primary_key = self.model.get('primary_key').first();
|
||||
if(primary_key) {
|
||||
let removedCols = primary_key.get('columns').where(
|
||||
{column: m.get('name')}
|
||||
);
|
||||
|
||||
primary_key.get('columns').remove(removedCols);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
canAdd: true,
|
||||
canEdit: true, canDelete: true,
|
||||
// For each row edit/delete button enable/disable
|
||||
canEditRow: true,
|
||||
canDeleteRow: true,
|
||||
allowMultipleEmptyRow: false,
|
||||
beforeAdd: function(newModel) {
|
||||
this.last_attnum++;
|
||||
newModel.set('attnum', this.last_attnum);
|
||||
return newModel;
|
||||
},
|
||||
},{
|
||||
// Here we will create tab control for constraints
|
||||
// We will hide the tab for ERD
|
||||
type: 'nested', control: 'tab', group: gettext('Constraints'), mode: ['properties'],
|
||||
schema: [{
|
||||
id: 'primary_key', label: '',
|
||||
model: this.pgBrowser.Nodes['primary_key'].model.extend({
|
||||
validate: ()=>{},
|
||||
}),
|
||||
subnode: this.pgBrowser.Nodes['primary_key'].model.extend({
|
||||
validate: ()=>{},
|
||||
}),
|
||||
editable: false, type: 'collection',
|
||||
},
|
||||
],
|
||||
}],
|
||||
validate: function() {
|
||||
var msg,
|
||||
name = this.get('name'),
|
||||
schema = this.get('schema');
|
||||
|
||||
if (
|
||||
_.isUndefined(name) || _.isNull(name) ||
|
||||
String(name).replace(/^\s+|\s+$/g, '') == ''
|
||||
) {
|
||||
msg = gettext('Table name cannot be empty.');
|
||||
this.errorModel.set('name', msg);
|
||||
return msg;
|
||||
}
|
||||
this.errorModel.unset('name');
|
||||
if (
|
||||
_.isUndefined(schema) || _.isNull(schema) ||
|
||||
String(schema).replace(/^\s+|\s+$/g, '') == ''
|
||||
) {
|
||||
msg = gettext('Table schema cannot be empty.');
|
||||
this.errorModel.set('schema', msg);
|
||||
return msg;
|
||||
}
|
||||
this.errorModel.unset('schema');
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
return new dialogModel(attributes);
|
||||
}
|
||||
|
||||
createOrGetDialog(type) {
|
||||
const dialogName = this.dialogName();
|
||||
|
||||
if (!Alertify[dialogName]) {
|
||||
Alertify.dialog(dialogName, () => {
|
||||
return new DialogWrapper(
|
||||
`<div class="${dialogName}"></div>`,
|
||||
null,
|
||||
type,
|
||||
$,
|
||||
this.pgBrowser,
|
||||
Alertify,
|
||||
Backform
|
||||
);
|
||||
});
|
||||
}
|
||||
return Alertify[dialogName];
|
||||
}
|
||||
|
||||
show(title, attributes, colTypes, schemas, sVersion, callback) {
|
||||
let dialogTitle = title || gettext('Unknown');
|
||||
const dialog = this.createOrGetDialog('table_dialog');
|
||||
dialog(dialogTitle, this.getDataModel(attributes, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
|
||||
}
|
||||
}
|
32
web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
Normal file
32
web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import TableDialog, {transformToSupported as transformToSupportedTable} from './TableDialog';
|
||||
import OneToManyDialog from './OneToManyDialog';
|
||||
import ManyToManyDialog from './ManyToManyDialog';
|
||||
import pgBrowser from 'top/browser/static/js/browser';
|
||||
import 'sources/backgrid.pgadmin';
|
||||
import 'sources/backform.pgadmin';
|
||||
|
||||
export default function getDialog(dialogName) {
|
||||
if(dialogName === 'entity_dialog') {
|
||||
return new TableDialog(pgBrowser);
|
||||
} else if(dialogName === 'onetomany_dialog') {
|
||||
return new OneToManyDialog(pgBrowser);
|
||||
} else if(dialogName === 'manytomany_dialog') {
|
||||
return new ManyToManyDialog(pgBrowser);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformToSupported(type, data) {
|
||||
if(type == 'table') {
|
||||
return transformToSupportedTable(data);
|
||||
}
|
||||
return data;
|
||||
}
|
30
web/pgadmin/tools/erd/static/js/erd_tool/index.js
Normal file
30
web/pgadmin/tools/erd/static/js/erd_tool/index.js
Normal file
@ -0,0 +1,30 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import BodyWidget from './ui_components/BodyWidget';
|
||||
import getDialog, {transformToSupported} from './dialogs';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import pgWindow from 'sources/window';
|
||||
|
||||
export default class ERDTool {
|
||||
constructor(container, params) {
|
||||
this.container = document.querySelector(container);
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
render() {
|
||||
/* Mount the React ERD tool to the container */
|
||||
ReactDOM.render(
|
||||
<BodyWidget params={this.params} getDialog={getDialog} transformToSupported={transformToSupported} pgAdmin={pgWindow.pgAdmin} alertify={Alertify} />,
|
||||
this.container
|
||||
);
|
||||
}
|
||||
}
|
288
web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
Normal file
288
web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
Normal file
@ -0,0 +1,288 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
RightAngleLinkModel,
|
||||
RightAngleLinkWidget,
|
||||
DefaultLinkFactory,
|
||||
PortModelAlignment,
|
||||
LinkWidget,
|
||||
PointModel
|
||||
} from '@projectstorm/react-diagrams';
|
||||
import {Point} from '@projectstorm/geometry';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const OneToManyModel = {
|
||||
local_table_uid: undefined,
|
||||
local_column_attnum: undefined,
|
||||
referenced_table_uid: undefined,
|
||||
referenced_column_attnum: undefined,
|
||||
}
|
||||
|
||||
export class OneToManyLinkModel extends RightAngleLinkModel {
|
||||
constructor({data, ...options}) {
|
||||
super({
|
||||
type: 'onetomany',
|
||||
width: 1,
|
||||
class: 'link-onetomany',
|
||||
locked: true,
|
||||
...options
|
||||
});
|
||||
|
||||
this._data = {
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
serializeData(nodesDict) {
|
||||
let data = this.getData();
|
||||
let target = nodesDict[data['local_table_uid']].getData();
|
||||
let source = nodesDict[data['referenced_table_uid']].getData();
|
||||
return {
|
||||
'schema': target.schema,
|
||||
'table': target.name,
|
||||
'remote_schema': source.schema,
|
||||
'remote_table': source.name,
|
||||
'columns': [{
|
||||
'local_column': _.find(target.columns, (col)=>data.local_column_attnum == col.attnum).name,
|
||||
'referenced': _.find(source.columns, (col)=>data.referenced_column_attnum == col.attnum).name,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
...super.serialize(),
|
||||
data: this.getData()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const CustomLinkEndWidget = props => {
|
||||
const { point, rotation, tx, ty, type } = props;
|
||||
|
||||
const svgForType = (itype) => {
|
||||
if(itype == 'many') {
|
||||
return (
|
||||
<>
|
||||
<circle className="svg-link-ele svg-otom-circle" cx="0" cy="16" r={props.width*1.75} strokeWidth={props.width} />
|
||||
<polyline className="svg-link-ele" points="-8,0 0,15 0,0 0,30 0,15 8,0" fill="none" strokeWidth={props.width} />
|
||||
</>
|
||||
)
|
||||
} else if (type == 'one') {
|
||||
return (
|
||||
<polyline className="svg-link-ele" points="-8,15 0,15 0,0 0,30 0,15 8,15" fill="none" strokeWidth={props.width} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<g transform={'translate(' + point.getPosition().x + ', ' + point.getPosition().y + ')'}>
|
||||
<g transform={'translate('+tx+','+ty+')'}>
|
||||
<g style={{ transform: 'rotate(' + rotation + 'deg)' }}>
|
||||
{svgForType(type)}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export class OneToManyLinkWidget extends RightAngleLinkWidget {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
endPointTranslation(alignment, offset) {
|
||||
let degree = 0;
|
||||
let tx = 0, ty = 0;
|
||||
switch(alignment) {
|
||||
case PortModelAlignment.BOTTOM:
|
||||
ty = -offset;
|
||||
break;
|
||||
case PortModelAlignment.LEFT:
|
||||
degree = 90;
|
||||
tx = offset
|
||||
break;
|
||||
case PortModelAlignment.TOP:
|
||||
degree = 180;
|
||||
ty = offset;
|
||||
break;
|
||||
case PortModelAlignment.RIGHT:
|
||||
degree = -90;
|
||||
tx = -offset;
|
||||
break;
|
||||
}
|
||||
return [degree, tx, ty];
|
||||
}
|
||||
|
||||
addCustomWidgetPoint(type, endpoint, point) {
|
||||
let offset = 30;
|
||||
const [rotation, tx, ty] = this.endPointTranslation(endpoint.options.alignment, offset);
|
||||
if(!point) {
|
||||
point = this.props.link.point(
|
||||
endpoint.getX()-tx, endpoint.getY()-ty, {'one': 1, 'many': 2}[type]
|
||||
);
|
||||
} else {
|
||||
point.setPosition(endpoint.getX()-tx, endpoint.getY()-ty);
|
||||
}
|
||||
|
||||
return {
|
||||
type: type,
|
||||
point: point,
|
||||
rotation: rotation,
|
||||
tx: tx,
|
||||
ty: ty
|
||||
}
|
||||
}
|
||||
|
||||
generateCustomEndWidget({type, point, rotation, tx, ty}) {
|
||||
return (
|
||||
<CustomLinkEndWidget
|
||||
key={point.getID()}
|
||||
point={point}
|
||||
rotation={rotation}
|
||||
tx={tx}
|
||||
ty={ty}
|
||||
type={type}
|
||||
colorSelected={this.props.link.getOptions().selectedColor}
|
||||
color={this.props.link.getOptions().color}
|
||||
width={this.props.width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
draggingEvent(event, index) {
|
||||
let points = this.props.link.getPoints();
|
||||
// get moving difference. Index + 1 will work because links indexes has
|
||||
// length = points.lenght - 1
|
||||
let dx = Math.abs(points[index].getX() - points[index + 1].getX());
|
||||
let dy = Math.abs(points[index].getY() - points[index + 1].getY());
|
||||
|
||||
// moving with y direction
|
||||
if (dx === 0) {
|
||||
this.calculatePositions(points, event, index, 'x');
|
||||
} else if (dy === 0) {
|
||||
this.calculatePositions(points, event, index, 'y');
|
||||
}
|
||||
this.props.link.setFirstAndLastPathsDirection();
|
||||
}
|
||||
|
||||
handleMove = function(event) {
|
||||
this.props.link.getTargetPort()
|
||||
this.draggingEvent(event, this.dragging_index);
|
||||
this.props.link.fireEvent({}, 'positionChanged');
|
||||
}.bind(this);
|
||||
|
||||
render() {
|
||||
//ensure id is present for all points on the path
|
||||
let points = this.props.link.getPoints();
|
||||
let paths = [];
|
||||
|
||||
let onePoint = this.addCustomWidgetPoint('one', this.props.link.getSourcePort(), points[0]);
|
||||
let manyPoint = this.addCustomWidgetPoint('many', this.props.link.getTargetPort(), points[points.length-1]);
|
||||
|
||||
if (!this.state.canDrag && points.length > 2) {
|
||||
// Those points and its position only will be moved
|
||||
for (let i = 1; i < points.length; i += points.length - 2) {
|
||||
if (i - 1 === 0) {
|
||||
if (this.props.link.getFirstPathXdirection()) {
|
||||
points[i].setPosition(points[i].getX(), points[i - 1].getY());
|
||||
} else {
|
||||
points[i].setPosition(points[i - 1].getX(), points[i].getY());
|
||||
}
|
||||
} else {
|
||||
if (this.props.link.getLastPathXdirection()) {
|
||||
points[i - 1].setPosition(points[i - 1].getX(), points[i].getY());
|
||||
} else {
|
||||
points[i - 1].setPosition(points[i].getX(), points[i - 1].getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is existing link which has two points add one
|
||||
if (points.length === 2 && !this.state.canDrag) {
|
||||
this.props.link.addPoint(
|
||||
new PointModel({
|
||||
link: this.props.link,
|
||||
position: new Point(onePoint.point.getX(), manyPoint.point.getY())
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
paths.push(this.generateCustomEndWidget(onePoint));
|
||||
for (let j = 0; j < points.length - 1; j++) {
|
||||
paths.push(
|
||||
this.generateLink(
|
||||
LinkWidget.generateLinePath(points[j], points[j + 1]),
|
||||
{
|
||||
'data-linkid': this.props.link.getID(),
|
||||
'data-point': j,
|
||||
onMouseDown: (event) => {
|
||||
if (event.button === 0) {
|
||||
this.setState({ canDrag: true });
|
||||
this.dragging_index = j;
|
||||
// Register mouse move event to track mouse position
|
||||
// On mouse up these events are unregistered check "this.handleUp"
|
||||
window.addEventListener('mousemove', this.handleMove);
|
||||
window.addEventListener('mouseup', this.handleUp);
|
||||
}
|
||||
},
|
||||
onMouseEnter: (event) => {
|
||||
this.setState({ selected: true });
|
||||
this.props.link.lastHoverIndexOfPath = j;
|
||||
}
|
||||
},
|
||||
j
|
||||
)
|
||||
);
|
||||
}
|
||||
paths.push(this.generateCustomEndWidget(manyPoint));
|
||||
|
||||
|
||||
this.refPaths = [];
|
||||
return <g data-default-link-test={this.props.link.getOptions().testName}>{paths}</g>;
|
||||
}
|
||||
}
|
||||
|
||||
export class OneToManyLinkFactory extends DefaultLinkFactory {
|
||||
constructor() {
|
||||
super('onetomany');
|
||||
}
|
||||
|
||||
generateModel(event) {
|
||||
return new OneToManyLinkModel(event.initialConfig);
|
||||
}
|
||||
|
||||
generateReactWidget(event) {
|
||||
return <OneToManyLinkWidget color='#fff' width={1} smooth={true} link={event.model} diagramEngine={this.engine} factory={this} />;
|
||||
}
|
||||
|
||||
generateLinkSegment(model, selected, path) {
|
||||
return (
|
||||
<path
|
||||
className={'svg-link-ele path ' + (selected ? 'selected' : '')}
|
||||
stroke={model.getOptions().color}
|
||||
selected={selected}
|
||||
strokeWidth={model.getOptions().width}
|
||||
d={path}
|
||||
>
|
||||
</path>
|
||||
);
|
||||
}
|
||||
}
|
202
web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
Normal file
202
web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
Normal file
@ -0,0 +1,202 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { DefaultNodeModel, PortWidget } from '@projectstorm/react-diagrams';
|
||||
import { AbstractReactFactory } from '@projectstorm/react-canvas-core';
|
||||
import _ from 'lodash';
|
||||
import { IconButton, DetailsToggleButton } from '../ui_components/ToolBar';
|
||||
|
||||
const TYPE = 'table';
|
||||
|
||||
export class TableNodeModel extends DefaultNodeModel {
|
||||
constructor({otherInfo, ...options}) {
|
||||
super({
|
||||
...options,
|
||||
type: TYPE
|
||||
});
|
||||
|
||||
this._note = otherInfo.note || '';
|
||||
|
||||
this._data = {
|
||||
columns: [],
|
||||
...otherInfo.data,
|
||||
};
|
||||
}
|
||||
|
||||
getPortName(attnum) {
|
||||
return `coll-port-${attnum}`;
|
||||
}
|
||||
|
||||
setNote(note) {
|
||||
this._note = note;
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return this._note;
|
||||
}
|
||||
|
||||
addColumn(col) {
|
||||
this._data.columns.push(col);
|
||||
}
|
||||
|
||||
getColumnAt(attnum) {
|
||||
return _.find(this.getColumns(), (col)=>col.attnum==attnum);
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
return this._data.columns;
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this._data['name'] = name;
|
||||
}
|
||||
|
||||
cloneData(name) {
|
||||
let newData = {
|
||||
...this.getData()
|
||||
};
|
||||
if(name) {
|
||||
newData['name'] = name
|
||||
}
|
||||
return newData;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
let self = this;
|
||||
/* Remove the links if column dropped */
|
||||
_.differenceWith(this._data.columns, data.columns, function(existing, incoming) {
|
||||
return existing.attnum == incoming.attnum;
|
||||
}).forEach((col)=>{
|
||||
let existPort = self.getPort(self.getPortName(col.attnum));
|
||||
if(existPort) {
|
||||
existPort.removeAllLinks();
|
||||
self.removePort(existPort);
|
||||
}
|
||||
});
|
||||
this._data = data;
|
||||
this.fireEvent({}, 'nodeUpdated');
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
getSchemaTableName() {
|
||||
return [this._data.schema, this._data.name];
|
||||
}
|
||||
|
||||
remove() {
|
||||
Object.values(this.getPorts()).forEach((port)=>{
|
||||
port.removeAllLinks();
|
||||
});
|
||||
super.remove();
|
||||
}
|
||||
|
||||
serializeData() {
|
||||
return this.getData();
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
...super.serialize(),
|
||||
otherInfo: {
|
||||
data: this.getData(),
|
||||
note: this.getNote(),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TableNodeWidget extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show_details: true
|
||||
}
|
||||
|
||||
this.props.node.registerListener({
|
||||
toggleDetails: (event) => {
|
||||
this.setState({show_details: event.show_details});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
generateColumn(col) {
|
||||
let port = this.props.node.getPort(this.props.node.getPortName(col.attnum));
|
||||
return (
|
||||
<div className='d-flex col-row' key={col.attnum}>
|
||||
<div className='d-flex col-row-data'>
|
||||
<div><span className={'wcTabIcon ' + (col.is_primary_key?'icon-primary_key':'icon-column')}></span></div>
|
||||
<div>
|
||||
<span className='col-name'>{col.name}</span>
|
||||
{this.state.show_details &&
|
||||
<span className='col-datatype'>{col.cltype}{col.attlen ? ('('+ col.attlen + (col.attprecision ? ','+col.attprecision : '') +')') : ''}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto col-row-port">{this.generatePort(port)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generatePort = port => {
|
||||
if(port) {
|
||||
return (
|
||||
<PortWidget engine={this.props.engine} port={port} key={port.getID()} className={"port-" + port.options.alignment} />
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
toggleShowDetails = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState((prevState)=>({show_details: !prevState.show_details}));
|
||||
}
|
||||
|
||||
render() {
|
||||
let node_data = this.props.node.getData();
|
||||
return (
|
||||
<div className={"table-node " + (this.props.node.isSelected() ? 'selected': '') } onDoubleClick={()=>{this.props.node.fireEvent({}, 'editNode')}}>
|
||||
<div className="table-toolbar">
|
||||
<DetailsToggleButton className='btn-xs' showDetails={this.state.show_details} onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
|
||||
{this.props.node.getNote() &&
|
||||
<IconButton icon="far fa-sticky-note" className="btn-xs btn-warning ml-auto" onClick={()=>{
|
||||
this.props.node.fireEvent({}, 'showNote')
|
||||
}} title="Check note" />}
|
||||
</div>
|
||||
<div className="table-schema">
|
||||
<span className="wcTabIcon icon-schema"></span>
|
||||
{node_data.schema}
|
||||
</div>
|
||||
<div className="table-name">
|
||||
<span className="wcTabIcon icon-table"></span>
|
||||
{node_data.name}
|
||||
</div>
|
||||
<div className="table-cols">
|
||||
{_.map(node_data.columns, (col)=>this.generateColumn(col))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TableNodeFactory extends AbstractReactFactory {
|
||||
constructor() {
|
||||
super(TYPE);
|
||||
}
|
||||
|
||||
generateModel(event) {
|
||||
return new TableNodeModel(event.initialConfig);
|
||||
}
|
||||
|
||||
generateReactWidget(event) {
|
||||
return <TableNodeWidget engine={this.engine} node={event.model} />;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { PortModel } from '@projectstorm/react-diagrams-core';
|
||||
import {OneToManyLinkModel} from '../links/OneToManyLink';
|
||||
import { AbstractModelFactory } from '@projectstorm/react-canvas-core';
|
||||
|
||||
const TYPE = 'onetomany';
|
||||
|
||||
export default class OneToManyPortModel extends PortModel {
|
||||
constructor({options}) {
|
||||
super({
|
||||
...options,
|
||||
type: TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
removeAllLinks() {
|
||||
Object.values(this.getLinks()).forEach((link)=>{
|
||||
link.remove();
|
||||
});
|
||||
}
|
||||
|
||||
createLinkModel() {
|
||||
return new OneToManyLinkModel({});
|
||||
}
|
||||
}
|
||||
|
||||
export class OneToManyPortFactory extends AbstractModelFactory {
|
||||
constructor() {
|
||||
super(TYPE);
|
||||
}
|
||||
|
||||
generateModel(event) {
|
||||
return new OneToManyPortModel(event.initialConfig||{});
|
||||
}
|
||||
}
|
@ -0,0 +1,681 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import * as React from 'react';
|
||||
import { CanvasWidget } from '@projectstorm/react-canvas-core';
|
||||
import axios from 'axios';
|
||||
import { Action, InputType } from '@projectstorm/react-canvas-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ERDCore from '../ERDCore';
|
||||
import ToolBar, {IconButton, DetailsToggleButton, ButtonGroup} from './ToolBar';
|
||||
import ConnectionBar, { STATUS as CONNECT_STATUS } from './ConnectionBar';
|
||||
import Loader from './Loader';
|
||||
import FloatingNote from './FloatingNote';
|
||||
import {setPanelTitle} from '../../erd_module';
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool';
|
||||
|
||||
/* Custom react-diagram action for keyboard events */
|
||||
export class KeyboardShortcutAction extends Action {
|
||||
constructor(shortcut_handlers=[]) {
|
||||
super({
|
||||
type: InputType.KEY_DOWN,
|
||||
fire: ({ event })=>{
|
||||
this.callHandler(event);
|
||||
}
|
||||
});
|
||||
this.shortcuts = {};
|
||||
|
||||
for(let i=0; i<shortcut_handlers.length; i++){
|
||||
let [key, handler] = shortcut_handlers[i];
|
||||
this.shortcuts[this.shortcutKey(key.alt, key.control, key.shift, false, key.key.key_code)] = handler;
|
||||
}
|
||||
}
|
||||
|
||||
shortcutKey(altKey, ctrlKey, shiftKey, metaKey, keyCode) {
|
||||
return `${altKey}:${ctrlKey}:${shiftKey}:${metaKey}:${keyCode}`;
|
||||
}
|
||||
|
||||
callHandler(event) {
|
||||
let handler = this.shortcuts[this.shortcutKey(event.altKey, event.ctrlKey, event.shiftKey, event.metaKey, event.keyCode)];
|
||||
if(handler) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The main body container for the ERD */
|
||||
export default class BodyWidget extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
conn_status: CONNECT_STATUS.DISCONNECTED,
|
||||
server_version: null,
|
||||
any_item_selected: false,
|
||||
single_node_selected: false,
|
||||
single_link_selected: false,
|
||||
coll_types: [],
|
||||
loading_msg: null,
|
||||
note_open: false,
|
||||
note_node: null,
|
||||
current_file: null,
|
||||
dirty: false,
|
||||
show_details: true,
|
||||
preferences: {},
|
||||
}
|
||||
this.diagram = new ERDCore();
|
||||
this.fileInputRef = React.createRef();
|
||||
this.diagramContainerRef = React.createRef();
|
||||
this.canvasEle = null;
|
||||
this.noteRefEle = null;
|
||||
this.noteNode = null;
|
||||
this.keyboardActionObj = null;
|
||||
|
||||
this.onLoadDiagram = this.onLoadDiagram.bind(this);
|
||||
this.onSaveDiagram = this.onSaveDiagram.bind(this);
|
||||
this.onSaveAsDiagram = this.onSaveAsDiagram.bind(this);
|
||||
this.onSQLClick = this.onSQLClick.bind(this);
|
||||
this.onAddNewNode = this.onAddNewNode.bind(this);
|
||||
this.onEditNode = this.onEditNode.bind(this);
|
||||
this.onCloneNode = this.onCloneNode.bind(this);
|
||||
this.onDeleteNode = this.onDeleteNode.bind(this);
|
||||
this.onNoteClick = this.onNoteClick.bind(this);
|
||||
this.onNoteClose = this.onNoteClose.bind(this);
|
||||
this.onOneToManyClick = this.onOneToManyClick.bind(this);
|
||||
this.onManyToManyClick = this.onManyToManyClick.bind(this);
|
||||
this.onAutoDistribute = this.onAutoDistribute.bind(this);
|
||||
this.onDetailsToggle = this.onDetailsToggle.bind(this);
|
||||
this.onHelpClick = this.onHelpClick.bind(this);
|
||||
this.diagram.zoomToFit = this.diagram.zoomToFit.bind(this.diagram);
|
||||
this.diagram.zoomIn = this.diagram.zoomIn.bind(this.diagram);
|
||||
this.diagram.zoomOut = this.diagram.zoomOut.bind(this.diagram);
|
||||
}
|
||||
|
||||
registerModelEvents() {
|
||||
let diagramEvents = {
|
||||
'offsetUpdated': (event)=>{
|
||||
this.realignGrid({backgroundPosition: `${event.offsetX}px ${event.offsetY}px`});
|
||||
event.stopPropagation();
|
||||
},
|
||||
'zoomUpdated': (event)=>{
|
||||
let { gridSize } = this.diagram.getModel().getOptions();
|
||||
let bgSize = gridSize*event.zoom/100;
|
||||
this.realignGrid({backgroundSize: `${bgSize*3}px ${bgSize*3}px`});
|
||||
},
|
||||
'nodesSelectionChanged': ()=>{
|
||||
this.setState({
|
||||
single_node_selected: this.diagram.getSelectedNodes().length == 1,
|
||||
any_item_selected: this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0,
|
||||
});
|
||||
},
|
||||
'linksSelectionChanged': ()=>{
|
||||
this.setState({
|
||||
single_link_selected: this.diagram.getSelectedLinks().length == 1,
|
||||
any_item_selected: this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0,
|
||||
});
|
||||
},
|
||||
'linksUpdated': () => {
|
||||
this.setState({dirty: true});
|
||||
},
|
||||
'nodesUpdated': ()=>{
|
||||
this.setState({dirty: true});
|
||||
},
|
||||
'showNote': (event)=>{
|
||||
this.showNote(event.node);
|
||||
},
|
||||
'editNode': (event) => {
|
||||
this.addEditNode(event.node);
|
||||
}
|
||||
};
|
||||
Object.keys(diagramEvents).forEach(eventName => {
|
||||
this.diagram.registerModelEvent(eventName, diagramEvents[eventName]);
|
||||
});
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
/* First deregister to avoid double events */
|
||||
this.keyboardActionObj && this.diagram.deregisterKeyAction(this.keyboardActionObj);
|
||||
|
||||
this.keyboardActionObj = new KeyboardShortcutAction([
|
||||
[this.state.preferences.open_project, this.onLoadDiagram],
|
||||
[this.state.preferences.save_project, this.onSaveDiagram],
|
||||
[this.state.preferences.save_project_as, this.onSaveAsDiagram],
|
||||
[this.state.preferences.generate_sql, this.onSQLClick],
|
||||
[this.state.preferences.add_table, this.onAddNewNode],
|
||||
[this.state.preferences.edit_table, this.onEditNode],
|
||||
[this.state.preferences.clone_table, this.onCloneNode],
|
||||
[this.state.preferences.drop_table, this.onDeleteNode],
|
||||
[this.state.preferences.add_edit_note, this.onNoteClick],
|
||||
[this.state.preferences.one_to_many, this.onOneToManyClick],
|
||||
[this.state.preferences.many_to_many, this.onManyToManyClick],
|
||||
[this.state.preferences.auto_align, this.onAutoDistribute],
|
||||
[this.state.preferences.zoom_to_fit, this.diagram.zoomToFit],
|
||||
[this.state.preferences.zoom_in, this.diagram.zoomIn],
|
||||
[this.state.preferences.zoom_out, this.diagram.zoomOut]
|
||||
]);
|
||||
|
||||
this.diagram.registerKeyAction(this.keyboardActionObj);
|
||||
}
|
||||
|
||||
handleAxiosCatch(err) {
|
||||
let alert = this.props.alertify.alert().set('title', gettext('Error'));
|
||||
if (err.response) {
|
||||
// client received an error response (5xx, 4xx)
|
||||
alert.set('message', `${err.response.statusText} - ${err.response.data.errormsg}`).show();
|
||||
console.error('response error', err.response);
|
||||
} else if (err.request) {
|
||||
// client never received a response, or request never left
|
||||
alert.set('message', gettext('Client error') + ':' + err).show();
|
||||
console.error('client eror', err);
|
||||
} else {
|
||||
alert.set('message', err.message).show();
|
||||
console.error('other error', err);
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setLoading('Preparing');
|
||||
this.setTitle(this.state.current_file);
|
||||
this.setState({
|
||||
preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
|
||||
}, this.registerKeyboardShortcuts);
|
||||
this.registerModelEvents();
|
||||
this.realignGrid({
|
||||
backgroundSize: '45px 45px',
|
||||
backgroundPosition: '0px 0px',
|
||||
});
|
||||
|
||||
this.props.pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', this.openFile, this);
|
||||
this.props.pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', this.saveFile, this);
|
||||
this.props.pgAdmin.Browser.onPreferencesChange('erd', () => {
|
||||
this.setState({
|
||||
preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
|
||||
}, ()=>this.registerKeyboardShortcuts());
|
||||
});
|
||||
|
||||
let done = await this.initConnection();
|
||||
if(!done) return;
|
||||
|
||||
done = await this.loadPrequisiteData();
|
||||
if(!done) return;
|
||||
|
||||
if(this.props.params.gen) {
|
||||
await this.loadTablesData();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if(this.state.dirty) {
|
||||
this.setTitle(this.state.current_file, true);
|
||||
}
|
||||
}
|
||||
|
||||
getDialog(dialogName) {
|
||||
if(dialogName === 'entity_dialog') {
|
||||
return (title, attributes, callback)=>{
|
||||
this.props.getDialog(dialogName).show(
|
||||
title, attributes, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
|
||||
);
|
||||
};
|
||||
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
|
||||
return (title, attributes, callback)=>{
|
||||
this.props.getDialog(dialogName).show(
|
||||
title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(message) {
|
||||
this.setState({loading_msg: message});
|
||||
}
|
||||
|
||||
realignGrid({backgroundSize, backgroundPosition}) {
|
||||
if(backgroundSize) {
|
||||
this.canvasEle.style.backgroundSize = backgroundSize;
|
||||
}
|
||||
if(backgroundPosition) {
|
||||
this.canvasEle.style.backgroundPosition = backgroundPosition;
|
||||
}
|
||||
}
|
||||
|
||||
addEditNode(node) {
|
||||
let dialog = this.getDialog('entity_dialog');
|
||||
if(node) {
|
||||
let [schema, table] = node.getSchemaTableName();
|
||||
dialog(_.escape(`Table: ${table} (${schema})`), node.getData(), (newData)=>{
|
||||
node.setData(newData);
|
||||
this.diagram.repaint();
|
||||
});
|
||||
} else {
|
||||
dialog('New table', {name: this.diagram.getNextTableName()}, (newData)=>{
|
||||
let newNode = this.diagram.addNode(newData);
|
||||
newNode.setSelected(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onEditNode() {
|
||||
const selected = this.diagram.getSelectedNodes();
|
||||
if(selected.length == 1) {
|
||||
this.addEditNode(selected[0]);
|
||||
}
|
||||
}
|
||||
|
||||
onAddNewNode() {
|
||||
this.addEditNode();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteNode() {
|
||||
this.props.alertify.confirm(
|
||||
gettext('Delete ?'),
|
||||
gettext('You have selected %s tables and %s links.', this.diagram.getSelectedNodes().length, this.diagram.getSelectedLinks().length)
|
||||
+ '<br />' + gettext('Are you sure you want to delete ?'),
|
||||
() => {
|
||||
this.diagram.getSelectedNodes().forEach((node)=>{
|
||||
node.setSelected(false);
|
||||
node.remove();
|
||||
});
|
||||
this.diagram.getSelectedLinks().forEach((link)=>{
|
||||
link.getTargetPort().remove();
|
||||
link.getSourcePort().remove();
|
||||
link.setSelected(false);
|
||||
link.remove();
|
||||
});
|
||||
this.diagram.repaint();
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
onAutoDistribute() {
|
||||
this.diagram.dagreDistributeNodes();
|
||||
}
|
||||
|
||||
onDetailsToggle() {
|
||||
this.setState((prevState)=>({
|
||||
show_details: !prevState.show_details
|
||||
}), ()=>{
|
||||
this.diagram.getModel().getNodes().forEach((node)=>{
|
||||
node.fireEvent({show_details: this.state.show_details}, 'toggleDetails');
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onHelpClick() {
|
||||
let url = url_for('help.static', {'filename': 'erd.html'});
|
||||
window.open(url, 'pgadmin_help');
|
||||
}
|
||||
|
||||
onLoadDiagram() {
|
||||
var params = {
|
||||
'supported_types': ['pgerd'], // file types allowed
|
||||
'dialog_type': 'select_file', // open select file dialog
|
||||
};
|
||||
this.props.pgAdmin.FileManager.init();
|
||||
this.props.pgAdmin.FileManager.show_dialog(params);
|
||||
}
|
||||
|
||||
openFile(fileName) {
|
||||
axios.post(url_for('sqleditor.load_file'), {
|
||||
'file_name': decodeURI(fileName)
|
||||
}).then((res)=>{
|
||||
this.setState({
|
||||
current_file: fileName,
|
||||
dirty: false,
|
||||
});
|
||||
this.setTitle(fileName);
|
||||
this.diagram.deserialize(res.data);
|
||||
this.registerModelEvents();
|
||||
}).catch((err)=>{
|
||||
this.handleAxiosCatch(err);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveDiagram(isSaveAs=false) {
|
||||
if(this.state.current_file && !isSaveAs) {
|
||||
this.saveFile(this.state.current_file);
|
||||
} else {
|
||||
var params = {
|
||||
'supported_types': ['pgerd'],
|
||||
'dialog_type': 'create_file',
|
||||
'dialog_title': 'Save File',
|
||||
'btn_primary': 'Save',
|
||||
};
|
||||
this.props.pgAdmin.FileManager.init();
|
||||
this.props.pgAdmin.FileManager.show_dialog(params);
|
||||
}
|
||||
}
|
||||
|
||||
onSaveAsDiagram() {
|
||||
this.onSaveDiagram(true);
|
||||
}
|
||||
|
||||
saveFile(fileName) {
|
||||
axios.post(url_for('sqleditor.save_file'), {
|
||||
'file_name': decodeURI(fileName),
|
||||
'file_content': JSON.stringify(this.diagram.serialize(this.props.pgAdmin.Browser.utils.app_version_int))
|
||||
}).then(()=>{
|
||||
this.props.alertify.success(gettext('Project saved successfully.'));
|
||||
this.setState({
|
||||
current_file: fileName,
|
||||
dirty: false,
|
||||
});
|
||||
this.setTitle(fileName);
|
||||
}).catch((err)=>{
|
||||
this.handleAxiosCatch(err);
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentProjectName(path) {
|
||||
let currPath = path || this.state.current_file || 'Untitled';
|
||||
return currPath.split('\\').pop().split('/').pop();
|
||||
}
|
||||
|
||||
setTitle(title, dirty=false) {
|
||||
if(title === null || title === '') {
|
||||
title = 'Untitled';
|
||||
}
|
||||
title = this.getCurrentProjectName(title) + (dirty ? '*': '');
|
||||
if (this.new_browser_tab) {
|
||||
window.document.title = title;
|
||||
} else {
|
||||
_.each(this.props.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
|
||||
if (p.isVisible()) {
|
||||
setPanelTitle(p, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSQLClick() {
|
||||
let scriptHeader = gettext('-- This script was generated by a beta version of 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', {
|
||||
trans_id: this.props.params.trans_id,
|
||||
sgid: this.props.params.sgid,
|
||||
sid: this.props.params.sid,
|
||||
did: this.props.params.did
|
||||
});
|
||||
|
||||
this.setLoading(gettext('Preparing the SQL...'));
|
||||
axios.post(url, this.diagram.serializeData())
|
||||
.then((resp)=>{
|
||||
let sqlScript = resp.data.data;
|
||||
sqlScript = scriptHeader + 'BEGIN;\n' + sqlScript + '\nEND;';
|
||||
|
||||
let parentData = {
|
||||
sgid: this.props.params.sgid,
|
||||
sid: this.props.params.sid,
|
||||
did: this.props.params.did,
|
||||
stype: this.props.params.server_type,
|
||||
}
|
||||
|
||||
let sqlId = `erd${this.props.params.trans_id}`;
|
||||
localStorage.setItem(sqlId, sqlScript);
|
||||
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgAdmin.DataGrid, this.props.alertify);
|
||||
})
|
||||
.catch((error)=>{
|
||||
this.handleAxiosCatch(error);
|
||||
})
|
||||
.then(()=>{
|
||||
this.setLoading(null);
|
||||
})
|
||||
}
|
||||
|
||||
onOneToManyClick() {
|
||||
let dialog = this.getDialog('onetomany_dialog');
|
||||
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};
|
||||
dialog('One to many relation', initData, (newData)=>{
|
||||
let newLink = this.diagram.addLink(newData, 'onetomany');
|
||||
this.diagram.clearSelection();
|
||||
newLink.setSelected(true);
|
||||
this.diagram.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
onManyToManyClick() {
|
||||
let dialog = this.getDialog('manytomany_dialog');
|
||||
let initData = {left_table_uid: this.diagram.getSelectedNodes()[0].getID()};
|
||||
dialog('Many to many relation', initData, (newData)=>{
|
||||
let nodes = this.diagram.getModel().getNodesDict();
|
||||
let left_table = nodes[newData.left_table_uid];
|
||||
let right_table = nodes[newData.right_table_uid];
|
||||
let tableData = {
|
||||
name: `${left_table.getData().name}_${right_table.getData().name}`,
|
||||
schema: left_table.getData().schema,
|
||||
columns: [{
|
||||
...left_table.getColumnAt(newData.left_table_column_attnum),
|
||||
'name': `${left_table.getData().name}_${left_table.getColumnAt(newData.left_table_column_attnum).name}`,
|
||||
'is_primary_key': false,
|
||||
'attnum': 0,
|
||||
},{
|
||||
...right_table.getColumnAt(newData.right_table_column_attnum),
|
||||
'name': `${right_table.getData().name}_${right_table.getColumnAt(newData.right_table_column_attnum).name}`,
|
||||
'is_primary_key': false,
|
||||
'attnum': 1,
|
||||
}]
|
||||
}
|
||||
let newNode = this.diagram.addNode(tableData);
|
||||
this.diagram.clearSelection();
|
||||
newNode.setSelected(true);
|
||||
|
||||
let linkData = {
|
||||
local_table_uid: newNode.getID(),
|
||||
local_column_attnum: newNode.getColumns()[0].attnum,
|
||||
referenced_table_uid: newData.left_table_uid,
|
||||
referenced_column_attnum : newData.left_table_column_attnum,
|
||||
}
|
||||
this.diagram.addLink(linkData, 'onetomany');
|
||||
|
||||
linkData = {
|
||||
local_table_uid: newNode.getID(),
|
||||
local_column_attnum: newNode.getColumns()[1].attnum,
|
||||
referenced_table_uid: newData.right_table_uid,
|
||||
referenced_column_attnum : newData.right_table_column_attnum,
|
||||
}
|
||||
|
||||
this.diagram.addLink(linkData, 'onetomany');
|
||||
|
||||
this.diagram.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
showNote(noteNode) {
|
||||
if(noteNode) {
|
||||
this.noteRefEle = this.diagram.getEngine().getNodeElement(noteNode);
|
||||
this.setState({
|
||||
note_node: noteNode,
|
||||
note_open: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onNoteClick(e) {
|
||||
let noteNode = this.diagram.getSelectedNodes()[0];
|
||||
this.showNote(noteNode);
|
||||
}
|
||||
|
||||
onNoteClose(updated) {
|
||||
this.setState({note_open: false});
|
||||
updated && this.diagram.fireEvent({}, 'nodesUpdated', true);
|
||||
}
|
||||
|
||||
async initConnection() {
|
||||
this.setLoading(gettext('Initializing connection...'));
|
||||
this.setState({conn_status: CONNECT_STATUS.CONNECTING});
|
||||
|
||||
let initUrl = url_for('erd.initialize', {
|
||||
trans_id: this.props.params.trans_id,
|
||||
sgid: this.props.params.sgid,
|
||||
sid: this.props.params.sid,
|
||||
did: this.props.params.did
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await axios.post(initUrl);
|
||||
this.setState({
|
||||
conn_status: CONNECT_STATUS.CONNECTED,
|
||||
server_version: response.data.data.serverVersion
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.setState({conn_status: CONNECT_STATUS.FAILED});
|
||||
this.handleAxiosCatch(error);
|
||||
return false;
|
||||
} finally {
|
||||
this.setLoading(null);
|
||||
}
|
||||
}
|
||||
|
||||
/* Get all prequisite in one conn since
|
||||
* we have only one connection
|
||||
*/
|
||||
async loadPrequisiteData() {
|
||||
this.setLoading(gettext('Fetching required data...'));
|
||||
let url = url_for('erd.prequisite', {
|
||||
trans_id: this.props.params.trans_id,
|
||||
sgid: this.props.params.sgid,
|
||||
sid: this.props.params.sid,
|
||||
did: this.props.params.did
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await axios.get(url);
|
||||
let data = response.data.data;
|
||||
this.diagram.setCache('colTypes', data['col_types']);
|
||||
this.diagram.setCache('schemas', data['schemas']);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.handleAxiosCatch(error);
|
||||
return false;
|
||||
} finally {
|
||||
this.setLoading(null);
|
||||
}
|
||||
}
|
||||
|
||||
async loadTablesData() {
|
||||
this.setLoading(gettext('Fetching schema data...'));
|
||||
let url = url_for('erd.tables', {
|
||||
trans_id: this.props.params.trans_id,
|
||||
sgid: this.props.params.sgid,
|
||||
sid: this.props.params.sid,
|
||||
did: this.props.params.did
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await axios.get(url);
|
||||
let tables = response.data.data.map((table)=>{
|
||||
return this.props.transformToSupported('table', table);
|
||||
});
|
||||
this.diagram.deserializeData(tables);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.handleAxiosCatch(error);
|
||||
return false;
|
||||
} finally {
|
||||
this.setLoading(null);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<ToolBar id="btn-toolbar">
|
||||
<ButtonGroup>
|
||||
<IconButton id="open-file" icon="fa fa-folder-open" onClick={this.onLoadDiagram} title={gettext('Load from file')}
|
||||
shortcut={this.state.preferences.open_project}/>
|
||||
<IconButton id="save-erd" icon="fa fa-save" onClick={()=>{this.onSaveDiagram()}} title={gettext('Save project')}
|
||||
shortcut={this.state.preferences.save_project} disabled={!this.state.dirty}/>
|
||||
<IconButton id="save-as-erd" icon="fa fa-share-square" onClick={this.onSaveAsDiagram} title={gettext('Save as')}
|
||||
shortcut={this.state.preferences.save_project_as}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="save-sql" icon="fa fa-file-code" onClick={this.onSQLClick} title={gettext('Generate SQL')}
|
||||
shortcut={this.state.preferences.generate_sql}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="add-node" icon="fa fa-plus-square" onClick={this.onAddNewNode} title={gettext('Add table')}
|
||||
shortcut={this.state.preferences.add_table}/>
|
||||
<IconButton id="edit-node" icon="fa fa-pencil-alt" onClick={this.onEditNode} title={gettext('Edit table')}
|
||||
shortcut={this.state.preferences.edit_table} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
|
||||
<IconButton id="clone-node" icon="fa fa-clone" onClick={this.onCloneNode} title={gettext('Clone table')}
|
||||
shortcut={this.state.preferences.clone_table} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
|
||||
<IconButton id="delete-node" icon="fa fa-trash-alt" onClick={this.onDeleteNode} title={gettext('Drop table/link')}
|
||||
shortcut={this.state.preferences.drop_table} disabled={!this.state.any_item_selected}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="add-note" icon="fa fa-sticky-note" onClick={this.onNoteClick} title={gettext('Add/Edit note')}
|
||||
shortcut={this.state.preferences.add_edit_note} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
|
||||
<IconButton id="add-onetomany" text="1M" onClick={this.onOneToManyClick} title={gettext('One-to-Many link')}
|
||||
shortcut={this.state.preferences.one_to_many} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
|
||||
<IconButton id="add-manytomany" text="MM" onClick={this.onManyToManyClick} title={gettext('Many-to-Many link')}
|
||||
shortcut={this.state.preferences.many_to_many} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="auto-align" icon="fa fa-magic" onClick={this.onAutoDistribute} title={gettext('Auto align')}
|
||||
shortcut={this.state.preferences.auto_align} />
|
||||
<DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details} />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="zoom-to-fit" icon="fa fa-compress" onClick={this.diagram.zoomToFit} title={gettext('Zoom to fit')}
|
||||
shortcut={this.state.preferences.zoom_to_fit}/>
|
||||
<IconButton id="zoom-in" icon="fa fa-search-plus" onClick={this.diagram.zoomIn} title={gettext('Zoom in')}
|
||||
shortcut={this.state.preferences.zoom_in}/>
|
||||
<IconButton id="zoom-out" icon="fa fa-search-minus" onClick={this.diagram.zoomOut} title={gettext('Zoom out')}
|
||||
shortcut={this.state.preferences.zoom_out}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<IconButton id="help" icon="fa fa-question" onClick={this.onHelpClick} title={gettext('Help')} />
|
||||
</ButtonGroup>
|
||||
</ToolBar>
|
||||
<ConnectionBar statusId="btn-conn-status" status={this.state.conn_status} bgcolor={this.props.params.bgcolor}
|
||||
fgcolor={this.props.params.fgcolor} title={this.props.params.title}/>
|
||||
<FloatingNote open={this.state.note_open} onClose={this.onNoteClose}
|
||||
reference={this.noteRefEle} noteNode={this.state.note_node} appendTo={this.diagramContainerRef.current} rows={8}/>
|
||||
<div className="diagram-container" ref={this.diagramContainerRef}>
|
||||
<Loader message={this.state.loading_msg} autoEllipsis={true}/>
|
||||
<CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current}} engine={this.diagram.getEngine()} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BodyWidget.propTypes = {
|
||||
params:PropTypes.shape({
|
||||
trans_id: PropTypes.number.isRequired,
|
||||
sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
server_type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
bgcolor: PropTypes.string,
|
||||
fgcolor: PropTypes.string,
|
||||
gen: PropTypes.bool.isRequired
|
||||
}),
|
||||
getDialog: PropTypes.func.isRequired,
|
||||
transformToSupported: PropTypes.func.isRequired,
|
||||
pgAdmin: PropTypes.object.isRequired,
|
||||
alertify: PropTypes.object.isRequired
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const STATUS = {
|
||||
CONNECTED: 1,
|
||||
DISCONNECTED: 2,
|
||||
CONNECTING: 3,
|
||||
FAILED: 4,
|
||||
}
|
||||
|
||||
/* The connection bar component */
|
||||
export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title}) {
|
||||
return (
|
||||
<div className="connection_status_wrapper d-flex">
|
||||
<div id={statusId}
|
||||
role="status"
|
||||
className="connection_status d-flex justify-content-center align-items-center" data-container="body"
|
||||
data-toggle="popover" data-placement="bottom"
|
||||
data-content=""
|
||||
data-panel-visible="visible"
|
||||
tabIndex="0">
|
||||
<span className={'pg-font-icon d-flex m-auto '
|
||||
+ (status == STATUS.CONNECTED ? 'icon-query-tool-connected' : '')
|
||||
+ (status == (STATUS.DISCONNECTED || STATUS.FAILED) ? 'icon-query-tool-disconnected ' : '')
|
||||
+ (status == STATUS.CONNECTING ? 'obtaining-conn' : '')}
|
||||
aria-hidden="true" title="" role="img">
|
||||
</span>
|
||||
</div>
|
||||
<div className="connection-info btn-group" role="group" aria-label="">
|
||||
<div className="editor-title"
|
||||
style={{backgroundColor: bgcolor, color: fgcolor}}>
|
||||
{status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
|
||||
{status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConnectionBar.propTypes = {
|
||||
statusId: PropTypes.string.isRequired,
|
||||
status: PropTypes.oneOf(Object.values(STATUS)).isRequired,
|
||||
bgcolor: PropTypes.string,
|
||||
fgcolor: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableNodeModel } from '../nodes/TableNode';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
|
||||
/* The note component of ERD. It uses tippy to create the floating note */
|
||||
export default function FloatingNote({open, onClose, reference, rows, noteNode, ...tippyProps}) {
|
||||
const textRef = React.useRef(null);
|
||||
const [text, setText] = useState('');
|
||||
const [header, setHeader] = useState('');
|
||||
useEffect(()=>{
|
||||
if(noteNode) {
|
||||
setText(noteNode.getNote());
|
||||
let [schema, name] = noteNode.getSchemaTableName();
|
||||
setHeader(`${name} (${schema})`);
|
||||
}
|
||||
|
||||
if(open) {
|
||||
textRef?.current.focus();
|
||||
textRef?.current.dispatchEvent(new KeyboardEvent('keypress'));
|
||||
}
|
||||
}, [noteNode, open]);
|
||||
|
||||
return (
|
||||
<Tippy render={(attrs)=>(
|
||||
<div className="floating-note" {...attrs}>
|
||||
<div className="note-header">{gettext('Note')}:</div>
|
||||
<div className="note-body">
|
||||
<div className="p-1">{header}</div>
|
||||
<textarea ref={textRef} className="pg-textarea" value={text} rows={rows} onChange={(e)=>setText(e.target.value)}></textarea>
|
||||
<div className="pg_buttons">
|
||||
<button className="btn btn-primary long_text_editor pg-alertify-button" data-label="OK"
|
||||
onClick={()=>{
|
||||
let updated = (noteNode.getNote() != text);
|
||||
noteNode.setNote(text);
|
||||
if(onClose) onClose(updated);
|
||||
}}>
|
||||
<span className="fa fa-check pg-alertify-button"></span> {gettext('OK')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
visible={open}
|
||||
interactive={true}
|
||||
animation={false}
|
||||
reference={reference}
|
||||
placement='auto-end'
|
||||
{...tippyProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FloatingNote.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
reference: CustomPropTypes.ref,
|
||||
rows: PropTypes.number,
|
||||
noteNode: PropTypes.object,
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/* The loader/spinner component */
|
||||
export default function Loader({message, autoEllipsis=false}) {
|
||||
if(message || message == '') {
|
||||
return (
|
||||
<div className="pg-sp-container">
|
||||
<div className="pg-sp-content">
|
||||
<div className="row">
|
||||
<div className="col-12 pg-sp-icon"></div>
|
||||
</div>
|
||||
<div className="row"><div className="col-12 pg-sp-text">{message}{autoEllipsis ? '...':''}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Loader.propTypes = {
|
||||
message: PropTypes.string,
|
||||
autoEllipsis: PropTypes.bool,
|
||||
};
|
@ -0,0 +1,136 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { forwardRef } from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import {isMac} from 'sources/keyboard_shortcuts';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
|
||||
/* The base icon button.
|
||||
React does not pass ref prop to child component hierarchy.
|
||||
Use forwardRef for the same
|
||||
*/
|
||||
const BaseIconButton = forwardRef((props, ref)=>{
|
||||
const {icon, text, className, ...otherProps} = props;
|
||||
|
||||
return(
|
||||
<button ref={ref} className={className} {...otherProps}>
|
||||
{icon && <span className={`${icon} sql-icon-lg`} aria-hidden="true" role="img"></span>}
|
||||
{text && <span className="text-icon">{text}</span>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
BaseIconButton.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
ref: CustomPropTypes.ref,
|
||||
}
|
||||
|
||||
|
||||
/* The tooltip content to show shortcut details */
|
||||
export function Shortcut({shortcut}) {
|
||||
let keys = [];
|
||||
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
|
||||
shortcut.control && keys.push('Ctrl');
|
||||
shortcut.shift && keys.push('Shift');
|
||||
keys.push(shortcut.key.char.toUpperCase());
|
||||
return (
|
||||
<div style={{justifyContent: 'center', marginTop: '0.125rem'}} className="d-flex">
|
||||
{keys.map((key, i)=>{
|
||||
return <div key={i} className="shortcut-key">{key}</div>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const shortcutPropType = PropTypes.shape({
|
||||
alt: PropTypes.bool,
|
||||
control: PropTypes.bool,
|
||||
shift: PropTypes.bool,
|
||||
key: PropTypes.shape({
|
||||
char: PropTypes.string,
|
||||
}),
|
||||
});
|
||||
|
||||
Shortcut.propTypes = {
|
||||
shortcut: shortcutPropType,
|
||||
};
|
||||
|
||||
/* The icon button component which can have a tooltip based on props.
|
||||
React does not pass ref prop to child component hierarchy.
|
||||
Use forwardRef for the same
|
||||
*/
|
||||
export const IconButton = forwardRef((props, ref) => {
|
||||
const {title, shortcut, className, ...otherProps} = props;
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<Tippy content={
|
||||
<>
|
||||
{<div style={{textAlign: 'center'}}>{title}</div>}
|
||||
{shortcut && <Shortcut shortcut={shortcut} />}
|
||||
</>
|
||||
}>
|
||||
<BaseIconButton ref={ref} className={'btn btn-sm btn-primary-icon ' + (className || '')} {...otherProps}/>
|
||||
</Tippy>
|
||||
);
|
||||
} else {
|
||||
return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>
|
||||
}
|
||||
});
|
||||
|
||||
IconButton.propTypes = {
|
||||
title: PropTypes.string,
|
||||
shortcut: shortcutPropType,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
/* Toggle button, icon changes based on value */
|
||||
export function DetailsToggleButton({showDetails, ...props}) {
|
||||
return (
|
||||
<IconButton
|
||||
icon={showDetails ? 'far fa-eye' : 'fas fa-low-vision'}
|
||||
title={showDetails ? gettext('Show fewer details') : gettext("Show more details") }
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
DetailsToggleButton.propTypes = {
|
||||
showDetails: PropTypes.bool,
|
||||
}
|
||||
|
||||
/* Button group container */
|
||||
export function ButtonGroup({className, children}) {
|
||||
return (
|
||||
<div className={'btn-group mr-1 ' + (className ? className : '')} role="group" aria-label="save group">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ButtonGroup.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
/* Toolbar container */
|
||||
export default function ToolBar({id, children}) {
|
||||
return (
|
||||
<div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ButtonGroup.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}
|
35
web/pgadmin/tools/erd/static/js/erd_tool_hook.js
Normal file
35
web/pgadmin/tools/erd/static/js/erd_tool_hook.js
Normal file
@ -0,0 +1,35 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
define([
|
||||
'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser',
|
||||
'pgadmin.browser.server.privilege', 'pgadmin.node.database', 'pgadmin.node.primary_key',
|
||||
'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.file_manager',
|
||||
], function(
|
||||
pgAdmin, ERDToolModule
|
||||
) {
|
||||
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
|
||||
var ERDTool = ERDToolModule.default;
|
||||
|
||||
/* Return back, this has been called more than once */
|
||||
if (pgTools.ERDToolHook)
|
||||
return pgTools.ERDToolHook;
|
||||
|
||||
pgTools.ERDToolHook = {
|
||||
load: function(params) {
|
||||
/* Create the ERD Tool object and render it */
|
||||
let erdObj = new ERDTool('#erd-tool-container', params);
|
||||
erdObj.render();
|
||||
},
|
||||
};
|
||||
|
||||
return pgTools.ERDToolHook;
|
||||
});
|
||||
|
||||
|
23
web/pgadmin/tools/erd/static/js/index.js
Normal file
23
web/pgadmin/tools/erd/static/js/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import pgBrowser from 'top/browser/static/js/browser';
|
||||
import * as csrfToken from 'sources/csrf';
|
||||
import {initialize} from './erd_module';
|
||||
var wcDocker = window.wcDocker;
|
||||
|
||||
let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser, wcDocker);
|
||||
|
||||
module.exports = {
|
||||
pgBrowser: pgBrowserOut,
|
||||
};
|
189
web/pgadmin/tools/erd/static/scss/_erd.scss
Normal file
189
web/pgadmin/tools/erd/static/scss/_erd.scss
Normal file
@ -0,0 +1,189 @@
|
||||
.shortcut-key {
|
||||
padding: 0 0.25rem;
|
||||
border: 1px solid $border-color;
|
||||
margin-right: 0.125rem;
|
||||
border-radius: $btn-border-radius;
|
||||
}
|
||||
|
||||
#erd-tool-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.file-input-hidden {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.erd-hint-bar {
|
||||
background: $sql-gutters-bg;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.floating-note {
|
||||
width: 250px;
|
||||
border: $panel-border;
|
||||
border-radius: $panel-border-radius;
|
||||
box-shadow: $dialog-box-shadow;
|
||||
background-color: $alert-dialog-body-bg !important;
|
||||
color: $color-fg !important;
|
||||
|
||||
.note-header {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: $alert-header-bg;
|
||||
font-size: $font-size-base;
|
||||
font-weight: bold;
|
||||
color: $alert-header-fg;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 0rem;
|
||||
border-top-left-radius: $panel-border-radius;
|
||||
border-top-right-radius: $panel-border-radius;
|
||||
border-bottom: none;
|
||||
margin: -$alertify-borderremove-margin; //-24px is default by alertify
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.note-body {
|
||||
& textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom: $border-width solid $erd-node-border-color;
|
||||
border-top: $border-width solid $erd-node-border-color;
|
||||
}
|
||||
|
||||
& .pg_buttons {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diagram-canvas{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $color-fg;
|
||||
font-family: sans-serif;
|
||||
background-image: $erd-bg-grid;
|
||||
cursor: unset;
|
||||
|
||||
.table-node {
|
||||
background-color: $input-bg;
|
||||
border: $border-width solid $erd-node-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
position: relative;
|
||||
width: 175px;
|
||||
font-size: 0.8em;
|
||||
|
||||
&.selected {
|
||||
border-color: $input-focus-border-color;
|
||||
box-shadow: $input-btn-focus-box-shadow;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
background: $editor-toolbar-bg;
|
||||
border-bottom: $border-width solid $erd-node-border-color;
|
||||
padding: 0.125rem;
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-schema {
|
||||
border-bottom: $border-width solid $erd-node-border-color;
|
||||
padding: $erd-row-padding;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-name {
|
||||
border-bottom: $border-width*2 solid $erd-node-border-color;
|
||||
padding: $erd-row-padding;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cols {
|
||||
.col-row {
|
||||
border-bottom: $border-width solid $erd-node-border-color;
|
||||
.col-row-data {
|
||||
padding: $erd-row-padding;
|
||||
width: 100%;
|
||||
|
||||
.col-name {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.col-row-port {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg-link-ele {
|
||||
stroke: $erd-link-color;
|
||||
}
|
||||
|
||||
.svg-link-ele.path {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@keyframes svg-link-ele-selected {
|
||||
from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.svg-link-ele.selected {
|
||||
stroke: $erd-link-selected-color;
|
||||
stroke-dasharray: 10, 2;
|
||||
animation: svg-link-ele-selected 1s linear infinite;
|
||||
}
|
||||
|
||||
.svg-link-ele.svg-otom-circle {
|
||||
fill: $erd-link-color;
|
||||
}
|
||||
|
||||
.custom-node-color{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.circle-port{
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
background: darkgray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circle-port:hover{
|
||||
background: mediumpurple;
|
||||
}
|
||||
|
||||
.port {
|
||||
display: inline-block;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
55
web/pgadmin/tools/erd/templates/erd/index.html
Normal file
55
web/pgadmin/tools/erd/templates/erd/index.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{title}}{% endblock %}
|
||||
|
||||
{% block css_link %}
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<style>
|
||||
body {padding: 0px;}
|
||||
{% if is_desktop_mode and is_linux %}
|
||||
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
|
||||
.alertify-notifier{-webkit-transform: none;}
|
||||
.alertify-notifier .ajs-message{-webkit-transform: none;}
|
||||
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
|
||||
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
|
||||
{% endif %}
|
||||
</style>
|
||||
<div id="erd-tool-container" class="d-flex flex-column">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block init_script %}
|
||||
try {
|
||||
require(
|
||||
['sources/generated/browser_nodes', 'sources/generated/codemirror'],
|
||||
function() {
|
||||
require(['sources/generated/erd_tool'], function(erdToolHook) {
|
||||
var erdToolHook = erdToolHook || pgAdmin.Tools.ERDToolHook;
|
||||
erdToolHook.load({{ params|safe }});
|
||||
|
||||
if(window.opener) {
|
||||
$(window).on('unload', function(ev) {
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '{{close_url}}'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
$(window).on('beforeunload', function(ev) {
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '{{close_url}}'
|
||||
});
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
console.log(arguments);
|
||||
});
|
||||
},
|
||||
function() {
|
||||
console.log(arguments);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
{% endblock %}
|
15
web/pgadmin/tools/erd/tests/__init__.py
Normal file
15
web/pgadmin/tools/erd/tests/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
|
||||
|
||||
class ERDGenerateTestCase(BaseTestGenerator):
|
||||
def runTest(self):
|
||||
return
|
25
web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
Normal file
25
web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
CREATE TABLE public.newtable1
|
||||
(
|
||||
id integer,
|
||||
col1 character varying(50),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE public.newtable2
|
||||
(
|
||||
table1_id integer,
|
||||
col2 character varying(50),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE public.newtable3
|
||||
(
|
||||
)
|
||||
;
|
||||
|
||||
ALTER TABLE public.newtable2
|
||||
ADD FOREIGN KEY (table1_id)
|
||||
REFERENCES public.newtable1 (id)
|
||||
NOT VALID;
|
34
web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
Normal file
34
web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
CREATE TABLE public.newtable1
|
||||
(
|
||||
id integer,
|
||||
col1 character varying(50),
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
WITH (
|
||||
OIDS = FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE public.newtable2
|
||||
(
|
||||
table1_id integer,
|
||||
col2 character varying(50),
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
WITH (
|
||||
OIDS = FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE public.newtable3
|
||||
(
|
||||
)
|
||||
|
||||
WITH (
|
||||
OIDS = FALSE
|
||||
);
|
||||
|
||||
ALTER TABLE public.newtable2
|
||||
ADD FOREIGN KEY (table1_id)
|
||||
REFERENCES public.newtable1 (id)
|
||||
NOT VALID;
|
55
web/pgadmin/tools/erd/tests/test_close.py
Normal file
55
web/pgadmin/tools/erd/tests/test_close.py
Normal file
@ -0,0 +1,55 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
|
||||
|
||||
class ERDClose(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
|
||||
def runTest(self):
|
||||
db_con = database_utils.connect_database(self,
|
||||
self.sgid,
|
||||
self.sid,
|
||||
self.did)
|
||||
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to database to add the schema.")
|
||||
|
||||
url = '/erd/initialize/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
url = '/erd/close/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.delete(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
54
web/pgadmin/tools/erd/tests/test_initialize.py
Normal file
54
web/pgadmin/tools/erd/tests/test_initialize.py
Normal file
@ -0,0 +1,54 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
|
||||
|
||||
class ERDInitialize(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
|
||||
def runTest(self):
|
||||
db_con = database_utils.connect_database(self,
|
||||
self.sgid,
|
||||
self.sid,
|
||||
self.did)
|
||||
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to database to add the schema.")
|
||||
|
||||
url = '/erd/initialize/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.data.decode('utf-8'))
|
||||
self.assertEqual(response_data['data'], {
|
||||
'connId': '123344',
|
||||
'serverVersion': self.server_information['server_version'],
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
44
web/pgadmin/tools/erd/tests/test_panel.py
Normal file
44
web/pgadmin/tools/erd/tests/test_panel.py
Normal file
@ -0,0 +1,44 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
|
||||
|
||||
class ERDPanel(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
|
||||
def runTest(self):
|
||||
url = '/erd/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
|
||||
'&did={did}&gen=false'.\
|
||||
format(trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.post(
|
||||
url, data={"title": "panel_title", "close_url": "the/close/url"},
|
||||
content_type="application/x-www-form-urlencoded")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
52
web/pgadmin/tools/erd/tests/test_prequisite.py
Normal file
52
web/pgadmin/tools/erd/tests/test_prequisite.py
Normal file
@ -0,0 +1,52 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
|
||||
|
||||
class ERDPrequisite(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
|
||||
def runTest(self):
|
||||
db_con = database_utils.connect_database(self,
|
||||
self.sgid,
|
||||
self.sid,
|
||||
self.did)
|
||||
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to database to add the schema.")
|
||||
|
||||
url = '/erd/prequisite/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.data.decode('utf-8'))
|
||||
self.assertIn('col_types', response_data['data'])
|
||||
self.assertIn('schemas', response_data['data'])
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
90
web/pgadmin/tools/erd/tests/test_sql.py
Normal file
90
web/pgadmin/tools/erd/tests/test_sql.py
Normal file
@ -0,0 +1,90 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
from pgadmin.utils.versioned_template_loader import \
|
||||
get_version_mapping_directories
|
||||
from os import path
|
||||
|
||||
|
||||
class ERDSql(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
self.maxDiff = None
|
||||
|
||||
def get_expected_sql(self):
|
||||
sql_base_path = path.join(
|
||||
path.dirname(path.realpath(__file__)), 'sql')
|
||||
|
||||
# Iterate the version mapping directories.
|
||||
for version_mapping in \
|
||||
get_version_mapping_directories(self.server['type']):
|
||||
if version_mapping['number'] > \
|
||||
self.server_information['server_version']:
|
||||
continue
|
||||
|
||||
complete_path = path.join(
|
||||
sql_base_path, version_mapping['name'])
|
||||
|
||||
if not path.exists(complete_path):
|
||||
complete_path = path.join(sql_base_path, 'default')
|
||||
break
|
||||
|
||||
data_sql = ''
|
||||
with open(path.join(complete_path, 'test_sql_output.sql')) as fp:
|
||||
data_sql = fp.read()
|
||||
|
||||
return data_sql
|
||||
|
||||
def runTest(self):
|
||||
db_con = database_utils.connect_database(self,
|
||||
self.sgid,
|
||||
self.sid,
|
||||
self.did)
|
||||
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to database to add the schema.")
|
||||
|
||||
url = '/erd/sql/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
curr_dir = path.dirname(__file__)
|
||||
|
||||
data_json = None
|
||||
with open(path.join(curr_dir, 'test_sql_input_data.json')) as fp:
|
||||
data_json = fp.read()
|
||||
|
||||
response = self.tester.post(url,
|
||||
data=data_json,
|
||||
content_type='html/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data_sql = self.get_expected_sql()
|
||||
|
||||
resp_sql = json.loads(response.data.decode('utf-8'))['data']
|
||||
self.assertEqual(resp_sql, data_sql)
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
106
web/pgadmin/tools/erd/tests/test_sql_input_data.json
Normal file
106
web/pgadmin/tools/erd/tests/test_sql_input_data.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"nodes": {
|
||||
"1d9dc56e-e4f9-48b9-889b-6084ec6446bf": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"attnum": 0,
|
||||
"cltype": "integer",
|
||||
"is_primary_key": true,
|
||||
"attnotnull": false,
|
||||
"attlen": null,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n"
|
||||
},
|
||||
{
|
||||
"name": "col1",
|
||||
"attnum": 1,
|
||||
"cltype": "character varying",
|
||||
"min_val_attlen": 1,
|
||||
"max_val_attlen": 2147483647,
|
||||
"is_primary_key": false,
|
||||
"attnotnull": false,
|
||||
"attlen": 50,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n"
|
||||
}
|
||||
],
|
||||
"name": "newtable1",
|
||||
"schema": "public",
|
||||
"primary_key": [
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"column": "id"
|
||||
}
|
||||
],
|
||||
"include": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"c4fee4ad-cf32-4fc6-bb87-98b896bcab60": {
|
||||
"name": "newtable2",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"name": "table1_id",
|
||||
"attnum": 0,
|
||||
"cltype": "integer",
|
||||
"is_primary_key": false,
|
||||
"attnotnull": false,
|
||||
"attlen": null,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n",
|
||||
"old_attidentity": "a"
|
||||
},
|
||||
{
|
||||
"name": "col2",
|
||||
"attnum": 1,
|
||||
"cltype": "character varying",
|
||||
"min_val_attlen": 1,
|
||||
"max_val_attlen": 2147483647,
|
||||
"is_primary_key": false,
|
||||
"attnotnull": false,
|
||||
"attlen": 50,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n",
|
||||
"old_attidentity": "a"
|
||||
}
|
||||
],
|
||||
"primary_key": [
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"column": "id"
|
||||
}
|
||||
],
|
||||
"include": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"f001a770-d6fa-4572-b88b-11dd5e38d30c": {
|
||||
"columns": [],
|
||||
"name": "newtable3",
|
||||
"schema": "public",
|
||||
"primary_key": []
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"998de19a-caa0-431e-9cf7-97827f01022b": {
|
||||
"schema": "public",
|
||||
"table": "newtable2",
|
||||
"remote_schema": "public",
|
||||
"remote_table": "newtable1",
|
||||
"columns": [
|
||||
{
|
||||
"local_column": "table1_id",
|
||||
"referenced": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
79
web/pgadmin/tools/erd/tests/test_tables.py
Normal file
79
web/pgadmin/tools/erd/tests/test_tables.py
Normal file
@ -0,0 +1,79 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
|
||||
import utils as tables_utils
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
|
||||
utils as schema_utils
|
||||
|
||||
|
||||
class ERDTables(BaseTestGenerator):
|
||||
|
||||
def dropDB(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "erdtestdb"
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
|
||||
try:
|
||||
self.sgid = config_data["server_group"]
|
||||
self.tables = [
|
||||
["erd1", "table_1"], ["erd2", "table_2"]
|
||||
]
|
||||
|
||||
for tab in self.tables:
|
||||
connection = utils.get_db_connection(
|
||||
self.db_name, self.server['username'],
|
||||
self.server['db_password'], self.server['host'],
|
||||
self.server['port'])
|
||||
schema_utils.create_schema(connection, tab[0])
|
||||
tables_utils.create_table(self.server, self.db_name, tab[0],
|
||||
tab[1])
|
||||
connection.close()
|
||||
except Exception as _:
|
||||
self.dropDB()
|
||||
raise
|
||||
|
||||
def runTest(self):
|
||||
db_con = database_utils.connect_database(self,
|
||||
self.sgid,
|
||||
self.sid,
|
||||
self.did)
|
||||
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to database to add the schema.")
|
||||
|
||||
url = '/erd/tables/{trans_id}/{sgid}/{sid}/{did}'.format(
|
||||
trans_id=123344, sgid=self.sgid, sid=self.sid, did=self.did)
|
||||
|
||||
response = self.tester.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = json.loads(response.data.decode('utf-8'))
|
||||
self.assertEqual(self.tables, [[tab['schema'], tab['name']]
|
||||
for tab in response['data']])
|
||||
|
||||
def tearDown(self):
|
||||
self.dropDB()
|
71
web/pgadmin/tools/erd/utils.py
Normal file
71
web/pgadmin/tools/erd/utils.py
Normal file
@ -0,0 +1,71 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tables.utils \
|
||||
import BaseTableView
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
|
||||
import get_schemas
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
|
||||
import DataTypeReader
|
||||
|
||||
|
||||
class ERDTableView(BaseTableView, DataTypeReader):
|
||||
def __init__(self):
|
||||
super(BaseTableView, self).__init__(cmd='erd')
|
||||
|
||||
@BaseTableView.check_precondition
|
||||
def sql(self, conn_id=None, did=None, sid=None, data={}):
|
||||
return BaseTableView.get_sql(self, did, None, None, data, None)
|
||||
|
||||
@BaseTableView.check_precondition
|
||||
def get_types(self, conn_id=None, did=None, sid=None):
|
||||
condition = self.get_types_condition_sql(False)
|
||||
return DataTypeReader.get_types(self, self.conn, condition, True)
|
||||
|
||||
@BaseTableView.check_precondition
|
||||
def fetch_all_tables(self, conn_id=None, did=None, sid=None):
|
||||
status, schemas = get_schemas(self.conn, show_system_objects=False)
|
||||
if not status:
|
||||
return status, schemas
|
||||
|
||||
all_tables = []
|
||||
for row in schemas['rows']:
|
||||
status, res = \
|
||||
BaseTableView.fetch_tables(self, sid, did, row['oid'])
|
||||
if not status:
|
||||
return status, res
|
||||
|
||||
all_tables.extend(res.values())
|
||||
|
||||
return True, all_tables
|
||||
|
||||
|
||||
class ERDHelper:
|
||||
def __init__(self, conn_id, sid, did):
|
||||
self.conn_id = conn_id
|
||||
self.did = did
|
||||
self.sid = sid
|
||||
self.table_view = ERDTableView()
|
||||
self.link_view = None
|
||||
|
||||
def get_types(self):
|
||||
return self.table_view.get_types(
|
||||
conn_id=self.conn_id, did=self.did, sid=self.sid)
|
||||
|
||||
def get_table_sql(self, data):
|
||||
SQL, name = self.table_view.sql(
|
||||
conn_id=self.conn_id, did=self.did, sid=self.sid,
|
||||
data=data)
|
||||
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)
|
||||
|
||||
return status, res
|
@ -2651,6 +2651,12 @@ define('tools.querytool', [
|
||||
}
|
||||
|
||||
});
|
||||
} else if(url_params.sql_id) {
|
||||
let sqlValue = localStorage.getItem(url_params.sql_id);
|
||||
localStorage.removeItem(url_params.sql_id);
|
||||
if(sqlValue) {
|
||||
self.gridView.query_tool_obj.setValue(sqlValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -2668,7 +2674,7 @@ define('tools.querytool', [
|
||||
},
|
||||
|
||||
set_value_to_editor: function(query) {
|
||||
if (this.gridView && this.gridView.query_tool_obj && !_.isUndefined(query)) {
|
||||
if (this.gridView && this.gridView.query_tool_obj && !_.isUndefined(query) && query != '') {
|
||||
this.gridView.query_tool_obj.setValue(query);
|
||||
}
|
||||
},
|
||||
|
@ -36,7 +36,8 @@ class _PGCSRFProtect(CSRFProtect):
|
||||
'pgadmin.tools.debugger.direct_new',
|
||||
'pgadmin.tools.schema_diff.panel',
|
||||
'pgadmin.tools.schema_diff.ddl_compare',
|
||||
'pgadmin.authenticate.login'
|
||||
'pgadmin.authenticate.login',
|
||||
'pgadmin.tools.erd.panel',
|
||||
]
|
||||
|
||||
for exempt in exempt_views:
|
||||
|
382
web/regression/javascript/erd/erd_core_spec.js
Normal file
382
web/regression/javascript/erd/erd_core_spec.js
Normal file
@ -0,0 +1,382 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore';
|
||||
import * as createEngineLib from '@projectstorm/react-diagrams';
|
||||
import TEST_TABLES_DATA from './test_tables';
|
||||
|
||||
describe('ERDCore', ()=>{
|
||||
let eleFactory = jasmine.createSpyObj('nodeFactories', {
|
||||
'registerFactory': null,
|
||||
'getFactory': jasmine.createSpyObj('getFactory', ['generateModel', 'calculateRoutingMatrix']),
|
||||
});
|
||||
let erdEngine = jasmine.createSpyObj('engine', {
|
||||
'getNodeFactories': eleFactory,
|
||||
'getLinkFactories': eleFactory,
|
||||
'getPortFactories': eleFactory,
|
||||
'getActionEventBus': jasmine.createSpyObj('actionBus', ['fireAction', 'deregisterAction', 'registerAction']),
|
||||
'setModel': null,
|
||||
'getModel': jasmine.createSpyObj('modelObj', {
|
||||
'addNode': null,
|
||||
'clearSelection': null,
|
||||
'getNodesDict': null,
|
||||
'getLinks': null,
|
||||
'serialize': ()=>({
|
||||
'data': 'serialized',
|
||||
}),
|
||||
'addLink': null,
|
||||
'getNodes': null,
|
||||
'setZoomLevel': null,
|
||||
'getZoomLevel': null,
|
||||
'fireEvent': null,
|
||||
'registerListener': null,
|
||||
}),
|
||||
'repaintCanvas': null,
|
||||
'zoomToFit': null,
|
||||
'fireEvent': null,
|
||||
});
|
||||
|
||||
beforeAll(()=>{
|
||||
spyOn(createEngineLib, 'default').and.returnValue(erdEngine);
|
||||
});
|
||||
|
||||
it('initialization', ()=>{
|
||||
spyOn(ERDCore.prototype, 'initializeEngine').and.callThrough();
|
||||
spyOn(ERDCore.prototype, 'initializeModel').and.callThrough();
|
||||
spyOn(ERDCore.prototype, 'computeTableCounter').and.callThrough();
|
||||
let erdCoreObj = new ERDCore();
|
||||
expect(erdCoreObj.initializeEngine).toHaveBeenCalled();
|
||||
expect(erdCoreObj.initializeModel).toHaveBeenCalled();
|
||||
expect(erdCoreObj.computeTableCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('functions', ()=>{
|
||||
let erdCoreObj;
|
||||
|
||||
beforeAll(()=>{
|
||||
erdCoreObj = new ERDCore();
|
||||
});
|
||||
|
||||
describe('cache check', ()=>{
|
||||
it('for single value', ()=>{
|
||||
erdCoreObj.setCache('key1', 'value1');
|
||||
expect(erdCoreObj.getCache('key1')).toEqual('value1');
|
||||
});
|
||||
|
||||
it('for multiple value', ()=>{
|
||||
erdCoreObj.setCache({'key1': 'valuem1', 'key2': 'valuem2'});
|
||||
expect(erdCoreObj.getCache('key1')).toEqual('valuem1');
|
||||
expect(erdCoreObj.getCache('key2')).toEqual('valuem2');
|
||||
});
|
||||
});
|
||||
|
||||
it('registerModelEvent', ()=>{
|
||||
let fn = ()=>{};
|
||||
erdCoreObj.registerModelEvent('someEvent', fn);
|
||||
expect(erdCoreObj.getModel().registerListener).toHaveBeenCalledWith({
|
||||
'someEvent': fn,
|
||||
});
|
||||
});
|
||||
|
||||
it('getNextTableName', ()=>{
|
||||
expect(erdCoreObj.getNextTableName()).toEqual('newtable1');
|
||||
expect(erdCoreObj.getNextTableName()).toEqual('newtable2');
|
||||
});
|
||||
|
||||
it('getEngine', ()=>{
|
||||
expect(erdCoreObj.getEngine()).toBe(erdEngine);
|
||||
});
|
||||
|
||||
it('getNewNode', ()=>{
|
||||
let data = {name: 'table1'};
|
||||
erdCoreObj.getNewNode(data);
|
||||
|
||||
expect(erdEngine.getNodeFactories().getFactory().generateModel).toHaveBeenCalledWith({
|
||||
initialConfig: {
|
||||
otherInfo: {
|
||||
data:data,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getNewLink', ()=>{
|
||||
let data = {name: 'link1'};
|
||||
erdCoreObj.getNewLink('linktype', data);
|
||||
|
||||
expect(erdEngine.getLinkFactories().getFactory).toHaveBeenCalledWith('linktype');
|
||||
expect(erdEngine.getLinkFactories().getFactory().generateModel).toHaveBeenCalledWith({
|
||||
initialConfig: {
|
||||
data: data,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getNewPort', ()=>{
|
||||
let data = {name: 'link1'};
|
||||
let options = {opt1: 'val1'};
|
||||
erdCoreObj.getNewPort('porttype', data, options);
|
||||
|
||||
expect(erdEngine.getPortFactories().getFactory).toHaveBeenCalledWith('porttype');
|
||||
expect(erdEngine.getPortFactories().getFactory().generateModel).toHaveBeenCalledWith({
|
||||
initialConfig: {
|
||||
data:data,
|
||||
options:options,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('addNode', ()=>{
|
||||
let newNode = jasmine.createSpyObj('newNode', ['setPosition']);
|
||||
spyOn(erdCoreObj, 'getNewNode').and.returnValue(newNode);
|
||||
spyOn(erdCoreObj, 'clearSelection');
|
||||
|
||||
let data = {name: 'link1'};
|
||||
|
||||
/* Without position */
|
||||
erdCoreObj.addNode(data);
|
||||
expect(erdCoreObj.getNewNode).toHaveBeenCalledWith(data);
|
||||
expect(erdEngine.getModel().addNode).toHaveBeenCalledWith(newNode);
|
||||
expect(erdCoreObj.clearSelection).toHaveBeenCalled();
|
||||
|
||||
/* With position */
|
||||
erdCoreObj.addNode(data, [108, 108]);
|
||||
expect(erdCoreObj.getNewNode().setPosition).toHaveBeenCalledWith(108, 108);
|
||||
});
|
||||
|
||||
|
||||
it('addLink', ()=>{
|
||||
let nodesDict = {
|
||||
'id1': {
|
||||
serializeData: function(){ return {
|
||||
'name': 'table1',
|
||||
};},
|
||||
getPortName: function(attnum) {
|
||||
return `port-${attnum}`;
|
||||
},
|
||||
getPort: function() {
|
||||
return null;
|
||||
},
|
||||
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
|
||||
},
|
||||
'id2': {
|
||||
serializeData: function(){ return {
|
||||
'name': 'table2',
|
||||
};},
|
||||
getPortName: function(attnum) {
|
||||
return `port-${attnum}`;
|
||||
},
|
||||
getPort: function() {
|
||||
return null;
|
||||
},
|
||||
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
|
||||
},
|
||||
};
|
||||
let link = jasmine.createSpyObj('link', ['setSourcePort', 'setTargetPort']);
|
||||
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue(nodesDict);
|
||||
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,
|
||||
'referenced_table_uid': 'id1',
|
||||
'local_column_attnum': 3,
|
||||
'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'});
|
||||
|
||||
});
|
||||
|
||||
it('serialize', ()=>{
|
||||
let retVal = erdCoreObj.serialize();
|
||||
expect(retVal.hasOwnProperty('version')).toBeTruthy();
|
||||
expect(retVal.hasOwnProperty('data')).toBeTruthy();
|
||||
expect(erdEngine.getModel().serialize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deserialize', ()=>{
|
||||
let deserialValue = {
|
||||
'version': 123,
|
||||
'data': {
|
||||
'key': 'serialized',
|
||||
},
|
||||
};
|
||||
spyOn(erdCoreObj, 'initializeModel');
|
||||
erdCoreObj.deserialize(deserialValue);
|
||||
expect(erdCoreObj.initializeModel).toHaveBeenCalledWith(deserialValue.data);
|
||||
});
|
||||
|
||||
it('serializeData', ()=>{
|
||||
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue({
|
||||
'id1': {
|
||||
serializeData: function(){ return {
|
||||
'name': 'table1',
|
||||
};},
|
||||
},
|
||||
'id2': {
|
||||
serializeData: function(){ return {
|
||||
'name': 'table2',
|
||||
};},
|
||||
},
|
||||
});
|
||||
spyOn(erdEngine.getModel(), 'getLinks').and.returnValue([
|
||||
{
|
||||
serializeData: function(){ return {
|
||||
'name': 'link1',
|
||||
};},
|
||||
getID: function(){ return 'lid1'; },
|
||||
},
|
||||
{
|
||||
serializeData: function(){ return {
|
||||
'name': 'link2',
|
||||
};},
|
||||
getID: function(){ return 'lid2'; },
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(erdCoreObj.serializeData())).toEqual(JSON.stringify({
|
||||
nodes: {
|
||||
'id1': {'name': 'table1'},
|
||||
'id2': {'name': 'table2'},
|
||||
},
|
||||
links: {
|
||||
'lid1': {'name': 'link1'},
|
||||
'lid2': {'name': 'link2'},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('deserializeData', (done)=>{
|
||||
let nodesDict = {};
|
||||
TEST_TABLES_DATA.forEach((table)=>{
|
||||
nodesDict[`id-${table.name}`] = {
|
||||
getColumns: function() {
|
||||
return table.columns;
|
||||
},
|
||||
getPortName: function(attnum) {
|
||||
return `port-${attnum}`;
|
||||
},
|
||||
getPort: function(name) {
|
||||
return {'name': name};
|
||||
},
|
||||
addPort: function() {
|
||||
|
||||
},
|
||||
};
|
||||
});
|
||||
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue(nodesDict);
|
||||
|
||||
spyOn(erdCoreObj, 'getNewLink').and.callFake(function() {
|
||||
return {
|
||||
setSourcePort: function() {},
|
||||
setTargetPort: function() {},
|
||||
};
|
||||
});
|
||||
spyOn(erdCoreObj, 'getNewPort').and.returnValue({id: 'id'});
|
||||
spyOn(erdCoreObj, 'addNode').and.callFake(function(data) {
|
||||
return {
|
||||
getID: function() {
|
||||
return `id-${data.name}`;
|
||||
},
|
||||
};
|
||||
});
|
||||
spyOn(erdCoreObj, 'addLink');
|
||||
spyOn(erdCoreObj, 'dagreDistributeNodes');
|
||||
|
||||
erdCoreObj.deserializeData(TEST_TABLES_DATA);
|
||||
expect(erdCoreObj.addNode).toHaveBeenCalledTimes(TEST_TABLES_DATA.length);
|
||||
expect(erdCoreObj.addLink).toHaveBeenCalledTimes(1);
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(erdCoreObj.dagreDistributeNodes).toHaveBeenCalled();
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
it('clearSelection', ()=>{
|
||||
erdCoreObj.clearSelection();
|
||||
expect(erdEngine.getModel().clearSelection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('repaint', ()=>{
|
||||
erdCoreObj.repaint();
|
||||
expect(erdEngine.repaintCanvas).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getNodesData', ()=>{
|
||||
spyOn(erdEngine.getModel(), 'getNodes').and.returnValue([
|
||||
{getData: function () {return {name:'node1'};}},
|
||||
{getData: function () {return {name:'node2'};}},
|
||||
]);
|
||||
expect(JSON.stringify(erdCoreObj.getNodesData())).toEqual(JSON.stringify([
|
||||
{name:'node1'}, {name:'node2'},
|
||||
]));
|
||||
});
|
||||
|
||||
it('dagreDistributeNodes', ()=>{
|
||||
spyOn(erdCoreObj.dagre_engine, 'redistribute');
|
||||
erdCoreObj.dagreDistributeNodes();
|
||||
expect(erdEngine.getLinkFactories().getFactory().calculateRoutingMatrix).toHaveBeenCalled();
|
||||
expect(erdCoreObj.dagre_engine.redistribute).toHaveBeenCalledWith(erdEngine.getModel());
|
||||
});
|
||||
|
||||
it('zoomIn', ()=>{
|
||||
spyOn(erdEngine.getModel(), 'getZoomLevel').and.returnValue(100);
|
||||
spyOn(erdCoreObj, 'repaint');
|
||||
erdCoreObj.zoomIn();
|
||||
expect(erdEngine.getModel().setZoomLevel).toHaveBeenCalledWith(125);
|
||||
expect(erdCoreObj.repaint).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('zoomOut', ()=>{
|
||||
spyOn(erdEngine.getModel(), 'getZoomLevel').and.returnValue(100);
|
||||
spyOn(erdCoreObj, 'repaint');
|
||||
erdCoreObj.zoomOut();
|
||||
expect(erdEngine.getModel().setZoomLevel).toHaveBeenCalledWith(75);
|
||||
expect(erdCoreObj.repaint).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('zoomToFit', ()=>{
|
||||
erdCoreObj.zoomToFit();
|
||||
expect(erdEngine.zoomToFit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fireAction', ()=>{
|
||||
erdCoreObj.fireAction({key: 'xyz'});
|
||||
expect(erdEngine.getActionEventBus().fireAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fireEvent', ()=>{
|
||||
erdCoreObj.fireEvent({key: 'xyz'}, 'someevent', false);
|
||||
expect(erdEngine.fireEvent).toHaveBeenCalledWith({key: 'xyz'}, 'someevent');
|
||||
|
||||
erdCoreObj.fireEvent({key: 'xyz'}, 'someevent', true);
|
||||
expect(erdEngine.getModel().fireEvent).toHaveBeenCalledWith({key: 'xyz'}, 'someevent');
|
||||
});
|
||||
|
||||
it('registerKeyAction', ()=>{
|
||||
erdCoreObj.registerKeyAction({key: 'xyz'});
|
||||
expect(erdEngine.getActionEventBus().registerAction).toHaveBeenCalledWith({key: 'xyz'});
|
||||
});
|
||||
|
||||
it('deregisterKeyAction', ()=>{
|
||||
let action = {key: 'xyz'};
|
||||
erdCoreObj.deregisterKeyAction(action);
|
||||
expect(erdEngine.getActionEventBus().deregisterAction).toHaveBeenCalledWith({key: 'xyz'});
|
||||
});
|
||||
});
|
||||
});
|
34
web/regression/javascript/erd/erd_model_spec.js
Normal file
34
web/regression/javascript/erd/erd_model_spec.js
Normal file
@ -0,0 +1,34 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import ERDModel from 'pgadmin.tools.erd/erd_tool/ERDModel';
|
||||
|
||||
describe('ERDModel', ()=>{
|
||||
it('getNodesDict', ()=>{
|
||||
let model = new ERDModel();
|
||||
|
||||
spyOn(model, 'getNodes').and.returnValue([
|
||||
{
|
||||
name: 'test1',
|
||||
getID: function() {
|
||||
return 'id1';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
getID: function() {
|
||||
return 'id2';
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(model.getNodesDict())).toBe(JSON.stringify({
|
||||
'id1': {name: 'test1'},
|
||||
'id2': {name: 'test2'},
|
||||
}));
|
||||
});
|
||||
});
|
@ -0,0 +1,61 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import {KeyboardShortcutAction} from 'pgadmin.tools.erd/erd_tool/ui_components/BodyWidget';
|
||||
|
||||
describe('KeyboardShortcutAction', ()=>{
|
||||
let keyAction = null;
|
||||
let key1 = {
|
||||
alt: true,
|
||||
control: true,
|
||||
shift: false,
|
||||
key: {
|
||||
key_code: 65,
|
||||
},
|
||||
};
|
||||
let key2 = {
|
||||
alt: false,
|
||||
control: true,
|
||||
shift: false,
|
||||
key: {
|
||||
key_code: 66,
|
||||
},
|
||||
};
|
||||
let handler1 = jasmine.createSpy('handler1');
|
||||
let handler2 = jasmine.createSpy('handler2');
|
||||
|
||||
beforeAll(()=>{
|
||||
spyOn(KeyboardShortcutAction.prototype, 'shortcutKey').and.callThrough();
|
||||
keyAction = new KeyboardShortcutAction([
|
||||
[key1, handler1],
|
||||
[key2, handler2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(Object.keys(keyAction.shortcuts).length).toBe(2);
|
||||
});
|
||||
|
||||
it('shortcutKey', ()=>{
|
||||
expect(keyAction.shortcutKey(true, true, true, true, 65)).toBe('true:true:true:true:65');
|
||||
expect(keyAction.shortcutKey(true, false, true, true, 65)).toBe('true:false:true:true:65');
|
||||
expect(keyAction.shortcutKey(true, true, false, true, 65)).toBe('true:true:false:true:65');
|
||||
expect(keyAction.shortcutKey(true, true, true, false, 65)).toBe('true:true:true:false:65');
|
||||
expect(keyAction.shortcutKey(false, true, true, true, 65)).toBe('false:true:true:true:65');
|
||||
});
|
||||
|
||||
it('callHandler', ()=>{
|
||||
let keyEvent = {altKey: key1.alt, ctrlKey: key1.control, shiftKey: key1.shift, metaKey: false, keyCode:key1.key.key_code};
|
||||
keyAction.callHandler(keyEvent);
|
||||
expect(handler1).toHaveBeenCalled();
|
||||
|
||||
keyEvent = {altKey: key2.alt, ctrlKey: key2.control, shiftKey: key2.shift, metaKey: false, keyCode:key2.key.key_code};
|
||||
keyAction.callHandler(keyEvent);
|
||||
expect(handler2).toHaveBeenCalled();
|
||||
});
|
||||
});
|
133
web/regression/javascript/erd/onetomany_link_spec.js
Normal file
133
web/regression/javascript/erd/onetomany_link_spec.js
Normal file
@ -0,0 +1,133 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../helper/enzyme.helper';
|
||||
import {
|
||||
RightAngleLinkModel,
|
||||
} from '@projectstorm/react-diagrams';
|
||||
|
||||
import OneToManyPortModel from 'pgadmin.tools.erd/erd_tool/ports/OneToManyPort';
|
||||
import {OneToManyLinkModel, OneToManyLinkWidget, OneToManyLinkFactory} from 'pgadmin.tools.erd/erd_tool/links/OneToManyLink';
|
||||
|
||||
|
||||
describe('ERD OneToManyLinkModel', ()=>{
|
||||
let modelObj = null;
|
||||
beforeAll(()=>{
|
||||
spyOn(RightAngleLinkModel.prototype, 'serialize').and.returnValue({'key': 'value'});
|
||||
});
|
||||
beforeEach(()=>{
|
||||
modelObj = new OneToManyLinkModel({
|
||||
data: {
|
||||
local_table_uid: 'id1',
|
||||
local_column_attnum: 0,
|
||||
referenced_table_uid: 'id2',
|
||||
referenced_column_attnum: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(modelObj.getData()).toEqual({
|
||||
local_table_uid: 'id1',
|
||||
local_column_attnum: 0,
|
||||
referenced_table_uid: 'id2',
|
||||
referenced_column_attnum: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('setData', ()=>{
|
||||
modelObj.setData({
|
||||
local_column_attnum: 2,
|
||||
referenced_column_attnum: 4,
|
||||
});
|
||||
expect(modelObj.getData()).toEqual({
|
||||
local_column_attnum: 2,
|
||||
referenced_column_attnum: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('serializeData', ()=>{
|
||||
let nodesDict = {
|
||||
'id1': {
|
||||
getData: function(){ return {
|
||||
'name': 'table1',
|
||||
'schema': 'erd1',
|
||||
'columns': [
|
||||
{'name': 'col11', attnum: 0},
|
||||
{'name': 'col12', attnum: 1},
|
||||
],
|
||||
};},
|
||||
},
|
||||
'id2': {
|
||||
getData: function(){ return {
|
||||
'name': 'table2',
|
||||
'schema': 'erd2',
|
||||
'columns': [
|
||||
{'name': 'col21', attnum: 0},
|
||||
{'name': 'col22', attnum: 1},
|
||||
],
|
||||
};},
|
||||
},
|
||||
};
|
||||
|
||||
expect(modelObj.serializeData(nodesDict)).toEqual({
|
||||
'schema': 'erd1',
|
||||
'table': 'table1',
|
||||
'remote_schema': 'erd2',
|
||||
'remote_table': 'table2',
|
||||
'columns': [{
|
||||
'local_column': 'col11',
|
||||
'referenced': 'col22',
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('serialize', ()=>{
|
||||
let retVal = modelObj.serialize();
|
||||
expect(RightAngleLinkModel.prototype.serialize).toHaveBeenCalled();
|
||||
expect(retVal).toEqual({
|
||||
key: 'value',
|
||||
data: {
|
||||
local_table_uid: 'id1',
|
||||
local_column_attnum: 0,
|
||||
referenced_table_uid: 'id2',
|
||||
referenced_column_attnum: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERD OneToManyLinkWidget', ()=>{
|
||||
let linkFactory = new OneToManyLinkFactory();
|
||||
let engine = {
|
||||
getFactoryForLink: ()=>linkFactory,
|
||||
};
|
||||
let link = null;
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
|
||||
link = new OneToManyLinkModel({
|
||||
color: '#000',
|
||||
data: {
|
||||
local_table_uid: 'id1',
|
||||
local_column_attnum: 0,
|
||||
referenced_table_uid: 'id2',
|
||||
referenced_column_attnum: 1,
|
||||
},
|
||||
});
|
||||
link.setSourcePort(new OneToManyPortModel({options: {}}));
|
||||
link.setTargetPort(new OneToManyPortModel({options: {}}));
|
||||
});
|
||||
|
||||
it('render', ()=>{
|
||||
let linkWidget = mount(
|
||||
<svg><OneToManyLinkWidget link={link} diagramEngine={engine} factory={linkFactory} /></svg>
|
||||
);
|
||||
|
||||
let paths = linkWidget.find('g g');
|
||||
expect(paths.at(0).find('polyline').length).toBe(1);
|
||||
expect(paths.at(paths.length-1).find('polyline').length).toBe(1);
|
||||
expect(paths.at(paths.length-1).find('circle').length).toBe(1);
|
||||
});
|
||||
});
|
21
web/regression/javascript/erd/onetomany_port_spec.js
Normal file
21
web/regression/javascript/erd/onetomany_port_spec.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { PortModel } from '@projectstorm/react-diagrams-core';
|
||||
import OneToManyPortModel from 'pgadmin.tools.erd/erd_tool/ports/OneToManyPort';
|
||||
import {OneToManyLinkModel} from 'pgadmin.tools.erd/erd_tool/links/OneToManyLink';
|
||||
|
||||
describe('ERD OneToManyPortModel', ()=>{
|
||||
it('removeAllLinks', ()=>{
|
||||
let link1 = jasmine.createSpyObj('link1', ['remove']);
|
||||
let link2 = jasmine.createSpyObj('link2', ['remove']);
|
||||
spyOn(PortModel.prototype, 'getLinks').and.returnValue([link1, link2]);
|
||||
|
||||
let portObj = new OneToManyPortModel({options: {}});
|
||||
portObj.removeAllLinks();
|
||||
expect(link1.remove).toHaveBeenCalled();
|
||||
expect(link2.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('createLinkModel', ()=>{
|
||||
let portObj = new OneToManyPortModel({options: {}});
|
||||
expect(portObj.createLinkModel()).toBeInstanceOf(OneToManyLinkModel);
|
||||
});
|
||||
});
|
305
web/regression/javascript/erd/table_node_spec.js
Normal file
305
web/regression/javascript/erd/table_node_spec.js
Normal file
@ -0,0 +1,305 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../helper/enzyme.helper';
|
||||
import { DefaultNodeModel } from '@projectstorm/react-diagrams';
|
||||
|
||||
import {TableNodeModel, TableNodeWidget} from 'pgadmin.tools.erd/erd_tool/nodes/TableNode';
|
||||
import { IconButton, DetailsToggleButton } from 'pgadmin.tools.erd/erd_tool/ui_components/ToolBar';
|
||||
|
||||
|
||||
describe('ERD TableNodeModel', ()=>{
|
||||
let modelObj = null;
|
||||
beforeAll(()=>{
|
||||
spyOn(DefaultNodeModel.prototype, 'serialize').and.returnValue({'key': 'value'});
|
||||
});
|
||||
beforeEach(()=>{
|
||||
modelObj = new TableNodeModel({
|
||||
color: '#000',
|
||||
otherInfo: {
|
||||
note: 'some note',
|
||||
data: {
|
||||
name: 'table1',
|
||||
schema: 'erd',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(modelObj.getData()).toEqual({
|
||||
columns: [],
|
||||
name: 'table1',
|
||||
schema: 'erd',
|
||||
});
|
||||
expect(modelObj.getNote()).toBe('some note');
|
||||
expect(modelObj.getColumns()).toEqual([]);
|
||||
});
|
||||
|
||||
it('getPortName', ()=>{
|
||||
expect(modelObj.getPortName(2)).toBe('coll-port-2');
|
||||
});
|
||||
|
||||
it('setNote', ()=>{
|
||||
modelObj.setNote('some note to test');
|
||||
expect(modelObj.getNote()).toBe('some note to test');
|
||||
});
|
||||
|
||||
it('addColumn', ()=>{
|
||||
modelObj.addColumn({name: 'col1', not_null:false, attnum: 0});
|
||||
expect(modelObj.getColumns()).toEqual([{name: 'col1', not_null:false, attnum: 0}]);
|
||||
});
|
||||
|
||||
it('getColumnAt', ()=>{
|
||||
modelObj.addColumn({name: 'col1', not_null:false, attnum: 0});
|
||||
modelObj.addColumn({name: 'col2', not_null:false, attnum: 1});
|
||||
expect(modelObj.getColumnAt(0)).toEqual({name: 'col1', not_null:false, attnum: 0});
|
||||
expect(modelObj.getColumnAt(1)).toEqual({name: 'col2', not_null:false, attnum: 1});
|
||||
expect(modelObj.getColumnAt(2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('setName', ()=>{
|
||||
modelObj.setName('changedName');
|
||||
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']);
|
||||
|
||||
beforeEach(()=>{
|
||||
modelObj._data.columns = [
|
||||
{name: 'col1', not_null:false, attnum: 0},
|
||||
{name: 'col2', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:false, attnum: 2},
|
||||
];
|
||||
|
||||
spyOn(modelObj, 'getPort').and.callFake((portName)=>{
|
||||
/* If new port added there will not be any port */
|
||||
if(portName !== 'coll-port-3') {
|
||||
return existPort;
|
||||
}
|
||||
});
|
||||
spyOn(modelObj, 'removePort');
|
||||
spyOn(modelObj, 'getPortName');
|
||||
});
|
||||
|
||||
it('add columns', ()=>{
|
||||
existPort.removeAllLinks.calls.reset();
|
||||
modelObj.setData({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col1', not_null:false, attnum: 0},
|
||||
{name: 'col2', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:false, attnum: 2},
|
||||
{name: 'col4', not_null:false, attnum: 3},
|
||||
],
|
||||
});
|
||||
expect(modelObj.getData()).toEqual({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col1', not_null:false, attnum: 0},
|
||||
{name: 'col2', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:false, attnum: 2},
|
||||
{name: 'col4', not_null:false, attnum: 3},
|
||||
],
|
||||
});
|
||||
expect(existPort.removeAllLinks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update columns', ()=>{
|
||||
existPort.removeAllLinks.calls.reset();
|
||||
modelObj.setData({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col1', not_null:false, attnum: 0},
|
||||
{name: 'col2updated', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:true, attnum: 2},
|
||||
],
|
||||
});
|
||||
expect(modelObj.getData()).toEqual({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col1', not_null:false, attnum: 0},
|
||||
{name: 'col2updated', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:true, attnum: 2},
|
||||
],
|
||||
});
|
||||
expect(existPort.removeAllLinks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('remove columns', ()=>{
|
||||
existPort.removeAllLinks.calls.reset();
|
||||
modelObj.setData({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col2', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:false, attnum: 2},
|
||||
],
|
||||
});
|
||||
expect(modelObj.getData()).toEqual({
|
||||
name: 'noname',
|
||||
schema: 'erd',
|
||||
columns: [
|
||||
{name: 'col2', not_null:false, attnum: 1},
|
||||
{name: 'col3', not_null:false, attnum: 2},
|
||||
],
|
||||
});
|
||||
|
||||
expect(modelObj.getPortName).toHaveBeenCalledWith(0);
|
||||
expect(existPort.removeAllLinks).toHaveBeenCalled();
|
||||
expect(modelObj.removePort).toHaveBeenCalledWith(existPort);
|
||||
});
|
||||
});
|
||||
|
||||
it('getSchemaTableName', ()=>{
|
||||
expect(modelObj.getSchemaTableName()).toEqual(['erd', 'table1']);
|
||||
});
|
||||
|
||||
it('serializeData', ()=>{
|
||||
modelObj.addColumn({name: 'col1', not_null:false, attnum: 0});
|
||||
expect(modelObj.serializeData()).toEqual({
|
||||
name: 'table1',
|
||||
schema: 'erd',
|
||||
columns: [{name: 'col1', not_null:false, attnum: 0}],
|
||||
});
|
||||
});
|
||||
|
||||
it('serialize', ()=>{
|
||||
let retVal = modelObj.serialize();
|
||||
expect(DefaultNodeModel.prototype.serialize).toHaveBeenCalled();
|
||||
expect(retVal).toEqual({
|
||||
key: 'value',
|
||||
otherInfo: {
|
||||
data: {
|
||||
columns: [],
|
||||
name: 'table1',
|
||||
schema: 'erd',
|
||||
},
|
||||
note: 'some note',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERD TableNodeWidget', ()=>{
|
||||
let node = null;
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
|
||||
node = new TableNodeModel({
|
||||
color: '#000',
|
||||
otherInfo: {
|
||||
note: 'some note',
|
||||
data: {
|
||||
name: 'table1',
|
||||
schema: 'erd',
|
||||
columns: [{
|
||||
attnum: 0,
|
||||
is_primary_key: true,
|
||||
name: 'id',
|
||||
cltype: 'integer',
|
||||
attlen: null,
|
||||
attprecision: null,
|
||||
}, {
|
||||
attnum: 1,
|
||||
is_primary_key: false,
|
||||
name: 'amount',
|
||||
cltype: 'number',
|
||||
attlen: 10,
|
||||
attprecision: 5,
|
||||
}, {
|
||||
attnum: 2,
|
||||
is_primary_key: false,
|
||||
name: 'desc',
|
||||
cltype: 'character varrying',
|
||||
attlen: 50,
|
||||
attprecision: null,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('render', ()=>{
|
||||
let nodeWidget = mount(<TableNodeWidget node={node}/>);
|
||||
expect(nodeWidget.getDOMNode().className).toBe('table-node ');
|
||||
expect(nodeWidget.find('.table-node .table-toolbar').length).toBe(1);
|
||||
expect(nodeWidget.find('.table-node .table-schema').text()).toBe('erd');
|
||||
expect(nodeWidget.find('.table-node .table-name').text()).toBe('table1');
|
||||
expect(nodeWidget.find('.table-node .table-cols').length).toBe(1);
|
||||
expect(nodeWidget.find(DetailsToggleButton).length).toBe(1);
|
||||
expect(nodeWidget.find(IconButton).findWhere(n => n.prop('title')=='Check note').length).toBe(1);
|
||||
});
|
||||
|
||||
it('node selected', ()=>{
|
||||
spyOn(node, 'isSelected').and.returnValue(true);
|
||||
let nodeWidget = mount(<TableNodeWidget node={node}/>);
|
||||
expect(nodeWidget.getDOMNode().className).toBe('table-node selected');
|
||||
});
|
||||
|
||||
it('remove note', ()=>{
|
||||
node.setNote('');
|
||||
let nodeWidget = mount(<TableNodeWidget node={node}/>);
|
||||
expect(nodeWidget.find(IconButton).findWhere(n => n.prop('title')=='Check note').length).toBe(0);
|
||||
});
|
||||
|
||||
describe('generateColumn', ()=>{
|
||||
let nodeWidget = null;
|
||||
|
||||
beforeEach(()=>{
|
||||
nodeWidget = mount(<TableNodeWidget node={node}/>);
|
||||
});
|
||||
|
||||
it('count', ()=>{
|
||||
expect(nodeWidget.find('.table-node .table-cols .col-row').length).toBe(3);
|
||||
});
|
||||
|
||||
it('icons', ()=>{
|
||||
let cols = nodeWidget.find('.table-node .table-cols .col-row-data');
|
||||
expect(cols.at(0).find('.wcTabIcon').hasClass('icon-primary_key')).toBeTrue();
|
||||
expect(cols.at(1).find('.wcTabIcon').hasClass('icon-column')).toBeTrue();
|
||||
expect(cols.at(2).find('.wcTabIcon').hasClass('icon-column')).toBeTrue();
|
||||
});
|
||||
|
||||
it('column names', ()=>{
|
||||
let cols = nodeWidget.find('.table-node .table-cols .col-row-data');
|
||||
expect(cols.at(0).find('.col-name').text()).toBe('id');
|
||||
expect(cols.at(1).find('.col-name').text()).toBe('amount');
|
||||
expect(cols.at(2).find('.col-name').text()).toBe('desc');
|
||||
});
|
||||
|
||||
it('data types', ()=>{
|
||||
let cols = nodeWidget.find('.table-node .table-cols .col-row-data');
|
||||
expect(cols.at(0).find('.col-datatype').text()).toBe('integer');
|
||||
expect(cols.at(1).find('.col-datatype').text()).toBe('number(10,5)');
|
||||
expect(cols.at(2).find('.col-datatype').text()).toBe('character varrying(50)');
|
||||
});
|
||||
|
||||
it('show_details', (done)=>{
|
||||
nodeWidget.setState({show_details: false});
|
||||
expect(nodeWidget.find('.table-node .table-cols .col-row-data .col-datatype').length).toBe(0);
|
||||
|
||||
nodeWidget.instance().toggleShowDetails(jasmine.createSpyObj('event', ['preventDefault']));
|
||||
/* Dummy set state to wait for toggleShowDetails -> setState to complete */
|
||||
nodeWidget.setState({}, ()=>{
|
||||
expect(nodeWidget.find('.table-node .table-cols .col-row-data .col-datatype').length).toBe(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
651
web/regression/javascript/erd/test_tables.js
Normal file
651
web/regression/javascript/erd/test_tables.js
Normal file
@ -0,0 +1,651 @@
|
||||
export default [
|
||||
{
|
||||
'oid': 123456,
|
||||
'name': 'test1',
|
||||
'spcoid': 0,
|
||||
'relacl_str': null,
|
||||
'spcname': 'pg_default',
|
||||
'schema': 'schema1',
|
||||
'relowner': 'postgres',
|
||||
'relkind': 'r',
|
||||
'is_partitioned': false,
|
||||
'relhassubclass': false,
|
||||
'reltuples': '0',
|
||||
'description': null,
|
||||
'conname': null,
|
||||
'conkey': null,
|
||||
'isrepl': false,
|
||||
'triggercount': '0',
|
||||
'coll_inherits': [],
|
||||
'inherited_tables_cnt': '0',
|
||||
'relpersistence': false,
|
||||
'fillfactor': null,
|
||||
'parallel_workers': null,
|
||||
'toast_tuple_target': null,
|
||||
'autovacuum_enabled': 'x',
|
||||
'autovacuum_vacuum_threshold': null,
|
||||
'autovacuum_vacuum_scale_factor': null,
|
||||
'autovacuum_analyze_threshold': null,
|
||||
'autovacuum_analyze_scale_factor': null,
|
||||
'autovacuum_vacuum_cost_delay': null,
|
||||
'autovacuum_vacuum_cost_limit': null,
|
||||
'autovacuum_freeze_min_age': null,
|
||||
'autovacuum_freeze_max_age': null,
|
||||
'autovacuum_freeze_table_age': null,
|
||||
'toast_autovacuum_enabled': 'x',
|
||||
'toast_autovacuum_vacuum_threshold': null,
|
||||
'toast_autovacuum_vacuum_scale_factor': null,
|
||||
'toast_autovacuum_analyze_threshold': null,
|
||||
'toast_autovacuum_analyze_scale_factor': null,
|
||||
'toast_autovacuum_vacuum_cost_delay': null,
|
||||
'toast_autovacuum_vacuum_cost_limit': null,
|
||||
'toast_autovacuum_freeze_min_age': null,
|
||||
'toast_autovacuum_freeze_max_age': null,
|
||||
'toast_autovacuum_freeze_table_age': null,
|
||||
'reloptions': null,
|
||||
'toast_reloptions': null,
|
||||
'reloftype': 0,
|
||||
'typname': null,
|
||||
'typoid': null,
|
||||
'rlspolicy': false,
|
||||
'forcerlspolicy': false,
|
||||
'hastoasttable': false,
|
||||
'seclabels': null,
|
||||
'is_sys_table': false,
|
||||
'partition_scheme': '',
|
||||
'autovacuum_custom': false,
|
||||
'toast_autovacuum': false,
|
||||
'rows_cnt': 0,
|
||||
'vacuum_settings_str': '',
|
||||
'vacuum_table': [
|
||||
{
|
||||
'name': 'autovacuum_analyze_scale_factor',
|
||||
'setting': '0.1',
|
||||
'label': 'ANALYZE scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_analyze_threshold',
|
||||
'setting': '50',
|
||||
'label': 'ANALYZE base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_max_age',
|
||||
'setting': '200000000',
|
||||
'label': 'FREEZE maximum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_delay',
|
||||
'setting': '2',
|
||||
'label': 'VACUUM cost delay',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_limit',
|
||||
'setting': '-1',
|
||||
'label': 'VACUUM cost limit',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_scale_factor',
|
||||
'setting': '0.2',
|
||||
'label': 'VACUUM scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_threshold',
|
||||
'setting': '50',
|
||||
'label': 'VACUUM base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_min_age',
|
||||
'setting': '50000000',
|
||||
'label': 'FREEZE minimum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_table_age',
|
||||
'setting': '150000000',
|
||||
'label': 'FREEZE table age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
],
|
||||
'vacuum_toast': [
|
||||
{
|
||||
'name': 'autovacuum_freeze_max_age',
|
||||
'setting': '200000000',
|
||||
'label': 'FREEZE maximum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_delay',
|
||||
'setting': '2',
|
||||
'label': 'VACUUM cost delay',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_limit',
|
||||
'setting': '-1',
|
||||
'label': 'VACUUM cost limit',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_scale_factor',
|
||||
'setting': '0.2',
|
||||
'label': 'VACUUM scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_threshold',
|
||||
'setting': '50',
|
||||
'label': 'VACUUM base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_min_age',
|
||||
'setting': '50000000',
|
||||
'label': 'FREEZE minimum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_table_age',
|
||||
'setting': '150000000',
|
||||
'label': 'FREEZE table age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
],
|
||||
'columns': [{
|
||||
'name': 'id',
|
||||
'atttypid': 23,
|
||||
'attlen': null,
|
||||
'attnum': 1,
|
||||
'attndims': 0,
|
||||
'atttypmod': -1,
|
||||
'attacl': [],
|
||||
'attnotnull': true,
|
||||
'attoptions': null,
|
||||
'attstattarget': -1,
|
||||
'attstorage': 'p',
|
||||
'attidentity': '',
|
||||
'defval': null,
|
||||
'typname': 'integer',
|
||||
'displaytypname': 'integer',
|
||||
'cltype': 'integer',
|
||||
'elemoid': 23,
|
||||
'typnspname': 'pg_catalog',
|
||||
'defaultstorage': 'p',
|
||||
'description': null,
|
||||
'indkey': '1',
|
||||
'isdup': false,
|
||||
'collspcname': '',
|
||||
'is_fk': false,
|
||||
'seclabels': null,
|
||||
'is_sys_column': false,
|
||||
'colconstype': 'n',
|
||||
'genexpr': null,
|
||||
'relname': 'tab1',
|
||||
'is_view_only': false,
|
||||
'seqrelid': null,
|
||||
'seqtypid': null,
|
||||
'seqstart': null,
|
||||
'seqincrement': null,
|
||||
'seqmax': null,
|
||||
'seqmin': null,
|
||||
'seqcache': null,
|
||||
'seqcycle': null,
|
||||
'is_pk': true,
|
||||
'is_primary_key': true,
|
||||
'attprecision': null,
|
||||
'edit_types': [
|
||||
'bigint',
|
||||
'double precision',
|
||||
'information_schema.cardinal_number',
|
||||
'integer',
|
||||
'money',
|
||||
'numeric',
|
||||
'oid',
|
||||
'real',
|
||||
'regclass',
|
||||
'regconfig',
|
||||
'regdictionary',
|
||||
'regnamespace',
|
||||
'regoper',
|
||||
'regoperator',
|
||||
'regproc',
|
||||
'regprocedure',
|
||||
'regrole',
|
||||
'regtype',
|
||||
'smallint',
|
||||
],
|
||||
}],
|
||||
'primary_key': [],
|
||||
'unique_constraint': [],
|
||||
'check_constraint': [],
|
||||
'index': {},
|
||||
'rule': {},
|
||||
'trigger': {},
|
||||
'row_security_policy': {},
|
||||
},
|
||||
{
|
||||
'oid': 408229,
|
||||
'name': 'test2',
|
||||
'spcoid': 0,
|
||||
'relacl_str': null,
|
||||
'spcname': 'pg_default',
|
||||
'schema': 'erd',
|
||||
'relowner': 'postgres',
|
||||
'relkind': 'r',
|
||||
'is_partitioned': false,
|
||||
'relhassubclass': false,
|
||||
'reltuples': '0',
|
||||
'description': null,
|
||||
'conname': 'tab1_pkey',
|
||||
'conkey': [
|
||||
1,
|
||||
],
|
||||
'isrepl': false,
|
||||
'triggercount': '0',
|
||||
'coll_inherits': [],
|
||||
'inherited_tables_cnt': '0',
|
||||
'relpersistence': false,
|
||||
'fillfactor': null,
|
||||
'parallel_workers': null,
|
||||
'toast_tuple_target': null,
|
||||
'autovacuum_enabled': 'x',
|
||||
'autovacuum_vacuum_threshold': null,
|
||||
'autovacuum_vacuum_scale_factor': null,
|
||||
'autovacuum_analyze_threshold': null,
|
||||
'autovacuum_analyze_scale_factor': null,
|
||||
'autovacuum_vacuum_cost_delay': null,
|
||||
'autovacuum_vacuum_cost_limit': null,
|
||||
'autovacuum_freeze_min_age': null,
|
||||
'autovacuum_freeze_max_age': null,
|
||||
'autovacuum_freeze_table_age': null,
|
||||
'toast_autovacuum_enabled': 'x',
|
||||
'toast_autovacuum_vacuum_threshold': null,
|
||||
'toast_autovacuum_vacuum_scale_factor': null,
|
||||
'toast_autovacuum_analyze_threshold': null,
|
||||
'toast_autovacuum_analyze_scale_factor': null,
|
||||
'toast_autovacuum_vacuum_cost_delay': null,
|
||||
'toast_autovacuum_vacuum_cost_limit': null,
|
||||
'toast_autovacuum_freeze_min_age': null,
|
||||
'toast_autovacuum_freeze_max_age': null,
|
||||
'toast_autovacuum_freeze_table_age': null,
|
||||
'reloptions': null,
|
||||
'toast_reloptions': null,
|
||||
'reloftype': 0,
|
||||
'typname': null,
|
||||
'typoid': null,
|
||||
'rlspolicy': false,
|
||||
'forcerlspolicy': false,
|
||||
'hastoasttable': false,
|
||||
'seclabels': null,
|
||||
'is_sys_table': false,
|
||||
'partition_scheme': '',
|
||||
'autovacuum_custom': false,
|
||||
'toast_autovacuum': false,
|
||||
'rows_cnt': 0,
|
||||
'vacuum_settings_str': '',
|
||||
'vacuum_table': [
|
||||
{
|
||||
'name': 'autovacuum_analyze_scale_factor',
|
||||
'setting': '0.1',
|
||||
'label': 'ANALYZE scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_analyze_threshold',
|
||||
'setting': '50',
|
||||
'label': 'ANALYZE base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_max_age',
|
||||
'setting': '200000000',
|
||||
'label': 'FREEZE maximum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_delay',
|
||||
'setting': '2',
|
||||
'label': 'VACUUM cost delay',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_limit',
|
||||
'setting': '-1',
|
||||
'label': 'VACUUM cost limit',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_scale_factor',
|
||||
'setting': '0.2',
|
||||
'label': 'VACUUM scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_threshold',
|
||||
'setting': '50',
|
||||
'label': 'VACUUM base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_min_age',
|
||||
'setting': '50000000',
|
||||
'label': 'FREEZE minimum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_table_age',
|
||||
'setting': '150000000',
|
||||
'label': 'FREEZE table age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
],
|
||||
'vacuum_toast': [
|
||||
{
|
||||
'name': 'autovacuum_freeze_max_age',
|
||||
'setting': '200000000',
|
||||
'label': 'FREEZE maximum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_delay',
|
||||
'setting': '2',
|
||||
'label': 'VACUUM cost delay',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_cost_limit',
|
||||
'setting': '-1',
|
||||
'label': 'VACUUM cost limit',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_scale_factor',
|
||||
'setting': '0.2',
|
||||
'label': 'VACUUM scale factor',
|
||||
'column_type': 'number',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_vacuum_threshold',
|
||||
'setting': '50',
|
||||
'label': 'VACUUM base threshold',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_min_age',
|
||||
'setting': '50000000',
|
||||
'label': 'FREEZE minimum age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
{
|
||||
'name': 'autovacuum_freeze_table_age',
|
||||
'setting': '150000000',
|
||||
'label': 'FREEZE table age',
|
||||
'column_type': 'integer',
|
||||
},
|
||||
],
|
||||
'columns': [
|
||||
{
|
||||
'name': 'id',
|
||||
'atttypid': 23,
|
||||
'attlen': null,
|
||||
'attnum': 1,
|
||||
'attndims': 0,
|
||||
'atttypmod': -1,
|
||||
'attacl': [],
|
||||
'attnotnull': true,
|
||||
'attoptions': null,
|
||||
'attstattarget': -1,
|
||||
'attstorage': 'p',
|
||||
'attidentity': '',
|
||||
'defval': null,
|
||||
'typname': 'integer',
|
||||
'displaytypname': 'integer',
|
||||
'cltype': 'integer',
|
||||
'elemoid': 23,
|
||||
'typnspname': 'pg_catalog',
|
||||
'defaultstorage': 'p',
|
||||
'description': null,
|
||||
'indkey': '1',
|
||||
'isdup': false,
|
||||
'collspcname': '',
|
||||
'is_fk': false,
|
||||
'seclabels': null,
|
||||
'is_sys_column': false,
|
||||
'colconstype': 'n',
|
||||
'genexpr': null,
|
||||
'relname': 'tab1',
|
||||
'is_view_only': false,
|
||||
'seqrelid': null,
|
||||
'seqtypid': null,
|
||||
'seqstart': null,
|
||||
'seqincrement': null,
|
||||
'seqmax': null,
|
||||
'seqmin': null,
|
||||
'seqcache': null,
|
||||
'seqcycle': null,
|
||||
'is_pk': true,
|
||||
'is_primary_key': true,
|
||||
'attprecision': null,
|
||||
'edit_types': [
|
||||
'bigint',
|
||||
'double precision',
|
||||
'information_schema.cardinal_number',
|
||||
'integer',
|
||||
'money',
|
||||
'numeric',
|
||||
'oid',
|
||||
'real',
|
||||
'regclass',
|
||||
'regconfig',
|
||||
'regdictionary',
|
||||
'regnamespace',
|
||||
'regoper',
|
||||
'regoperator',
|
||||
'regproc',
|
||||
'regprocedure',
|
||||
'regrole',
|
||||
'regtype',
|
||||
'smallint',
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'col1col1col1col1col1col1col1col1',
|
||||
'atttypid': 23,
|
||||
'attlen': null,
|
||||
'attnum': 2,
|
||||
'attndims': 0,
|
||||
'atttypmod': -1,
|
||||
'attacl': [],
|
||||
'attnotnull': true,
|
||||
'attoptions': null,
|
||||
'attstattarget': -1,
|
||||
'attstorage': 'p',
|
||||
'attidentity': '',
|
||||
'defval': null,
|
||||
'typname': 'integer',
|
||||
'displaytypname': 'integer',
|
||||
'cltype': 'integer',
|
||||
'elemoid': 23,
|
||||
'typnspname': 'pg_catalog',
|
||||
'defaultstorage': 'p',
|
||||
'description': null,
|
||||
'indkey': '1',
|
||||
'isdup': false,
|
||||
'collspcname': '',
|
||||
'is_fk': true,
|
||||
'seclabels': null,
|
||||
'is_sys_column': false,
|
||||
'colconstype': 'n',
|
||||
'genexpr': null,
|
||||
'relname': 'tab1',
|
||||
'is_view_only': false,
|
||||
'seqrelid': null,
|
||||
'seqtypid': null,
|
||||
'seqstart': null,
|
||||
'seqincrement': null,
|
||||
'seqmax': null,
|
||||
'seqmin': null,
|
||||
'seqcache': null,
|
||||
'seqcycle': null,
|
||||
'is_pk': false,
|
||||
'is_primary_key': false,
|
||||
'attprecision': null,
|
||||
'edit_types': [
|
||||
'bigint',
|
||||
'double precision',
|
||||
'information_schema.cardinal_number',
|
||||
'integer',
|
||||
'integer',
|
||||
'money',
|
||||
'numeric',
|
||||
'oid',
|
||||
'real',
|
||||
'regclass',
|
||||
'regconfig',
|
||||
'regdictionary',
|
||||
'regnamespace',
|
||||
'regoper',
|
||||
'regoperator',
|
||||
'regproc',
|
||||
'regprocedure',
|
||||
'regrole',
|
||||
'regtype',
|
||||
'smallint',
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'col2',
|
||||
'atttypid': 23,
|
||||
'attlen': null,
|
||||
'attnum': 3,
|
||||
'attndims': 0,
|
||||
'atttypmod': -1,
|
||||
'attacl': [],
|
||||
'attnotnull': false,
|
||||
'attoptions': null,
|
||||
'attstattarget': -1,
|
||||
'attstorage': 'p',
|
||||
'attidentity': '',
|
||||
'defval': null,
|
||||
'typname': 'integer',
|
||||
'displaytypname': 'integer',
|
||||
'cltype': 'integer',
|
||||
'elemoid': 23,
|
||||
'typnspname': 'pg_catalog',
|
||||
'defaultstorage': 'p',
|
||||
'description': null,
|
||||
'indkey': '1',
|
||||
'isdup': false,
|
||||
'collspcname': '',
|
||||
'is_fk': false,
|
||||
'seclabels': null,
|
||||
'is_sys_column': false,
|
||||
'colconstype': 'n',
|
||||
'genexpr': null,
|
||||
'relname': 'tab1',
|
||||
'is_view_only': false,
|
||||
'seqrelid': null,
|
||||
'seqtypid': null,
|
||||
'seqstart': null,
|
||||
'seqincrement': null,
|
||||
'seqmax': null,
|
||||
'seqmin': null,
|
||||
'seqcache': null,
|
||||
'seqcycle': null,
|
||||
'is_pk': false,
|
||||
'is_primary_key': false,
|
||||
'attprecision': null,
|
||||
'edit_types': [
|
||||
'bigint',
|
||||
'double precision',
|
||||
'information_schema.cardinal_number',
|
||||
'integer',
|
||||
'integer',
|
||||
'integer',
|
||||
'money',
|
||||
'numeric',
|
||||
'oid',
|
||||
'real',
|
||||
'regclass',
|
||||
'regconfig',
|
||||
'regdictionary',
|
||||
'regnamespace',
|
||||
'regoper',
|
||||
'regoperator',
|
||||
'regproc',
|
||||
'regprocedure',
|
||||
'regrole',
|
||||
'regtype',
|
||||
'smallint',
|
||||
],
|
||||
},
|
||||
],
|
||||
'primary_key': [
|
||||
{
|
||||
'oid': 408232,
|
||||
'name': 'tab1_pkey',
|
||||
'col_count': 1,
|
||||
'spcname': 'pg_default',
|
||||
'comment': null,
|
||||
'condeferrable': false,
|
||||
'condeferred': false,
|
||||
'fillfactor': null,
|
||||
'columns': [
|
||||
{
|
||||
'column': 'id',
|
||||
},
|
||||
],
|
||||
'include': [],
|
||||
},
|
||||
],
|
||||
'unique_constraint': [],
|
||||
'foreign_key': [
|
||||
{
|
||||
'oid': 408239,
|
||||
'name': 'tab1_col1_fkey',
|
||||
'condeferrable': false,
|
||||
'condeferred': false,
|
||||
'confupdtype': 'a',
|
||||
'confdeltype': 'a',
|
||||
'confmatchtype': false,
|
||||
'conkey': [
|
||||
2,
|
||||
],
|
||||
'confkey': [
|
||||
1,
|
||||
],
|
||||
'confrelid': 408234,
|
||||
'fknsp': 'erd',
|
||||
'fktab': 'tab1',
|
||||
'refnsp': 'erd',
|
||||
'reftab': 'tab2',
|
||||
'comment': null,
|
||||
'convalidated': false,
|
||||
'columns': [
|
||||
{
|
||||
'local_column': 'col1col1col1col1col1col1col1col1',
|
||||
'references': 123456,
|
||||
'referenced': 'id',
|
||||
'references_table_name': 'schema1.test1',
|
||||
},
|
||||
],
|
||||
'remote_schema': 'schema1',
|
||||
'remote_table': 'test1',
|
||||
'coveringindex': null,
|
||||
'autoindex': true,
|
||||
'hasindex': false,
|
||||
},
|
||||
],
|
||||
'check_constraint': [],
|
||||
'index': {},
|
||||
'rule': {},
|
||||
'trigger': {},
|
||||
'row_security_policy': {},
|
||||
},
|
||||
];
|
514
web/regression/javascript/erd/ui_components/body_widget_spec.js
Normal file
514
web/regression/javascript/erd/ui_components/body_widget_spec.js
Normal file
@ -0,0 +1,514 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../../helper/enzyme.helper';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from 'axios/index';
|
||||
|
||||
import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore';
|
||||
import * as erdModule from 'pgadmin.tools.erd/erd_module';
|
||||
import erdPref from './erd_preferences';
|
||||
import BodyWidget from 'pgadmin.tools.erd/erd_tool/ui_components/BodyWidget';
|
||||
import * as ERDSqlTool from 'tools/datagrid/static/js/show_query_tool';
|
||||
|
||||
let pgAdmin = {
|
||||
Browser: {
|
||||
Events: {
|
||||
on: jasmine.createSpy('on'),
|
||||
},
|
||||
get_preferences_for_module: function() {
|
||||
return erdPref;
|
||||
},
|
||||
docker: {
|
||||
findPanels: function() {
|
||||
return [
|
||||
{
|
||||
isVisible: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
onPreferencesChange: ()=>{},
|
||||
utils: {
|
||||
app_version_int: 1234,
|
||||
},
|
||||
},
|
||||
FileManager: {
|
||||
init: jasmine.createSpy(),
|
||||
show_dialog: jasmine.createSpy(),
|
||||
},
|
||||
};
|
||||
|
||||
let alertify = jasmine.createSpyObj('alertify', {
|
||||
'success': null,
|
||||
'error': null,
|
||||
'confirm': null,
|
||||
'alert': {
|
||||
'set': ()=>{},
|
||||
},
|
||||
});
|
||||
|
||||
let tableDialog = jasmine.createSpyObj('TableDialog', ['show']);
|
||||
let otmDialog = jasmine.createSpyObj('otmDialog', ['show']);
|
||||
let mtmDialog = jasmine.createSpyObj('mtmDialog', ['show']);
|
||||
|
||||
let getDialog = (dialogName)=>{
|
||||
switch(dialogName) {
|
||||
case 'entity_dialog': return tableDialog;
|
||||
case 'onetomany_dialog': return otmDialog;
|
||||
case 'manytomany_dialog': return mtmDialog;
|
||||
}
|
||||
};
|
||||
|
||||
describe('ERD BodyWidget', ()=>{
|
||||
let body = null;
|
||||
let bodyInstance = null;
|
||||
let networkMock = null;
|
||||
let serverVersion = 120000;
|
||||
let colTypes = [
|
||||
{'label': 'integer', 'value': 'integer'},
|
||||
{'label': 'character varrying', 'value': 'character varrying'},
|
||||
];
|
||||
let schemas = [
|
||||
{'oid': 111, 'name': 'erd1'},
|
||||
{'oid': 222, 'name': 'erd2'},
|
||||
];
|
||||
let params = {
|
||||
bgcolor: null,
|
||||
client_platform: 'macos',
|
||||
did: '13637',
|
||||
fgcolor: null,
|
||||
gen: true,
|
||||
is_desktop_mode: true,
|
||||
is_linux: false,
|
||||
server_type: 'pg',
|
||||
sgid: '1',
|
||||
sid: '5',
|
||||
title: 'postgres/postgres@PostgreSQL 12',
|
||||
trans_id: 110008,
|
||||
};
|
||||
|
||||
beforeAll(()=>{
|
||||
spyOn(erdModule, 'setPanelTitle');
|
||||
spyOn(ERDCore.prototype, 'repaint');
|
||||
spyOn(ERDCore.prototype, 'deserializeData');
|
||||
spyOn(ERDCore.prototype, 'addNode').and.returnValue({
|
||||
setSelected: ()=>{},
|
||||
getColumns: ()=>([{attnum: 0}, {attnum: 1}]),
|
||||
getID: ()=>'newid1',
|
||||
});
|
||||
spyOn(ERDCore.prototype, 'addLink').and.returnValue({
|
||||
setSelected: ()=>{},
|
||||
});
|
||||
spyOn(alertify, 'confirm').and.callFake((arg1, arg2, okCallback)=>{
|
||||
okCallback();
|
||||
});
|
||||
|
||||
networkMock = new MockAdapter(axios);
|
||||
networkMock.onPost('/erd/initialize/110008/1/5/13637').reply(200, {'data': {
|
||||
serverVersion: serverVersion,
|
||||
}});
|
||||
networkMock.onGet('/erd/prequisite/110008/1/5/13637').reply(200, {'data': {
|
||||
'col_types': colTypes,
|
||||
'schemas': schemas,
|
||||
}});
|
||||
networkMock.onGet('/erd/tables/110008/1/5/13637').reply(200, {'data': []});
|
||||
|
||||
networkMock.onPost('/erd/sql/110008/1/5/13637').reply(200, {'data': 'SELECT 1;'});
|
||||
|
||||
networkMock.onPost('/sqleditor/load_file/').reply(200, {'data': 'data'});
|
||||
networkMock.onPost('/sqleditor/save_file/').reply(200, {'data': 'data'});
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
|
||||
bodyInstance = body.instance();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
networkMock.restore();
|
||||
if(body) {
|
||||
body.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('constructor', (done)=>{
|
||||
|
||||
expect(body.find('ToolBar').length).toBe(1);
|
||||
expect(body.find('ConnectionBar').length).toBe(1);
|
||||
expect(body.find('FloatingNote').length).toBe(1);
|
||||
expect(body.find('.diagram-container Loader').length).toBe(1);
|
||||
expect(body.find('.diagram-container CanvasWidget').length).toBe(1);
|
||||
|
||||
body.instance().setState({}, ()=>{
|
||||
let instance = body.instance();
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(body.state()).toEqual(jasmine.objectContaining({
|
||||
server_version: serverVersion,
|
||||
preferences: erdPref,
|
||||
}));
|
||||
expect(instance.diagram.getCache('colTypes')).toEqual(colTypes);
|
||||
expect(instance.diagram.getCache('schemas')).toEqual(schemas);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('event offsetUpdated', (done)=>{
|
||||
bodyInstance.diagram.fireEvent({offsetX: 4, offsetY: 5}, 'offsetUpdated', true);
|
||||
setTimeout(()=>{
|
||||
expect(bodyInstance.canvasEle.style.backgroundPosition).toBe('4px 5px');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event zoomUpdated', (done)=>{
|
||||
spyOn(bodyInstance.diagram.getModel(), 'getOptions').and.returnValue({gridSize: 15});
|
||||
bodyInstance.diagram.fireEvent({zoom: 20}, 'zoomUpdated', true);
|
||||
setTimeout(()=>{
|
||||
expect(bodyInstance.canvasEle.style.backgroundSize).toBe('9px 9px');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event nodesSelectionChanged', (done)=>{
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([{key:'value'}]);
|
||||
bodyInstance.diagram.fireEvent({}, 'nodesSelectionChanged', true);
|
||||
setTimeout(()=>{
|
||||
expect(body.state().single_node_selected).toBe(true);
|
||||
expect(body.state().any_item_selected).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event linksSelectionChanged', (done)=>{
|
||||
spyOn(bodyInstance.diagram, 'getSelectedLinks').and.returnValue([{key:'value'}]);
|
||||
bodyInstance.diagram.fireEvent({}, 'linksSelectionChanged', true);
|
||||
setTimeout(()=>{
|
||||
expect(body.state().single_link_selected).toBe(true);
|
||||
expect(body.state().any_item_selected).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event linksUpdated', (done)=>{
|
||||
bodyInstance.diagram.fireEvent({}, 'linksUpdated', true);
|
||||
setTimeout(()=>{
|
||||
expect(body.state().dirty).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event nodesUpdated', (done)=>{
|
||||
bodyInstance.diagram.fireEvent({}, 'nodesUpdated', true);
|
||||
setTimeout(()=>{
|
||||
expect(body.state().dirty).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event showNote', (done)=>{
|
||||
let noteNode = {key: 'value', getNote: ()=>'a note'};
|
||||
spyOn(bodyInstance, 'showNote');
|
||||
bodyInstance.diagram.fireEvent({node: noteNode}, 'showNote', true);
|
||||
setTimeout(()=>{
|
||||
expect(bodyInstance.showNote).toHaveBeenCalledWith(noteNode);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('event editNode', (done)=>{
|
||||
let node = {key: 'value', getNote: ()=>'a note'};
|
||||
spyOn(bodyInstance, 'addEditNode');
|
||||
bodyInstance.diagram.fireEvent({node: node}, 'editNode', true);
|
||||
setTimeout(()=>{
|
||||
expect(bodyInstance.addEditNode).toHaveBeenCalledWith(node);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getDialog', ()=>{
|
||||
bodyInstance.getDialog('entity_dialog')();
|
||||
expect(tableDialog.show).toHaveBeenCalled();
|
||||
|
||||
bodyInstance.getDialog('onetomany_dialog')();
|
||||
expect(otmDialog.show).toHaveBeenCalled();
|
||||
|
||||
bodyInstance.getDialog('manytomany_dialog')();
|
||||
expect(mtmDialog.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('addEditNode', ()=>{
|
||||
/* New */
|
||||
tableDialog.show.calls.reset();
|
||||
bodyInstance.addEditNode();
|
||||
expect(tableDialog.show).toHaveBeenCalled();
|
||||
|
||||
let saveCallback = tableDialog.show.calls.mostRecent().args[5];
|
||||
let newData = {key: 'value'};
|
||||
saveCallback(newData);
|
||||
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
|
||||
|
||||
/* Existing */
|
||||
tableDialog.show.calls.reset();
|
||||
let node = jasmine.createSpyObj('node',{
|
||||
getSchemaTableName: ['erd1', 'table1'],
|
||||
setData: null,
|
||||
getData: null,
|
||||
});
|
||||
bodyInstance.addEditNode(node);
|
||||
expect(tableDialog.show).toHaveBeenCalled();
|
||||
|
||||
saveCallback = tableDialog.show.calls.mostRecent().args[5];
|
||||
newData = {key: 'value'};
|
||||
saveCallback(newData);
|
||||
expect(node.setData).toHaveBeenCalledWith(newData);
|
||||
});
|
||||
|
||||
it('onEditNode', ()=>{
|
||||
let node = {key: 'value'};
|
||||
spyOn(bodyInstance, 'addEditNode');
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
|
||||
bodyInstance.onEditNode();
|
||||
expect(bodyInstance.addEditNode).toHaveBeenCalledWith(node);
|
||||
});
|
||||
|
||||
it('onAddNewNode', ()=>{
|
||||
spyOn(bodyInstance, 'addEditNode');
|
||||
bodyInstance.onAddNewNode();
|
||||
expect(bodyInstance.addEditNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onCloneNode', ()=>{
|
||||
let node = jasmine.createSpyObj('node',{
|
||||
getSchemaTableName: ['erd1', 'table1'],
|
||||
setData: null,
|
||||
getData: null,
|
||||
cloneData: {key: 'value'},
|
||||
getPosition: {x: 30, y: 30},
|
||||
});
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
|
||||
spyOn(bodyInstance.diagram, 'getNextTableName').and.returnValue('newtable1');
|
||||
bodyInstance.onCloneNode();
|
||||
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith({key: 'value'}, [50, 50]);
|
||||
});
|
||||
|
||||
it('onDeleteNode', (done)=>{
|
||||
let node = jasmine.createSpyObj('node',{
|
||||
getSchemaTableName: ['erd1', 'table1'],
|
||||
setData: null,
|
||||
getData: null,
|
||||
cloneData: {key: 'value'},
|
||||
getPosition: {x: 30, y: 30},
|
||||
remove: null,
|
||||
setSelected: null,
|
||||
});
|
||||
let link = jasmine.createSpyObj('link', {
|
||||
remove: null,
|
||||
setSelected: null,
|
||||
getTargetPort: jasmine.createSpyObj('port', ['remove']),
|
||||
getSourcePort: jasmine.createSpyObj('port', ['remove']),
|
||||
});
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
|
||||
spyOn(bodyInstance.diagram, 'getSelectedLinks').and.returnValue([link]);
|
||||
|
||||
bodyInstance.onDeleteNode();
|
||||
setTimeout(()=>{
|
||||
expect(node.remove).toHaveBeenCalled();
|
||||
expect(link.remove).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('onAutoDistribute', ()=>{
|
||||
spyOn(bodyInstance.diagram, 'dagreDistributeNodes');
|
||||
bodyInstance.onAutoDistribute();
|
||||
expect(bodyInstance.diagram.dagreDistributeNodes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onDetailsToggle', (done)=>{
|
||||
let node = jasmine.createSpyObj('node',['fireEvent']);
|
||||
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
|
||||
'getNodes': ()=>[node],
|
||||
});
|
||||
|
||||
let show_details = body.state().show_details;
|
||||
bodyInstance.onDetailsToggle();
|
||||
body.setState({}, ()=>{
|
||||
expect(body.state().show_details).toBe(!show_details);
|
||||
expect(node.fireEvent).toHaveBeenCalledWith({show_details: !show_details}, 'toggleDetails');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('onLoadDiagram', ()=>{
|
||||
bodyInstance.onLoadDiagram();
|
||||
expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('openFile', (done)=>{
|
||||
spyOn(bodyInstance.diagram, 'deserialize');
|
||||
bodyInstance.openFile('test.pgerd');
|
||||
setTimeout(()=>{
|
||||
expect(body.state()).toEqual(jasmine.objectContaining({
|
||||
current_file: 'test.pgerd',
|
||||
dirty: false,
|
||||
}));
|
||||
expect(bodyInstance.diagram.deserialize).toHaveBeenCalledWith({data: 'data'});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('onSaveDiagram', (done)=>{
|
||||
body.setState({
|
||||
current_file: 'newfile.pgerd',
|
||||
});
|
||||
bodyInstance.onSaveDiagram();
|
||||
setTimeout(()=>{
|
||||
expect(body.state()).toEqual(jasmine.objectContaining({
|
||||
current_file: 'newfile.pgerd',
|
||||
dirty: false,
|
||||
}));
|
||||
done();
|
||||
});
|
||||
|
||||
bodyInstance.onSaveDiagram(true);
|
||||
expect(pgAdmin.FileManager.show_dialog).toHaveBeenCalledWith({
|
||||
'supported_types': ['pgerd'],
|
||||
'dialog_type': 'create_file',
|
||||
'dialog_title': 'Save File',
|
||||
'btn_primary': 'Save',
|
||||
});
|
||||
});
|
||||
|
||||
it('onSaveAsDiagram', ()=>{
|
||||
spyOn(bodyInstance, 'onSaveDiagram');
|
||||
bodyInstance.onSaveAsDiagram();
|
||||
expect(bodyInstance.onSaveDiagram).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('onSQLClick', (done)=>{
|
||||
spyOn(bodyInstance.diagram, 'serializeData').and.returnValue({key: 'value'});
|
||||
spyOn(ERDSqlTool, 'showERDSqlTool');
|
||||
spyOn(localStorage, 'setItem');
|
||||
bodyInstance.onSQLClick();
|
||||
|
||||
setTimeout(()=>{
|
||||
let sql = '-- This script was generated by a beta version of 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;';
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('erd'+params.trans_id, sql);
|
||||
expect(ERDSqlTool.showERDSqlTool).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('onOneToManyClick', ()=>{
|
||||
let node = jasmine.createSpyObj('node',{
|
||||
getID: 'id1',
|
||||
});
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
|
||||
|
||||
otmDialog.show.calls.reset();
|
||||
bodyInstance.onOneToManyClick();
|
||||
expect(otmDialog.show).toHaveBeenCalled();
|
||||
|
||||
let saveCallback = otmDialog.show.calls.mostRecent().args[4];
|
||||
let newData = {key: 'value'};
|
||||
saveCallback(newData);
|
||||
expect(bodyInstance.diagram.addLink).toHaveBeenCalledWith(newData, 'onetomany');
|
||||
});
|
||||
|
||||
it('onManyToManyClick', ()=>{
|
||||
let node = jasmine.createSpyObj('node',{
|
||||
getID: 'id1',
|
||||
});
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
|
||||
|
||||
mtmDialog.show.calls.reset();
|
||||
bodyInstance.onManyToManyClick();
|
||||
expect(mtmDialog.show).toHaveBeenCalled();
|
||||
|
||||
/* onSave */
|
||||
let nodesDict = {
|
||||
'id1': {
|
||||
getID: ()=>'id1',
|
||||
getData: ()=>({name: 'table1', schema: 'erd1'}),
|
||||
getColumnAt: ()=>({name: 'col1', type: 'type1', attnum: 0}),
|
||||
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
|
||||
},
|
||||
'id2': {
|
||||
getID: ()=>'id2',
|
||||
getData: ()=>({name: 'table2', schema: 'erd2'}),
|
||||
getColumnAt: ()=>({name: 'col2', type: 'type2', attnum: 1}),
|
||||
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
|
||||
},
|
||||
};
|
||||
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
|
||||
'getNodesDict': ()=>nodesDict,
|
||||
});
|
||||
spyOn(bodyInstance.diagram, 'addLink');
|
||||
let saveCallback = mtmDialog.show.calls.mostRecent().args[4];
|
||||
let newData = {
|
||||
left_table_uid: 'id1',
|
||||
left_table_column_attnum: 1,
|
||||
right_table_uid: 'id2',
|
||||
right_table_column_attnum: 2,
|
||||
};
|
||||
|
||||
bodyInstance.diagram.addNode.calls.reset();
|
||||
bodyInstance.diagram.addLink.calls.reset();
|
||||
saveCallback(newData);
|
||||
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith({
|
||||
name: 'table1_table2',
|
||||
schema: 'erd1',
|
||||
columns: [
|
||||
{
|
||||
type: 'type1',
|
||||
name: 'table1_col1',
|
||||
is_primary_key: false,
|
||||
attnum: 0,
|
||||
},
|
||||
{
|
||||
type: 'type2',
|
||||
name: 'table2_col2',
|
||||
is_primary_key: false,
|
||||
attnum: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let linkData = {
|
||||
local_table_uid: 'newid1',
|
||||
local_column_attnum: 0,
|
||||
referenced_table_uid: 'id1',
|
||||
referenced_column_attnum : 1,
|
||||
};
|
||||
expect(bodyInstance.diagram.addLink.calls.argsFor(0)).toEqual([linkData, 'onetomany']);
|
||||
linkData = {
|
||||
local_table_uid: 'newid1',
|
||||
local_column_attnum: 1,
|
||||
referenced_table_uid: 'id2',
|
||||
referenced_column_attnum : 2,
|
||||
};
|
||||
expect(bodyInstance.diagram.addLink.calls.argsFor(1)).toEqual([linkData, 'onetomany']);
|
||||
});
|
||||
|
||||
it('onNoteClick', ()=>{
|
||||
let noteNode = {key: 'value', getNote: ()=>'a note'};
|
||||
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([noteNode]);
|
||||
spyOn(bodyInstance.diagram.getEngine(), 'getNodeElement').and.returnValue(null);
|
||||
spyOn(bodyInstance.diagram.getEngine(), 'getNodeElement').and.returnValue(null);
|
||||
spyOn(bodyInstance, 'setState');
|
||||
bodyInstance.onNoteClick();
|
||||
expect(bodyInstance.setState).toHaveBeenCalledWith({
|
||||
note_node: noteNode,
|
||||
note_open: true,
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../../helper/enzyme.helper';
|
||||
|
||||
import ConnectionBar, {STATUS} from 'pgadmin.tools.erd/erd_tool/ui_components/ConnectionBar';
|
||||
|
||||
describe('ERD ConnectionBar', ()=>{
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
it('<ConnectionBar /> comp', ()=>{
|
||||
let connBar = mount(<ConnectionBar statusId="conn-bar" status={STATUS.DISCONNECTED} title="test title"/>);
|
||||
|
||||
expect(connBar.find('.editor-title').text()).toBe('test title');
|
||||
|
||||
connBar.setProps({status: STATUS.CONNECTING});
|
||||
expect(connBar.find('.editor-title').text()).toBe('(Obtaining connection...) test title');
|
||||
|
||||
connBar.setProps({bgcolor: '#000', fgcolor: '#fff'});
|
||||
expect(connBar.find('.editor-title').prop('style').backgroundColor).toBe('#000');
|
||||
expect(connBar.find('.editor-title').prop('style').color).toBe('#fff');
|
||||
});
|
||||
});
|
147
web/regression/javascript/erd/ui_components/erd_preferences.js
Normal file
147
web/regression/javascript/erd/ui_components/erd_preferences.js
Normal file
@ -0,0 +1,147 @@
|
||||
export default {
|
||||
'erd_new_browser_tab': false,
|
||||
'open_project': {
|
||||
'alt': false,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 79,
|
||||
'char': 'o',
|
||||
},
|
||||
},
|
||||
'save_project': {
|
||||
'alt': false,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's',
|
||||
},
|
||||
},
|
||||
'save_project_as': {
|
||||
'alt': false,
|
||||
'shift': true,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's',
|
||||
},
|
||||
},
|
||||
'generate_sql': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 83,
|
||||
'char': 's',
|
||||
},
|
||||
},
|
||||
'download_image': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 73,
|
||||
'char': 'i',
|
||||
},
|
||||
},
|
||||
'add_table': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 65,
|
||||
'char': 'a',
|
||||
},
|
||||
},
|
||||
'edit_table': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 69,
|
||||
'char': 'e',
|
||||
},
|
||||
},
|
||||
'clone_table': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 67,
|
||||
'char': 'c',
|
||||
},
|
||||
},
|
||||
'drop_table': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 68,
|
||||
'char': 'd',
|
||||
},
|
||||
},
|
||||
'add_edit_note': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 78,
|
||||
'char': 'n',
|
||||
},
|
||||
},
|
||||
'one_to_many': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 79,
|
||||
'char': 'o',
|
||||
},
|
||||
},
|
||||
'many_to_many': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 77,
|
||||
'char': 'm',
|
||||
},
|
||||
},
|
||||
'auto_align': {
|
||||
'alt': true,
|
||||
'shift': false,
|
||||
'control': true,
|
||||
'key': {
|
||||
'key_code': 76,
|
||||
'char': 'l',
|
||||
},
|
||||
},
|
||||
'zoom_to_fit': {
|
||||
'alt': true,
|
||||
'shift': true,
|
||||
'control': false,
|
||||
'key': {
|
||||
'key_code': 70,
|
||||
'char': 'f',
|
||||
},
|
||||
},
|
||||
'zoom_in': {
|
||||
'alt': true,
|
||||
'shift': true,
|
||||
'control': false,
|
||||
'key': {
|
||||
'key_code': 187,
|
||||
'char': '+',
|
||||
},
|
||||
},
|
||||
'zoom_out': {
|
||||
'alt': true,
|
||||
'shift': true,
|
||||
'control': false,
|
||||
'key': {
|
||||
'key_code': 189,
|
||||
'char': '-',
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../../helper/enzyme.helper';
|
||||
|
||||
import FloatingNote from 'pgadmin.tools.erd/erd_tool/ui_components/FloatingNote';
|
||||
|
||||
describe('ERD FloatingNote', ()=>{
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
it('<FloatingNote /> on OK click', ()=>{
|
||||
let floatNote = null;
|
||||
let onClose = jasmine.createSpy('onClose');
|
||||
let noteNode = {
|
||||
getNote: function() {
|
||||
return 'some note';
|
||||
},
|
||||
setNote: jasmine.createSpy('setNote'),
|
||||
getSchemaTableName: function() {
|
||||
return ['schema1', 'table1'];
|
||||
},
|
||||
};
|
||||
|
||||
floatNote = mount(<FloatingNote open={false} onClose={onClose}
|
||||
reference={null} noteNode={noteNode} appendTo={document.body} rows={8}/>);
|
||||
|
||||
floatNote.find('textarea').simulate('change', {
|
||||
target: {
|
||||
value: 'the new note',
|
||||
},
|
||||
});
|
||||
floatNote.find('button[data-label="OK"]').simulate('click');
|
||||
expect(noteNode.setNote).toHaveBeenCalledWith('the new note');
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
23
web/regression/javascript/erd/ui_components/loader_spec.js
Normal file
23
web/regression/javascript/erd/ui_components/loader_spec.js
Normal file
@ -0,0 +1,23 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import '../../helper/enzyme.helper';
|
||||
|
||||
import Loader from 'pgadmin.tools.erd/erd_tool/ui_components/Loader';
|
||||
|
||||
describe('ERD Loader', ()=>{
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
it('<Loader /> comp', ()=>{
|
||||
let loaderComp = shallow(<Loader />);
|
||||
expect(loaderComp.isEmptyRender()).toBeTrue();
|
||||
|
||||
loaderComp.setProps({message: 'test message'});
|
||||
expect(loaderComp.find('.pg-sp-text').text()).toBe('test message');
|
||||
|
||||
loaderComp.setProps({autoEllipsis: true});
|
||||
expect(loaderComp.find('.pg-sp-text').text()).toBe('test message...');
|
||||
});
|
||||
});
|
76
web/regression/javascript/erd/ui_components/toolbar_spec.js
Normal file
76
web/regression/javascript/erd/ui_components/toolbar_spec.js
Normal file
@ -0,0 +1,76 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import {mount, shallow} from 'enzyme';
|
||||
import '../../helper/enzyme.helper';
|
||||
|
||||
import ToolBar, {ButtonGroup, DetailsToggleButton, IconButton, Shortcut} from 'pgadmin.tools.erd/erd_tool/ui_components/ToolBar';
|
||||
|
||||
describe('ERD Toolbar', ()=>{
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
it('<Toolbar /> comp', ()=>{
|
||||
let toolBar = mount(<ToolBar id="id1"><div className="test"></div></ToolBar>);
|
||||
expect(toolBar.getDOMNode().id).toBe('id1');
|
||||
expect(toolBar.find('.test').length).toBe(1);
|
||||
});
|
||||
|
||||
it('<ButtonGroup /> comp', ()=>{
|
||||
let btnGrp = mount(<ButtonGroup><div className="test"></div></ButtonGroup>);
|
||||
expect(btnGrp.getDOMNode().className).toBe('btn-group mr-1 ');
|
||||
expect(btnGrp.find('.test').length).toBe(1);
|
||||
btnGrp.unmount();
|
||||
|
||||
btnGrp = mount(<ButtonGroup className="someclass"></ButtonGroup>);
|
||||
expect(btnGrp.getDOMNode().className).toBe('btn-group mr-1 someclass');
|
||||
});
|
||||
|
||||
it('<DetailsToggleButton /> comp', ()=>{
|
||||
let toggle = shallow(<DetailsToggleButton showDetails={true} />);
|
||||
let btn = toggle.find(IconButton);
|
||||
expect(btn.prop('icon')).toBe('far fa-eye');
|
||||
expect(btn.prop('title')).toBe('Show fewer details');
|
||||
|
||||
toggle.setProps({showDetails: false});
|
||||
btn = toggle.find(IconButton);
|
||||
expect(btn.prop('icon')).toBe('fas fa-low-vision');
|
||||
expect(btn.prop('title')).toBe('Show more details');
|
||||
});
|
||||
|
||||
it('<IconButton /> comp', ()=>{
|
||||
let btn = mount(<IconButton />);
|
||||
|
||||
let tippy = btn.find(Tippy);
|
||||
expect(tippy.length).toBe(0);
|
||||
|
||||
btn.setProps({title: 'test title'});
|
||||
tippy = btn.find(Tippy);
|
||||
expect(tippy.length).toBe(1);
|
||||
|
||||
expect(btn.find('button').getDOMNode().className).toBe('btn btn-sm btn-primary-icon ');
|
||||
|
||||
btn.setProps({icon: 'fa fa-icon'});
|
||||
expect(btn.find('button .sql-icon-lg').getDOMNode().className).toBe('fa fa-icon sql-icon-lg');
|
||||
});
|
||||
|
||||
it('<Shortcut /> comp', ()=>{
|
||||
let key = {
|
||||
alt: true,
|
||||
control: true,
|
||||
shift: false,
|
||||
key: {
|
||||
key_code: 65,
|
||||
char: 'a',
|
||||
},
|
||||
};
|
||||
let shortcutComp = mount(<Shortcut shortcut={key}/>);
|
||||
|
||||
expect(shortcutComp.find('.shortcut-key').length).toBe(3);
|
||||
|
||||
key.alt = false;
|
||||
shortcutComp.setProps({shortcut: key});
|
||||
expect(shortcutComp.find('.shortcut-key').length).toBe(2);
|
||||
});
|
||||
});
|
@ -22,5 +22,11 @@ define(function () {
|
||||
'search_objects.types': '/search_objects/types/<int:sid>/<int:did>',
|
||||
'search_objects.search': '/search_objects/search/<int:sid>/<int:did>',
|
||||
'dashboard.dashboard_stats': '/dashboard/dashboard_stats',
|
||||
'sqleditor.load_file': '/sqleditor/load_file/',
|
||||
'sqleditor.save_file': '/sqleditor/save_file/',
|
||||
'erd.initialize': '/erd/initialize/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
'erd.sql': '/erd/sql/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
'erd.prequisite': '/erd/prequisite/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
'erd.tables': '/erd/tables/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
|
||||
};
|
||||
});
|
||||
|
@ -355,6 +355,7 @@ module.exports = [{
|
||||
sqleditor: './pgadmin/tools/sqleditor/static/js/sqleditor.js',
|
||||
debugger_direct: './pgadmin/tools/debugger/static/js/direct.js',
|
||||
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
|
||||
erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js',
|
||||
file_utils: './pgadmin/misc/file_manager/static/js/utility.js',
|
||||
'pgadmin.style': pgadminCssStyles,
|
||||
pgadmin: pgadminScssStyles,
|
||||
@ -501,7 +502,8 @@ module.exports = [{
|
||||
',pgadmin.node.pga_job' +
|
||||
',pgadmin.tools.schema_diff' +
|
||||
',pgadmin.tools.storage_manager' +
|
||||
',pgadmin.tools.search_objects',
|
||||
',pgadmin.tools.search_objects' +
|
||||
',pgadmin.tools.erd_module',
|
||||
},
|
||||
}, {
|
||||
test: require.resolve('snapsvg'),
|
||||
|
@ -147,6 +147,10 @@ var webpackShimConfig = {
|
||||
'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.es5.min'),
|
||||
'mousetrap': path.join(__dirname, './node_modules/mousetrap'),
|
||||
'tablesorter-metric': path.join(__dirname, './node_modules/tablesorter/dist/js/parsers/parser-metric.min'),
|
||||
'pathfinding': path.join(__dirname, 'node_modules/pathfinding'),
|
||||
'dagre': path.join(__dirname, 'node_modules/dagre'),
|
||||
'graphlib': path.join(__dirname, 'node_modules/graphlib'),
|
||||
'react': path.join(__dirname, 'node_modules/react'),
|
||||
|
||||
// AciTree
|
||||
'jquery.acitree': path.join(__dirname, './node_modules/acitree/js/jquery.aciTree.min'),
|
||||
@ -275,6 +279,8 @@ var webpackShimConfig = {
|
||||
'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'),
|
||||
'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js/search_objects'),
|
||||
'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'),
|
||||
'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
|
||||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
|
||||
'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
|
||||
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
|
||||
'pgadmin.user_management.current_user': '/user_management/current_user',
|
||||
|
@ -113,6 +113,7 @@ module.exports = {
|
||||
'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'),
|
||||
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
|
||||
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
|
||||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
|
||||
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
|
||||
'tools': path.join(__dirname, './pgadmin/tools/'),
|
||||
},
|
||||
|
2912
web/yarn.lock
2912
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user