public inbox for [email protected]  
help / color / mirror / Atom feed
[pgAdmin][RM1802] ERD Tool (Beta)
21+ messages / 4 participants
[nested] [flat]

* [pgAdmin][RM1802] ERD Tool (Beta)
@ 2020-12-25 10:01  Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2020-12-25 10:01 UTC (permalink / raw)
  To: pgadmin-hackers

Hi Hackers,

Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the details:
1) Create a diagram from scratch or generate for an existing DB.
2) Generate "Create" DDL from the diagram.
3) Save the diagram and resume it later.
4) Supports basic table fields, one-to-many relationships, many-to-many
relationships, adding notes.
5) Test cases added with 75-80% test coverage.

Please review.

-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802.patch (502.1K, 3-RM1802.patch)
  download | inline diff:
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
index 622a949e8..152752e5b 100644
--- a/web/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -18,6 +18,7 @@ module.exports = {
     'eslint:recommended',
     'plugin:react/recommended',
   ],
+  'parser': 'babel-eslint',
   'parserOptions': {
     'ecmaVersion': 2018,
     'ecmaFeatures': {
diff --git a/web/package.json b/web/package.json
index b77776569..ec27c5498 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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.2.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",
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 77120cd59..46417ae96 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -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()
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
index 51323a273..a15df20d1 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
@@ -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)
-
-            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
+        status, res = BaseTableView.fetch_tables(self, sid, did, scid, tid)
+        if not status:
+            current_app.logger.error(res)
+            return False
 
-    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):
         """
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
index c122a4be6..e072a4391 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
@@ -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)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
index 1df83e99d..ce03cd1be 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
@@ -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
 
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
index 22fd79a9c..c8dd96f82 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
@@ -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 %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
index 1b90f8c46..16c8eca37 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
@@ -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
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
index 10fce304f..d00d08833 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
@@ -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
diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
index 3605e1d57..eb5c73f1e 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
@@ -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') {
diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js
index 516b62697..c0a2fc58e 100644
--- a/web/pgadmin/browser/templates/browser/js/utils.js
+++ b/web/pgadmin/browser/templates/browser/js/utils.js
@@ -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}}',
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 7ed9b725d..eba8ffafa 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -10,6 +10,7 @@
 define('bundled_browser',[
   'pgadmin.browser',
   'sources/browser/index',
+  'top/tools/erd/static/js/index',
 ], function(pgBrowser) {
   pgBrowser.init();
 });
diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js
index b35cedef0..dca935662 100644
--- a/web/pgadmin/static/js/backform.pgadmin.js
+++ b/web/pgadmin/static/js/backform.pgadmin.js
@@ -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),
diff --git a/web/pgadmin/static/js/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid.pgadmin.js
index def9c18f6..5033965f2 100644
--- a/web/pgadmin/static/js/backgrid.pgadmin.js
+++ b/web/pgadmin/static/js/backgrid.pgadmin.js
@@ -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();
       }
diff --git a/web/pgadmin/static/scss/_bootstrap.overrides.scss b/web/pgadmin/static/scss/_bootstrap.overrides.scss
index 29c4a433c..0d4d68f24 100644
--- a/web/pgadmin/static/scss/_bootstrap.overrides.scss
+++ b/web/pgadmin/static/scss/_bootstrap.overrides.scss
@@ -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;
@@ -370,6 +379,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%;
 }
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index fadd71b7b..f8f2bcf99 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -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 {
diff --git a/web/pgadmin/static/scss/_tippy.overrides.scss b/web/pgadmin/static/scss/_tippy.overrides.scss
new file mode 100644
index 000000000..9750434b7
--- /dev/null
+++ b/web/pgadmin/static/scss/_tippy.overrides.scss
@@ -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;
+  }
+}
diff --git a/web/pgadmin/static/scss/pgadmin.scss b/web/pgadmin/static/scss/pgadmin.scss
index 19e79adf2..cc751b77e 100644
--- a/web/pgadmin/static/scss/pgadmin.scss
+++ b/web/pgadmin/static/scss/pgadmin.scss
@@ -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';
diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss
index 9d913788d..b71c86a4c 100644
--- a/web/pgadmin/static/scss/resources/_default.variables.scss
+++ b/web/pgadmin/static/scss/resources/_default.variables.scss
@@ -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");
diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
index 129b9a214..d8ca65b50 100644
--- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
@@ -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;
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
index bae5e36d6..53197cd83 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
@@ -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);
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
index a28091268..36abadac3 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -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);
 
diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py
new file mode 100644
index 000000000..1cff397f2
--- /dev/null
+++ b/web/pgadmin/tools/erd/__init__.py
@@ -0,0 +1,587 @@
+##########################################################################
+#
+# 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 schema_diff frame."""
+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.utils.exception import ConnectionLost, SSHTunnelConnectionLost
+from pgadmin.browser.server_groups.servers.databases.schemas.utils \
+    import DataTypeReader
+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_DISPLAY, \
+    PREF_LABEL_KEYBOARD_SHORTCUTS
+from .utils import ERDHelper
+
+MODULE_NAME = 'erd'
+
+
+class ERDModule(PgAdminModule):
+    """
+    class SchemaDiffModule(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 for Schema Diff module
+        """
+        return [
+            'erd.panel',
+            'erd.initialize',
+            'erd.prequisite',
+            'erd.sql',
+            'erd.tables',
+            'erd.close'
+        ]
+
+    def register_preferences(self):
+        self.preference.register(
+            'display', 'erd_new_browser_tab',
+            gettext("Open in new browser tab"), 'boolean', False,
+            category_label=gettext('Display'),
+            help_str=gettext('If set to True, the ERD tool '
+                             'will be opened in a new browser tab.')
+        )
+
+        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',
+            'download_image',
+            gettext('Download image'),
+            'keyboardshortcut',
+            {
+                'alt': True,
+                'shift': False,
+                'control': True,
+                'key': {
+                    'key_code': 73,
+                    'char': 'i'
+                }
+            },
+            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')
+
+
[email protected](
+    '/panel/<int:trans_id>',
+    methods=["POST"],
+    endpoint='panel'
+)
+@login_required
+def panel(trans_id):
+    """
+    This method calls index.html to render the schema diff.
+
+    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),
+    )
+
+
[email protected](
+    '/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
+    """
+    connect = True
+    # Read the data if present. Skipping read may cause connection
+    # reset error if data is sent from the client
+    if request.data:
+        _ = request.data
+
+    # req_args = request.args
+    # if ('recreate' in req_args and
+    #     req_args['recreate'] == '1'):
+    #     connect = False
+
+    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)
+            return internal_server_error(errormsg=str(msg))
+
+        return conn
+    except Exception as e:
+        app.logger.error(e)
+        raise
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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})
diff --git a/web/pgadmin/tools/erd/static/js/erd_module.js b/web/pgadmin/tools/erd/static/js/erd_module.js
new file mode 100644
index 000000000..1291833de
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_module.js
@@ -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 self = this;
+      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>
+      `;
+
+      if (self.preferences.new_browser_tab) {
+        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;
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
new file mode 100644
index 000000000..f64a84f5e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
@@ -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).map((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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
new file mode 100644
index 000000000..ae73db8ac
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
@@ -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]));
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js
new file mode 100644
index 000000000..9b6e54fa9
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js
@@ -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';
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js
new file mode 100644
index 000000000..abec79f5e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js
new file mode 100644
index 000000000..0f4a9ce3e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
new file mode 100644
index 000000000..e5d38f71e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
@@ -0,0 +1,731 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 self = 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});
+        }
+        self.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() {
+        self.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(),
+                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(),
+                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,
+          subnode: this.pgBrowser.Nodes['primary_key'].model,
+          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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
new file mode 100644
index 000000000..eb0ab1336
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
@@ -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;
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
new file mode 100644
index 000000000..1bd4c3bc6
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
@@ -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
+    );
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
new file mode 100644
index 000000000..260e3dc57
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
@@ -0,0 +1,301 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 = (type) => {
+    if(type == '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:
+        degree = 0;
+        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 = [];
+
+    // Get points based on link orientation
+    let pointLeft = points[0];
+    let pointRight = points[points.length - 1];
+    let hadToSwitch = false;
+    if (pointLeft.getX() > pointRight.getX()) {
+      pointLeft = points[points.length - 1];
+      pointRight = points[0];
+      hadToSwitch = true;
+    }
+    let dy = Math.abs(points[0].getY() - points[points.length - 1].getY());
+    let dx = Math.abs(points[0].getX() - points[points.length - 1].getX());
+
+    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>
+    );
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
new file mode 100644
index 000000000..05d368776
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
@@ -0,0 +1,203 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 { PortModelAlignment, DefaultNodeModel } from '@projectstorm/react-diagrams';
+import { 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>&nbsp;
+            {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} />;
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
new file mode 100644
index 000000000..6f6e2c0f2
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
@@ -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||{});
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
new file mode 100644
index 000000000..2846640a3
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
@@ -0,0 +1,684 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 html2canvas from 'html2canvas';
+
+import gettext from 'sources/gettext';
+import url_for from 'sources/url_for';
+import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool';
+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';
+
+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();
+    }
+  }
+}
+
+export default class BodyWidget extends React.Component {
+  constructor() {
+    super();
+    this.state = {
+      conn_status: CONNECT_STATUS.DISCONNECTED,
+      server_version: null,
+      no_node_selected: true,
+      no_link_selected: true,
+      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.onImageClick = this.onImageClick.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': ()=>{
+        let selected = this.diagram.getSelectedNodes();
+        this.setState({no_node_selected: selected.length == 0});
+      },
+      'linksSelectionChanged': ()=>{
+        let selected = this.diagram.getSelectedLinks();
+        this.setState({no_link_selected: selected.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.download_image, this.onImageClick],
+      [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) {
+      done = 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') {
+      return (title, attributes, callback)=>{
+          this.props.getDialog(dialogName).show(
+              title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
+          );
+      };
+    } else if(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.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}`;
+        sessionStorage.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);
+      })
+  }
+
+  onImageClick() {
+    /* Not working completely */
+    return;
+    this.setLoading(gettext('Preparing the image...'));
+    var svgElements = this.canvasEle.querySelectorAll('svg');
+    svgElements.forEach(function(svg) {
+      svg.setAttribute("width", svg.getBoundingClientRect().width);
+      svg.setAttribute("height", svg.getBoundingClientRect().height);
+      svg.style.width = null;
+      svg.style.height= null;
+    });
+    this.canvasEle.style.overflow = 'auto';
+    html2canvas(this.canvasEle, {
+      scrollX: 0,
+      scrollY: 0
+    }).then((canvas)=>{
+      let link = document.createElement('a');
+      link.setAttribute('href', canvas.toDataURL('image/png'));
+      link.setAttribute('download', this.getCurrentProjectName() + '.png');
+      link.click();
+    }).catch((err)=>{
+      console.log(err);
+    }).then(()=>{
+      this.canvasEle.style.overflow = 'hidden';
+      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);
+      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>
+        <IconButton id="save-image" icon="far fa-file-image" onClick={this.onImageClick} title={gettext('Download as image')}
+            shortcut={this.state.preferences.download_image} className={'d-none'}/>
+        <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.no_node_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.no_node_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.no_node_selected && this.state.no_link_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.no_node_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.no_node_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.no_node_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>
+      </>
+    );
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
new file mode 100644
index 000000000..cb7e7155f
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
@@ -0,0 +1,47 @@
+/////////////////////////////////////////////////////////////
+//
+// 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';
+
+export const STATUS = {
+  CONNECTED: 1,
+  DISCONNECTED: 2,
+  CONNECTING: 3,
+  FAILED: 4,
+}
+
+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>
+  )
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
new file mode 100644
index 000000000..f714e1f5d
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
@@ -0,0 +1,59 @@
+/////////////////////////////////////////////////////////////
+//
+// 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';
+
+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>&nbsp;{gettext('OK')}
+            </button>
+          </div>
+        </div>
+      </div>
+      )}
+      visible={open}
+      interactive={true}
+      animation={false}
+      reference={reference}
+      placement='auto-end'
+      {...tippyProps}
+    />
+  );
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx
new file mode 100644
index 000000000..96e248162
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx
@@ -0,0 +1,27 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import React from 'react';
+
+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;
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
new file mode 100644
index 000000000..5341ac406
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
@@ -0,0 +1,83 @@
+/////////////////////////////////////////////////////////////
+//
+// 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';
+
+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>
+  );
+});
+
+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>
+  )
+}
+
+export const IconButton = forwardRef((props, ref) => {
+  const {title, shortcut, className, ...otherProps} = props;
+
+  if (title) {
+    return (
+      <Tippy content={
+        <>
+          {title && <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}/>
+  }
+});
+
+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} />
+  );
+}
+
+export function ButtonGroup({className, children}) {
+  return (
+    <div className={'btn-group mr-1 ' + (className ? className : '')} role="group" aria-label="save group">
+      {children}
+    </div>
+  )
+}
+
+export default function ToolBar({id, children}) {
+  return (
+    <div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
+      {children}
+    </div>
+  )
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool_hook.js b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js
new file mode 100644
index 000000000..14a65fbaa
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js
@@ -0,0 +1,36 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+define([
+  'sources/url_for', 'jquery',
+  '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(
+  url_for, $, 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;
+});
+
+
diff --git a/web/pgadmin/tools/erd/static/js/index.js b/web/pgadmin/tools/erd/static/js/index.js
new file mode 100644
index 000000000..b968a5223
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/index.js
@@ -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,
+};
diff --git a/web/pgadmin/tools/erd/static/scss/_erd.scss b/web/pgadmin/tools/erd/static/scss/_erd.scss
new file mode 100644
index 000000000..c9b7d3185
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/scss/_erd.scss
@@ -0,0 +1,192 @@
+.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-status-bar {
+    background: $sql-gutters-bg;
+    border-top: $panel-border;
+    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;
+
+    // overflow: auto;
+    // 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;
+    }
+  }
+}
diff --git a/web/pgadmin/tools/erd/templates/erd/index.html b/web/pgadmin/tools/erd/templates/erd/index.html
new file mode 100644
index 000000000..10a0896f5
--- /dev/null
+++ b/web/pgadmin/tools/erd/templates/erd/index.html
@@ -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 %}
diff --git a/web/pgadmin/tools/erd/tests/__init__.py b/web/pgadmin/tools/erd/tests/__init__.py
new file mode 100644
index 000000000..8c8c486cc
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/__init__.py
@@ -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
diff --git a/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql b/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
new file mode 100644
index 000000000..e3d6ab762
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
@@ -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;
diff --git a/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql b/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
new file mode 100644
index 000000000..330e81443
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
@@ -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;
diff --git a/web/pgadmin/tools/erd/tests/test_close.py b/web/pgadmin/tools/erd/tests/test_close.py
new file mode 100644
index 000000000..008ddab39
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_close.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_initialize.py b/web/pgadmin/tools/erd/tests/test_initialize.py
new file mode 100644
index 000000000..d586bb3b6
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_initialize.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_panel.py b/web/pgadmin/tools/erd/tests/test_panel.py
new file mode 100644
index 000000000..ad44d004a
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_panel.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_prequisite.py b/web/pgadmin/tools/erd/tests/test_prequisite.py
new file mode 100644
index 000000000..fed6a0652
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_prequisite.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_sql.py b/web/pgadmin/tools/erd/tests/test_sql.py
new file mode 100644
index 000000000..352a1dc83
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_sql.py
@@ -0,0 +1,91 @@
+##########################################################################
+#
+# 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):
+        pass
+        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)
diff --git a/web/pgadmin/tools/erd/tests/test_sql_input_data.json b/web/pgadmin/tools/erd/tests/test_sql_input_data.json
new file mode 100644
index 000000000..3c8182e68
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_sql_input_data.json
@@ -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"
+        }
+      ]
+    }
+  }
+}
diff --git a/web/pgadmin/tools/erd/tests/test_tables.py b/web/pgadmin/tools/erd/tests/test_tables.py
new file mode 100644
index 000000000..b8de11646
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_tables.py
@@ -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()
diff --git a/web/pgadmin/tools/erd/utils.py b/web/pgadmin/tools/erd/utils.py
new file mode 100644
index 000000000..1999a71e5
--- /dev/null
+++ b/web/pgadmin/tools/erd/utils.py
@@ -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
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index abd33a47b..5d74fd546 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -2651,6 +2651,11 @@ define('tools.querytool', [
                 }
 
               });
+          } else if(url_params.sql_id) {
+            let sqlValue = sessionStorage.getItem(url_params.sql_id);
+            if(sqlValue) {
+              self.gridView.query_tool_obj.setValue(sqlValue);
+            }
           }
         }
         else {
@@ -2668,7 +2673,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);
         }
       },
diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py
index 434e653a7..1e6aef7d8 100644
--- a/web/pgadmin/utils/csrf.py
+++ b/web/pgadmin/utils/csrf.py
@@ -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:
diff --git a/web/regression/javascript/erd/erd_core_spec.js b/web/regression/javascript/erd/erd_core_spec.js
new file mode 100644
index 000000000..c488ffdf2
--- /dev/null
+++ b/web/regression/javascript/erd/erd_core_spec.js
@@ -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'});
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/erd_model_spec.js b/web/regression/javascript/erd/erd_model_spec.js
new file mode 100644
index 000000000..cfdcc0983
--- /dev/null
+++ b/web/regression/javascript/erd/erd_model_spec.js
@@ -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'},
+    }));
+  });
+});
diff --git a/web/regression/javascript/erd/keyboard_shortcut_action_spec.js b/web/regression/javascript/erd/keyboard_shortcut_action_spec.js
new file mode 100644
index 000000000..3134795ea
--- /dev/null
+++ b/web/regression/javascript/erd/keyboard_shortcut_action_spec.js
@@ -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();
+  });
+});
diff --git a/web/regression/javascript/erd/onetomany_link_spec.js b/web/regression/javascript/erd/onetomany_link_spec.js
new file mode 100644
index 000000000..a7590db64
--- /dev/null
+++ b/web/regression/javascript/erd/onetomany_link_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/erd/onetomany_port_spec.js b/web/regression/javascript/erd/onetomany_port_spec.js
new file mode 100644
index 000000000..0ad4831ef
--- /dev/null
+++ b/web/regression/javascript/erd/onetomany_port_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js
new file mode 100644
index 000000000..d8edcdb44
--- /dev/null
+++ b/web/regression/javascript/erd/table_node_spec.js
@@ -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();
+      });
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/test_tables.js b/web/regression/javascript/erd/test_tables.js
new file mode 100644
index 000000000..e2dc43ef8
--- /dev/null
+++ b/web/regression/javascript/erd/test_tables.js
@@ -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': {},
+  },
+];
diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js
new file mode 100644
index 000000000..1771ddcb9
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js
@@ -0,0 +1,505 @@
+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,
+  '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: ()=>{},
+    });
+
+    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} 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().no_node_selected).toBe(false);
+      done();
+    });
+  });
+
+  it('event linksSelectionChanged', (done)=>{
+    spyOn(bodyInstance.diagram, 'getSelectedLinks').and.returnValue([{key:'value'}]);
+    bodyInstance.diagram.fireEvent({}, 'linksSelectionChanged', true);
+    setTimeout(()=>{
+      expect(body.state().no_link_selected).toBe(false);
+      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', ()=>{
+    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();
+    expect(node.remove).toHaveBeenCalled();
+    expect(link.remove).toHaveBeenCalled();
+  });
+
+  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(sessionStorage, '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(sessionStorage.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,
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/connection_bar_spec.js b/web/regression/javascript/erd/ui_components/connection_bar_spec.js
new file mode 100644
index 000000000..0f5b935a9
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/connection_bar_spec.js
@@ -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 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');
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/erd_preferences.js b/web/regression/javascript/erd/ui_components/erd_preferences.js
new file mode 100644
index 000000000..d91b158ce
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/erd_preferences.js
@@ -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': '-',
+    },
+  },
+};
diff --git a/web/regression/javascript/erd/ui_components/floating_note_spec.js b/web/regression/javascript/erd/ui_components/floating_note_spec.js
new file mode 100644
index 000000000..f786267bf
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/floating_note_spec.js
@@ -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();
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/loader_spec.js b/web/regression/javascript/erd/ui_components/loader_spec.js
new file mode 100644
index 000000000..b14ed30aa
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/loader_spec.js
@@ -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...');
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/toolbar_spec.js b/web/regression/javascript/erd/ui_components/toolbar_spec.js
new file mode 100644
index 000000000..0429d718d
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/toolbar_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 806f59001..3a028c415 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -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>',
   };
 });
diff --git a/web/webpack.config.js b/web/webpack.config.js
index c9ddf80c2..b7744552f 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -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'),
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 155fb9eaf..e3d9df89c 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -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',
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index ba6f99d6b..a4383d6aa 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -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/'),
     },
diff --git a/web/yarn.lock b/web/yarn.lock
index c9450d755..3b1943994 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -2,70 +2,54 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
-  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+  integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
   dependencies:
     "@babel/highlight" "^7.10.4"
 
-"@babel/core@^7.7.5":
-  version "7.11.6"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
-  integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
+"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
+  integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==
+
+"@babel/core@^7.10.2", "@babel/core@^7.7.5":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd"
+  integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==
   dependencies:
     "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.11.6"
-    "@babel/helper-module-transforms" "^7.11.0"
-    "@babel/helpers" "^7.10.4"
-    "@babel/parser" "^7.11.5"
-    "@babel/template" "^7.10.4"
-    "@babel/traverse" "^7.11.5"
-    "@babel/types" "^7.11.5"
+    "@babel/generator" "^7.12.10"
+    "@babel/helper-module-transforms" "^7.12.1"
+    "@babel/helpers" "^7.12.5"
+    "@babel/parser" "^7.12.10"
+    "@babel/template" "^7.12.7"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.10"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.1"
     json5 "^2.1.2"
     lodash "^4.17.19"
-    resolve "^1.3.2"
-    semver "^5.4.1"
-    source-map "^0.5.0"
-
-"@babel/core@~7.6.0":
-  version "7.6.4"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff"
-  integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ==
-  dependencies:
-    "@babel/code-frame" "^7.5.5"
-    "@babel/generator" "^7.6.4"
-    "@babel/helpers" "^7.6.2"
-    "@babel/parser" "^7.6.4"
-    "@babel/template" "^7.6.0"
-    "@babel/traverse" "^7.6.3"
-    "@babel/types" "^7.6.3"
-    convert-source-map "^1.1.0"
-    debug "^4.1.0"
-    json5 "^2.1.0"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.6.4":
-  version "7.11.6"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
-  integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
+"@babel/generator@^7.12.10", "@babel/generator@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
+  integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
   dependencies:
-    "@babel/types" "^7.11.5"
+    "@babel/types" "^7.12.11"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
-"@babel/helper-annotate-as-pure@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
-  integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==
+"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
+  integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4":
   version "7.10.4"
@@ -75,43 +59,34 @@
     "@babel/helper-explode-assignable-expression" "^7.10.4"
     "@babel/types" "^7.10.4"
 
-"@babel/helper-builder-react-jsx-experimental@^7.10.4", "@babel/helper-builder-react-jsx-experimental@^7.11.5":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.11.5.tgz#4ea43dd63857b0a35cd1f1b161dc29b43414e79f"
-  integrity sha512-Vc4aPJnRZKWfzeCBsqTBnzulVNjABVdahSPhtdMD3Vs80ykx4a87jTHtF/VR+alSrDmNvat7l13yrRHauGcHVw==
+"@babel/helper-compilation-targets@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831"
+  integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-module-imports" "^7.10.4"
-    "@babel/types" "^7.11.5"
-
-"@babel/helper-builder-react-jsx@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d"
-  integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/compat-data" "^7.12.5"
+    "@babel/helper-validator-option" "^7.12.1"
+    browserslist "^4.14.5"
+    semver "^5.5.0"
 
-"@babel/helper-create-class-features-plugin@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d"
-  integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==
+"@babel/helper-create-class-features-plugin@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e"
+  integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
-    "@babel/helper-member-expression-to-functions" "^7.10.5"
+    "@babel/helper-member-expression-to-functions" "^7.12.1"
     "@babel/helper-optimise-call-expression" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.10.4"
 
-"@babel/helper-create-regexp-features-plugin@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8"
-  integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==
+"@babel/helper-create-regexp-features-plugin@^7.12.1":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f"
+  integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-regex" "^7.10.4"
-    regexpu-core "^4.7.0"
+    regexpu-core "^4.7.1"
 
 "@babel/helper-define-map@^7.10.4":
   version "7.10.5"
@@ -123,27 +98,27 @@
     lodash "^4.17.19"
 
 "@babel/helper-explode-assignable-expression@^7.10.4":
-  version "7.11.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b"
-  integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz#8006a466695c4ad86a2a5f2fb15b5f2c31ad5633"
+  integrity sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-function-name@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
-  integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
+  integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.10.4"
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/helper-get-function-arity" "^7.12.10"
+    "@babel/template" "^7.12.7"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-get-function-arity@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
-  integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+"@babel/helper-get-function-arity@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
+  integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-hoist-variables@^7.10.4":
   version "7.10.4"
@@ -152,117 +127,115 @@
   dependencies:
     "@babel/types" "^7.10.4"
 
-"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
-  integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==
+"@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
+  integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.7"
 
-"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
-  integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
+  integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.5"
 
-"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
-  integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==
+"@babel/helper-module-transforms@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c"
+  integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==
   dependencies:
-    "@babel/helper-module-imports" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
-    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-module-imports" "^7.12.1"
+    "@babel/helper-replace-supers" "^7.12.1"
+    "@babel/helper-simple-access" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.11.0"
+    "@babel/helper-validator-identifier" "^7.10.4"
     "@babel/template" "^7.10.4"
-    "@babel/types" "^7.11.0"
+    "@babel/traverse" "^7.12.1"
+    "@babel/types" "^7.12.1"
     lodash "^4.17.19"
 
-"@babel/helper-optimise-call-expression@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
-  integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+"@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d"
+  integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0":
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
   integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
 
-"@babel/helper-regex@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
-  integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==
-  dependencies:
-    lodash "^4.17.19"
-
-"@babel/helper-remap-async-to-generator@^7.10.4":
-  version "7.11.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d"
-  integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==
+"@babel/helper-remap-async-to-generator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd"
+  integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-wrap-function" "^7.10.4"
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-replace-supers@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
-  integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
+"@babel/helper-replace-supers@^7.12.1":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d"
+  integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.10.4"
-    "@babel/helper-optimise-call-expression" "^7.10.4"
-    "@babel/traverse" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/helper-member-expression-to-functions" "^7.12.7"
+    "@babel/helper-optimise-call-expression" "^7.12.10"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-simple-access@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
-  integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
+"@babel/helper-simple-access@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136"
+  integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==
   dependencies:
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-skip-transparent-expression-wrappers@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729"
-  integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==
+"@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
+  integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
-  integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
+"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
+  integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-validator-identifier@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
-  integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
+
+"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
+  integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==
 
 "@babel/helper-wrap-function@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87"
-  integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==
+  version "7.12.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz#3332339fc4d1fbbf1c27d7958c27d34708e990d9"
+  integrity sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
     "@babel/template" "^7.10.4"
     "@babel/traverse" "^7.10.4"
     "@babel/types" "^7.10.4"
 
-"@babel/helpers@^7.10.4", "@babel/helpers@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
-  integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
+"@babel/helpers@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e"
+  integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==
   dependencies:
     "@babel/template" "^7.10.4"
-    "@babel/traverse" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/traverse" "^7.12.5"
+    "@babel/types" "^7.12.5"
 
 "@babel/highlight@^7.10.4":
   version "7.10.4"
@@ -273,514 +246,735 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.6.4":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
-  integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
+  integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
 
-"@babel/plugin-proposal-async-generator-functions@^7.2.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
-  integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==
+"@babel/plugin-proposal-async-generator-functions@^7.12.1":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
+  integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-remap-async-to-generator" "^7.10.4"
+    "@babel/helper-remap-async-to-generator" "^7.12.1"
     "@babel/plugin-syntax-async-generators" "^7.8.0"
 
-"@babel/plugin-proposal-class-properties@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807"
-  integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==
+"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de"
+  integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.10.4"
+    "@babel/helper-create-class-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-proposal-dynamic-import@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e"
-  integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==
+"@babel/plugin-proposal-dynamic-import@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc"
+  integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-dynamic-import" "^7.8.0"
 
-"@babel/plugin-proposal-json-strings@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db"
-  integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==
+"@babel/plugin-proposal-export-namespace-from@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4"
+  integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+
+"@babel/plugin-proposal-json-strings@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c"
+  integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-json-strings" "^7.8.0"
 
-"@babel/plugin-proposal-object-rest-spread@^7.6.2":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af"
-  integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==
+"@babel/plugin-proposal-logical-assignment-operators@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751"
+  integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c"
+  integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b"
+  integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+
+"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.9.6":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
+  integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
-    "@babel/plugin-transform-parameters" "^7.10.4"
+    "@babel/plugin-transform-parameters" "^7.12.1"
 
-"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd"
-  integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==
+"@babel/plugin-proposal-optional-catch-binding@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942"
+  integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
 
-"@babel/plugin-proposal-unicode-property-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d"
-  integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==
+"@babel/plugin-proposal-optional-chaining@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c"
+  integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-private-methods@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389"
+  integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-class-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-proposal-unicode-property-regex@^7.12.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072"
+  integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0":
+"@babel/plugin-syntax-class-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978"
+  integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
   integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0":
+"@babel/plugin-syntax-export-namespace-from@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
+  integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
   integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-jsx@^7.10.4":
+"@babel/plugin-syntax-jsx@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
+  integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+  integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.10.4":
   version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
-  integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
+  integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
   integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-transform-arrow-functions@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd"
-  integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0"
+  integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-async-to-generator@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37"
-  integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==
+"@babel/plugin-transform-arrow-functions@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3"
+  integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==
   dependencies:
-    "@babel/helper-module-imports" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-remap-async-to-generator" "^7.10.4"
 
-"@babel/plugin-transform-block-scoped-functions@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8"
-  integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==
+"@babel/plugin-transform-async-to-generator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1"
+  integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==
   dependencies:
+    "@babel/helper-module-imports" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-remap-async-to-generator" "^7.12.1"
 
-"@babel/plugin-transform-block-scoping@^7.6.3":
-  version "7.11.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215"
-  integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==
+"@babel/plugin-transform-block-scoped-functions@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9"
+  integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-classes@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7"
-  integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==
+"@babel/plugin-transform-block-scoping@^7.12.11":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca"
+  integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-transform-classes@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6"
+  integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-define-map" "^7.10.4"
     "@babel/helper-function-name" "^7.10.4"
     "@babel/helper-optimise-call-expression" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.10.4"
     globals "^11.1.0"
 
-"@babel/plugin-transform-computed-properties@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb"
-  integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==
+"@babel/plugin-transform-computed-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852"
+  integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-destructuring@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5"
-  integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==
+"@babel/plugin-transform-destructuring@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847"
+  integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-dotall-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee"
-  integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==
+"@babel/plugin-transform-dotall-regex@^7.12.1", "@babel/plugin-transform-dotall-regex@^7.4.4":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975"
+  integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-duplicate-keys@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47"
-  integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==
+"@babel/plugin-transform-duplicate-keys@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228"
+  integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-exponentiation-operator@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e"
-  integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==
+"@babel/plugin-transform-exponentiation-operator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0"
+  integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==
   dependencies:
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-for-of@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9"
-  integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==
+"@babel/plugin-transform-for-of@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa"
+  integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-function-name@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7"
-  integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==
+"@babel/plugin-transform-function-name@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667"
+  integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c"
-  integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==
+"@babel/plugin-transform-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57"
+  integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-member-expression-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7"
-  integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==
+"@babel/plugin-transform-member-expression-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad"
+  integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-modules-amd@^7.5.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1"
-  integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==
+"@babel/plugin-transform-modules-amd@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9"
+  integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-commonjs@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0"
-  integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==
+"@babel/plugin-transform-modules-commonjs@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648"
+  integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.4"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-simple-access" "^7.12.1"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-systemjs@^7.5.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85"
-  integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==
+"@babel/plugin-transform-modules-systemjs@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086"
+  integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==
   dependencies:
     "@babel/helper-hoist-variables" "^7.10.4"
-    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-validator-identifier" "^7.10.4"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-umd@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e"
-  integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==
+"@babel/plugin-transform-modules-umd@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902"
+  integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.4"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-named-capturing-groups-regex@^7.6.3":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6"
-  integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==
+"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753"
+  integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
 
-"@babel/plugin-transform-new-target@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888"
-  integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==
+"@babel/plugin-transform-new-target@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0"
+  integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-object-super@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894"
-  integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==
+"@babel/plugin-transform-object-super@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e"
+  integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
 
-"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.4.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a"
-  integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
+"@babel/plugin-transform-parameters@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d"
+  integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-property-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0"
-  integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==
+"@babel/plugin-transform-property-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd"
+  integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-react-display-name@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz#b5795f4e3e3140419c3611b7a2a3832b9aef328d"
-  integrity sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw==
+"@babel/plugin-transform-react-display-name@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d"
+  integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-react-jsx-development@^7.10.4":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.11.5.tgz#e1439e6a57ee3d43e9f54ace363fb29cefe5d7b6"
-  integrity sha512-cImAmIlKJ84sDmpQzm4/0q/2xrXlDezQoixy3qoz1NJeZL/8PRon6xZtluvr4H4FzwlDGI5tCcFupMnXGtr+qw==
+"@babel/plugin-transform-react-jsx-development@^7.12.7":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7"
+  integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==
   dependencies:
-    "@babel/helper-builder-react-jsx-experimental" "^7.11.5"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    "@babel/plugin-transform-react-jsx" "^7.12.12"
 
-"@babel/plugin-transform-react-jsx-self@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369"
-  integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==
+"@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e"
+  integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==
   dependencies:
+    "@babel/helper-annotate-as-pure" "^7.12.10"
+    "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    "@babel/plugin-syntax-jsx" "^7.12.1"
+    "@babel/types" "^7.12.12"
 
-"@babel/plugin-transform-react-jsx-source@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4"
-  integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA==
+"@babel/plugin-transform-react-pure-annotations@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42"
+  integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==
   dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
 
-"@babel/plugin-transform-react-jsx@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2"
-  integrity sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==
+"@babel/plugin-transform-regenerator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753"
+  integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==
   dependencies:
-    "@babel/helper-builder-react-jsx" "^7.10.4"
-    "@babel/helper-builder-react-jsx-experimental" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    regenerator-transform "^0.14.2"
 
-"@babel/plugin-transform-react-pure-annotations@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501"
-  integrity sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A==
+"@babel/plugin-transform-reserved-words@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8"
+  integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-regenerator@^7.4.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63"
-  integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==
+"@babel/plugin-transform-shorthand-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3"
+  integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==
   dependencies:
-    regenerator-transform "^0.14.2"
+    "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-reserved-words@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd"
-  integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==
+"@babel/plugin-transform-spread@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e"
+  integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
 
-"@babel/plugin-transform-shorthand-properties@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6"
-  integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==
+"@babel/plugin-transform-sticky-regex@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad"
+  integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-spread@^7.6.2":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc"
-  integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==
+"@babel/plugin-transform-template-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843"
+  integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0"
 
-"@babel/plugin-transform-sticky-regex@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d"
-  integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==
+"@babel/plugin-transform-typeof-symbol@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b"
+  integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-regex" "^7.10.4"
 
-"@babel/plugin-transform-template-literals@^7.4.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c"
-  integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==
+"@babel/plugin-transform-unicode-escapes@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709"
+  integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-typeof-symbol@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc"
-  integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==
+"@babel/plugin-transform-unicode-regex@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb"
+  integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==
   dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-unicode-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8"
-  integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==
+"@babel/preset-env@^7.10.2":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9"
+  integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/compat-data" "^7.12.7"
+    "@babel/helper-compilation-targets" "^7.12.5"
+    "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-validator-option" "^7.12.11"
+    "@babel/plugin-proposal-async-generator-functions" "^7.12.1"
+    "@babel/plugin-proposal-class-properties" "^7.12.1"
+    "@babel/plugin-proposal-dynamic-import" "^7.12.1"
+    "@babel/plugin-proposal-export-namespace-from" "^7.12.1"
+    "@babel/plugin-proposal-json-strings" "^7.12.1"
+    "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
+    "@babel/plugin-proposal-numeric-separator" "^7.12.7"
+    "@babel/plugin-proposal-object-rest-spread" "^7.12.1"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.12.1"
+    "@babel/plugin-proposal-optional-chaining" "^7.12.7"
+    "@babel/plugin-proposal-private-methods" "^7.12.1"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.12.1"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-class-properties" "^7.12.1"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.12.1"
+    "@babel/plugin-transform-arrow-functions" "^7.12.1"
+    "@babel/plugin-transform-async-to-generator" "^7.12.1"
+    "@babel/plugin-transform-block-scoped-functions" "^7.12.1"
+    "@babel/plugin-transform-block-scoping" "^7.12.11"
+    "@babel/plugin-transform-classes" "^7.12.1"
+    "@babel/plugin-transform-computed-properties" "^7.12.1"
+    "@babel/plugin-transform-destructuring" "^7.12.1"
+    "@babel/plugin-transform-dotall-regex" "^7.12.1"
+    "@babel/plugin-transform-duplicate-keys" "^7.12.1"
+    "@babel/plugin-transform-exponentiation-operator" "^7.12.1"
+    "@babel/plugin-transform-for-of" "^7.12.1"
+    "@babel/plugin-transform-function-name" "^7.12.1"
+    "@babel/plugin-transform-literals" "^7.12.1"
+    "@babel/plugin-transform-member-expression-literals" "^7.12.1"
+    "@babel/plugin-transform-modules-amd" "^7.12.1"
+    "@babel/plugin-transform-modules-commonjs" "^7.12.1"
+    "@babel/plugin-transform-modules-systemjs" "^7.12.1"
+    "@babel/plugin-transform-modules-umd" "^7.12.1"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1"
+    "@babel/plugin-transform-new-target" "^7.12.1"
+    "@babel/plugin-transform-object-super" "^7.12.1"
+    "@babel/plugin-transform-parameters" "^7.12.1"
+    "@babel/plugin-transform-property-literals" "^7.12.1"
+    "@babel/plugin-transform-regenerator" "^7.12.1"
+    "@babel/plugin-transform-reserved-words" "^7.12.1"
+    "@babel/plugin-transform-shorthand-properties" "^7.12.1"
+    "@babel/plugin-transform-spread" "^7.12.1"
+    "@babel/plugin-transform-sticky-regex" "^7.12.7"
+    "@babel/plugin-transform-template-literals" "^7.12.1"
+    "@babel/plugin-transform-typeof-symbol" "^7.12.10"
+    "@babel/plugin-transform-unicode-escapes" "^7.12.1"
+    "@babel/plugin-transform-unicode-regex" "^7.12.1"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.12.11"
+    core-js-compat "^3.8.0"
+    semver "^5.5.0"
 
-"@babel/preset-env@~7.6.0":
-  version "7.6.3"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.3.tgz#9e1bf05a2e2d687036d24c40e4639dc46cef2271"
-  integrity sha512-CWQkn7EVnwzlOdR5NOm2+pfgSNEZmvGjOhlCHBDq0J8/EStr+G+FvPEiz9B56dR6MoiUFjXhfE4hjLoAKKJtIQ==
+"@babel/preset-modules@^0.1.3":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e"
+  integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==
   dependencies:
-    "@babel/helper-module-imports" "^7.0.0"
     "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
-    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
-    "@babel/plugin-proposal-json-strings" "^7.2.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
-    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
-    "@babel/plugin-syntax-async-generators" "^7.2.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
-    "@babel/plugin-syntax-json-strings" "^7.2.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-transform-arrow-functions" "^7.2.0"
-    "@babel/plugin-transform-async-to-generator" "^7.5.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
-    "@babel/plugin-transform-block-scoping" "^7.6.3"
-    "@babel/plugin-transform-classes" "^7.5.5"
-    "@babel/plugin-transform-computed-properties" "^7.2.0"
-    "@babel/plugin-transform-destructuring" "^7.6.0"
-    "@babel/plugin-transform-dotall-regex" "^7.6.2"
-    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
-    "@babel/plugin-transform-for-of" "^7.4.4"
-    "@babel/plugin-transform-function-name" "^7.4.4"
-    "@babel/plugin-transform-literals" "^7.2.0"
-    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
-    "@babel/plugin-transform-modules-amd" "^7.5.0"
-    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
-    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
-    "@babel/plugin-transform-modules-umd" "^7.2.0"
-    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.3"
-    "@babel/plugin-transform-new-target" "^7.4.4"
-    "@babel/plugin-transform-object-super" "^7.5.5"
-    "@babel/plugin-transform-parameters" "^7.4.4"
-    "@babel/plugin-transform-property-literals" "^7.2.0"
-    "@babel/plugin-transform-regenerator" "^7.4.5"
-    "@babel/plugin-transform-reserved-words" "^7.2.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
-    "@babel/plugin-transform-spread" "^7.6.2"
-    "@babel/plugin-transform-sticky-regex" "^7.2.0"
-    "@babel/plugin-transform-template-literals" "^7.4.4"
-    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
-    "@babel/plugin-transform-unicode-regex" "^7.6.2"
-    "@babel/types" "^7.6.3"
-    browserslist "^4.6.0"
-    core-js-compat "^3.1.1"
-    invariant "^2.2.2"
-    js-levenshtein "^1.1.3"
-    semver "^5.5.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
 
 "@babel/preset-react@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf"
-  integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw==
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9"
+  integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-transform-react-display-name" "^7.10.4"
-    "@babel/plugin-transform-react-jsx" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-development" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-self" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-source" "^7.10.4"
-    "@babel/plugin-transform-react-pure-annotations" "^7.10.4"
-
-"@babel/runtime@^7.8.4":
-  version "7.11.2"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
-  integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+    "@babel/plugin-transform-react-display-name" "^7.12.1"
+    "@babel/plugin-transform-react-jsx" "^7.12.10"
+    "@babel/plugin-transform-react-jsx-development" "^7.12.7"
+    "@babel/plugin-transform-react-pure-annotations" "^7.12.1"
+
+"@babel/runtime-corejs3@^7.9.6":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4"
+  integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==
   dependencies:
+    core-js-pure "^3.0.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/template@^7.10.4", "@babel/template@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
-  integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
+"@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+  integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
   dependencies:
-    "@babel/code-frame" "^7.10.4"
-    "@babel/parser" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    regenerator-runtime "^0.13.4"
 
-"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.6.3":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
-  integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
+"@babel/template@^7.10.4", "@babel/template@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
+  integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
   dependencies:
     "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.11.5"
-    "@babel/helper-function-name" "^7.10.4"
-    "@babel/helper-split-export-declaration" "^7.11.0"
-    "@babel/parser" "^7.11.5"
-    "@babel/types" "^7.11.5"
+    "@babel/parser" "^7.12.7"
+    "@babel/types" "^7.12.7"
+
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
+  integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
+  dependencies:
+    "@babel/code-frame" "^7.12.11"
+    "@babel/generator" "^7.12.11"
+    "@babel/helper-function-name" "^7.12.11"
+    "@babel/helper-split-export-declaration" "^7.12.11"
+    "@babel/parser" "^7.12.11"
+    "@babel/types" "^7.12.12"
     debug "^4.1.0"
     globals "^11.1.0"
     lodash "^4.17.19"
 
-"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.6.3":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
-  integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
+"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
+  integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.10.4"
+    "@babel/helper-validator-identifier" "^7.12.11"
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
+"@emotion/cache@^10.0.27":
+  version "10.0.29"
+  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
+  integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+  dependencies:
+    "@emotion/sheet" "0.9.4"
+    "@emotion/stylis" "0.8.5"
+    "@emotion/utils" "0.11.3"
+    "@emotion/weak-memoize" "0.2.5"
+
+"@emotion/core@^10.0.14":
+  version "10.1.1"
+  resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3"
+  integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/cache" "^10.0.27"
+    "@emotion/css" "^10.0.27"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/sheet" "0.9.4"
+    "@emotion/utils" "0.11.3"
+
+"@emotion/css@^10.0.27":
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c"
+  integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
+  dependencies:
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/utils" "0.11.3"
+    babel-plugin-emotion "^10.0.27"
+
+"@emotion/[email protected]":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+  integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/[email protected]":
+  version "0.8.8"
+  resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
+  integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
+  dependencies:
+    "@emotion/memoize" "0.7.4"
+
+"@emotion/[email protected]":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
+  version "0.11.16"
+  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
+  integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+  dependencies:
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/unitless" "0.7.5"
+    "@emotion/utils" "0.11.3"
+    csstype "^2.5.7"
+
+"@emotion/[email protected]":
+  version "0.9.4"
+  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
+  integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+
+"@emotion/styled-base@^10.0.27":
+  version "10.0.31"
+  resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a"
+  integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/is-prop-valid" "0.8.8"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/utils" "0.11.3"
+
+"@emotion/styled@^10.0.14":
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf"
+  integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==
+  dependencies:
+    "@emotion/styled-base" "^10.0.27"
+    babel-plugin-emotion "^10.0.27"
+
+"@emotion/[email protected]":
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
+  integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+
+"@emotion/[email protected]":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
+  integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/[email protected]":
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
+  integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+
+"@emotion/[email protected]":
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+  integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
 "@fortawesome/fontawesome-free@^5.14.0":
   version "5.15.1"
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz#ccfef6ddbe59f8fe8f694783e1d3eb88902dc5eb"
@@ -791,12 +985,62 @@
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
   integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
 
+"@popperjs/core@^2.4.4":
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
+  integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
+
+"@projectstorm/geometry@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/geometry/-/geometry-6.3.0.tgz#2c37dee4b907ed0086e91680b815cd16188353f0"
+  integrity sha512-Fc2JVAkZPnQMAKdn4FtAApvl40S+sUL9E2usRoXcnqRbwmhD3WHBLhmDIvUTArIyKJJOTzyQW4+O7+NcyrK9/Q==
+
+"@projectstorm/react-canvas-core@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-canvas-core/-/react-canvas-core-6.3.0.tgz#dfe3072bb3817910c650a1fa004ce439f5bd0669"
+  integrity sha512-mChKZpdfrTWdGvg1OB0EOaCz5ovC6spY/MJfGtxYek5gU36mjoAXE+L6uLzjELTM3J/TJhDkm3Wd7jW9vCNUyA==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+
+"@projectstorm/react-diagrams-core@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-core/-/react-diagrams-core-6.3.0.tgz#4e225e86a0389f383020b4a9e8bbce2e55781cd2"
+  integrity sha512-xdAA+Rz5vE0adNa/rloXvjDQ2MFlicmdcZHEFTrPH+nV8miAqhHimzbCge8ypAwQyVXS8CLdxUF2RQA/1B8Sng==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+    "@projectstorm/react-canvas-core" "^6.3.0"
+
+"@projectstorm/react-diagrams-defaults@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-defaults/-/react-diagrams-defaults-6.3.0.tgz#2479de1abb3e638df04f33f09413d326b27e29ea"
+  integrity sha512-5qBFg9sSl3OlIt5Edv45uECgkjqjpg+PSRYderOXJjEQi3SYhf9l0YlfMzpTvt5GmVnyX7gQProyt/p/e4e9rQ==
+  dependencies:
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+
+"@projectstorm/react-diagrams-routing@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-routing/-/react-diagrams-routing-6.3.0.tgz#38b8973ac35e40639dde7490840217d8cb1e66fb"
+  integrity sha512-kgq0MNrjbeEcpQceOORL/5bOQJNF70c/If2x/jqdEKfswzGC9VeUYOKZvzYCRhxsOcpn9hCA6+6Wkd8e4zFzYQ==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+    "@projectstorm/react-diagrams-defaults" "^6.3.0"
+
+"@projectstorm/react-diagrams@^6.2.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams/-/react-diagrams-6.3.0.tgz#bd435699f04b63ff57b8434a259aa2dc3b494129"
+  integrity sha512-qHPhlTBsZk/iwXAPXMh6qzmk66+7U4cShSwXM/3OqDkrBi6yI7nLtdY5VhioO0qBhhOyR8v1C8oeYGFqY0pa8Q==
+  dependencies:
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+    "@projectstorm/react-diagrams-defaults" "^6.3.0"
+    "@projectstorm/react-diagrams-routing" "^6.3.0"
+
 "@simonwep/pickr@^1.5.1":
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.7.4.tgz#b14fcd945890388b870cd6db4d6c78d531f25141"
-  integrity sha512-fq7jgKJT21uWGC1mARBHvvd1JYlEf93o7SuVOB4Lr0x/2UPuNC9Oe9n/GzVeg4oVtqMDfh1wIEJpsdOJEZb+3g==
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.8.0.tgz#adbff9a4f7f0e59dec9946508c5e481b7abae0f8"
+  integrity sha512-VaSD7TwktOsro5nQ/FjRx5JAJ09k5CNfGRHacgVRxeVPolUQwelz1SjL8HAOKZwTSmcnIObptpHABQS4zgN7sw==
   dependencies:
-    core-js "^3.6.5"
+    core-js "^3.8.0"
     nanopop "^2.1.0"
 
 "@sindresorhus/is@^0.7.0":
@@ -804,21 +1048,43 @@
   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
   integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
 
+"@tippyjs/react@^4.2.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.0.tgz#a1cb369d0051099e8a7e4ceb59f809abd9955283"
+  integrity sha512-T6UcHtwtGkvgsBQ4bNp8BtXGxa2ujfOkWUogYkRtN4UVJ2QRgDdFoJeaPxdndnVYFEa2uTVxSFxs8QkSkZ2Gdw==
+  dependencies:
+    tippy.js "^6.2.0"
+
+"@types/estree@*":
+  version "0.0.45"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
+  integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
+
 "@types/json-schema@^7.0.5":
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
 
 "@types/node@*":
-  version "14.11.5"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305"
-  integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ==
+  version "14.14.16"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b"
+  integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
 "@types/q@^1.5.1":
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
+"@types/raf@^3.4.0":
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
+  integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
+
 "@webassemblyjs/[email protected]":
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@@ -1023,7 +1289,7 @@ acorn@^6.0.7, acorn@^6.4.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
   integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 
-acorn@^7.0.0, acorn@^7.1.1:
+acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
   version "7.4.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@@ -1077,9 +1343,9 @@ ajv@^5.0.0:
     json-schema-traverse "^0.3.0"
 
 ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.9.1:
-  version "6.12.5"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
-  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
@@ -1088,7 +1354,7 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.9.1:
 
 "alertifyjs@git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263":
   version "1.7.1"
-  resolved "git+https://github.com/EnterpriseDB/AlertifyJS.git#72c1d794f5b6d4ec13a68d123c08f19021afe263"
+  resolved "git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263"
 
 alphanum-sort@^1.0.0:
   version "1.0.2"
@@ -1161,9 +1427,9 @@ aproba@^1.1.1:
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
 arch@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
-  integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
+  integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
 
 archive-type@^4.0.0:
   version "4.0.0"
@@ -1209,13 +1475,15 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-includes@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
-  integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+array-includes@^3.1.1, array-includes@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8"
+  integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0"
+    es-abstract "^1.18.0-next.1"
+    get-intrinsic "^1.0.1"
     is-string "^1.0.5"
 
 array-union@^1.0.1:
@@ -1244,20 +1512,22 @@ array.prototype.find@^2.1.1:
     es-abstract "^1.17.4"
 
 array.prototype.flat@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
-  integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
+  integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 array.prototype.flatmap@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443"
-  integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
+  integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
     function-bind "^1.1.1"
 
 arraybuffer.slice@~0.0.7:
@@ -1334,11 +1604,11 @@ autoprefixer@^9.6.4:
     postcss-value-parser "^4.1.0"
 
 axios-mock-adapter@^1.17.0:
-  version "1.18.2"
-  resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.18.2.tgz#01fa9e88e692e8f1bbc1ad1200dde672486e03c7"
-  integrity sha512-e5aTsPy2Viov22zNpFTlid76W1Scz82pXeEwwCXdtO85LROhHAF8pHF2qDhiyMONLxKyY3lQ+S4UCsKgrlx8Hw==
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz#9d72e321a6c5418e1eff067aa99761a86c5188a4"
+  integrity sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==
   dependencies:
-    fast-deep-equal "^3.1.1"
+    fast-deep-equal "^3.1.3"
     is-buffer "^2.0.3"
 
 axios@^0.18.1:
@@ -1358,6 +1628,18 @@ babel-code-frame@^6.26.0:
     esutils "^2.0.2"
     js-tokens "^3.0.2"
 
+babel-eslint@^10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
+  integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/parser" "^7.7.0"
+    "@babel/traverse" "^7.7.0"
+    "@babel/types" "^7.7.0"
+    eslint-visitor-keys "^1.0.0"
+    resolve "^1.12.0"
+
 babel-generator@^6.18.0:
   version "6.26.1"
   resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
@@ -1372,91 +1654,15 @@ babel-generator@^6.18.0:
     source-map "^0.5.7"
     trim-right "^1.0.1"
 
-babel-helper-call-delegate@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
-  integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=
+babel-loader@^8.1.0:
+  version "8.2.2"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
+  integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==
   dependencies:
-    babel-helper-hoist-variables "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-helper-define-map@^6.24.1:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
-  integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=
-  dependencies:
-    babel-helper-function-name "^6.24.1"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-helper-function-name@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
-  integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=
-  dependencies:
-    babel-helper-get-function-arity "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-helper-get-function-arity@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
-  integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-hoist-variables@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
-  integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-optimise-call-expression@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
-  integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-regex@^6.24.1:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
-  integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=
-  dependencies:
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-helper-replace-supers@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
-  integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo=
-  dependencies:
-    babel-helper-optimise-call-expression "^6.24.1"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-loader@~8.0.5:
-  version "8.0.6"
-  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb"
-  integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==
-  dependencies:
-    find-cache-dir "^2.0.0"
-    loader-utils "^1.0.2"
-    mkdirp "^0.5.1"
-    pify "^4.0.1"
+    find-cache-dir "^3.3.1"
+    loader-utils "^1.4.0"
+    make-dir "^3.1.0"
+    schema-utils "^2.6.5"
 
 babel-messages@^6.23.0:
   version "6.23.0"
@@ -1465,13 +1671,6 @@ babel-messages@^6.23.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-check-es2015-constants@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
-  integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=
-  dependencies:
-    babel-runtime "^6.22.0"
-
 babel-plugin-dynamic-import-node@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
@@ -1479,233 +1678,37 @@ babel-plugin-dynamic-import-node@^2.3.3:
   dependencies:
     object.assign "^4.1.0"
 
[email protected]:
-  version "7.0.0-beta.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-7.0.0-beta.3.tgz#7f781c180899dafd88f132f69472397549be48e5"
-  integrity sha512-21/MnmUFduLr4JzxrKMm/MeF+Jjyi5UdZo38IqzrP0sLhmPbal5ZAUJ4HgWH4339SdjnYgENacbY5wfk/zxTGg==
-
-babel-plugin-transform-es2015-arrow-functions@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
-  integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-block-scoped-functions@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
-  integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-block-scoping@^6.9.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
-  integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=
-  dependencies:
-    babel-runtime "^6.26.0"
-    babel-template "^6.26.0"
-    babel-traverse "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-plugin-transform-es2015-classes@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
-  integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=
-  dependencies:
-    babel-helper-define-map "^6.24.1"
-    babel-helper-function-name "^6.24.1"
-    babel-helper-optimise-call-expression "^6.24.1"
-    babel-helper-replace-supers "^6.24.1"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-computed-properties@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
-  integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-es2015-destructuring@^6.9.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
-  integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-duplicate-keys@^6.6.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
-  integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4=
+babel-plugin-emotion@^10.0.27:
+  version "10.0.33"
+  resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03"
+  integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==
   dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-for-of@^6.6.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
-  integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-function-name@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
-  integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=
-  dependencies:
-    babel-helper-function-name "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-literals@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
-  integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-modules-amd@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
-  integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=
-  dependencies:
-    babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.6.0:
-  version "6.26.2"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
-  integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==
-  dependencies:
-    babel-plugin-transform-strict-mode "^6.24.1"
-    babel-runtime "^6.26.0"
-    babel-template "^6.26.0"
-    babel-types "^6.26.0"
-
-babel-plugin-transform-es2015-object-super@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
-  integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40=
-  dependencies:
-    babel-helper-replace-supers "^6.24.1"
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-parameters@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
-  integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=
-  dependencies:
-    babel-helper-call-delegate "^6.24.1"
-    babel-helper-get-function-arity "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-shorthand-properties@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
-  integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-spread@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
-  integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-sticky-regex@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
-  integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw=
-  dependencies:
-    babel-helper-regex "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-template-literals@^6.6.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
-  integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-typeof-symbol@^6.6.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
-  integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-unicode-regex@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
-  integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek=
-  dependencies:
-    babel-helper-regex "^6.24.1"
-    babel-runtime "^6.22.0"
-    regexpu-core "^2.0.0"
-
-babel-plugin-transform-object-rest-spread@^7.0.0-beta.3:
-  version "7.0.0-beta.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-7.0.0-beta.3.tgz#5c409f3cd70819dbb3382d2056971c5ebe01393a"
-  integrity sha512-NOlhrq1CmxyuI94vNsqMhRPMuL5VG2EKUOIJQ0bwNiXBiwWRLdPoWyPT+Irrx5g4g0PkFgA46tnRj7Dc4ZGsxg==
-  dependencies:
-    babel-plugin-syntax-object-rest-spread "7.0.0-beta.3"
+    "@babel/helper-module-imports" "^7.0.0"
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/serialize" "^0.11.16"
+    babel-plugin-macros "^2.0.0"
+    babel-plugin-syntax-jsx "^6.18.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^1.0.5"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
 
-babel-plugin-transform-regenerator@^6.9.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
-  integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=
+babel-plugin-macros@^2.0.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
+  integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
   dependencies:
-    regenerator-transform "^0.10.0"
+    "@babel/runtime" "^7.7.2"
+    cosmiconfig "^6.0.0"
+    resolve "^1.12.0"
 
-babel-plugin-transform-strict-mode@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
-  integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
+babel-plugin-syntax-jsx@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+  integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
 
-babel-preset-es2015-without-strict@~0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/babel-preset-es2015-without-strict/-/babel-preset-es2015-without-strict-0.0.4.tgz#88c9f36e79d4762c58347b1a698a07c35b6bda5d"
-  integrity sha1-iMnzbnnUdixYNHsaaYoHw1tr2l0=
-  dependencies:
-    babel-plugin-check-es2015-constants "^6.3.13"
-    babel-plugin-transform-es2015-arrow-functions "^6.3.13"
-    babel-plugin-transform-es2015-block-scoped-functions "^6.3.13"
-    babel-plugin-transform-es2015-block-scoping "^6.9.0"
-    babel-plugin-transform-es2015-classes "^6.9.0"
-    babel-plugin-transform-es2015-computed-properties "^6.3.13"
-    babel-plugin-transform-es2015-destructuring "^6.9.0"
-    babel-plugin-transform-es2015-duplicate-keys "^6.6.0"
-    babel-plugin-transform-es2015-for-of "^6.6.0"
-    babel-plugin-transform-es2015-function-name "^6.9.0"
-    babel-plugin-transform-es2015-literals "^6.3.13"
-    babel-plugin-transform-es2015-modules-commonjs "^6.6.0"
-    babel-plugin-transform-es2015-object-super "^6.3.13"
-    babel-plugin-transform-es2015-parameters "^6.9.0"
-    babel-plugin-transform-es2015-shorthand-properties "^6.3.13"
-    babel-plugin-transform-es2015-spread "^6.3.13"
-    babel-plugin-transform-es2015-sticky-regex "^6.3.13"
-    babel-plugin-transform-es2015-template-literals "^6.6.0"
-    babel-plugin-transform-es2015-typeof-symbol "^6.6.0"
-    babel-plugin-transform-es2015-unicode-regex "^6.3.13"
-    babel-plugin-transform-regenerator "^6.9.0"
-
-babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+babel-runtime@^6.22.0, babel-runtime@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -1713,7 +1716,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
     core-js "^2.4.0"
     regenerator-runtime "^0.11.0"
 
-babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+babel-template@^6.16.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
   integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=
@@ -1724,7 +1727,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
     babylon "^6.18.0"
     lodash "^4.17.4"
 
-babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
   integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
@@ -1739,7 +1742,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
     invariant "^2.2.2"
     lodash "^4.17.4"
 
-babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+babel-types@^6.18.0, babel-types@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
   integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
@@ -1823,10 +1826,15 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+base64-arraybuffer@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+  integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
+
+base64-js@^1.0.2, base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 [email protected]:
   version "1.0.0"
@@ -1966,7 +1974,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
   integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
 
-bn.js@^5.1.1:
+bn.js@^5.0.0, bn.js@^5.1.1:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
   integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
@@ -2004,10 +2012,10 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/bootstrap4-toggle/-/bootstrap4-toggle-3.4.0.tgz#58264d4c7fd24eb2e09cca5156a338b2b22a4792"
   integrity sha512-vKfBgsjICW6mOqb264qwwbbGtpcOx+p9jOlQmLfRZ07iGE+b5YbQlY4ft9aAhPulh7V3oKOtEuuq0FcSKAI4bQ==
 
-bootstrap@>=4.1.2, bootstrap@^4.3.1:
-  version "4.5.2"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.2.tgz#a85c4eda59155f0d71186b6e6ad9b875813779ab"
-  integrity sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==
+bootstrap@^4.3.1, bootstrap@^4.5.2:
+  version "4.5.3"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
+  integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
 
 [email protected]:
   version "2.1.2"
@@ -2108,11 +2116,11 @@ browserify-des@^1.0.0:
     safe-buffer "^5.1.2"
 
 browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
-  integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+  integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
   dependencies:
-    bn.js "^4.1.0"
+    bn.js "^5.0.0"
     randombytes "^2.0.1"
 
 browserify-sign@^4.0.0:
@@ -2137,7 +2145,61 @@ browserify-zlib@^0.2.0, browserify-zlib@~0.2.0:
   dependencies:
     pako "~1.0.5"
 
-browserify@^16.1.0, browserify@~16.2.3:
+browserify@^16.1.0:
+  version "16.5.2"
+  resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.2.tgz#d926835e9280fa5fd57f5bc301f2ef24a972ddfe"
+  integrity sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g==
+  dependencies:
+    JSONStream "^1.0.3"
+    assert "^1.4.0"
+    browser-pack "^6.0.1"
+    browser-resolve "^2.0.0"
+    browserify-zlib "~0.2.0"
+    buffer "~5.2.1"
+    cached-path-relative "^1.0.0"
+    concat-stream "^1.6.0"
+    console-browserify "^1.1.0"
+    constants-browserify "~1.0.0"
+    crypto-browserify "^3.0.0"
+    defined "^1.0.0"
+    deps-sort "^2.0.0"
+    domain-browser "^1.2.0"
+    duplexer2 "~0.1.2"
+    events "^2.0.0"
+    glob "^7.1.0"
+    has "^1.0.0"
+    htmlescape "^1.1.0"
+    https-browserify "^1.0.0"
+    inherits "~2.0.1"
+    insert-module-globals "^7.0.0"
+    labeled-stream-splicer "^2.0.0"
+    mkdirp-classic "^0.5.2"
+    module-deps "^6.2.3"
+    os-browserify "~0.3.0"
+    parents "^1.0.1"
+    path-browserify "~0.0.0"
+    process "~0.11.0"
+    punycode "^1.3.2"
+    querystring-es3 "~0.2.0"
+    read-only-stream "^2.0.0"
+    readable-stream "^2.0.2"
+    resolve "^1.1.4"
+    shasum "^1.0.0"
+    shell-quote "^1.6.1"
+    stream-browserify "^2.0.0"
+    stream-http "^3.0.0"
+    string_decoder "^1.1.1"
+    subarg "^1.0.0"
+    syntax-error "^1.1.1"
+    through2 "^2.0.0"
+    timers-browserify "^1.0.1"
+    tty-browserify "0.0.1"
+    url "~0.11.0"
+    util "~0.10.1"
+    vm-browserify "^1.0.0"
+    xtend "^4.0.0"
+
+browserify@~16.2.3:
   version "16.2.3"
   resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.2.3.tgz#7ee6e654ba4f92bce6ab3599c3485b1cc7a0ad0b"
   integrity sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==
@@ -2191,15 +2253,16 @@ browserify@^16.1.0, browserify@~16.2.3:
     vm-browserify "^1.0.0"
     xtend "^4.0.0"
 
-browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.0, browserslist@^4.8.5:
-  version "4.14.5"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015"
-  integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==
+browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0:
+  version "4.16.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b"
+  integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==
   dependencies:
-    caniuse-lite "^1.0.30001135"
-    electron-to-chromium "^1.3.571"
-    escalade "^3.1.0"
-    node-releases "^1.1.61"
+    caniuse-lite "^1.0.30001165"
+    colorette "^1.2.1"
+    electron-to-chromium "^1.3.621"
+    escalade "^3.1.1"
+    node-releases "^1.1.67"
 
 buffer-alloc-unsafe@^1.1.0:
   version "1.1.0"
@@ -2244,9 +2307,17 @@ buffer@^4.3.0:
     isarray "^1.0.0"
 
 buffer@^5.0.2, buffer@^5.2.1:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
-  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
+buffer@~5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
+  integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==
   dependencies:
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
@@ -2339,6 +2410,14 @@ cached-path-relative@^1.0.0, cached-path-relative@^1.0.2:
   resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db"
   integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==
 
+call-bind@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
+  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.0"
+
 caller-callsite@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
@@ -2396,10 +2475,22 @@ caniuse-api@^3.0.0:
     lodash.memoize "^4.1.2"
     lodash.uniq "^4.5.0"
 
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
-  version "1.0.30001146"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c"
-  integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
+  version "1.0.30001170"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7"
+  integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==
+
+canvg@^3.0.7:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.7.tgz#e45b87a64116af906917f7cad57d370ea372d682"
+  integrity sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==
+  dependencies:
+    "@babel/runtime-corejs3" "^7.9.6"
+    "@types/raf" "^3.4.0"
+    raf "^3.4.1"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^5.0.5"
 
 caw@^2.0.0, caw@^2.0.1:
   version "2.0.1"
@@ -2445,9 +2536,9 @@ chardet@^0.7.0:
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
 chart.js@^2.9.3:
-  version "2.9.3"
-  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
-  integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
+  version "2.9.4"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
+  integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
   dependencies:
     chartjs-color "^2.1.0"
     moment "^2.10.2"
@@ -2472,19 +2563,46 @@ check-types@^8.0.3:
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
   integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
 
+cheerio-select-tmp@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
+  integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==
+  dependencies:
+    css-select "^3.1.2"
+    css-what "^4.0.0"
+    domelementtype "^2.1.0"
+    domhandler "^4.0.0"
+    domutils "^2.4.4"
+
 cheerio@^1.0.0-rc.3:
-  version "1.0.0-rc.3"
-  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
-  integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
-  dependencies:
-    css-select "~1.2.0"
-    dom-serializer "~0.1.1"
-    entities "~1.1.1"
-    htmlparser2 "^3.9.1"
-    lodash "^4.15.0"
-    parse5 "^3.0.1"
-
-"chokidar@>=2.0.0 <4.0.0", chokidar@^2.1.1, chokidar@^2.1.8:
+  version "1.0.0-rc.5"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f"
+  integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==
+  dependencies:
+    cheerio-select-tmp "^0.1.0"
+    dom-serializer "~1.2.0"
+    domhandler "^4.0.0"
+    entities "~2.1.0"
+    htmlparser2 "^6.0.0"
+    parse5 "^6.0.0"
+    parse5-htmlparser2-tree-adapter "^6.0.0"
+
+"chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.4.1:
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
+  integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.5.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
+chokidar@^2.1.1, chokidar@^2.1.8:
   version "2.1.8"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
   integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
@@ -2503,21 +2621,6 @@ cheerio@^1.0.0-rc.3:
   optionalDependencies:
     fsevents "^1.2.7"
 
-chokidar@^3.0.0, chokidar@^3.4.1:
-  version "3.4.2"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
-  integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==
-  dependencies:
-    anymatch "~3.1.1"
-    braces "~3.0.2"
-    glob-parent "~5.1.0"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.4.0"
-  optionalDependencies:
-    fsevents "~2.1.2"
-
 chownr@^1.1.1, chownr@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -2595,6 +2698,13 @@ [email protected]:
   dependencies:
     mimic-response "^1.0.0"
 
+closest@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/closest/-/closest-0.0.1.tgz#26da6f80b3e0e17e71f80f12782819e9f653495c"
+  integrity sha1-JtpvgLPg4X5x+A8SeCgZ6fZTSVw=
+  dependencies:
+    matches-selector "0.0.1"
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -2610,9 +2720,9 @@ coa@^2.0.2:
     q "^1.1.2"
 
 codemirror@^5.54.0:
-  version "5.58.1"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.58.1.tgz#ec6bf38ad2a17f74c61bd00cc6dc5a69bd167854"
-  integrity sha512-UGb/ueu20U4xqWk8hZB3xIfV2/SFqnSLYONiM3wTMDqko0bsYrsAkGGhqUzbRkYm89aBKPyHtuNEbVWF9FTFzw==
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.0.tgz#6d8132055459aabf21d04cae5cf5c430e5c57bb9"
+  integrity sha512-UGzSkCacY9z0rSpQ3wnTWRN2nvRE6foDXnJltWW8pazInR/R+3gXHrao4IFQMv/bSBvFBxt8/HPpkpKAS54x5Q==
 
 collection-visit@^1.0.0:
   version "1.0.0"
@@ -2636,31 +2746,31 @@ color-convert@^2.0.1:
   dependencies:
     color-name "~1.1.4"
 
[email protected], color-name@^1.0.0:
[email protected]:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@~1.1.4:
+color-name@^1.0.0, color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-string@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
-  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+color-string@^1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
+  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
 color@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
-  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
+  integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==
   dependencies:
     color-convert "^1.9.1"
-    color-string "^1.5.2"
+    color-string "^1.5.4"
 
 colorette@^1.2.1:
   version "1.2.1"
@@ -2688,9 +2798,9 @@ commander@^2.12.2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, comm
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
 commander@^6.0.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
-  integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -2777,18 +2887,18 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.0, convert-source-map@^1.1.3, convert-source-map@~1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
-  integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=
-
-convert-source-map@^1.5.0, convert-source-map@^1.7.0:
+convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
+convert-source-map@~1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+  integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=
+
 [email protected]:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -2839,23 +2949,28 @@ copy-webpack-plugin@^5.1.0:
     serialize-javascript "^4.0.0"
     webpack-log "^2.0.0"
 
-core-js-compat@^3.1.1:
-  version "3.6.5"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c"
-  integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==
+core-js-compat@^3.8.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e"
+  integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==
   dependencies:
-    browserslist "^4.8.5"
+    browserslist "^4.15.0"
     semver "7.0.0"
 
+core-js-pure@^3.0.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119"
+  integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==
+
 core-js@^2.4.0:
-  version "2.6.11"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
-  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+  version "2.6.12"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
-core-js@^3.2.1, core-js@^3.6.5:
-  version "3.6.5"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
-  integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
+core-js@^3.2.1, core-js@^3.8.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47"
+  integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==
 
 core-util-is@~1.0.0:
   version "1.0.2"
@@ -2872,6 +2987,17 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.13.1"
     parse-json "^4.0.0"
 
+cosmiconfig@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
+  integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.1.0"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.7.2"
+
 create-ecdh@^4.0.0:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -2960,6 +3086,13 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
[email protected]:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+  integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
+  dependencies:
+    base64-arraybuffer "^0.2.0"
+
 [email protected]:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@@ -2991,15 +3124,16 @@ css-select@^2.0.0:
     domutils "^1.7.0"
     nth-check "^1.0.2"
 
-css-select@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
-  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+css-select@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
+  integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
   dependencies:
-    boolbase "~1.0.0"
-    css-what "2.1"
-    domutils "1.5.1"
-    nth-check "~1.0.1"
+    boolbase "^1.0.0"
+    css-what "^4.0.0"
+    domhandler "^4.0.0"
+    domutils "^2.4.3"
+    nth-check "^2.0.0"
 
 [email protected]:
   version "1.0.0-alpha.37"
@@ -3009,23 +3143,23 @@ [email protected]:
     mdn-data "2.0.4"
     source-map "^0.6.1"
 
[email protected]:
-  version "1.0.0-alpha.39"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
-  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+css-tree@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5"
+  integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==
   dependencies:
-    mdn-data "2.0.6"
+    mdn-data "2.0.14"
     source-map "^0.6.1"
 
[email protected]:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
-  integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
-
 css-what@^3.2.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
-  integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
+  integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
+
+css-what@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
+  integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
 
 cssesc@^3.0.0:
   version "3.0.0"
@@ -3101,11 +3235,16 @@ cssnano@^4.1.10:
     postcss "^7.0.0"
 
 csso@^4.0.2:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
-  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
   dependencies:
-    css-tree "1.0.0-alpha.39"
+    css-tree "^1.1.2"
+
+csstype@^2.5.7:
+  version "2.6.14"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de"
+  integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==
 
 cubic2quad@^1.0.0:
   version "1.1.1"
@@ -3138,6 +3277,14 @@ cyclist@^1.0.1:
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
   integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
 
+dagre@^0.8.4:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
+  integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
+  dependencies:
+    graphlib "^2.1.8"
+    lodash "^4.17.15"
+
 dash-ast@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37"
@@ -3163,16 +3310,16 @@ debug@=3.1.0, debug@~3.1.0:
     ms "2.0.0"
 
 debug@^3.2.6:
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
 debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
-  integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
   dependencies:
     ms "2.1.2"
 
@@ -3391,40 +3538,46 @@ dom-serialize@^2.2.0:
     extend "^3.0.0"
     void-elements "^2.0.0"
 
-dom-serializer@0, dom-serializer@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
-  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+dom-serializer@^1.0.1, dom-serializer@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
+  integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
   dependencies:
-    domelementtype "^1.3.0"
-    entities "^1.1.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    entities "^2.0.0"
 
 domain-browser@^1.1.1, domain-browser@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
   integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
 
-domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+domelementtype@1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
   integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
 
-domhandler@^2.3.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
-  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
-  dependencies:
-    domelementtype "1"
+domelementtype@^2.0.1, domelementtype@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
+  integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
 
[email protected]:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
-  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+domhandler@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e"
+  integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==
   dependencies:
-    dom-serializer "0"
-    domelementtype "1"
+    domelementtype "^2.1.0"
 
-domutils@^1.5.1, domutils@^1.7.0:
+domutils@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
   integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
@@ -3432,6 +3585,15 @@ domutils@^1.5.1, domutils@^1.7.0:
     dom-serializer "0"
     domelementtype "1"
 
+domutils@^2.4.3, domutils@^2.4.4:
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
+  integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+
 dot-prop@^5.2.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -3521,10 +3683,10 @@ ejs@~3.0.2:
   resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.2.tgz#745b01cdcfe38c1c6a2da3bbb2d9957060a31226"
   integrity sha512-IncmUpn1yN84hy2shb0POJ80FWrfGNY0cxO9f4v+/sG7qcBvAtVWUA1IdzY/8EYUmOVhoKJVdJjNd3AZcnxOjA==
 
-electron-to-chromium@^1.3.571:
-  version "1.3.578"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0"
-  integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q==
+electron-to-chromium@^1.3.621:
+  version "1.3.633"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe"
+  integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==
 
 elliptic@^6.5.3:
   version "6.5.3"
@@ -3615,10 +3777,10 @@ ent@~2.2.0:
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
   integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
 
-entities@^1.1.1, entities@~1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
-  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+entities@^2.0.0, entities@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 enzyme-adapter-react-16@^1.15.2:
   version "1.15.5"
@@ -3636,14 +3798,15 @@ enzyme-adapter-react-16@^1.15.2:
     semver "^5.7.0"
 
 enzyme-adapter-utils@^1.13.1:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz#59c1b734b0927543e3d8dc477299ec957feb312d"
-  integrity sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0"
+  integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==
   dependencies:
     airbnb-prop-types "^2.16.0"
-    function.prototype.name "^1.1.2"
-    object.assign "^4.1.0"
-    object.fromentries "^2.0.2"
+    function.prototype.name "^1.1.3"
+    has "^1.0.3"
+    object.assign "^4.1.2"
+    object.fromentries "^2.0.3"
     prop-types "^15.7.2"
     semver "^5.7.1"
 
@@ -3692,9 +3855,9 @@ enzyme@^3.11.0:
     string.prototype.trim "^1.2.1"
 
 errno@^0.1.3, errno@~0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
-  integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
   dependencies:
     prr "~1.0.1"
 
@@ -3705,7 +3868,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4:
   version "1.17.7"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
   integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
@@ -3749,10 +3912,10 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-escalade@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
-  integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 
 escape-html@~1.0.3:
   version "1.0.3"
@@ -3765,20 +3928,20 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 eslint-plugin-react@^7.20.5:
-  version "7.21.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.3.tgz#71655d2af5155b19285ec929dd2cdc67a4470b52"
-  integrity sha512-OI4GwTCqyIb4ipaOEGLWdaOHCXZZydStAsBEPB2e1ZfNM37bojpgO1BoOQbFb0eLVz3QLDx7b+6kYcrxCuJfhw==
+  version "7.21.5"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz#50b21a412b9574bfe05b21db176e8b7b3b15bff3"
+  integrity sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==
   dependencies:
     array-includes "^3.1.1"
     array.prototype.flatmap "^1.2.3"
     doctrine "^2.1.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.4.1"
+    jsx-ast-utils "^2.4.1 || ^3.0.0"
     object.entries "^1.1.2"
     object.fromentries "^2.0.2"
     object.values "^1.1.1"
     prop-types "^15.7.2"
-    resolve "^1.17.0"
+    resolve "^1.18.1"
     string.prototype.matchall "^4.0.2"
 
 eslint-scope@^4.0.2, eslint-scope@^4.0.3:
@@ -4103,7 +4266,7 @@ fast-deep-equal@^1.0.0:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
   integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=
 
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -4249,7 +4412,7 @@ [email protected], finalhandler@~1.1.2:
     statuses "~1.5.0"
     unpipe "~1.0.0"
 
-find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
+find-cache-dir@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
   integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
@@ -4267,6 +4430,11 @@ find-cache-dir@^3.3.1:
     make-dir "^3.0.2"
     pkg-dir "^4.1.0"
 
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4329,13 +4497,18 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
[email protected], follow-redirects@^1.0.0:
[email protected]:
   version "1.5.10"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
   integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
   dependencies:
     debug "=3.1.0"
 
+follow-redirects@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
+  integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
+
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -4427,29 +4600,30 @@ function-bind@^1.1.1:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
-function.prototype.name@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45"
-  integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==
+function.prototype.name@^1.1.2, function.prototype.name@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe"
+  integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    functions-have-names "^1.2.0"
+    es-abstract "^1.18.0-next.1"
+    functions-have-names "^1.2.1"
 
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
-functions-have-names@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
-  integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==
+functions-have-names@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
+  integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
 
 gensync@^1.0.0-beta.1:
-  version "1.0.0-beta.1"
-  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
-  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
 geometry-interfaces@^1.1.4:
   version "1.1.4"
@@ -4466,6 +4640,15 @@ get-caller-file@^2.0.1:
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
+get-intrinsic@^1.0.0, get-intrinsic@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
+  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
 get-proxy@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93"
@@ -4657,6 +4840,13 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
   integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
 
+graphlib@^2.1.8:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
+  integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+  dependencies:
+    lodash "^4.17.15"
+
 gzip-size@^5.0.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@@ -4771,6 +4961,11 @@ hat@^0.0.3:
   resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
   integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=
 
[email protected]:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.5.tgz#713b65590ebcc40fcbeeaf55e851694092b39af1"
+  integrity sha1-cTtlWQ68xA/L7q9V6FFpQJKzmvE=
+
 hex-color-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@@ -4829,29 +5024,39 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+html-to-image@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-0.1.3.tgz#9661a54c30ed9c4b269f4612416b1c7e887de3a7"
+  integrity sha512-8JTGEAAdJGL/nlp3wb/WI8fLMx2dHKOFZMdsvdon23D45ZdtsXDeRm39Wddf04ludQe3OPmvjMJ9nPjI/7hPlg==
+
+html2canvas@^1.0.0-rc.7:
+  version "1.0.0-rc.7"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+  integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
+  dependencies:
+    css-line-break "1.1.1"
+
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
   integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
 
-htmlparser2@^3.9.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
-  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+htmlparser2@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
+  integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==
   dependencies:
-    domelementtype "^1.3.1"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^3.1.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    domutils "^2.4.4"
+    entities "^2.0.0"
 
 [email protected]:
   version "3.8.1"
   resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
   integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==
 
[email protected], http-errors@~1.7.2:
[email protected]:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
   integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@@ -4862,6 +5067,17 @@ [email protected], http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-proxy@^1.13.0:
   version "1.18.1"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
@@ -4906,10 +5122,10 @@ icss-utils@^4.0.0:
   dependencies:
     postcss "^7.0.14"
 
-ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+ieee754@^1.1.13, ieee754@^1.1.4:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
 iferr@^0.1.5:
   version "0.1.5"
@@ -5027,10 +5243,10 @@ import-fresh@^2.0.0:
     caller-path "^2.0.0"
     resolve-from "^3.0.0"
 
-import-fresh@^3.0.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
-  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+import-fresh@^3.0.0, import-fresh@^3.1.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
   dependencies:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
@@ -5103,7 +5319,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, [email protected], inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5119,9 +5335,9 @@ [email protected]:
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
 inline-source-map@~0.6.0:
   version "0.6.2"
@@ -5150,9 +5366,9 @@ inquirer@^6.2.2:
     through "^2.3.6"
 
 insert-module-globals@^7.0.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.0.tgz#ec87e5b42728479e327bd5c5c71611ddfb4752ba"
-  integrity sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3"
+  integrity sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==
   dependencies:
     JSONStream "^1.0.3"
     acorn-node "^1.5.2"
@@ -5227,6 +5443,13 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-any-array@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-0.1.0.tgz#e5a27965e3371610c10c66bca3be37ee177bdf8e"
+  integrity sha512-6Kkl1RnvfdkmXM6ZlP+kELGBMA74Nq5pSOm9gIKDaPRe9KQlIJzonrOgq0Jzn/iElB6F2/olpLgWYeVySzrSRg==
+  dependencies:
+    rollup "^1.31.1"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -5252,9 +5475,11 @@ is-binary-path@~2.1.0:
     binary-extensions "^2.0.0"
 
 is-boolean-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
-  integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
+  integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
+  dependencies:
+    call-bind "^1.0.0"
 
 is-buffer@^1.1.0, is-buffer@^1.1.5:
   version "1.1.6"
@@ -5262,9 +5487,9 @@ is-buffer@^1.1.0, is-buffer@^1.1.5:
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
 is-buffer@^2.0.2, is-buffer@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
-  integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
+  integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
 
 is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
   version "1.2.2"
@@ -5283,6 +5508,13 @@ is-color-stop@^1.0.0:
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
+is-core-module@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
 is-cwebp-readable@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-cwebp-readable/-/is-cwebp-readable-2.0.1.tgz#afb93b0c0abd0a25101016ae33aea8aedf926d26"
@@ -5396,9 +5628,9 @@ is-natural-number@^4.0.1:
   integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
 
 is-negative-zero@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
-  integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
 
 is-number-object@^1.0.4:
   version "1.0.4"
@@ -5423,9 +5655,9 @@ is-obj@^2.0.0:
   integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
 
 is-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
-  integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
+  integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
 
 is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
@@ -5628,7 +5860,12 @@ isurl@^1.0.0-alpha5:
     has-to-string-tag-x "^1.2.0"
     is-object "^1.0.1"
 
-jasmine-core@^3.3, jasmine-core@~3.3.0:
+jasmine-core@^3.3:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
+  integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
+
+jasmine-core@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e"
   integrity sha512-3/xSmG/d35hf80BEN66Y6g9Ca5l/Isdeg/j6zvbTYlTzeKinzmaTM4p9am5kYqOmE05D7s1t8FGjzdSnbUbceA==
@@ -5660,16 +5897,11 @@ jquery-ui@>=1.8.0, jquery-ui@^1.12.1:
   resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
   integrity sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=
 
-jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, jquery@^3.0, jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1:
+jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
   integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
 
-js-levenshtein@^1.1.3:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
-  integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
-
 js-string-escape@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
@@ -5686,9 +5918,9 @@ js-tokens@^3.0.2:
   integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
 
 js-yaml@^3.12.0, js-yaml@^3.13.1:
-  version "3.14.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
-  integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
   dependencies:
     argparse "^1.0.7"
     esprima "^4.0.0"
@@ -5730,6 +5962,11 @@ json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
@@ -5759,7 +5996,7 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.0, json5@^2.1.2:
+json5@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
   integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
@@ -5783,13 +6020,13 @@ jsonparse@^1.2.0:
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
 
-jsx-ast-utils@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
-  integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==
+"jsx-ast-utils@^2.4.1 || ^3.0.0":
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
+  integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==
   dependencies:
-    array-includes "^3.1.1"
-    object.assign "^4.1.0"
+    array-includes "^3.1.2"
+    object.assign "^4.1.2"
 
 karma-babel-preprocessor@^8.0.0:
   version "8.0.1"
@@ -5963,6 +6200,11 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -6093,7 +6335,7 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.14.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5:
+lodash@4.*, lodash@^4.14.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5:
   version "4.17.20"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
   integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -6192,7 +6434,7 @@ make-dir@^2.0.0:
     pify "^4.0.1"
     semver "^5.6.0"
 
-make-dir@^3.0.0, make-dir@^3.0.2:
+make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
   integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -6217,9 +6459,14 @@ map-visit@^1.0.0:
     object-visit "^1.0.0"
 
 marked@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.0.tgz#7221ce2395fa6cf6d722e6f2871a32d3513c85ca"
-  integrity sha512-tiRxakgbNPBr301ihe/785NntvYyhxlqcL3YaC8CaxJQh7kiaEtrN9B/eK2I2943Yjkh5gw25chYFDQhOMCwMA==
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb"
+  integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA==
+
[email protected]:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/matches-selector/-/matches-selector-0.0.1.tgz#1df5262243ae341c1a0804dd302048267ac713bb"
+  integrity sha1-HfUmIkOuNBwaCATdMCBIJnrHE7s=
 
 md5.js@^1.3.4:
   version "1.3.5"
@@ -6230,16 +6477,16 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
[email protected]:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
 [email protected]:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
   integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
 
[email protected]:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
-  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
-
 [email protected]:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6347,9 +6594,9 @@ [email protected]:
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
 
 mime@^2.0.3, mime@^2.3.1, mime@^2.4.4:
-  version "2.4.6"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
-  integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
+  version "2.4.7"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
+  integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
 
 mimic-fn@^1.0.0:
   version "1.2.0"
@@ -6449,6 +6696,11 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
+mkdirp-classic@^0.5.2:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
 mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -6456,7 +6708,37 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
-module-deps@^6.0.0:
+ml-array-max@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ml-array-max/-/ml-array-max-1.2.0.tgz#141595131fe10208dd89897ce98ab7fd382a3951"
+  integrity sha512-3UH7XCdjINxbtBWj1EuHMeI242Q3uLuC4rTpSybBWUpGjnG/BefAFxmTolUCuXDM59mJ/G/re80CQbaVIuMjQA==
+  dependencies:
+    is-any-array "^0.1.0"
+
+ml-array-min@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ml-array-min/-/ml-array-min-1.2.0.tgz#6880ea319250a99ec73bc2799ac005c10a0f0489"
+  integrity sha512-Wgf2+lCndLy1SbeOZSUqlkxD9T1CXPT7CIlNGAZRRQI35wsqvfuNtLNH4qKFx8kNjlq3VGXKOSBHeiXR31vaTA==
+  dependencies:
+    is-any-array "^0.1.0"
+
+ml-array-rescale@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/ml-array-rescale/-/ml-array-rescale-1.3.2.tgz#ebdeaaa84d15f714dbb00e94a6a9336ddcf10413"
+  integrity sha512-kiXwdVCGrer7rLnjR6Q9ZgP6e9rbnmQvYVUMLXyqNg4+zOs+jek8yBupqPZPDr+NvlSE5OuMnfAbP1oA63kHBA==
+  dependencies:
+    is-any-array "^0.1.0"
+    ml-array-max "^1.2.0"
+    ml-array-min "^1.2.0"
+
+ml-matrix@^6.5.0:
+  version "6.5.3"
+  resolved "https://registry.yarnpkg.com/ml-matrix/-/ml-matrix-6.5.3.tgz#0c4bb26714607cac76d289b943171c7083418eed"
+  integrity sha512-wXrn+ccApJ6gHktxmosOzs6B6M0huadahDpcgPYIAJggpqN7CtV4Vd7zpW6Lel/1oM5yCULcrbRJ1A5gF/GYDA==
+  dependencies:
+    ml-array-rescale "^1.3.2"
+
+module-deps@^6.0.0, module-deps@^6.2.3:
   version "6.2.3"
   resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee"
   integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==
@@ -6477,25 +6759,23 @@ module-deps@^6.0.0:
     through2 "^2.0.0"
     xtend "^4.0.0"
 
-moment-timezone@^0.4.0:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.4.1.tgz#81f598c3ad5e22cdad796b67ecd8d88d0f5baa06"
-  integrity sha1-gfWYw61eIs2teWtn7NjYjQ9bqgY=
-  dependencies:
-    moment ">= 2.6.0"
-
-moment-timezone@^0.5.11, moment-timezone@^0.5.23:
-  version "0.5.31"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
-  integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
+moment-timezone@^0.5.23, moment-timezone@^0.5.28, moment-timezone@^0.5.31:
+  version "0.5.32"
+  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2"
+  integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==
   dependencies:
     moment ">= 2.9.0"
 
-"moment@>= 2.6.0", "moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2, moment@^2.24.0:
+"moment@>= 2.9.0", moment@^2.10.2, moment@^2.24.0, moment@^2.29.0:
   version "2.29.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
   integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
 
+moment@~2.24.0:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
 moo@^0.5.0:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
@@ -6537,20 +6817,25 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
[email protected], ms@^2.1.1:
[email protected]:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 [email protected]:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
 nan@^2.12.1:
-  version "2.14.1"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
-  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+  version "2.14.2"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
+  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -6580,15 +6865,14 @@ natural-compare@^1.4.0:
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
 nearley@^2.7.10:
-  version "2.19.7"
-  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.7.tgz#eafbe3e2d8ccfe70adaa5c026ab1f9709c116218"
-  integrity sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==
+  version "2.20.1"
+  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
+  integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
   dependencies:
     commander "^2.19.0"
     moo "^0.5.0"
     railroad-diagrams "^1.0.0"
     randexp "0.4.6"
-    semver "^5.4.1"
 
 neatequal@^1.0.0:
   version "1.0.0"
@@ -6641,10 +6925,10 @@ node-libs-browser@^2.2.1:
     util "^0.11.0"
     vm-browserify "^1.0.1"
 
-node-releases@^1.1.61:
-  version "1.1.61"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e"
-  integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==
+node-releases@^1.1.67:
+  version "1.1.67"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
+  integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
 
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
   version "2.5.0"
@@ -6702,13 +6986,20 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
-nth-check@^1.0.2, nth-check@~1.0.1:
+nth-check@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
   integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
   dependencies:
     boolbase "~1.0.0"
 
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
 null-check@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
@@ -6739,17 +7030,17 @@ object-copy@^0.1.0:
     kind-of "^3.0.3"
 
 object-inspect@^1.7.0, object-inspect@^1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
-  integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
+  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
 
 object-is@^1.0.2, object-is@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
-  integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
+  integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
 
 object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -6763,42 +7054,44 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.0, object.assign@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
-  integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
+object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.0"
     has-symbols "^1.0.1"
     object-keys "^1.1.1"
 
 object.entries@^1.1.1, object.entries@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
-  integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
+  integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
-object.fromentries@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
-  integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
+object.fromentries@^2.0.2, object.fromentries@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
+  integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 object.getownpropertydescriptors@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
-  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544"
+  integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 object.pick@^1.3.0:
   version "1.3.0"
@@ -6808,13 +7101,13 @@ object.pick@^1.3.0:
     isobject "^3.0.1"
 
 object.values@^1.1.0, object.values@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
-  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
+  integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 on-finished@~2.3.0:
@@ -7061,17 +7354,32 @@ parse-json@^4.0.0:
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
+parse-json@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
+  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
 parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
-parse5@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
-  integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+parse5-htmlparser2-tree-adapter@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
   dependencies:
-    "@types/node" "*"
+    parse5 "^6.0.1"
+
+parse5@^6.0.0, parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 [email protected]:
   version "0.0.5"
@@ -7170,6 +7478,23 @@ path-type@^3.0.0:
   dependencies:
     pify "^3.0.0"
 
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+pathfinding@^0.4.18:
+  version "0.4.18"
+  resolved "https://registry.yarnpkg.com/pathfinding/-/pathfinding-0.4.18.tgz#a9990f6fa22b7ef196e5651b049165403a045fe8"
+  integrity sha1-qZkPb6IrfvGW5WUbBJFlQDoEX+g=
+  dependencies:
+    heap "0.2.5"
+
+paths-js@^0.4.9:
+  version "0.4.11"
+  resolved "https://registry.yarnpkg.com/paths-js/-/paths-js-0.4.11.tgz#b2a9d5f94ee9949aa8fee945f78a12abff44599e"
+  integrity sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==
+
 pbkdf2@^3.0.3:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
@@ -7247,7 +7572,7 @@ pngquant-bin@^5.0.0:
     execa "^0.10.0"
     logalot "^2.0.0"
 
-popper.js@^1.14.3, popper.js@^1.14.7:
+popper.js@^1.14.7, popper.js@^1.16.1:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
   integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@@ -7621,11 +7946,6 @@ prepend-http@^2.0.0:
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
   integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
 
-private@^0.1.6:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
-
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -7823,18 +8143,18 @@ [email protected]:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-raw-loader@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405"
-  integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==
+raw-loader@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f"
+  integrity sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==
   dependencies:
     loader-utils "^1.1.0"
-    schema-utils "^1.0.0"
+    schema-utils "^2.0.1"
 
 react-dom@^16.13.1:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
-  integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -7847,19 +8167,26 @@ react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6:
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
 react-test-renderer@^16.0.0-0:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
-  integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
+  integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
   dependencies:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
     react-is "^16.8.6"
     scheduler "^0.19.1"
 
+react-to-print@^2.10.3:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.12.1.tgz#120f3116fef55a141f553548388624b976596835"
+  integrity sha512-+zGNUYQKaae7Wp0JL2JcoWM0lDd5csasRmTqRZlJLOj6F4sRmJsT7ZPgH0SL6ZQ7gWz0hwglhLlLoyF85lGgHw==
+  dependencies:
+    prop-types "^15.7.2"
+
 react@^16.13.1:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
-  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -7912,7 +8239,7 @@ readable-stream@^1.0.33:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^3.1.1, readable-stream@^3.6.0:
+readable-stream@^3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -7930,10 +8257,10 @@ readdirp@^2.2.1:
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
-readdirp@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
-  integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
+readdirp@~3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
+  integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
   dependencies:
     picomatch "^2.2.1"
 
@@ -7957,10 +8284,10 @@ regenerate-unicode-properties@^8.2.0:
   dependencies:
     regenerate "^1.4.0"
 
-regenerate@^1.2.1, regenerate@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
-  integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
+regenerate@^1.4.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
+  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
 regenerator-runtime@^0.11.0:
   version "0.11.1"
@@ -7972,15 +8299,6 @@ regenerator-runtime@^0.13.4:
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
   integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
 
-regenerator-transform@^0.10.0:
-  version "0.10.1"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
-  integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==
-  dependencies:
-    babel-runtime "^6.18.0"
-    babel-types "^6.19.0"
-    private "^0.1.6"
-
 regenerator-transform@^0.14.2:
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
@@ -8009,16 +8327,7 @@ regexpp@^2.0.1:
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
   integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
 
-regexpu-core@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
-  integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=
-  dependencies:
-    regenerate "^1.2.1"
-    regjsgen "^0.2.0"
-    regjsparser "^0.1.4"
-
-regexpu-core@^4.7.0:
+regexpu-core@^4.7.1:
   version "4.7.1"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
   integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
@@ -8030,23 +8339,11 @@ regexpu-core@^4.7.0:
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.2.0"
 
-regjsgen@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
-  integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=
-
 regjsgen@^0.5.1:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
   integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
 
-regjsparser@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
-  integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=
-  dependencies:
-    jsesc "~0.5.0"
-
 regjsparser@^0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
@@ -8101,6 +8398,11 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -8136,11 +8438,12 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
   integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
 
-resolve@^1.1.4, resolve@^1.10.0, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+resolve@^1.1.4, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.4.0:
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
+  integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
   dependencies:
+    is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
 [email protected]:
@@ -8178,6 +8481,11 @@ rgba-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0=
+
 [email protected]:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@@ -8200,6 +8508,15 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
+rollup@^1.31.1:
+  version "1.32.1"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
+  integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==
+  dependencies:
+    "@types/estree" "*"
+    "@types/node" "*"
+    acorn "^7.1.0"
+
 rst-selector-parser@^2.2.3:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
@@ -8271,9 +8588,9 @@ sass-resources-loader@^2.0.0:
     loader-utils "^2.0.0"
 
 sass@^1.24.4:
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.27.0.tgz#0657ff674206b95ec20dc638a93e179c78f6ada2"
-  integrity sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig==
+  version "1.30.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.30.0.tgz#60bbbbaf76ba10117e61c6c24f00161c3d60610e"
+  integrity sha512-26EUhOXRLaUY7+mWuRFqGeGGNmhB1vblpTENO1Z7mAzzIZeVxZr9EZoaY1kyGLFWdSOZxRMAufiN2mkbO6dAlw==
   dependencies:
     chokidar ">=2.0.0 <4.0.0"
 
@@ -8306,7 +8623,7 @@ schema-utils@^1.0.0:
     ajv-errors "^1.0.0"
     ajv-keywords "^3.1.0"
 
-schema-utils@^2.6.6:
+schema-utils@^2.0.1, schema-utils@^2.6.5, schema-utils@^2.6.6:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
   integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
@@ -8472,7 +8789,7 @@ shim-loader@^1.0.1:
     precond "^0.2.3"
     webpack-sources "^0.2.3"
 
-side-channel@^1.0.2:
+side-channel@^1.0.2, side-channel@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
   integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
@@ -8698,9 +9015,9 @@ spdx-expression-parse@^3.0.0:
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
-  integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
+  integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
 
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
@@ -8753,6 +9070,11 @@ stable@^0.1.8:
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
+stackblur-canvas@^2.0.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz#2b2eba910cb46f6feae918e1c402f863d602c01b"
+  integrity sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -8801,6 +9123,16 @@ stream-http@^2.0.0, stream-http@^2.7.2:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
+stream-http@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564"
+  integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.4"
+    readable-stream "^3.6.0"
+    xtend "^4.0.2"
+
 stream-shift@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
@@ -8858,40 +9190,42 @@ string.prototype.codepointat@^0.2.0:
   integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
 
 string.prototype.matchall@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e"
-  integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz#24243399bc31b0a49d19e2b74171a15653ec996a"
+  integrity sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0"
+    es-abstract "^1.18.0-next.1"
     has-symbols "^1.0.1"
     internal-slot "^1.0.2"
     regexp.prototype.flags "^1.3.0"
-    side-channel "^1.0.2"
+    side-channel "^1.0.3"
 
 string.prototype.trim@^1.2.1:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz#f538d0bacd98fc4297f0bef645226d5aaebf59f3"
-  integrity sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7"
+  integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.0"
+    es-abstract "^1.18.0-next.1"
 
 string.prototype.trimend@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
-  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
+  integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string.prototype.trimstart@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
-  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
+  integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string_decoder@^1.0.0, string_decoder@^1.1.1:
   version "1.3.0"
@@ -9021,7 +9355,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
-svg-pathdata@^5.0.0:
+svg-pathdata@^5.0.0, svg-pathdata@^5.0.5:
   version "5.0.5"
   resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-5.0.5.tgz#65e8d765642ba15fe15434444087d082bc526b29"
   integrity sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==
@@ -9134,24 +9468,24 @@ tempfile@^2.0.0:
     uuid "^3.0.1"
 
 tempusdominus-bootstrap-4@^5.1.2:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.1.2.tgz#3c9906ca6e5d563faa0b81b2fdc6aa79cad9c0be"
-  integrity sha512-ksD8qc4wOJeE19wvryXmEpRzMUSZu4wSOdG6zKSn8l4ccad16249KOX1j0CccyZpuuES/n4FLqLAUB+Dd1LTBA==
+  version "5.39.0"
+  resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.39.0.tgz#f13dcfec6c41b37c5fe509f08bd513590c64411f"
+  integrity sha512-vYnkmQYQq4+A51WyRc/6e03eM0BHDoPaxd556K1pd4Nhr0eGeB3+Mi9b+3CDx4189fg3gQlrsKzgJiHPRwSX3Q==
   dependencies:
-    bootstrap ">=4.1.2"
-    jquery "^3.0"
-    moment "^2.22.2"
-    moment-timezone "^0.5.11"
-    popper.js "^1.14.3"
+    bootstrap "^4.5.2"
+    jquery "^3.5.1"
+    moment "^2.29.0"
+    moment-timezone "^0.5.31"
+    popper.js "^1.16.1"
 
 tempusdominus-core@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/tempusdominus-core/-/tempusdominus-core-5.0.3.tgz#808642e47a83f45d7ef18c1597fd7b1d413d69e5"
-  integrity sha512-52lClmU33gb6J6I/S9uGDrgQwccq3Yw9SlZerTgGLOzOB3Sc9pgIVBirfPMsMcx8nPsg6mA5ItFAH/5BZiQThg==
+  version "5.19.0"
+  resolved "https://registry.yarnpkg.com/tempusdominus-core/-/tempusdominus-core-5.19.0.tgz#ccbd2c35109b0a4b96c61513e53e0175ec4896bd"
+  integrity sha512-7a4oBQw4cjz6C87BLRg3KHVvzpnPlnRTkuDZ7SwcJayQQ4QgOryX5u6wj0q07TXhgtMQLCntZO6nVhHIKPaeUw==
   dependencies:
-    jquery "^3.0"
-    moment "^2.22.2"
-    moment-timezone "^0.4.0"
+    jquery "^3.5.0"
+    moment "~2.24.0"
+    moment-timezone "^0.5.28"
 
 terser-webpack-plugin@^1.4.3:
   version "1.4.5"
@@ -9223,9 +9557,9 @@ timers-browserify@^1.0.1:
     process "~0.11.0"
 
 timers-browserify@^2.0.4:
-  version "2.0.11"
-  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
-  integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+  version "2.0.12"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee"
+  integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==
   dependencies:
     setimmediate "^1.0.4"
 
@@ -9234,6 +9568,13 @@ timsort@^0.3.0:
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
   integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
 
+tippy.js@^6.2.0:
+  version "6.2.7"
+  resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.7.tgz#62fb34eda23f7d78151ddca922b62818c1ab9869"
+  integrity sha512-k+kWF9AJz5xLQHBi3K/XlmJiyu+p9gsCyc5qZhxxGaJWIW8SMjw1R+C7saUnP33IM8gUhDA2xX//ejRSwqR0tA==
+  dependencies:
+    "@popperjs/core" "^2.4.4"
+
 [email protected], [email protected], tmp@^0.0.33:
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9331,9 +9672,9 @@ tryer@^1.0.1:
   integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
 
 tslib@^1.9.0:
-  version "1.14.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6"
-  integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 [email protected]:
   version "2.0.1"
@@ -9411,9 +9752,9 @@ undeclared-identifiers@^1.1.2:
     xtend "^4.0.1"
 
 underscore@>=1.7.0, underscore@>=1.8.3, underscore@^1.8.0, underscore@^1.8.3, underscore@^1.9.1:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
-  integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97"
+  integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==
 
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
@@ -9608,9 +9949,9 @@ uuid@^3.0.1, uuid@^3.3.2:
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
 v8-compile-cache@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
-  integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
+  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
 
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
@@ -9660,23 +10001,23 @@ watchify@~3.11.1:
     through2 "^2.0.0"
     xtend "^4.0.0"
 
-watchpack-chokidar2@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
-  integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
+watchpack-chokidar2@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
+  integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==
   dependencies:
     chokidar "^2.1.8"
 
 watchpack@^1.7.4:
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
-  integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
+  integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
   dependencies:
     graceful-fs "^4.1.2"
     neo-async "^2.5.0"
   optionalDependencies:
     chokidar "^3.4.1"
-    watchpack-chokidar2 "^2.0.0"
+    watchpack-chokidar2 "^2.0.1"
 
 "webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#c4a3398b89588408dc705895675bce7bd7660d36":
   version "2.2.4-dev"
@@ -9706,7 +10047,7 @@ webpack-bundle-analyzer@^3.5.1:
     opener "^1.5.1"
     ws "^6.0.0"
 
-webpack-cli@^3.2.3:
+webpack-cli@^3.3.11:
   version "3.3.12"
   resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
   integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
@@ -9724,9 +10065,9 @@ webpack-cli@^3.2.3:
     yargs "^13.3.2"
 
 webpack-dev-middleware@^3.7.0:
-  version "3.7.2"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
-  integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
+  integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==
   dependencies:
     memory-fs "^0.4.1"
     mime "^2.4.4"
@@ -9743,9 +10084,9 @@ webpack-log@^2.0.0:
     uuid "^3.3.2"
 
 webpack-require-from@^1.8.0:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/webpack-require-from/-/webpack-require-from-1.8.1.tgz#18fd6fc18f0920a097a6f43855ac335f486bfb06"
-  integrity sha512-jKpr/6CH6sW3QH4E5cf/UNK4Dpsk4E1x6IpiIjeZQ9rOhr5jqpGCyFW6e/LEFpJ3U9AzRtaUHgahR7QFQnttLQ==
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/webpack-require-from/-/webpack-require-from-1.8.2.tgz#d0c6838553315bb8802f879ab43c29309f5c80fb"
+  integrity sha512-N9kDFNGNEnmuM/riUm/yNXhefAUCUG50sVV509n0WtCdKlIjMYebVwGsEujDKRRiy539J84oOZlrZim9FJXNPA==
 
 webpack-sources@^0.2.3:
   version "0.2.3"
@@ -9881,9 +10222,9 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
 
 y18n@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
-  integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
+  integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
 
 yallist@^2.1.2:
   version "2.1.2"
@@ -9900,6 +10241,11 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
+yaml@^1.7.2:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
+  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+
 yargs-parser@^13.1.2:
   version "13.1.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2020-12-25 11:03  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2020-12-25 11:03 UTC (permalink / raw)
  To: Khushboo Vashi <[email protected]>; +Cc: pgadmin-hackers; Aditya Toshniwal <[email protected]>

Hi Khushboo,

Can you please review it?

On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi Hackers,
>
> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the details:
> 1) Create a diagram from scratch or generate for an existing DB.
> 2) Generate "Create" DDL from the diagram.
> 3) Save the diagram and resume it later.
> 4) Supports basic table fields, one-to-many relationships, many-to-many
> relationships, adding notes.
> 5) Test cases added with 75-80% test coverage.
>
> Please review.
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2020-12-28 05:23  Khushboo Vashi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Khushboo Vashi @ 2020-12-28 05:23 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgadmin-hackers; Aditya Toshniwal <[email protected]>

On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <[email protected]>
wrote:

> Hi Khushboo,
>
> Can you please review it?
>
> On it.

> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>> details:
>> 1) Create a diagram from scratch or generate for an existing DB.
>> 2) Generate "Create" DDL from the diagram.
>> 3) Save the diagram and resume it later.
>> 4) Supports basic table fields, one-to-many relationships, many-to-many
>> relationships, adding notes.
>> 5) Test cases added with 75-80% test coverage.
>>
>> Please review.
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-12 06:48  Khushboo Vashi <[email protected]>
  parent: Khushboo Vashi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Khushboo Vashi @ 2021-01-12 06:48 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgadmin-hackers; Aditya Toshniwal <[email protected]>

Hi Aditya,

The functionalities and the code looks good to me, however some of the
comments as below:


   - Correct the comments at some places (3 occurrences found  in
/erd/__init__.py)  which mention Schema diff instead of ERD.

Some comments in the JS/JSX file regarding components/functions (For
example, IconButton (forwardRef), Bodywidget etc.) would

be great help as we all are new to React.


   - Remove the unused imports (for ex bad_request) in /erd/__init__.py
   - Remove commented code

# req_args = request.args
# if ('recreate' in req_args and
#     req_args['recreate'] == '1'):
#     connect = False


   - TableNode.jsx, below two lines can be combined.

import { PortModelAlignment, DefaultNodeModel } from
'@projectstorm/react-diagrams';
import { PortWidget } from '@projectstorm/react-diagrams';


   - onImageClick function in BodyWidget.jsx is no use I think, so it
should be removed.


   - I got some console errors while adding/editing tables. Refer to
the attached screenshot.
   - In the column Edit Mode, while deleting the primary key, it gives
the error, which does not go away with any further modifications.
   - While generating the SQL, if the server is disconnected, a proper
error message should be thrown, right now some server side error is
coming.
   - Please remove ... from the menu title (New ERD Project(Beta)...)
as it is not opening a dialog.
   - For large data sets, generate ERD hangs.
   - Opening the ERD panel in a new window is not working, it opens in
the same tab even if you have set the Preference "Open in new browser
tab" to True.
   - No shortcut is provided to open the ERD Tool.
   - SonarQube fixes required.
   -

*Suggestion:*

While removal of the FK link, If any of the table is selected, it is
being deleted with FK link.
Either we should warn the user OR make 2 different buttons for FK
removal and table removal as the user may be confused if the selected
table is also removed with the FK.

*Observations:*

Lodash has been used in this module in place of Underscore, though the
dependency is already introduced by another module,
but we have mentioned it in the package.json file, which is somewhat not
convincing to me.
Lodash is more advanced than Underscore but we should pick anyone as it
will be easy to manage.


Table dialog code is duplicate of the table node, as it was difficult to
extend it because it was attached to the tree.
So, we need to keep in mind that while implementing React in pgAdmin, the
nodes should be properly detached from the tree itself, so we can reuse it.

Thanks,
Khushboo


On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
[email protected]> wrote:

>
>
> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Hi Khushboo,
>>
>> Can you please review it?
>>
>> On it.
>
>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>> details:
>>> 1) Create a diagram from scratch or generate for an existing DB.
>>> 2) Generate "Create" DDL from the diagram.
>>> 3) Save the diagram and resume it later.
>>> 4) Supports basic table fields, one-to-many relationships, many-to-many
>>> relationships, adding notes.
>>> 5) Test cases added with 75-80% test coverage.
>>>
>>> Please review.
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>


Attachments:

  [image/png] erd_column_editing.png (105.7K, 3-erd_column_editing.png)
  download | view image

  [image/png] erd_console_errors.png (461.5K, 4-erd_console_errors.png)
  download | view image

^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-15 13:30  Aditya Toshniwal <[email protected]>
  parent: Khushboo Vashi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-15 13:30 UTC (permalink / raw)
  To: Khushboo Vashi <[email protected]>; +Cc: Akshay Joshi <[email protected]>; pgadmin-hackers

Hi,

I've fixed the issues. You can find the comments inline.
I've also added PropTypes for the components for increased validation.

On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
[email protected]> wrote:

> Hi Aditya,
>
> The functionalities and the code looks good to me, however some of the comments as below:
>
>
>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>
> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>
> be great help as we all are new to React.
>
> Done. Added comments to the components.

>
>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>
> Removed.

>
>    - Remove commented code
>
> # req_args = request.args
> # if ('recreate' in req_args and
> #     req_args['recreate'] == '1'):
> #     connect = False
>
> Removed.

>
>    - TableNode.jsx, below two lines can be combined.
>
> import { PortModelAlignment, DefaultNodeModel } from
> '@projectstorm/react-diagrams';
> import { PortWidget } from '@projectstorm/react-diagrams';
>
> Done.

>
>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>
> I wanted to keep the code as it will be used in future. Anyway, I've
removed the code.

>
>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>
> I tried but I didn't get any. Looking at the screenshot, the error is from
the underlying library. Can't do much in this.

>
>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>
> Fixed.

>
>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>
> It will show connection lost error now. Fixed.

>
>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>
> Done.

>
>    - For large data sets, generate ERD hangs.
>
> It shows the spinner and waits for the response to come from the back end.
I've used the existing table fetching code which is used at other places.
I'll create an RM to improve the back end code for fetching the tables data
which will help the schema diff tool as well.

>
>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>
> Fixed. Added the setting in "Tab settings".

>
>    - No shortcut is provided to open the ERD Tool.
>
> A shortcut is helpful if we are using it frequently. ERD tool won't be
used that frequently. We already have a limited number of keys available
for shortcuts. I think we should roll out without shortcut for now. If
there is a user demand for it then we can think of adding it.

>
>    - SonarQube fixes required.
>
> Fixed.

>
>    -
>
> *Suggestion:*
>
> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>
> I've added a confirmation dialog which will show the number of tables and
links selected. This way user will know what he has selected before
deleting.

> *Observations:*
>
> Lodash has been used in this module in place of Underscore, though the
> dependency is already introduced by another module,
> but we have mentioned it in the package.json file, which is somewhat not
> convincing to me.
> Lodash is more advanced than Underscore but we should pick anyone as it
> will be easy to manage.
>
> TL;DR; we cannot.
lodash is a peer dependency for react-diagrams (and some existing modules
in pgAdmin) so it will come to package.json without choice. We cannot
remove underscore because it is a dependency of backbone. Underscore is
outdated, and I cannot migrate the complete pgAdmin code. So, I decided to
go with 100/0 method. All the new codes will use lodash only as we'll phase
out underscore with time. Just like jQuery vs ReactJS.

>
>
> Table dialog code is duplicate of the table node, as it was difficult to
> extend it because it was attached to the tree.
> So, we need to keep in mind that while implementing React in pgAdmin, the
> nodes should be properly detached from the tree itself, so we can reuse it.
>
> Yes. I agree. We need to separate out data source from UI going forward with
React.

>
> Thanks,
> Khushboo
>
>
> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
> [email protected]> wrote:
>
>>
>>
>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Hi Khushboo,
>>>
>>> Can you please review it?
>>>
>>> On it.
>>
>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>> details:
>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>> 2) Generate "Create" DDL from the diagram.
>>>> 3) Save the diagram and resume it later.
>>>> 4) Supports basic table fields, one-to-many relationships, many-to-many
>>>> relationships, adding notes.
>>>> 5) Test cases added with 75-80% test coverage.
>>>>
>>>> Please review.
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Principal Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>

-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802_v2.patch (504.6K, 3-RM1802_v2.patch)
  download | inline diff:
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
index 1fdd07fbf..9b75cd809 100644
--- a/web/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -18,6 +18,7 @@ module.exports = {
     'eslint:recommended',
     'plugin:react/recommended',
   ],
+  'parser': 'babel-eslint',
   'parserOptions': {
     'ecmaVersion': 2018,
     'ecmaFeatures': {
diff --git a/web/package.json b/web/package.json
index b77776569..c1cc5c492 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 1bae28f9c..0b9af2ca1 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -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()
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 640a05468..3b335be12 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -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.'),
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
index 450d8c5ab..d6d4eae97 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
@@ -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)
-
-            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
+        status, res = BaseTableView.fetch_tables(self, sid, did, scid, tid)
+        if not status:
+            current_app.logger.error(res)
+            return False
 
-    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):
         """
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
index cae6c157c..3e99e5698 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py
@@ -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)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
index 1a72d8ab3..d700ccff2 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/__init__.py
@@ -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
 
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
index 22fd79a9c..c8dd96f82 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/foreign_key/sql/default/create.sql
@@ -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 %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
index 66bea8128..1e501b1cb 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/utils.py
@@ -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
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
index 297f05194..db455cd64 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py
@@ -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
diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
index 6a97dd5ba..b937493f7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
@@ -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') {
diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js
index 79fccaa03..8d35bfd25 100644
--- a/web/pgadmin/browser/templates/browser/js/utils.js
+++ b/web/pgadmin/browser/templates/browser/js/utils.js
@@ -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}}',
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 19549a137..14140a2fd 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -10,6 +10,7 @@
 define('bundled_browser',[
   'pgadmin.browser',
   'sources/browser/index',
+  'top/tools/erd/static/js/index',
 ], function(pgBrowser) {
   pgBrowser.init();
 });
diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js
index 24295c747..f7d980bdf 100644
--- a/web/pgadmin/static/js/backform.pgadmin.js
+++ b/web/pgadmin/static/js/backform.pgadmin.js
@@ -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),
diff --git a/web/pgadmin/static/js/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid.pgadmin.js
index f94e7828c..675725e07 100644
--- a/web/pgadmin/static/js/backgrid.pgadmin.js
+++ b/web/pgadmin/static/js/backgrid.pgadmin.js
@@ -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();
       }
diff --git a/web/pgadmin/static/js/custom_prop_types.js b/web/pgadmin/static/js/custom_prop_types.js
new file mode 100644
index 000000000..4e4f45b9b
--- /dev/null
+++ b/web/pgadmin/static/js/custom_prop_types.js
@@ -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;
diff --git a/web/pgadmin/static/scss/_bootstrap.overrides.scss b/web/pgadmin/static/scss/_bootstrap.overrides.scss
index 29c4a433c..0d4d68f24 100644
--- a/web/pgadmin/static/scss/_bootstrap.overrides.scss
+++ b/web/pgadmin/static/scss/_bootstrap.overrides.scss
@@ -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;
@@ -370,6 +379,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%;
 }
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index fadd71b7b..f8f2bcf99 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -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 {
diff --git a/web/pgadmin/static/scss/_tippy.overrides.scss b/web/pgadmin/static/scss/_tippy.overrides.scss
new file mode 100644
index 000000000..9750434b7
--- /dev/null
+++ b/web/pgadmin/static/scss/_tippy.overrides.scss
@@ -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;
+  }
+}
diff --git a/web/pgadmin/static/scss/pgadmin.scss b/web/pgadmin/static/scss/pgadmin.scss
index 19e79adf2..cc751b77e 100644
--- a/web/pgadmin/static/scss/pgadmin.scss
+++ b/web/pgadmin/static/scss/pgadmin.scss
@@ -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';
diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss
index 9d913788d..b71c86a4c 100644
--- a/web/pgadmin/static/scss/resources/_default.variables.scss
+++ b/web/pgadmin/static/scss/resources/_default.variables.scss
@@ -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");
diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
index 129b9a214..d8ca65b50 100644
--- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
@@ -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;
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
index 598ed52b4..46db6a70a 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
@@ -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);
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
index 90131dd40..867b739d8 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -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);
 
diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py
new file mode 100644
index 000000000..380dc12ec
--- /dev/null
+++ b/web/pgadmin/tools/erd/__init__.py
@@ -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')
+
+
[email protected](
+    '/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),
+    )
+
+
[email protected](
+    '/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
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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
+    )
+
+
[email protected]('/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})
diff --git a/web/pgadmin/tools/erd/static/js/erd_module.js b/web/pgadmin/tools/erd/static/js/erd_module.js
new file mode 100644
index 000000000..6aa22a869
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_module.js
@@ -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;
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
new file mode 100644
index 000000000..5cbec4974
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
new file mode 100644
index 000000000..ae73db8ac
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDModel.js
@@ -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]));
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js
new file mode 100644
index 000000000..9b6e54fa9
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/DialogWrapper.js
@@ -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';
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js
new file mode 100644
index 000000000..abec79f5e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/ManyToManyDialog.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js
new file mode 100644
index 000000000..0f4a9ce3e
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToManyDialog.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
new file mode 100644
index 000000000..1b6bb99b7
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
@@ -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);
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
new file mode 100644
index 000000000..eb0ab1336
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.js
@@ -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;
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
new file mode 100644
index 000000000..1bd4c3bc6
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
@@ -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
+    );
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
new file mode 100644
index 000000000..f7bed2f63
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
@@ -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>
+    );
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
new file mode 100644
index 000000000..2016af598
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
@@ -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>&nbsp;
+            {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} />;
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
new file mode 100644
index 000000000..6f6e2c0f2
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
@@ -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||{});
+  }
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
new file mode 100644
index 000000000..af9271d21
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
@@ -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
+};
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
new file mode 100644
index 000000000..22df72e3c
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
@@ -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,
+};
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
new file mode 100644
index 000000000..a66cc4d0b
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
@@ -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>&nbsp;{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,
+};
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx
new file mode 100644
index 000000000..1934db2fe
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/Loader.jsx
@@ -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,
+};
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
new file mode 100644
index 000000000..6e025d3a3
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
@@ -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,
+}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool_hook.js b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js
new file mode 100644
index 000000000..fce76270c
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/erd_tool_hook.js
@@ -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;
+});
+
+
diff --git a/web/pgadmin/tools/erd/static/js/index.js b/web/pgadmin/tools/erd/static/js/index.js
new file mode 100644
index 000000000..b968a5223
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/js/index.js
@@ -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,
+};
diff --git a/web/pgadmin/tools/erd/static/scss/_erd.scss b/web/pgadmin/tools/erd/static/scss/_erd.scss
new file mode 100644
index 000000000..733dd53c6
--- /dev/null
+++ b/web/pgadmin/tools/erd/static/scss/_erd.scss
@@ -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;
+    }
+  }
+}
diff --git a/web/pgadmin/tools/erd/templates/erd/index.html b/web/pgadmin/tools/erd/templates/erd/index.html
new file mode 100644
index 000000000..10a0896f5
--- /dev/null
+++ b/web/pgadmin/tools/erd/templates/erd/index.html
@@ -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 %}
diff --git a/web/pgadmin/tools/erd/tests/__init__.py b/web/pgadmin/tools/erd/tests/__init__.py
new file mode 100644
index 000000000..8c8c486cc
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/__init__.py
@@ -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
diff --git a/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql b/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
new file mode 100644
index 000000000..e3d6ab762
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/sql/12_plus/test_sql_output.sql
@@ -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;
diff --git a/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql b/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
new file mode 100644
index 000000000..330e81443
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/sql/default/test_sql_output.sql
@@ -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;
diff --git a/web/pgadmin/tools/erd/tests/test_close.py b/web/pgadmin/tools/erd/tests/test_close.py
new file mode 100644
index 000000000..008ddab39
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_close.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_initialize.py b/web/pgadmin/tools/erd/tests/test_initialize.py
new file mode 100644
index 000000000..d586bb3b6
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_initialize.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_panel.py b/web/pgadmin/tools/erd/tests/test_panel.py
new file mode 100644
index 000000000..ad44d004a
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_panel.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_prequisite.py b/web/pgadmin/tools/erd/tests/test_prequisite.py
new file mode 100644
index 000000000..fed6a0652
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_prequisite.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_sql.py b/web/pgadmin/tools/erd/tests/test_sql.py
new file mode 100644
index 000000000..172795c61
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_sql.py
@@ -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)
diff --git a/web/pgadmin/tools/erd/tests/test_sql_input_data.json b/web/pgadmin/tools/erd/tests/test_sql_input_data.json
new file mode 100644
index 000000000..3c8182e68
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_sql_input_data.json
@@ -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"
+        }
+      ]
+    }
+  }
+}
diff --git a/web/pgadmin/tools/erd/tests/test_tables.py b/web/pgadmin/tools/erd/tests/test_tables.py
new file mode 100644
index 000000000..b8de11646
--- /dev/null
+++ b/web/pgadmin/tools/erd/tests/test_tables.py
@@ -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()
diff --git a/web/pgadmin/tools/erd/utils.py b/web/pgadmin/tools/erd/utils.py
new file mode 100644
index 000000000..1999a71e5
--- /dev/null
+++ b/web/pgadmin/tools/erd/utils.py
@@ -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
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 612612c30..48364f6c5 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -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);
         }
       },
diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py
index 12a94a541..23abfffaf 100644
--- a/web/pgadmin/utils/csrf.py
+++ b/web/pgadmin/utils/csrf.py
@@ -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:
diff --git a/web/regression/javascript/erd/erd_core_spec.js b/web/regression/javascript/erd/erd_core_spec.js
new file mode 100644
index 000000000..c488ffdf2
--- /dev/null
+++ b/web/regression/javascript/erd/erd_core_spec.js
@@ -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'});
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/erd_model_spec.js b/web/regression/javascript/erd/erd_model_spec.js
new file mode 100644
index 000000000..cfdcc0983
--- /dev/null
+++ b/web/regression/javascript/erd/erd_model_spec.js
@@ -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'},
+    }));
+  });
+});
diff --git a/web/regression/javascript/erd/keyboard_shortcut_action_spec.js b/web/regression/javascript/erd/keyboard_shortcut_action_spec.js
new file mode 100644
index 000000000..3134795ea
--- /dev/null
+++ b/web/regression/javascript/erd/keyboard_shortcut_action_spec.js
@@ -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();
+  });
+});
diff --git a/web/regression/javascript/erd/onetomany_link_spec.js b/web/regression/javascript/erd/onetomany_link_spec.js
new file mode 100644
index 000000000..a7590db64
--- /dev/null
+++ b/web/regression/javascript/erd/onetomany_link_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/erd/onetomany_port_spec.js b/web/regression/javascript/erd/onetomany_port_spec.js
new file mode 100644
index 000000000..0ad4831ef
--- /dev/null
+++ b/web/regression/javascript/erd/onetomany_port_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js
new file mode 100644
index 000000000..d8edcdb44
--- /dev/null
+++ b/web/regression/javascript/erd/table_node_spec.js
@@ -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();
+      });
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/test_tables.js b/web/regression/javascript/erd/test_tables.js
new file mode 100644
index 000000000..e2dc43ef8
--- /dev/null
+++ b/web/regression/javascript/erd/test_tables.js
@@ -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': {},
+  },
+];
diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js
new file mode 100644
index 000000000..16928980f
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js
@@ -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,
+    });
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/connection_bar_spec.js b/web/regression/javascript/erd/ui_components/connection_bar_spec.js
new file mode 100644
index 000000000..17398a54e
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/connection_bar_spec.js
@@ -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');
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/erd_preferences.js b/web/regression/javascript/erd/ui_components/erd_preferences.js
new file mode 100644
index 000000000..d91b158ce
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/erd_preferences.js
@@ -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': '-',
+    },
+  },
+};
diff --git a/web/regression/javascript/erd/ui_components/floating_note_spec.js b/web/regression/javascript/erd/ui_components/floating_note_spec.js
new file mode 100644
index 000000000..f786267bf
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/floating_note_spec.js
@@ -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();
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/loader_spec.js b/web/regression/javascript/erd/ui_components/loader_spec.js
new file mode 100644
index 000000000..b14ed30aa
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/loader_spec.js
@@ -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...');
+  });
+});
diff --git a/web/regression/javascript/erd/ui_components/toolbar_spec.js b/web/regression/javascript/erd/ui_components/toolbar_spec.js
new file mode 100644
index 000000000..0429d718d
--- /dev/null
+++ b/web/regression/javascript/erd/ui_components/toolbar_spec.js
@@ -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);
+  });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index d37b7ddb6..77894ac34 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -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>',
   };
 });
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 96ef708ab..c225b31b8 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -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'),
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 6fc26021e..7d25a2d56 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -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',
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index 37557e0cc..7cc1923e9 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -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/'),
     },
diff --git a/web/yarn.lock b/web/yarn.lock
index c9450d755..ada962e94 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -2,70 +2,54 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
-  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+  integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
   dependencies:
     "@babel/highlight" "^7.10.4"
 
-"@babel/core@^7.7.5":
-  version "7.11.6"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
-  integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
+"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
+  integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==
+
+"@babel/core@^7.10.2", "@babel/core@^7.7.5":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd"
+  integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==
   dependencies:
     "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.11.6"
-    "@babel/helper-module-transforms" "^7.11.0"
-    "@babel/helpers" "^7.10.4"
-    "@babel/parser" "^7.11.5"
-    "@babel/template" "^7.10.4"
-    "@babel/traverse" "^7.11.5"
-    "@babel/types" "^7.11.5"
+    "@babel/generator" "^7.12.10"
+    "@babel/helper-module-transforms" "^7.12.1"
+    "@babel/helpers" "^7.12.5"
+    "@babel/parser" "^7.12.10"
+    "@babel/template" "^7.12.7"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.10"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.1"
     json5 "^2.1.2"
     lodash "^4.17.19"
-    resolve "^1.3.2"
-    semver "^5.4.1"
-    source-map "^0.5.0"
-
-"@babel/core@~7.6.0":
-  version "7.6.4"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff"
-  integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ==
-  dependencies:
-    "@babel/code-frame" "^7.5.5"
-    "@babel/generator" "^7.6.4"
-    "@babel/helpers" "^7.6.2"
-    "@babel/parser" "^7.6.4"
-    "@babel/template" "^7.6.0"
-    "@babel/traverse" "^7.6.3"
-    "@babel/types" "^7.6.3"
-    convert-source-map "^1.1.0"
-    debug "^4.1.0"
-    json5 "^2.1.0"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.6.4":
-  version "7.11.6"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
-  integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
+"@babel/generator@^7.12.10", "@babel/generator@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
+  integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
   dependencies:
-    "@babel/types" "^7.11.5"
+    "@babel/types" "^7.12.11"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
-"@babel/helper-annotate-as-pure@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
-  integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==
+"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
+  integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4":
   version "7.10.4"
@@ -75,43 +59,34 @@
     "@babel/helper-explode-assignable-expression" "^7.10.4"
     "@babel/types" "^7.10.4"
 
-"@babel/helper-builder-react-jsx-experimental@^7.10.4", "@babel/helper-builder-react-jsx-experimental@^7.11.5":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.11.5.tgz#4ea43dd63857b0a35cd1f1b161dc29b43414e79f"
-  integrity sha512-Vc4aPJnRZKWfzeCBsqTBnzulVNjABVdahSPhtdMD3Vs80ykx4a87jTHtF/VR+alSrDmNvat7l13yrRHauGcHVw==
+"@babel/helper-compilation-targets@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831"
+  integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-module-imports" "^7.10.4"
-    "@babel/types" "^7.11.5"
-
-"@babel/helper-builder-react-jsx@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d"
-  integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/compat-data" "^7.12.5"
+    "@babel/helper-validator-option" "^7.12.1"
+    browserslist "^4.14.5"
+    semver "^5.5.0"
 
-"@babel/helper-create-class-features-plugin@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d"
-  integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==
+"@babel/helper-create-class-features-plugin@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e"
+  integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
-    "@babel/helper-member-expression-to-functions" "^7.10.5"
+    "@babel/helper-member-expression-to-functions" "^7.12.1"
     "@babel/helper-optimise-call-expression" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.10.4"
 
-"@babel/helper-create-regexp-features-plugin@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8"
-  integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==
+"@babel/helper-create-regexp-features-plugin@^7.12.1":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f"
+  integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
-    "@babel/helper-regex" "^7.10.4"
-    regexpu-core "^4.7.0"
+    regexpu-core "^4.7.1"
 
 "@babel/helper-define-map@^7.10.4":
   version "7.10.5"
@@ -123,27 +98,27 @@
     lodash "^4.17.19"
 
 "@babel/helper-explode-assignable-expression@^7.10.4":
-  version "7.11.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b"
-  integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz#8006a466695c4ad86a2a5f2fb15b5f2c31ad5633"
+  integrity sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-function-name@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
-  integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
+  integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.10.4"
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/helper-get-function-arity" "^7.12.10"
+    "@babel/template" "^7.12.7"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-get-function-arity@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
-  integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+"@babel/helper-get-function-arity@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
+  integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
 "@babel/helper-hoist-variables@^7.10.4":
   version "7.10.4"
@@ -152,117 +127,115 @@
   dependencies:
     "@babel/types" "^7.10.4"
 
-"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
-  integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==
+"@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
+  integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.7"
 
-"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
-  integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
+  integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.5"
 
-"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
-  integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==
+"@babel/helper-module-transforms@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c"
+  integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==
   dependencies:
-    "@babel/helper-module-imports" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
-    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-module-imports" "^7.12.1"
+    "@babel/helper-replace-supers" "^7.12.1"
+    "@babel/helper-simple-access" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.11.0"
+    "@babel/helper-validator-identifier" "^7.10.4"
     "@babel/template" "^7.10.4"
-    "@babel/types" "^7.11.0"
+    "@babel/traverse" "^7.12.1"
+    "@babel/types" "^7.12.1"
     lodash "^4.17.19"
 
-"@babel/helper-optimise-call-expression@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
-  integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+"@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d"
+  integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==
   dependencies:
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.10"
 
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0":
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
   integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
 
-"@babel/helper-regex@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
-  integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==
-  dependencies:
-    lodash "^4.17.19"
-
-"@babel/helper-remap-async-to-generator@^7.10.4":
-  version "7.11.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d"
-  integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==
+"@babel/helper-remap-async-to-generator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd"
+  integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-wrap-function" "^7.10.4"
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-replace-supers@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
-  integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
+"@babel/helper-replace-supers@^7.12.1":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d"
+  integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.10.4"
-    "@babel/helper-optimise-call-expression" "^7.10.4"
-    "@babel/traverse" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/helper-member-expression-to-functions" "^7.12.7"
+    "@babel/helper-optimise-call-expression" "^7.12.10"
+    "@babel/traverse" "^7.12.10"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-simple-access@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
-  integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
+"@babel/helper-simple-access@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136"
+  integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==
   dependencies:
-    "@babel/template" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-skip-transparent-expression-wrappers@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729"
-  integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==
+"@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
+  integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.1"
 
-"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
-  integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
+"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
+  integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
   dependencies:
-    "@babel/types" "^7.11.0"
+    "@babel/types" "^7.12.11"
 
-"@babel/helper-validator-identifier@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
-  integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
+
+"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
+  integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==
 
 "@babel/helper-wrap-function@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87"
-  integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==
+  version "7.12.3"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz#3332339fc4d1fbbf1c27d7958c27d34708e990d9"
+  integrity sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
     "@babel/template" "^7.10.4"
     "@babel/traverse" "^7.10.4"
     "@babel/types" "^7.10.4"
 
-"@babel/helpers@^7.10.4", "@babel/helpers@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
-  integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
+"@babel/helpers@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e"
+  integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==
   dependencies:
     "@babel/template" "^7.10.4"
-    "@babel/traverse" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    "@babel/traverse" "^7.12.5"
+    "@babel/types" "^7.12.5"
 
 "@babel/highlight@^7.10.4":
   version "7.10.4"
@@ -273,514 +246,735 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.6.4":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
-  integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
+  integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
 
-"@babel/plugin-proposal-async-generator-functions@^7.2.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
-  integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==
+"@babel/plugin-proposal-async-generator-functions@^7.12.1":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
+  integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-remap-async-to-generator" "^7.10.4"
+    "@babel/helper-remap-async-to-generator" "^7.12.1"
     "@babel/plugin-syntax-async-generators" "^7.8.0"
 
-"@babel/plugin-proposal-class-properties@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807"
-  integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==
+"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de"
+  integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.10.4"
+    "@babel/helper-create-class-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-proposal-dynamic-import@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e"
-  integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==
+"@babel/plugin-proposal-dynamic-import@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc"
+  integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-dynamic-import" "^7.8.0"
 
-"@babel/plugin-proposal-json-strings@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db"
-  integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==
+"@babel/plugin-proposal-export-namespace-from@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4"
+  integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+
+"@babel/plugin-proposal-json-strings@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c"
+  integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-json-strings" "^7.8.0"
 
-"@babel/plugin-proposal-object-rest-spread@^7.6.2":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af"
-  integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==
+"@babel/plugin-proposal-logical-assignment-operators@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751"
+  integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c"
+  integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b"
+  integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+
+"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.9.6":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
+  integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
-    "@babel/plugin-transform-parameters" "^7.10.4"
+    "@babel/plugin-transform-parameters" "^7.12.1"
 
-"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd"
-  integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==
+"@babel/plugin-proposal-optional-catch-binding@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942"
+  integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
     "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
 
-"@babel/plugin-proposal-unicode-property-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d"
-  integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==
+"@babel/plugin-proposal-optional-chaining@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c"
+  integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-private-methods@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389"
+  integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-class-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-proposal-unicode-property-regex@^7.12.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072"
+  integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0":
+"@babel/plugin-syntax-class-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978"
+  integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
   integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0":
+"@babel/plugin-syntax-export-namespace-from@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
+  integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
   integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-jsx@^7.10.4":
+"@babel/plugin-syntax-jsx@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
+  integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+  integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.10.4":
   version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
-  integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
+  integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
   integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-transform-arrow-functions@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd"
-  integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0"
+  integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-async-to-generator@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37"
-  integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==
+"@babel/plugin-transform-arrow-functions@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3"
+  integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==
   dependencies:
-    "@babel/helper-module-imports" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-remap-async-to-generator" "^7.10.4"
 
-"@babel/plugin-transform-block-scoped-functions@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8"
-  integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==
+"@babel/plugin-transform-async-to-generator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1"
+  integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==
   dependencies:
+    "@babel/helper-module-imports" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-remap-async-to-generator" "^7.12.1"
 
-"@babel/plugin-transform-block-scoping@^7.6.3":
-  version "7.11.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215"
-  integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==
+"@babel/plugin-transform-block-scoped-functions@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9"
+  integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-classes@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7"
-  integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==
+"@babel/plugin-transform-block-scoping@^7.12.11":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca"
+  integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-transform-classes@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6"
+  integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-define-map" "^7.10.4"
     "@babel/helper-function-name" "^7.10.4"
     "@babel/helper-optimise-call-expression" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
     "@babel/helper-split-export-declaration" "^7.10.4"
     globals "^11.1.0"
 
-"@babel/plugin-transform-computed-properties@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb"
-  integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==
+"@babel/plugin-transform-computed-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852"
+  integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-destructuring@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5"
-  integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==
+"@babel/plugin-transform-destructuring@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847"
+  integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-dotall-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee"
-  integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==
+"@babel/plugin-transform-dotall-regex@^7.12.1", "@babel/plugin-transform-dotall-regex@^7.4.4":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975"
+  integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-duplicate-keys@^7.5.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47"
-  integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==
+"@babel/plugin-transform-duplicate-keys@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228"
+  integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-exponentiation-operator@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e"
-  integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==
+"@babel/plugin-transform-exponentiation-operator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0"
+  integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==
   dependencies:
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-for-of@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9"
-  integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==
+"@babel/plugin-transform-for-of@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa"
+  integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-function-name@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7"
-  integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==
+"@babel/plugin-transform-function-name@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667"
+  integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==
   dependencies:
     "@babel/helper-function-name" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c"
-  integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==
+"@babel/plugin-transform-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57"
+  integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-member-expression-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7"
-  integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==
+"@babel/plugin-transform-member-expression-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad"
+  integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-modules-amd@^7.5.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1"
-  integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==
+"@babel/plugin-transform-modules-amd@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9"
+  integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-commonjs@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0"
-  integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==
+"@babel/plugin-transform-modules-commonjs@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648"
+  integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.4"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-simple-access" "^7.12.1"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-systemjs@^7.5.0":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85"
-  integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==
+"@babel/plugin-transform-modules-systemjs@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086"
+  integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==
   dependencies:
     "@babel/helper-hoist-variables" "^7.10.4"
-    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-validator-identifier" "^7.10.4"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-umd@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e"
-  integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==
+"@babel/plugin-transform-modules-umd@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902"
+  integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==
   dependencies:
-    "@babel/helper-module-transforms" "^7.10.4"
+    "@babel/helper-module-transforms" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-named-capturing-groups-regex@^7.6.3":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6"
-  integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==
+"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753"
+  integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
 
-"@babel/plugin-transform-new-target@^7.4.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888"
-  integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==
+"@babel/plugin-transform-new-target@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0"
+  integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-object-super@^7.5.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894"
-  integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==
+"@babel/plugin-transform-object-super@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e"
+  integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
 
-"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.4.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a"
-  integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
+"@babel/plugin-transform-parameters@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d"
+  integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-property-literals@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0"
-  integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==
+"@babel/plugin-transform-property-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd"
+  integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-react-display-name@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz#b5795f4e3e3140419c3611b7a2a3832b9aef328d"
-  integrity sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw==
+"@babel/plugin-transform-react-display-name@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d"
+  integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-react-jsx-development@^7.10.4":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.11.5.tgz#e1439e6a57ee3d43e9f54ace363fb29cefe5d7b6"
-  integrity sha512-cImAmIlKJ84sDmpQzm4/0q/2xrXlDezQoixy3qoz1NJeZL/8PRon6xZtluvr4H4FzwlDGI5tCcFupMnXGtr+qw==
+"@babel/plugin-transform-react-jsx-development@^7.12.7":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7"
+  integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==
   dependencies:
-    "@babel/helper-builder-react-jsx-experimental" "^7.11.5"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    "@babel/plugin-transform-react-jsx" "^7.12.12"
 
-"@babel/plugin-transform-react-jsx-self@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369"
-  integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==
+"@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e"
+  integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==
   dependencies:
+    "@babel/helper-annotate-as-pure" "^7.12.10"
+    "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    "@babel/plugin-syntax-jsx" "^7.12.1"
+    "@babel/types" "^7.12.12"
 
-"@babel/plugin-transform-react-jsx-source@^7.10.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4"
-  integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA==
+"@babel/plugin-transform-react-pure-annotations@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42"
+  integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==
   dependencies:
+    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
 
-"@babel/plugin-transform-react-jsx@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2"
-  integrity sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==
+"@babel/plugin-transform-regenerator@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753"
+  integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==
   dependencies:
-    "@babel/helper-builder-react-jsx" "^7.10.4"
-    "@babel/helper-builder-react-jsx-experimental" "^7.10.4"
-    "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-syntax-jsx" "^7.10.4"
+    regenerator-transform "^0.14.2"
 
-"@babel/plugin-transform-react-pure-annotations@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501"
-  integrity sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A==
+"@babel/plugin-transform-reserved-words@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8"
+  integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-regenerator@^7.4.5":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63"
-  integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==
+"@babel/plugin-transform-shorthand-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3"
+  integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==
   dependencies:
-    regenerator-transform "^0.14.2"
+    "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-reserved-words@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd"
-  integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==
+"@babel/plugin-transform-spread@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e"
+  integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
 
-"@babel/plugin-transform-shorthand-properties@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6"
-  integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==
+"@babel/plugin-transform-sticky-regex@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad"
+  integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-spread@^7.6.2":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc"
-  integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==
+"@babel/plugin-transform-template-literals@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843"
+  integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0"
 
-"@babel/plugin-transform-sticky-regex@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d"
-  integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==
+"@babel/plugin-transform-typeof-symbol@^7.12.10":
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b"
+  integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/helper-regex" "^7.10.4"
 
-"@babel/plugin-transform-template-literals@^7.4.4":
-  version "7.10.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c"
-  integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==
+"@babel/plugin-transform-unicode-escapes@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709"
+  integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-typeof-symbol@^7.2.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc"
-  integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==
+"@babel/plugin-transform-unicode-regex@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb"
+  integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==
   dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.12.1"
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-transform-unicode-regex@^7.6.2":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8"
-  integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==
+"@babel/preset-env@^7.10.2":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9"
+  integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.10.4"
+    "@babel/compat-data" "^7.12.7"
+    "@babel/helper-compilation-targets" "^7.12.5"
+    "@babel/helper-module-imports" "^7.12.5"
     "@babel/helper-plugin-utils" "^7.10.4"
+    "@babel/helper-validator-option" "^7.12.11"
+    "@babel/plugin-proposal-async-generator-functions" "^7.12.1"
+    "@babel/plugin-proposal-class-properties" "^7.12.1"
+    "@babel/plugin-proposal-dynamic-import" "^7.12.1"
+    "@babel/plugin-proposal-export-namespace-from" "^7.12.1"
+    "@babel/plugin-proposal-json-strings" "^7.12.1"
+    "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
+    "@babel/plugin-proposal-numeric-separator" "^7.12.7"
+    "@babel/plugin-proposal-object-rest-spread" "^7.12.1"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.12.1"
+    "@babel/plugin-proposal-optional-chaining" "^7.12.7"
+    "@babel/plugin-proposal-private-methods" "^7.12.1"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.12.1"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-class-properties" "^7.12.1"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.12.1"
+    "@babel/plugin-transform-arrow-functions" "^7.12.1"
+    "@babel/plugin-transform-async-to-generator" "^7.12.1"
+    "@babel/plugin-transform-block-scoped-functions" "^7.12.1"
+    "@babel/plugin-transform-block-scoping" "^7.12.11"
+    "@babel/plugin-transform-classes" "^7.12.1"
+    "@babel/plugin-transform-computed-properties" "^7.12.1"
+    "@babel/plugin-transform-destructuring" "^7.12.1"
+    "@babel/plugin-transform-dotall-regex" "^7.12.1"
+    "@babel/plugin-transform-duplicate-keys" "^7.12.1"
+    "@babel/plugin-transform-exponentiation-operator" "^7.12.1"
+    "@babel/plugin-transform-for-of" "^7.12.1"
+    "@babel/plugin-transform-function-name" "^7.12.1"
+    "@babel/plugin-transform-literals" "^7.12.1"
+    "@babel/plugin-transform-member-expression-literals" "^7.12.1"
+    "@babel/plugin-transform-modules-amd" "^7.12.1"
+    "@babel/plugin-transform-modules-commonjs" "^7.12.1"
+    "@babel/plugin-transform-modules-systemjs" "^7.12.1"
+    "@babel/plugin-transform-modules-umd" "^7.12.1"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1"
+    "@babel/plugin-transform-new-target" "^7.12.1"
+    "@babel/plugin-transform-object-super" "^7.12.1"
+    "@babel/plugin-transform-parameters" "^7.12.1"
+    "@babel/plugin-transform-property-literals" "^7.12.1"
+    "@babel/plugin-transform-regenerator" "^7.12.1"
+    "@babel/plugin-transform-reserved-words" "^7.12.1"
+    "@babel/plugin-transform-shorthand-properties" "^7.12.1"
+    "@babel/plugin-transform-spread" "^7.12.1"
+    "@babel/plugin-transform-sticky-regex" "^7.12.7"
+    "@babel/plugin-transform-template-literals" "^7.12.1"
+    "@babel/plugin-transform-typeof-symbol" "^7.12.10"
+    "@babel/plugin-transform-unicode-escapes" "^7.12.1"
+    "@babel/plugin-transform-unicode-regex" "^7.12.1"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.12.11"
+    core-js-compat "^3.8.0"
+    semver "^5.5.0"
 
-"@babel/preset-env@~7.6.0":
-  version "7.6.3"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.3.tgz#9e1bf05a2e2d687036d24c40e4639dc46cef2271"
-  integrity sha512-CWQkn7EVnwzlOdR5NOm2+pfgSNEZmvGjOhlCHBDq0J8/EStr+G+FvPEiz9B56dR6MoiUFjXhfE4hjLoAKKJtIQ==
+"@babel/preset-modules@^0.1.3":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e"
+  integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==
   dependencies:
-    "@babel/helper-module-imports" "^7.0.0"
     "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
-    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
-    "@babel/plugin-proposal-json-strings" "^7.2.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
-    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
-    "@babel/plugin-syntax-async-generators" "^7.2.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
-    "@babel/plugin-syntax-json-strings" "^7.2.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-transform-arrow-functions" "^7.2.0"
-    "@babel/plugin-transform-async-to-generator" "^7.5.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
-    "@babel/plugin-transform-block-scoping" "^7.6.3"
-    "@babel/plugin-transform-classes" "^7.5.5"
-    "@babel/plugin-transform-computed-properties" "^7.2.0"
-    "@babel/plugin-transform-destructuring" "^7.6.0"
-    "@babel/plugin-transform-dotall-regex" "^7.6.2"
-    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
-    "@babel/plugin-transform-for-of" "^7.4.4"
-    "@babel/plugin-transform-function-name" "^7.4.4"
-    "@babel/plugin-transform-literals" "^7.2.0"
-    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
-    "@babel/plugin-transform-modules-amd" "^7.5.0"
-    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
-    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
-    "@babel/plugin-transform-modules-umd" "^7.2.0"
-    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.3"
-    "@babel/plugin-transform-new-target" "^7.4.4"
-    "@babel/plugin-transform-object-super" "^7.5.5"
-    "@babel/plugin-transform-parameters" "^7.4.4"
-    "@babel/plugin-transform-property-literals" "^7.2.0"
-    "@babel/plugin-transform-regenerator" "^7.4.5"
-    "@babel/plugin-transform-reserved-words" "^7.2.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
-    "@babel/plugin-transform-spread" "^7.6.2"
-    "@babel/plugin-transform-sticky-regex" "^7.2.0"
-    "@babel/plugin-transform-template-literals" "^7.4.4"
-    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
-    "@babel/plugin-transform-unicode-regex" "^7.6.2"
-    "@babel/types" "^7.6.3"
-    browserslist "^4.6.0"
-    core-js-compat "^3.1.1"
-    invariant "^2.2.2"
-    js-levenshtein "^1.1.3"
-    semver "^5.5.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
 
 "@babel/preset-react@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf"
-  integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw==
+  version "7.12.10"
+  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9"
+  integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
-    "@babel/plugin-transform-react-display-name" "^7.10.4"
-    "@babel/plugin-transform-react-jsx" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-development" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-self" "^7.10.4"
-    "@babel/plugin-transform-react-jsx-source" "^7.10.4"
-    "@babel/plugin-transform-react-pure-annotations" "^7.10.4"
-
-"@babel/runtime@^7.8.4":
-  version "7.11.2"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
-  integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+    "@babel/plugin-transform-react-display-name" "^7.12.1"
+    "@babel/plugin-transform-react-jsx" "^7.12.10"
+    "@babel/plugin-transform-react-jsx-development" "^7.12.7"
+    "@babel/plugin-transform-react-pure-annotations" "^7.12.1"
+
+"@babel/runtime-corejs3@^7.9.6":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4"
+  integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==
   dependencies:
+    core-js-pure "^3.0.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/template@^7.10.4", "@babel/template@^7.6.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
-  integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
+"@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+  integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
   dependencies:
-    "@babel/code-frame" "^7.10.4"
-    "@babel/parser" "^7.10.4"
-    "@babel/types" "^7.10.4"
+    regenerator-runtime "^0.13.4"
 
-"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.6.3":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
-  integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
+"@babel/template@^7.10.4", "@babel/template@^7.12.7":
+  version "7.12.7"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
+  integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
   dependencies:
     "@babel/code-frame" "^7.10.4"
-    "@babel/generator" "^7.11.5"
-    "@babel/helper-function-name" "^7.10.4"
-    "@babel/helper-split-export-declaration" "^7.11.0"
-    "@babel/parser" "^7.11.5"
-    "@babel/types" "^7.11.5"
+    "@babel/parser" "^7.12.7"
+    "@babel/types" "^7.12.7"
+
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
+  integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
+  dependencies:
+    "@babel/code-frame" "^7.12.11"
+    "@babel/generator" "^7.12.11"
+    "@babel/helper-function-name" "^7.12.11"
+    "@babel/helper-split-export-declaration" "^7.12.11"
+    "@babel/parser" "^7.12.11"
+    "@babel/types" "^7.12.12"
     debug "^4.1.0"
     globals "^11.1.0"
     lodash "^4.17.19"
 
-"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.6.3":
-  version "7.11.5"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
-  integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
+"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
+  version "7.12.12"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
+  integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.10.4"
+    "@babel/helper-validator-identifier" "^7.12.11"
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
+"@emotion/cache@^10.0.27":
+  version "10.0.29"
+  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
+  integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+  dependencies:
+    "@emotion/sheet" "0.9.4"
+    "@emotion/stylis" "0.8.5"
+    "@emotion/utils" "0.11.3"
+    "@emotion/weak-memoize" "0.2.5"
+
+"@emotion/core@^10.0.14":
+  version "10.1.1"
+  resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3"
+  integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/cache" "^10.0.27"
+    "@emotion/css" "^10.0.27"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/sheet" "0.9.4"
+    "@emotion/utils" "0.11.3"
+
+"@emotion/css@^10.0.27":
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c"
+  integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
+  dependencies:
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/utils" "0.11.3"
+    babel-plugin-emotion "^10.0.27"
+
+"@emotion/[email protected]":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+  integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/[email protected]":
+  version "0.8.8"
+  resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
+  integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
+  dependencies:
+    "@emotion/memoize" "0.7.4"
+
+"@emotion/[email protected]":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
+  version "0.11.16"
+  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
+  integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+  dependencies:
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/unitless" "0.7.5"
+    "@emotion/utils" "0.11.3"
+    csstype "^2.5.7"
+
+"@emotion/[email protected]":
+  version "0.9.4"
+  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
+  integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+
+"@emotion/styled-base@^10.0.27":
+  version "10.0.31"
+  resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a"
+  integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/is-prop-valid" "0.8.8"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/utils" "0.11.3"
+
+"@emotion/styled@^10.0.14":
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf"
+  integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==
+  dependencies:
+    "@emotion/styled-base" "^10.0.27"
+    babel-plugin-emotion "^10.0.27"
+
+"@emotion/[email protected]":
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
+  integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+
+"@emotion/[email protected]":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
+  integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/[email protected]":
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
+  integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+
+"@emotion/[email protected]":
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+  integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
 "@fortawesome/fontawesome-free@^5.14.0":
   version "5.15.1"
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz#ccfef6ddbe59f8fe8f694783e1d3eb88902dc5eb"
@@ -791,12 +985,62 @@
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
   integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
 
+"@popperjs/core@^2.4.4":
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
+  integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
+
+"@projectstorm/geometry@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/geometry/-/geometry-6.3.0.tgz#2c37dee4b907ed0086e91680b815cd16188353f0"
+  integrity sha512-Fc2JVAkZPnQMAKdn4FtAApvl40S+sUL9E2usRoXcnqRbwmhD3WHBLhmDIvUTArIyKJJOTzyQW4+O7+NcyrK9/Q==
+
+"@projectstorm/react-canvas-core@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-canvas-core/-/react-canvas-core-6.3.0.tgz#dfe3072bb3817910c650a1fa004ce439f5bd0669"
+  integrity sha512-mChKZpdfrTWdGvg1OB0EOaCz5ovC6spY/MJfGtxYek5gU36mjoAXE+L6uLzjELTM3J/TJhDkm3Wd7jW9vCNUyA==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+
+"@projectstorm/react-diagrams-core@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-core/-/react-diagrams-core-6.3.0.tgz#4e225e86a0389f383020b4a9e8bbce2e55781cd2"
+  integrity sha512-xdAA+Rz5vE0adNa/rloXvjDQ2MFlicmdcZHEFTrPH+nV8miAqhHimzbCge8ypAwQyVXS8CLdxUF2RQA/1B8Sng==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+    "@projectstorm/react-canvas-core" "^6.3.0"
+
+"@projectstorm/react-diagrams-defaults@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-defaults/-/react-diagrams-defaults-6.3.0.tgz#2479de1abb3e638df04f33f09413d326b27e29ea"
+  integrity sha512-5qBFg9sSl3OlIt5Edv45uECgkjqjpg+PSRYderOXJjEQi3SYhf9l0YlfMzpTvt5GmVnyX7gQProyt/p/e4e9rQ==
+  dependencies:
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+
+"@projectstorm/react-diagrams-routing@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-routing/-/react-diagrams-routing-6.3.0.tgz#38b8973ac35e40639dde7490840217d8cb1e66fb"
+  integrity sha512-kgq0MNrjbeEcpQceOORL/5bOQJNF70c/If2x/jqdEKfswzGC9VeUYOKZvzYCRhxsOcpn9hCA6+6Wkd8e4zFzYQ==
+  dependencies:
+    "@projectstorm/geometry" "^6.3.0"
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+    "@projectstorm/react-diagrams-defaults" "^6.3.0"
+
+"@projectstorm/react-diagrams@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams/-/react-diagrams-6.3.0.tgz#bd435699f04b63ff57b8434a259aa2dc3b494129"
+  integrity sha512-qHPhlTBsZk/iwXAPXMh6qzmk66+7U4cShSwXM/3OqDkrBi6yI7nLtdY5VhioO0qBhhOyR8v1C8oeYGFqY0pa8Q==
+  dependencies:
+    "@projectstorm/react-diagrams-core" "^6.3.0"
+    "@projectstorm/react-diagrams-defaults" "^6.3.0"
+    "@projectstorm/react-diagrams-routing" "^6.3.0"
+
 "@simonwep/pickr@^1.5.1":
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.7.4.tgz#b14fcd945890388b870cd6db4d6c78d531f25141"
-  integrity sha512-fq7jgKJT21uWGC1mARBHvvd1JYlEf93o7SuVOB4Lr0x/2UPuNC9Oe9n/GzVeg4oVtqMDfh1wIEJpsdOJEZb+3g==
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.8.0.tgz#adbff9a4f7f0e59dec9946508c5e481b7abae0f8"
+  integrity sha512-VaSD7TwktOsro5nQ/FjRx5JAJ09k5CNfGRHacgVRxeVPolUQwelz1SjL8HAOKZwTSmcnIObptpHABQS4zgN7sw==
   dependencies:
-    core-js "^3.6.5"
+    core-js "^3.8.0"
     nanopop "^2.1.0"
 
 "@sindresorhus/is@^0.7.0":
@@ -804,21 +1048,43 @@
   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
   integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
 
+"@tippyjs/react@^4.2.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.0.tgz#a1cb369d0051099e8a7e4ceb59f809abd9955283"
+  integrity sha512-T6UcHtwtGkvgsBQ4bNp8BtXGxa2ujfOkWUogYkRtN4UVJ2QRgDdFoJeaPxdndnVYFEa2uTVxSFxs8QkSkZ2Gdw==
+  dependencies:
+    tippy.js "^6.2.0"
+
+"@types/estree@*":
+  version "0.0.45"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
+  integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
+
 "@types/json-schema@^7.0.5":
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
 
 "@types/node@*":
-  version "14.11.5"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305"
-  integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ==
+  version "14.14.16"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b"
+  integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
 "@types/q@^1.5.1":
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
+"@types/raf@^3.4.0":
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
+  integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
+
 "@webassemblyjs/[email protected]":
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@@ -1023,7 +1289,7 @@ acorn@^6.0.7, acorn@^6.4.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
   integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 
-acorn@^7.0.0, acorn@^7.1.1:
+acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
   version "7.4.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@@ -1077,9 +1343,9 @@ ajv@^5.0.0:
     json-schema-traverse "^0.3.0"
 
 ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.9.1:
-  version "6.12.5"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
-  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
@@ -1088,7 +1354,7 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.9.1:
 
 "alertifyjs@git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263":
   version "1.7.1"
-  resolved "git+https://github.com/EnterpriseDB/AlertifyJS.git#72c1d794f5b6d4ec13a68d123c08f19021afe263"
+  resolved "git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263"
 
 alphanum-sort@^1.0.0:
   version "1.0.2"
@@ -1161,9 +1427,9 @@ aproba@^1.1.1:
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
 arch@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
-  integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
+  integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
 
 archive-type@^4.0.0:
   version "4.0.0"
@@ -1209,13 +1475,15 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-includes@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
-  integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+array-includes@^3.1.1, array-includes@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8"
+  integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0"
+    es-abstract "^1.18.0-next.1"
+    get-intrinsic "^1.0.1"
     is-string "^1.0.5"
 
 array-union@^1.0.1:
@@ -1244,20 +1512,22 @@ array.prototype.find@^2.1.1:
     es-abstract "^1.17.4"
 
 array.prototype.flat@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
-  integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
+  integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 array.prototype.flatmap@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443"
-  integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
+  integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
     function-bind "^1.1.1"
 
 arraybuffer.slice@~0.0.7:
@@ -1334,11 +1604,11 @@ autoprefixer@^9.6.4:
     postcss-value-parser "^4.1.0"
 
 axios-mock-adapter@^1.17.0:
-  version "1.18.2"
-  resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.18.2.tgz#01fa9e88e692e8f1bbc1ad1200dde672486e03c7"
-  integrity sha512-e5aTsPy2Viov22zNpFTlid76W1Scz82pXeEwwCXdtO85LROhHAF8pHF2qDhiyMONLxKyY3lQ+S4UCsKgrlx8Hw==
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz#9d72e321a6c5418e1eff067aa99761a86c5188a4"
+  integrity sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==
   dependencies:
-    fast-deep-equal "^3.1.1"
+    fast-deep-equal "^3.1.3"
     is-buffer "^2.0.3"
 
 axios@^0.18.1:
@@ -1358,6 +1628,18 @@ babel-code-frame@^6.26.0:
     esutils "^2.0.2"
     js-tokens "^3.0.2"
 
+babel-eslint@^10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
+  integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/parser" "^7.7.0"
+    "@babel/traverse" "^7.7.0"
+    "@babel/types" "^7.7.0"
+    eslint-visitor-keys "^1.0.0"
+    resolve "^1.12.0"
+
 babel-generator@^6.18.0:
   version "6.26.1"
   resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
@@ -1372,91 +1654,15 @@ babel-generator@^6.18.0:
     source-map "^0.5.7"
     trim-right "^1.0.1"
 
-babel-helper-call-delegate@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
-  integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=
+babel-loader@^8.1.0:
+  version "8.2.2"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
+  integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==
   dependencies:
-    babel-helper-hoist-variables "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-helper-define-map@^6.24.1:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
-  integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=
-  dependencies:
-    babel-helper-function-name "^6.24.1"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-helper-function-name@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
-  integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=
-  dependencies:
-    babel-helper-get-function-arity "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-helper-get-function-arity@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
-  integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-hoist-variables@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
-  integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-optimise-call-expression@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
-  integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-helper-regex@^6.24.1:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
-  integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=
-  dependencies:
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-helper-replace-supers@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
-  integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo=
-  dependencies:
-    babel-helper-optimise-call-expression "^6.24.1"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-loader@~8.0.5:
-  version "8.0.6"
-  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb"
-  integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==
-  dependencies:
-    find-cache-dir "^2.0.0"
-    loader-utils "^1.0.2"
-    mkdirp "^0.5.1"
-    pify "^4.0.1"
+    find-cache-dir "^3.3.1"
+    loader-utils "^1.4.0"
+    make-dir "^3.1.0"
+    schema-utils "^2.6.5"
 
 babel-messages@^6.23.0:
   version "6.23.0"
@@ -1465,13 +1671,6 @@ babel-messages@^6.23.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-check-es2015-constants@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
-  integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=
-  dependencies:
-    babel-runtime "^6.22.0"
-
 babel-plugin-dynamic-import-node@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
@@ -1479,233 +1678,37 @@ babel-plugin-dynamic-import-node@^2.3.3:
   dependencies:
     object.assign "^4.1.0"
 
[email protected]:
-  version "7.0.0-beta.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-7.0.0-beta.3.tgz#7f781c180899dafd88f132f69472397549be48e5"
-  integrity sha512-21/MnmUFduLr4JzxrKMm/MeF+Jjyi5UdZo38IqzrP0sLhmPbal5ZAUJ4HgWH4339SdjnYgENacbY5wfk/zxTGg==
-
-babel-plugin-transform-es2015-arrow-functions@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
-  integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-block-scoped-functions@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
-  integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-block-scoping@^6.9.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
-  integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=
-  dependencies:
-    babel-runtime "^6.26.0"
-    babel-template "^6.26.0"
-    babel-traverse "^6.26.0"
-    babel-types "^6.26.0"
-    lodash "^4.17.4"
-
-babel-plugin-transform-es2015-classes@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
-  integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=
-  dependencies:
-    babel-helper-define-map "^6.24.1"
-    babel-helper-function-name "^6.24.1"
-    babel-helper-optimise-call-expression "^6.24.1"
-    babel-helper-replace-supers "^6.24.1"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-computed-properties@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
-  integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-es2015-destructuring@^6.9.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
-  integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-duplicate-keys@^6.6.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
-  integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4=
+babel-plugin-emotion@^10.0.27:
+  version "10.0.33"
+  resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03"
+  integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==
   dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-for-of@^6.6.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
-  integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-function-name@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
-  integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=
-  dependencies:
-    babel-helper-function-name "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-literals@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
-  integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-modules-amd@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
-  integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=
-  dependencies:
-    babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.6.0:
-  version "6.26.2"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
-  integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==
-  dependencies:
-    babel-plugin-transform-strict-mode "^6.24.1"
-    babel-runtime "^6.26.0"
-    babel-template "^6.26.0"
-    babel-types "^6.26.0"
-
-babel-plugin-transform-es2015-object-super@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
-  integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40=
-  dependencies:
-    babel-helper-replace-supers "^6.24.1"
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-parameters@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
-  integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=
-  dependencies:
-    babel-helper-call-delegate "^6.24.1"
-    babel-helper-get-function-arity "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-shorthand-properties@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
-  integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-spread@^6.3.13:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
-  integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-sticky-regex@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
-  integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw=
-  dependencies:
-    babel-helper-regex "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
-
-babel-plugin-transform-es2015-template-literals@^6.6.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
-  integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-typeof-symbol@^6.6.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
-  integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-es2015-unicode-regex@^6.3.13:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
-  integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek=
-  dependencies:
-    babel-helper-regex "^6.24.1"
-    babel-runtime "^6.22.0"
-    regexpu-core "^2.0.0"
-
-babel-plugin-transform-object-rest-spread@^7.0.0-beta.3:
-  version "7.0.0-beta.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-7.0.0-beta.3.tgz#5c409f3cd70819dbb3382d2056971c5ebe01393a"
-  integrity sha512-NOlhrq1CmxyuI94vNsqMhRPMuL5VG2EKUOIJQ0bwNiXBiwWRLdPoWyPT+Irrx5g4g0PkFgA46tnRj7Dc4ZGsxg==
-  dependencies:
-    babel-plugin-syntax-object-rest-spread "7.0.0-beta.3"
+    "@babel/helper-module-imports" "^7.0.0"
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/serialize" "^0.11.16"
+    babel-plugin-macros "^2.0.0"
+    babel-plugin-syntax-jsx "^6.18.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^1.0.5"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
 
-babel-plugin-transform-regenerator@^6.9.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
-  integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=
+babel-plugin-macros@^2.0.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
+  integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
   dependencies:
-    regenerator-transform "^0.10.0"
+    "@babel/runtime" "^7.7.2"
+    cosmiconfig "^6.0.0"
+    resolve "^1.12.0"
 
-babel-plugin-transform-strict-mode@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
-  integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-types "^6.24.1"
+babel-plugin-syntax-jsx@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+  integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
 
-babel-preset-es2015-without-strict@~0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/babel-preset-es2015-without-strict/-/babel-preset-es2015-without-strict-0.0.4.tgz#88c9f36e79d4762c58347b1a698a07c35b6bda5d"
-  integrity sha1-iMnzbnnUdixYNHsaaYoHw1tr2l0=
-  dependencies:
-    babel-plugin-check-es2015-constants "^6.3.13"
-    babel-plugin-transform-es2015-arrow-functions "^6.3.13"
-    babel-plugin-transform-es2015-block-scoped-functions "^6.3.13"
-    babel-plugin-transform-es2015-block-scoping "^6.9.0"
-    babel-plugin-transform-es2015-classes "^6.9.0"
-    babel-plugin-transform-es2015-computed-properties "^6.3.13"
-    babel-plugin-transform-es2015-destructuring "^6.9.0"
-    babel-plugin-transform-es2015-duplicate-keys "^6.6.0"
-    babel-plugin-transform-es2015-for-of "^6.6.0"
-    babel-plugin-transform-es2015-function-name "^6.9.0"
-    babel-plugin-transform-es2015-literals "^6.3.13"
-    babel-plugin-transform-es2015-modules-commonjs "^6.6.0"
-    babel-plugin-transform-es2015-object-super "^6.3.13"
-    babel-plugin-transform-es2015-parameters "^6.9.0"
-    babel-plugin-transform-es2015-shorthand-properties "^6.3.13"
-    babel-plugin-transform-es2015-spread "^6.3.13"
-    babel-plugin-transform-es2015-sticky-regex "^6.3.13"
-    babel-plugin-transform-es2015-template-literals "^6.6.0"
-    babel-plugin-transform-es2015-typeof-symbol "^6.6.0"
-    babel-plugin-transform-es2015-unicode-regex "^6.3.13"
-    babel-plugin-transform-regenerator "^6.9.0"
-
-babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+babel-runtime@^6.22.0, babel-runtime@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -1713,7 +1716,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
     core-js "^2.4.0"
     regenerator-runtime "^0.11.0"
 
-babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+babel-template@^6.16.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
   integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=
@@ -1724,7 +1727,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
     babylon "^6.18.0"
     lodash "^4.17.4"
 
-babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
   integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
@@ -1739,7 +1742,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
     invariant "^2.2.2"
     lodash "^4.17.4"
 
-babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+babel-types@^6.18.0, babel-types@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
   integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
@@ -1823,10 +1826,15 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+base64-arraybuffer@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+  integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
+
+base64-js@^1.0.2, base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 [email protected]:
   version "1.0.0"
@@ -1966,7 +1974,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
   integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
 
-bn.js@^5.1.1:
+bn.js@^5.0.0, bn.js@^5.1.1:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
   integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
@@ -2004,10 +2012,10 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/bootstrap4-toggle/-/bootstrap4-toggle-3.4.0.tgz#58264d4c7fd24eb2e09cca5156a338b2b22a4792"
   integrity sha512-vKfBgsjICW6mOqb264qwwbbGtpcOx+p9jOlQmLfRZ07iGE+b5YbQlY4ft9aAhPulh7V3oKOtEuuq0FcSKAI4bQ==
 
-bootstrap@>=4.1.2, bootstrap@^4.3.1:
-  version "4.5.2"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.2.tgz#a85c4eda59155f0d71186b6e6ad9b875813779ab"
-  integrity sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==
+bootstrap@^4.3.1, bootstrap@^4.5.2:
+  version "4.5.3"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
+  integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
 
 [email protected]:
   version "2.1.2"
@@ -2108,11 +2116,11 @@ browserify-des@^1.0.0:
     safe-buffer "^5.1.2"
 
 browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
-  integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+  integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
   dependencies:
-    bn.js "^4.1.0"
+    bn.js "^5.0.0"
     randombytes "^2.0.1"
 
 browserify-sign@^4.0.0:
@@ -2137,7 +2145,61 @@ browserify-zlib@^0.2.0, browserify-zlib@~0.2.0:
   dependencies:
     pako "~1.0.5"
 
-browserify@^16.1.0, browserify@~16.2.3:
+browserify@^16.1.0:
+  version "16.5.2"
+  resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.2.tgz#d926835e9280fa5fd57f5bc301f2ef24a972ddfe"
+  integrity sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g==
+  dependencies:
+    JSONStream "^1.0.3"
+    assert "^1.4.0"
+    browser-pack "^6.0.1"
+    browser-resolve "^2.0.0"
+    browserify-zlib "~0.2.0"
+    buffer "~5.2.1"
+    cached-path-relative "^1.0.0"
+    concat-stream "^1.6.0"
+    console-browserify "^1.1.0"
+    constants-browserify "~1.0.0"
+    crypto-browserify "^3.0.0"
+    defined "^1.0.0"
+    deps-sort "^2.0.0"
+    domain-browser "^1.2.0"
+    duplexer2 "~0.1.2"
+    events "^2.0.0"
+    glob "^7.1.0"
+    has "^1.0.0"
+    htmlescape "^1.1.0"
+    https-browserify "^1.0.0"
+    inherits "~2.0.1"
+    insert-module-globals "^7.0.0"
+    labeled-stream-splicer "^2.0.0"
+    mkdirp-classic "^0.5.2"
+    module-deps "^6.2.3"
+    os-browserify "~0.3.0"
+    parents "^1.0.1"
+    path-browserify "~0.0.0"
+    process "~0.11.0"
+    punycode "^1.3.2"
+    querystring-es3 "~0.2.0"
+    read-only-stream "^2.0.0"
+    readable-stream "^2.0.2"
+    resolve "^1.1.4"
+    shasum "^1.0.0"
+    shell-quote "^1.6.1"
+    stream-browserify "^2.0.0"
+    stream-http "^3.0.0"
+    string_decoder "^1.1.1"
+    subarg "^1.0.0"
+    syntax-error "^1.1.1"
+    through2 "^2.0.0"
+    timers-browserify "^1.0.1"
+    tty-browserify "0.0.1"
+    url "~0.11.0"
+    util "~0.10.1"
+    vm-browserify "^1.0.0"
+    xtend "^4.0.0"
+
+browserify@~16.2.3:
   version "16.2.3"
   resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.2.3.tgz#7ee6e654ba4f92bce6ab3599c3485b1cc7a0ad0b"
   integrity sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==
@@ -2191,15 +2253,16 @@ browserify@^16.1.0, browserify@~16.2.3:
     vm-browserify "^1.0.0"
     xtend "^4.0.0"
 
-browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.0, browserslist@^4.8.5:
-  version "4.14.5"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015"
-  integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==
+browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0:
+  version "4.16.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b"
+  integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==
   dependencies:
-    caniuse-lite "^1.0.30001135"
-    electron-to-chromium "^1.3.571"
-    escalade "^3.1.0"
-    node-releases "^1.1.61"
+    caniuse-lite "^1.0.30001165"
+    colorette "^1.2.1"
+    electron-to-chromium "^1.3.621"
+    escalade "^3.1.1"
+    node-releases "^1.1.67"
 
 buffer-alloc-unsafe@^1.1.0:
   version "1.1.0"
@@ -2244,9 +2307,17 @@ buffer@^4.3.0:
     isarray "^1.0.0"
 
 buffer@^5.0.2, buffer@^5.2.1:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
-  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
+buffer@~5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
+  integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==
   dependencies:
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
@@ -2339,6 +2410,14 @@ cached-path-relative@^1.0.0, cached-path-relative@^1.0.2:
   resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db"
   integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==
 
+call-bind@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
+  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.0"
+
 caller-callsite@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
@@ -2396,10 +2475,22 @@ caniuse-api@^3.0.0:
     lodash.memoize "^4.1.2"
     lodash.uniq "^4.5.0"
 
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
-  version "1.0.30001146"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c"
-  integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
+  version "1.0.30001170"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7"
+  integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==
+
+canvg@^3.0.7:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.7.tgz#e45b87a64116af906917f7cad57d370ea372d682"
+  integrity sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==
+  dependencies:
+    "@babel/runtime-corejs3" "^7.9.6"
+    "@types/raf" "^3.4.0"
+    raf "^3.4.1"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^5.0.5"
 
 caw@^2.0.0, caw@^2.0.1:
   version "2.0.1"
@@ -2445,9 +2536,9 @@ chardet@^0.7.0:
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
 chart.js@^2.9.3:
-  version "2.9.3"
-  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
-  integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
+  version "2.9.4"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
+  integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
   dependencies:
     chartjs-color "^2.1.0"
     moment "^2.10.2"
@@ -2472,19 +2563,46 @@ check-types@^8.0.3:
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
   integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
 
+cheerio-select-tmp@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
+  integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==
+  dependencies:
+    css-select "^3.1.2"
+    css-what "^4.0.0"
+    domelementtype "^2.1.0"
+    domhandler "^4.0.0"
+    domutils "^2.4.4"
+
 cheerio@^1.0.0-rc.3:
-  version "1.0.0-rc.3"
-  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
-  integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
-  dependencies:
-    css-select "~1.2.0"
-    dom-serializer "~0.1.1"
-    entities "~1.1.1"
-    htmlparser2 "^3.9.1"
-    lodash "^4.15.0"
-    parse5 "^3.0.1"
-
-"chokidar@>=2.0.0 <4.0.0", chokidar@^2.1.1, chokidar@^2.1.8:
+  version "1.0.0-rc.5"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f"
+  integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==
+  dependencies:
+    cheerio-select-tmp "^0.1.0"
+    dom-serializer "~1.2.0"
+    domhandler "^4.0.0"
+    entities "~2.1.0"
+    htmlparser2 "^6.0.0"
+    parse5 "^6.0.0"
+    parse5-htmlparser2-tree-adapter "^6.0.0"
+
+"chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.4.1:
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
+  integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.5.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
+chokidar@^2.1.1, chokidar@^2.1.8:
   version "2.1.8"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
   integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
@@ -2503,21 +2621,6 @@ cheerio@^1.0.0-rc.3:
   optionalDependencies:
     fsevents "^1.2.7"
 
-chokidar@^3.0.0, chokidar@^3.4.1:
-  version "3.4.2"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
-  integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==
-  dependencies:
-    anymatch "~3.1.1"
-    braces "~3.0.2"
-    glob-parent "~5.1.0"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.4.0"
-  optionalDependencies:
-    fsevents "~2.1.2"
-
 chownr@^1.1.1, chownr@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -2595,6 +2698,13 @@ [email protected]:
   dependencies:
     mimic-response "^1.0.0"
 
+closest@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/closest/-/closest-0.0.1.tgz#26da6f80b3e0e17e71f80f12782819e9f653495c"
+  integrity sha1-JtpvgLPg4X5x+A8SeCgZ6fZTSVw=
+  dependencies:
+    matches-selector "0.0.1"
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -2610,9 +2720,9 @@ coa@^2.0.2:
     q "^1.1.2"
 
 codemirror@^5.54.0:
-  version "5.58.1"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.58.1.tgz#ec6bf38ad2a17f74c61bd00cc6dc5a69bd167854"
-  integrity sha512-UGb/ueu20U4xqWk8hZB3xIfV2/SFqnSLYONiM3wTMDqko0bsYrsAkGGhqUzbRkYm89aBKPyHtuNEbVWF9FTFzw==
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.0.tgz#6d8132055459aabf21d04cae5cf5c430e5c57bb9"
+  integrity sha512-UGzSkCacY9z0rSpQ3wnTWRN2nvRE6foDXnJltWW8pazInR/R+3gXHrao4IFQMv/bSBvFBxt8/HPpkpKAS54x5Q==
 
 collection-visit@^1.0.0:
   version "1.0.0"
@@ -2636,31 +2746,31 @@ color-convert@^2.0.1:
   dependencies:
     color-name "~1.1.4"
 
[email protected], color-name@^1.0.0:
[email protected]:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@~1.1.4:
+color-name@^1.0.0, color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-string@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
-  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+color-string@^1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
+  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
 color@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
-  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
+  integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==
   dependencies:
     color-convert "^1.9.1"
-    color-string "^1.5.2"
+    color-string "^1.5.4"
 
 colorette@^1.2.1:
   version "1.2.1"
@@ -2688,9 +2798,9 @@ commander@^2.12.2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, comm
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
 commander@^6.0.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
-  integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -2777,18 +2887,18 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.0, convert-source-map@^1.1.3, convert-source-map@~1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
-  integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=
-
-convert-source-map@^1.5.0, convert-source-map@^1.7.0:
+convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
+convert-source-map@~1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+  integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=
+
 [email protected]:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -2839,23 +2949,28 @@ copy-webpack-plugin@^5.1.0:
     serialize-javascript "^4.0.0"
     webpack-log "^2.0.0"
 
-core-js-compat@^3.1.1:
-  version "3.6.5"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c"
-  integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==
+core-js-compat@^3.8.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e"
+  integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==
   dependencies:
-    browserslist "^4.8.5"
+    browserslist "^4.15.0"
     semver "7.0.0"
 
+core-js-pure@^3.0.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119"
+  integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==
+
 core-js@^2.4.0:
-  version "2.6.11"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
-  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+  version "2.6.12"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
-core-js@^3.2.1, core-js@^3.6.5:
-  version "3.6.5"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
-  integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
+core-js@^3.2.1, core-js@^3.8.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47"
+  integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==
 
 core-util-is@~1.0.0:
   version "1.0.2"
@@ -2872,6 +2987,17 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.13.1"
     parse-json "^4.0.0"
 
+cosmiconfig@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
+  integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.1.0"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.7.2"
+
 create-ecdh@^4.0.0:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -2960,6 +3086,13 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
[email protected]:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+  integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
+  dependencies:
+    base64-arraybuffer "^0.2.0"
+
 [email protected]:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@@ -2991,15 +3124,16 @@ css-select@^2.0.0:
     domutils "^1.7.0"
     nth-check "^1.0.2"
 
-css-select@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
-  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+css-select@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
+  integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
   dependencies:
-    boolbase "~1.0.0"
-    css-what "2.1"
-    domutils "1.5.1"
-    nth-check "~1.0.1"
+    boolbase "^1.0.0"
+    css-what "^4.0.0"
+    domhandler "^4.0.0"
+    domutils "^2.4.3"
+    nth-check "^2.0.0"
 
 [email protected]:
   version "1.0.0-alpha.37"
@@ -3009,23 +3143,23 @@ [email protected]:
     mdn-data "2.0.4"
     source-map "^0.6.1"
 
[email protected]:
-  version "1.0.0-alpha.39"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
-  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+css-tree@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5"
+  integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==
   dependencies:
-    mdn-data "2.0.6"
+    mdn-data "2.0.14"
     source-map "^0.6.1"
 
[email protected]:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
-  integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
-
 css-what@^3.2.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
-  integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
+  integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
+
+css-what@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
+  integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
 
 cssesc@^3.0.0:
   version "3.0.0"
@@ -3101,11 +3235,16 @@ cssnano@^4.1.10:
     postcss "^7.0.0"
 
 csso@^4.0.2:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
-  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
   dependencies:
-    css-tree "1.0.0-alpha.39"
+    css-tree "^1.1.2"
+
+csstype@^2.5.7:
+  version "2.6.14"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de"
+  integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==
 
 cubic2quad@^1.0.0:
   version "1.1.1"
@@ -3138,6 +3277,14 @@ cyclist@^1.0.1:
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
   integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
 
+dagre@^0.8.4:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
+  integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
+  dependencies:
+    graphlib "^2.1.8"
+    lodash "^4.17.15"
+
 dash-ast@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37"
@@ -3163,16 +3310,16 @@ debug@=3.1.0, debug@~3.1.0:
     ms "2.0.0"
 
 debug@^3.2.6:
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
 debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
-  integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
   dependencies:
     ms "2.1.2"
 
@@ -3391,40 +3538,46 @@ dom-serialize@^2.2.0:
     extend "^3.0.0"
     void-elements "^2.0.0"
 
-dom-serializer@0, dom-serializer@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
-  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+dom-serializer@^1.0.1, dom-serializer@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
+  integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
   dependencies:
-    domelementtype "^1.3.0"
-    entities "^1.1.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    entities "^2.0.0"
 
 domain-browser@^1.1.1, domain-browser@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
   integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
 
-domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+domelementtype@1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
   integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
 
-domhandler@^2.3.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
-  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
-  dependencies:
-    domelementtype "1"
+domelementtype@^2.0.1, domelementtype@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
+  integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
 
[email protected]:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
-  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+domhandler@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e"
+  integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==
   dependencies:
-    dom-serializer "0"
-    domelementtype "1"
+    domelementtype "^2.1.0"
 
-domutils@^1.5.1, domutils@^1.7.0:
+domutils@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
   integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
@@ -3432,6 +3585,15 @@ domutils@^1.5.1, domutils@^1.7.0:
     dom-serializer "0"
     domelementtype "1"
 
+domutils@^2.4.3, domutils@^2.4.4:
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
+  integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+
 dot-prop@^5.2.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -3521,10 +3683,10 @@ ejs@~3.0.2:
   resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.2.tgz#745b01cdcfe38c1c6a2da3bbb2d9957060a31226"
   integrity sha512-IncmUpn1yN84hy2shb0POJ80FWrfGNY0cxO9f4v+/sG7qcBvAtVWUA1IdzY/8EYUmOVhoKJVdJjNd3AZcnxOjA==
 
-electron-to-chromium@^1.3.571:
-  version "1.3.578"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0"
-  integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q==
+electron-to-chromium@^1.3.621:
+  version "1.3.633"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe"
+  integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==
 
 elliptic@^6.5.3:
   version "6.5.3"
@@ -3615,10 +3777,10 @@ ent@~2.2.0:
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
   integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
 
-entities@^1.1.1, entities@~1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
-  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+entities@^2.0.0, entities@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 enzyme-adapter-react-16@^1.15.2:
   version "1.15.5"
@@ -3636,14 +3798,15 @@ enzyme-adapter-react-16@^1.15.2:
     semver "^5.7.0"
 
 enzyme-adapter-utils@^1.13.1:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz#59c1b734b0927543e3d8dc477299ec957feb312d"
-  integrity sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0"
+  integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==
   dependencies:
     airbnb-prop-types "^2.16.0"
-    function.prototype.name "^1.1.2"
-    object.assign "^4.1.0"
-    object.fromentries "^2.0.2"
+    function.prototype.name "^1.1.3"
+    has "^1.0.3"
+    object.assign "^4.1.2"
+    object.fromentries "^2.0.3"
     prop-types "^15.7.2"
     semver "^5.7.1"
 
@@ -3692,9 +3855,9 @@ enzyme@^3.11.0:
     string.prototype.trim "^1.2.1"
 
 errno@^0.1.3, errno@~0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
-  integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
   dependencies:
     prr "~1.0.1"
 
@@ -3705,7 +3868,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4:
   version "1.17.7"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
   integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
@@ -3749,10 +3912,10 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-escalade@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
-  integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 
 escape-html@~1.0.3:
   version "1.0.3"
@@ -3765,20 +3928,20 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 eslint-plugin-react@^7.20.5:
-  version "7.21.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.3.tgz#71655d2af5155b19285ec929dd2cdc67a4470b52"
-  integrity sha512-OI4GwTCqyIb4ipaOEGLWdaOHCXZZydStAsBEPB2e1ZfNM37bojpgO1BoOQbFb0eLVz3QLDx7b+6kYcrxCuJfhw==
+  version "7.21.5"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz#50b21a412b9574bfe05b21db176e8b7b3b15bff3"
+  integrity sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==
   dependencies:
     array-includes "^3.1.1"
     array.prototype.flatmap "^1.2.3"
     doctrine "^2.1.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.4.1"
+    jsx-ast-utils "^2.4.1 || ^3.0.0"
     object.entries "^1.1.2"
     object.fromentries "^2.0.2"
     object.values "^1.1.1"
     prop-types "^15.7.2"
-    resolve "^1.17.0"
+    resolve "^1.18.1"
     string.prototype.matchall "^4.0.2"
 
 eslint-scope@^4.0.2, eslint-scope@^4.0.3:
@@ -4103,7 +4266,7 @@ fast-deep-equal@^1.0.0:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
   integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=
 
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -4249,7 +4412,7 @@ [email protected], finalhandler@~1.1.2:
     statuses "~1.5.0"
     unpipe "~1.0.0"
 
-find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
+find-cache-dir@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
   integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
@@ -4267,6 +4430,11 @@ find-cache-dir@^3.3.1:
     make-dir "^3.0.2"
     pkg-dir "^4.1.0"
 
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4329,13 +4497,18 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
[email protected], follow-redirects@^1.0.0:
[email protected]:
   version "1.5.10"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
   integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
   dependencies:
     debug "=3.1.0"
 
+follow-redirects@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
+  integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
+
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -4427,29 +4600,30 @@ function-bind@^1.1.1:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
-function.prototype.name@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45"
-  integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==
+function.prototype.name@^1.1.2, function.prototype.name@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe"
+  integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    functions-have-names "^1.2.0"
+    es-abstract "^1.18.0-next.1"
+    functions-have-names "^1.2.1"
 
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
-functions-have-names@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
-  integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==
+functions-have-names@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
+  integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
 
 gensync@^1.0.0-beta.1:
-  version "1.0.0-beta.1"
-  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
-  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
 geometry-interfaces@^1.1.4:
   version "1.1.4"
@@ -4466,6 +4640,15 @@ get-caller-file@^2.0.1:
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
+get-intrinsic@^1.0.0, get-intrinsic@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
+  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
 get-proxy@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93"
@@ -4657,6 +4840,13 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
   integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
 
+graphlib@^2.1.8:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
+  integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+  dependencies:
+    lodash "^4.17.15"
+
 gzip-size@^5.0.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@@ -4771,6 +4961,11 @@ hat@^0.0.3:
   resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
   integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=
 
[email protected]:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.5.tgz#713b65590ebcc40fcbeeaf55e851694092b39af1"
+  integrity sha1-cTtlWQ68xA/L7q9V6FFpQJKzmvE=
+
 hex-color-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@@ -4829,29 +5024,39 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+html-to-image@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-0.1.3.tgz#9661a54c30ed9c4b269f4612416b1c7e887de3a7"
+  integrity sha512-8JTGEAAdJGL/nlp3wb/WI8fLMx2dHKOFZMdsvdon23D45ZdtsXDeRm39Wddf04ludQe3OPmvjMJ9nPjI/7hPlg==
+
+html2canvas@^1.0.0-rc.7:
+  version "1.0.0-rc.7"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+  integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
+  dependencies:
+    css-line-break "1.1.1"
+
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
   integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
 
-htmlparser2@^3.9.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
-  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+htmlparser2@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
+  integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==
   dependencies:
-    domelementtype "^1.3.1"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^3.1.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    domutils "^2.4.4"
+    entities "^2.0.0"
 
 [email protected]:
   version "3.8.1"
   resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
   integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==
 
[email protected], http-errors@~1.7.2:
[email protected]:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
   integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@@ -4862,6 +5067,17 @@ [email protected], http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-proxy@^1.13.0:
   version "1.18.1"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
@@ -4906,10 +5122,10 @@ icss-utils@^4.0.0:
   dependencies:
     postcss "^7.0.14"
 
-ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+ieee754@^1.1.13, ieee754@^1.1.4:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
 iferr@^0.1.5:
   version "0.1.5"
@@ -5027,10 +5243,10 @@ import-fresh@^2.0.0:
     caller-path "^2.0.0"
     resolve-from "^3.0.0"
 
-import-fresh@^3.0.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
-  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+import-fresh@^3.0.0, import-fresh@^3.1.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
   dependencies:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
@@ -5103,7 +5319,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, [email protected], inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5119,9 +5335,9 @@ [email protected]:
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
 inline-source-map@~0.6.0:
   version "0.6.2"
@@ -5150,9 +5366,9 @@ inquirer@^6.2.2:
     through "^2.3.6"
 
 insert-module-globals@^7.0.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.0.tgz#ec87e5b42728479e327bd5c5c71611ddfb4752ba"
-  integrity sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3"
+  integrity sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==
   dependencies:
     JSONStream "^1.0.3"
     acorn-node "^1.5.2"
@@ -5227,6 +5443,13 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-any-array@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-0.1.0.tgz#e5a27965e3371610c10c66bca3be37ee177bdf8e"
+  integrity sha512-6Kkl1RnvfdkmXM6ZlP+kELGBMA74Nq5pSOm9gIKDaPRe9KQlIJzonrOgq0Jzn/iElB6F2/olpLgWYeVySzrSRg==
+  dependencies:
+    rollup "^1.31.1"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -5252,9 +5475,11 @@ is-binary-path@~2.1.0:
     binary-extensions "^2.0.0"
 
 is-boolean-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
-  integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
+  integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
+  dependencies:
+    call-bind "^1.0.0"
 
 is-buffer@^1.1.0, is-buffer@^1.1.5:
   version "1.1.6"
@@ -5262,9 +5487,9 @@ is-buffer@^1.1.0, is-buffer@^1.1.5:
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
 is-buffer@^2.0.2, is-buffer@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
-  integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
+  integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
 
 is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
   version "1.2.2"
@@ -5283,6 +5508,13 @@ is-color-stop@^1.0.0:
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
+is-core-module@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
 is-cwebp-readable@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-cwebp-readable/-/is-cwebp-readable-2.0.1.tgz#afb93b0c0abd0a25101016ae33aea8aedf926d26"
@@ -5396,9 +5628,9 @@ is-natural-number@^4.0.1:
   integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
 
 is-negative-zero@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
-  integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
 
 is-number-object@^1.0.4:
   version "1.0.4"
@@ -5423,9 +5655,9 @@ is-obj@^2.0.0:
   integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
 
 is-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
-  integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
+  integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
 
 is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
@@ -5628,7 +5860,12 @@ isurl@^1.0.0-alpha5:
     has-to-string-tag-x "^1.2.0"
     is-object "^1.0.1"
 
-jasmine-core@^3.3, jasmine-core@~3.3.0:
+jasmine-core@^3.3:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
+  integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
+
+jasmine-core@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e"
   integrity sha512-3/xSmG/d35hf80BEN66Y6g9Ca5l/Isdeg/j6zvbTYlTzeKinzmaTM4p9am5kYqOmE05D7s1t8FGjzdSnbUbceA==
@@ -5660,16 +5897,11 @@ jquery-ui@>=1.8.0, jquery-ui@^1.12.1:
   resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
   integrity sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=
 
-jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, jquery@^3.0, jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1:
+jquery@>=1.2.6, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
   integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
 
-js-levenshtein@^1.1.3:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
-  integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
-
 js-string-escape@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
@@ -5686,9 +5918,9 @@ js-tokens@^3.0.2:
   integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
 
 js-yaml@^3.12.0, js-yaml@^3.13.1:
-  version "3.14.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
-  integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
   dependencies:
     argparse "^1.0.7"
     esprima "^4.0.0"
@@ -5730,6 +5962,11 @@ json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
@@ -5759,7 +5996,7 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.0, json5@^2.1.2:
+json5@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
   integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
@@ -5783,13 +6020,13 @@ jsonparse@^1.2.0:
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
 
-jsx-ast-utils@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
-  integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==
+"jsx-ast-utils@^2.4.1 || ^3.0.0":
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
+  integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==
   dependencies:
-    array-includes "^3.1.1"
-    object.assign "^4.1.0"
+    array-includes "^3.1.2"
+    object.assign "^4.1.2"
 
 karma-babel-preprocessor@^8.0.0:
   version "8.0.1"
@@ -5963,6 +6200,11 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -6093,7 +6335,7 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.14.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5:
+lodash@4.*, lodash@^4.14.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5:
   version "4.17.20"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
   integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -6192,7 +6434,7 @@ make-dir@^2.0.0:
     pify "^4.0.1"
     semver "^5.6.0"
 
-make-dir@^3.0.0, make-dir@^3.0.2:
+make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
   integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -6217,9 +6459,14 @@ map-visit@^1.0.0:
     object-visit "^1.0.0"
 
 marked@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.0.tgz#7221ce2395fa6cf6d722e6f2871a32d3513c85ca"
-  integrity sha512-tiRxakgbNPBr301ihe/785NntvYyhxlqcL3YaC8CaxJQh7kiaEtrN9B/eK2I2943Yjkh5gw25chYFDQhOMCwMA==
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb"
+  integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA==
+
[email protected]:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/matches-selector/-/matches-selector-0.0.1.tgz#1df5262243ae341c1a0804dd302048267ac713bb"
+  integrity sha1-HfUmIkOuNBwaCATdMCBIJnrHE7s=
 
 md5.js@^1.3.4:
   version "1.3.5"
@@ -6230,16 +6477,16 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
[email protected]:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
 [email protected]:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
   integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
 
[email protected]:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
-  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
-
 [email protected]:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6347,9 +6594,9 @@ [email protected]:
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
 
 mime@^2.0.3, mime@^2.3.1, mime@^2.4.4:
-  version "2.4.6"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
-  integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
+  version "2.4.7"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
+  integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
 
 mimic-fn@^1.0.0:
   version "1.2.0"
@@ -6449,6 +6696,11 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
+mkdirp-classic@^0.5.2:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
 mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -6456,7 +6708,37 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
-module-deps@^6.0.0:
+ml-array-max@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ml-array-max/-/ml-array-max-1.2.0.tgz#141595131fe10208dd89897ce98ab7fd382a3951"
+  integrity sha512-3UH7XCdjINxbtBWj1EuHMeI242Q3uLuC4rTpSybBWUpGjnG/BefAFxmTolUCuXDM59mJ/G/re80CQbaVIuMjQA==
+  dependencies:
+    is-any-array "^0.1.0"
+
+ml-array-min@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ml-array-min/-/ml-array-min-1.2.0.tgz#6880ea319250a99ec73bc2799ac005c10a0f0489"
+  integrity sha512-Wgf2+lCndLy1SbeOZSUqlkxD9T1CXPT7CIlNGAZRRQI35wsqvfuNtLNH4qKFx8kNjlq3VGXKOSBHeiXR31vaTA==
+  dependencies:
+    is-any-array "^0.1.0"
+
+ml-array-rescale@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/ml-array-rescale/-/ml-array-rescale-1.3.2.tgz#ebdeaaa84d15f714dbb00e94a6a9336ddcf10413"
+  integrity sha512-kiXwdVCGrer7rLnjR6Q9ZgP6e9rbnmQvYVUMLXyqNg4+zOs+jek8yBupqPZPDr+NvlSE5OuMnfAbP1oA63kHBA==
+  dependencies:
+    is-any-array "^0.1.0"
+    ml-array-max "^1.2.0"
+    ml-array-min "^1.2.0"
+
+ml-matrix@^6.5.0:
+  version "6.5.3"
+  resolved "https://registry.yarnpkg.com/ml-matrix/-/ml-matrix-6.5.3.tgz#0c4bb26714607cac76d289b943171c7083418eed"
+  integrity sha512-wXrn+ccApJ6gHktxmosOzs6B6M0huadahDpcgPYIAJggpqN7CtV4Vd7zpW6Lel/1oM5yCULcrbRJ1A5gF/GYDA==
+  dependencies:
+    ml-array-rescale "^1.3.2"
+
+module-deps@^6.0.0, module-deps@^6.2.3:
   version "6.2.3"
   resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee"
   integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==
@@ -6477,25 +6759,23 @@ module-deps@^6.0.0:
     through2 "^2.0.0"
     xtend "^4.0.0"
 
-moment-timezone@^0.4.0:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.4.1.tgz#81f598c3ad5e22cdad796b67ecd8d88d0f5baa06"
-  integrity sha1-gfWYw61eIs2teWtn7NjYjQ9bqgY=
-  dependencies:
-    moment ">= 2.6.0"
-
-moment-timezone@^0.5.11, moment-timezone@^0.5.23:
-  version "0.5.31"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
-  integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
+moment-timezone@^0.5.23, moment-timezone@^0.5.28, moment-timezone@^0.5.31:
+  version "0.5.32"
+  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2"
+  integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==
   dependencies:
     moment ">= 2.9.0"
 
-"moment@>= 2.6.0", "moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2, moment@^2.24.0:
+"moment@>= 2.9.0", moment@^2.10.2, moment@^2.24.0, moment@^2.29.0:
   version "2.29.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
   integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
 
+moment@~2.24.0:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
 moo@^0.5.0:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
@@ -6537,20 +6817,25 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
[email protected], ms@^2.1.1:
[email protected]:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 [email protected]:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
 nan@^2.12.1:
-  version "2.14.1"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
-  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+  version "2.14.2"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
+  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -6580,15 +6865,14 @@ natural-compare@^1.4.0:
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
 nearley@^2.7.10:
-  version "2.19.7"
-  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.7.tgz#eafbe3e2d8ccfe70adaa5c026ab1f9709c116218"
-  integrity sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==
+  version "2.20.1"
+  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
+  integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
   dependencies:
     commander "^2.19.0"
     moo "^0.5.0"
     railroad-diagrams "^1.0.0"
     randexp "0.4.6"
-    semver "^5.4.1"
 
 neatequal@^1.0.0:
   version "1.0.0"
@@ -6641,10 +6925,10 @@ node-libs-browser@^2.2.1:
     util "^0.11.0"
     vm-browserify "^1.0.1"
 
-node-releases@^1.1.61:
-  version "1.1.61"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e"
-  integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==
+node-releases@^1.1.67:
+  version "1.1.67"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
+  integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
 
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
   version "2.5.0"
@@ -6702,13 +6986,20 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
-nth-check@^1.0.2, nth-check@~1.0.1:
+nth-check@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
   integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
   dependencies:
     boolbase "~1.0.0"
 
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
 null-check@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
@@ -6739,17 +7030,17 @@ object-copy@^0.1.0:
     kind-of "^3.0.3"
 
 object-inspect@^1.7.0, object-inspect@^1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
-  integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
+  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
 
 object-is@^1.0.2, object-is@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
-  integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
+  integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
 
 object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -6763,42 +7054,44 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.0, object.assign@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
-  integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
+object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.0"
     has-symbols "^1.0.1"
     object-keys "^1.1.1"
 
 object.entries@^1.1.1, object.entries@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
-  integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
+  integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
-object.fromentries@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
-  integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
+object.fromentries@^2.0.2, object.fromentries@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
+  integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 object.getownpropertydescriptors@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
-  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544"
+  integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 object.pick@^1.3.0:
   version "1.3.0"
@@ -6808,13 +7101,13 @@ object.pick@^1.3.0:
     isobject "^3.0.1"
 
 object.values@^1.1.0, object.values@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
-  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
+  integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 on-finished@~2.3.0:
@@ -7061,17 +7354,32 @@ parse-json@^4.0.0:
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
+parse-json@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
+  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
 parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
-parse5@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
-  integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+parse5-htmlparser2-tree-adapter@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
   dependencies:
-    "@types/node" "*"
+    parse5 "^6.0.1"
+
+parse5@^6.0.0, parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 [email protected]:
   version "0.0.5"
@@ -7170,6 +7478,23 @@ path-type@^3.0.0:
   dependencies:
     pify "^3.0.0"
 
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+pathfinding@^0.4.18:
+  version "0.4.18"
+  resolved "https://registry.yarnpkg.com/pathfinding/-/pathfinding-0.4.18.tgz#a9990f6fa22b7ef196e5651b049165403a045fe8"
+  integrity sha1-qZkPb6IrfvGW5WUbBJFlQDoEX+g=
+  dependencies:
+    heap "0.2.5"
+
+paths-js@^0.4.9:
+  version "0.4.11"
+  resolved "https://registry.yarnpkg.com/paths-js/-/paths-js-0.4.11.tgz#b2a9d5f94ee9949aa8fee945f78a12abff44599e"
+  integrity sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==
+
 pbkdf2@^3.0.3:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
@@ -7247,7 +7572,7 @@ pngquant-bin@^5.0.0:
     execa "^0.10.0"
     logalot "^2.0.0"
 
-popper.js@^1.14.3, popper.js@^1.14.7:
+popper.js@^1.14.7, popper.js@^1.16.1:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
   integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@@ -7621,11 +7946,6 @@ prepend-http@^2.0.0:
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
   integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
 
-private@^0.1.6:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
-
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -7823,18 +8143,18 @@ [email protected]:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-raw-loader@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405"
-  integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==
+raw-loader@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f"
+  integrity sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==
   dependencies:
     loader-utils "^1.1.0"
-    schema-utils "^1.0.0"
+    schema-utils "^2.0.1"
 
 react-dom@^16.13.1:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
-  integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -7847,19 +8167,26 @@ react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6:
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
 react-test-renderer@^16.0.0-0:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
-  integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
+  integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
   dependencies:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
     react-is "^16.8.6"
     scheduler "^0.19.1"
 
+react-to-print@^2.10.3:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.12.1.tgz#120f3116fef55a141f553548388624b976596835"
+  integrity sha512-+zGNUYQKaae7Wp0JL2JcoWM0lDd5csasRmTqRZlJLOj6F4sRmJsT7ZPgH0SL6ZQ7gWz0hwglhLlLoyF85lGgHw==
+  dependencies:
+    prop-types "^15.7.2"
+
 react@^16.13.1:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
-  integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+  version "16.14.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -7912,7 +8239,7 @@ readable-stream@^1.0.33:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^3.1.1, readable-stream@^3.6.0:
+readable-stream@^3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -7930,10 +8257,10 @@ readdirp@^2.2.1:
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
-readdirp@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
-  integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
+readdirp@~3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
+  integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
   dependencies:
     picomatch "^2.2.1"
 
@@ -7957,10 +8284,10 @@ regenerate-unicode-properties@^8.2.0:
   dependencies:
     regenerate "^1.4.0"
 
-regenerate@^1.2.1, regenerate@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
-  integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
+regenerate@^1.4.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
+  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
 regenerator-runtime@^0.11.0:
   version "0.11.1"
@@ -7972,15 +8299,6 @@ regenerator-runtime@^0.13.4:
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
   integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
 
-regenerator-transform@^0.10.0:
-  version "0.10.1"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
-  integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==
-  dependencies:
-    babel-runtime "^6.18.0"
-    babel-types "^6.19.0"
-    private "^0.1.6"
-
 regenerator-transform@^0.14.2:
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
@@ -8009,16 +8327,7 @@ regexpp@^2.0.1:
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
   integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
 
-regexpu-core@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
-  integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=
-  dependencies:
-    regenerate "^1.2.1"
-    regjsgen "^0.2.0"
-    regjsparser "^0.1.4"
-
-regexpu-core@^4.7.0:
+regexpu-core@^4.7.1:
   version "4.7.1"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
   integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
@@ -8030,23 +8339,11 @@ regexpu-core@^4.7.0:
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.2.0"
 
-regjsgen@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
-  integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=
-
 regjsgen@^0.5.1:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
   integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
 
-regjsparser@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
-  integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=
-  dependencies:
-    jsesc "~0.5.0"
-
 regjsparser@^0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
@@ -8101,6 +8398,11 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -8136,11 +8438,12 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
   integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
 
-resolve@^1.1.4, resolve@^1.10.0, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+resolve@^1.1.4, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.4.0:
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
+  integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
   dependencies:
+    is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
 [email protected]:
@@ -8178,6 +8481,11 @@ rgba-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0=
+
 [email protected]:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@@ -8200,6 +8508,15 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
+rollup@^1.31.1:
+  version "1.32.1"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
+  integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==
+  dependencies:
+    "@types/estree" "*"
+    "@types/node" "*"
+    acorn "^7.1.0"
+
 rst-selector-parser@^2.2.3:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
@@ -8271,9 +8588,9 @@ sass-resources-loader@^2.0.0:
     loader-utils "^2.0.0"
 
 sass@^1.24.4:
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.27.0.tgz#0657ff674206b95ec20dc638a93e179c78f6ada2"
-  integrity sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig==
+  version "1.30.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.30.0.tgz#60bbbbaf76ba10117e61c6c24f00161c3d60610e"
+  integrity sha512-26EUhOXRLaUY7+mWuRFqGeGGNmhB1vblpTENO1Z7mAzzIZeVxZr9EZoaY1kyGLFWdSOZxRMAufiN2mkbO6dAlw==
   dependencies:
     chokidar ">=2.0.0 <4.0.0"
 
@@ -8306,7 +8623,7 @@ schema-utils@^1.0.0:
     ajv-errors "^1.0.0"
     ajv-keywords "^3.1.0"
 
-schema-utils@^2.6.6:
+schema-utils@^2.0.1, schema-utils@^2.6.5, schema-utils@^2.6.6:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
   integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
@@ -8472,7 +8789,7 @@ shim-loader@^1.0.1:
     precond "^0.2.3"
     webpack-sources "^0.2.3"
 
-side-channel@^1.0.2:
+side-channel@^1.0.2, side-channel@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
   integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
@@ -8698,9 +9015,9 @@ spdx-expression-parse@^3.0.0:
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
-  integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
+  integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
 
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
@@ -8753,6 +9070,11 @@ stable@^0.1.8:
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
+stackblur-canvas@^2.0.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz#2b2eba910cb46f6feae918e1c402f863d602c01b"
+  integrity sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -8801,6 +9123,16 @@ stream-http@^2.0.0, stream-http@^2.7.2:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
+stream-http@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564"
+  integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.4"
+    readable-stream "^3.6.0"
+    xtend "^4.0.2"
+
 stream-shift@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
@@ -8858,40 +9190,42 @@ string.prototype.codepointat@^0.2.0:
   integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
 
 string.prototype.matchall@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e"
-  integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz#24243399bc31b0a49d19e2b74171a15653ec996a"
+  integrity sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0"
+    es-abstract "^1.18.0-next.1"
     has-symbols "^1.0.1"
     internal-slot "^1.0.2"
     regexp.prototype.flags "^1.3.0"
-    side-channel "^1.0.2"
+    side-channel "^1.0.3"
 
 string.prototype.trim@^1.2.1:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz#f538d0bacd98fc4297f0bef645226d5aaebf59f3"
-  integrity sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7"
+  integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.0"
+    es-abstract "^1.18.0-next.1"
 
 string.prototype.trimend@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
-  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
+  integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string.prototype.trimstart@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
-  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
+  integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.5"
 
 string_decoder@^1.0.0, string_decoder@^1.1.1:
   version "1.3.0"
@@ -9021,7 +9355,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
-svg-pathdata@^5.0.0:
+svg-pathdata@^5.0.0, svg-pathdata@^5.0.5:
   version "5.0.5"
   resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-5.0.5.tgz#65e8d765642ba15fe15434444087d082bc526b29"
   integrity sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==
@@ -9134,24 +9468,24 @@ tempfile@^2.0.0:
     uuid "^3.0.1"
 
 tempusdominus-bootstrap-4@^5.1.2:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.1.2.tgz#3c9906ca6e5d563faa0b81b2fdc6aa79cad9c0be"
-  integrity sha512-ksD8qc4wOJeE19wvryXmEpRzMUSZu4wSOdG6zKSn8l4ccad16249KOX1j0CccyZpuuES/n4FLqLAUB+Dd1LTBA==
+  version "5.39.0"
+  resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.39.0.tgz#f13dcfec6c41b37c5fe509f08bd513590c64411f"
+  integrity sha512-vYnkmQYQq4+A51WyRc/6e03eM0BHDoPaxd556K1pd4Nhr0eGeB3+Mi9b+3CDx4189fg3gQlrsKzgJiHPRwSX3Q==
   dependencies:
-    bootstrap ">=4.1.2"
-    jquery "^3.0"
-    moment "^2.22.2"
-    moment-timezone "^0.5.11"
-    popper.js "^1.14.3"
+    bootstrap "^4.5.2"
+    jquery "^3.5.1"
+    moment "^2.29.0"
+    moment-timezone "^0.5.31"
+    popper.js "^1.16.1"
 
 tempusdominus-core@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/tempusdominus-core/-/tempusdominus-core-5.0.3.tgz#808642e47a83f45d7ef18c1597fd7b1d413d69e5"
-  integrity sha512-52lClmU33gb6J6I/S9uGDrgQwccq3Yw9SlZerTgGLOzOB3Sc9pgIVBirfPMsMcx8nPsg6mA5ItFAH/5BZiQThg==
+  version "5.19.0"
+  resolved "https://registry.yarnpkg.com/tempusdominus-core/-/tempusdominus-core-5.19.0.tgz#ccbd2c35109b0a4b96c61513e53e0175ec4896bd"
+  integrity sha512-7a4oBQw4cjz6C87BLRg3KHVvzpnPlnRTkuDZ7SwcJayQQ4QgOryX5u6wj0q07TXhgtMQLCntZO6nVhHIKPaeUw==
   dependencies:
-    jquery "^3.0"
-    moment "^2.22.2"
-    moment-timezone "^0.4.0"
+    jquery "^3.5.0"
+    moment "~2.24.0"
+    moment-timezone "^0.5.28"
 
 terser-webpack-plugin@^1.4.3:
   version "1.4.5"
@@ -9223,9 +9557,9 @@ timers-browserify@^1.0.1:
     process "~0.11.0"
 
 timers-browserify@^2.0.4:
-  version "2.0.11"
-  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
-  integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+  version "2.0.12"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee"
+  integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==
   dependencies:
     setimmediate "^1.0.4"
 
@@ -9234,6 +9568,13 @@ timsort@^0.3.0:
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
   integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
 
+tippy.js@^6.2.0:
+  version "6.2.7"
+  resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.7.tgz#62fb34eda23f7d78151ddca922b62818c1ab9869"
+  integrity sha512-k+kWF9AJz5xLQHBi3K/XlmJiyu+p9gsCyc5qZhxxGaJWIW8SMjw1R+C7saUnP33IM8gUhDA2xX//ejRSwqR0tA==
+  dependencies:
+    "@popperjs/core" "^2.4.4"
+
 [email protected], [email protected], tmp@^0.0.33:
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9331,9 +9672,9 @@ tryer@^1.0.1:
   integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
 
 tslib@^1.9.0:
-  version "1.14.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6"
-  integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 [email protected]:
   version "2.0.1"
@@ -9411,9 +9752,9 @@ undeclared-identifiers@^1.1.2:
     xtend "^4.0.1"
 
 underscore@>=1.7.0, underscore@>=1.8.3, underscore@^1.8.0, underscore@^1.8.3, underscore@^1.9.1:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
-  integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97"
+  integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==
 
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
@@ -9608,9 +9949,9 @@ uuid@^3.0.1, uuid@^3.3.2:
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
 v8-compile-cache@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
-  integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
+  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
 
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
@@ -9660,23 +10001,23 @@ watchify@~3.11.1:
     through2 "^2.0.0"
     xtend "^4.0.0"
 
-watchpack-chokidar2@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
-  integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
+watchpack-chokidar2@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
+  integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==
   dependencies:
     chokidar "^2.1.8"
 
 watchpack@^1.7.4:
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
-  integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
+  integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
   dependencies:
     graceful-fs "^4.1.2"
     neo-async "^2.5.0"
   optionalDependencies:
     chokidar "^3.4.1"
-    watchpack-chokidar2 "^2.0.0"
+    watchpack-chokidar2 "^2.0.1"
 
 "webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#c4a3398b89588408dc705895675bce7bd7660d36":
   version "2.2.4-dev"
@@ -9706,7 +10047,7 @@ webpack-bundle-analyzer@^3.5.1:
     opener "^1.5.1"
     ws "^6.0.0"
 
-webpack-cli@^3.2.3:
+webpack-cli@^3.3.11:
   version "3.3.12"
   resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
   integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
@@ -9724,9 +10065,9 @@ webpack-cli@^3.2.3:
     yargs "^13.3.2"
 
 webpack-dev-middleware@^3.7.0:
-  version "3.7.2"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
-  integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
+  integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==
   dependencies:
     memory-fs "^0.4.1"
     mime "^2.4.4"
@@ -9743,9 +10084,9 @@ webpack-log@^2.0.0:
     uuid "^3.3.2"
 
 webpack-require-from@^1.8.0:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/webpack-require-from/-/webpack-require-from-1.8.1.tgz#18fd6fc18f0920a097a6f43855ac335f486bfb06"
-  integrity sha512-jKpr/6CH6sW3QH4E5cf/UNK4Dpsk4E1x6IpiIjeZQ9rOhr5jqpGCyFW6e/LEFpJ3U9AzRtaUHgahR7QFQnttLQ==
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/webpack-require-from/-/webpack-require-from-1.8.2.tgz#d0c6838553315bb8802f879ab43c29309f5c80fb"
+  integrity sha512-N9kDFNGNEnmuM/riUm/yNXhefAUCUG50sVV509n0WtCdKlIjMYebVwGsEujDKRRiy539J84oOZlrZim9FJXNPA==
 
 webpack-sources@^0.2.3:
   version "0.2.3"
@@ -9881,9 +10222,9 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
 
 y18n@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
-  integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
+  integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
 
 yallist@^2.1.2:
   version "2.1.2"
@@ -9900,6 +10241,11 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
+yaml@^1.7.2:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
+  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+
 yargs-parser@^13.1.2:
   version "13.1.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-16 11:37  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2021-01-16 11:37 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Thanks, patch applied.

On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
> I've fixed the issues. You can find the comments inline.
> I've also added PropTypes for the components for increased validation.
>
> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
> [email protected]> wrote:
>
>> Hi Aditya,
>>
>> The functionalities and the code looks good to me, however some of the comments as below:
>>
>>
>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>
>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>
>> be great help as we all are new to React.
>>
>> Done. Added comments to the components.
>
>>
>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>
>> Removed.
>
>>
>>    - Remove commented code
>>
>> # req_args = request.args
>> # if ('recreate' in req_args and
>> #     req_args['recreate'] == '1'):
>> #     connect = False
>>
>> Removed.
>
>>
>>    - TableNode.jsx, below two lines can be combined.
>>
>> import { PortModelAlignment, DefaultNodeModel } from
>> '@projectstorm/react-diagrams';
>> import { PortWidget } from '@projectstorm/react-diagrams';
>>
>> Done.
>
>>
>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>
>> I wanted to keep the code as it will be used in future. Anyway, I've
> removed the code.
>
>>
>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>
>> I tried but I didn't get any. Looking at the screenshot, the error is
> from the underlying library. Can't do much in this.
>
>>
>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>
>> Fixed.
>
>>
>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>
>> It will show connection lost error now. Fixed.
>
>>
>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>
>> Done.
>
>>
>>    - For large data sets, generate ERD hangs.
>>
>> It shows the spinner and waits for the response to come from the back
> end. I've used the existing table fetching code which is used at other
> places. I'll create an RM to improve the back end code for fetching the
> tables data which will help the schema diff tool as well.
>
>>
>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>
>> Fixed. Added the setting in "Tab settings".
>
>>
>>    - No shortcut is provided to open the ERD Tool.
>>
>> A shortcut is helpful if we are using it frequently. ERD tool won't be
> used that frequently. We already have a limited number of keys available
> for shortcuts. I think we should roll out without shortcut for now. If
> there is a user demand for it then we can think of adding it.
>
>>
>>    - SonarQube fixes required.
>>
>> Fixed.
>
>>
>>    -
>>
>> *Suggestion:*
>>
>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>
>> I've added a confirmation dialog which will show the number of tables and
> links selected. This way user will know what he has selected before
> deleting.
>
>> *Observations:*
>>
>> Lodash has been used in this module in place of Underscore, though the
>> dependency is already introduced by another module,
>> but we have mentioned it in the package.json file, which is somewhat not
>> convincing to me.
>> Lodash is more advanced than Underscore but we should pick anyone as it
>> will be easy to manage.
>>
>> TL;DR; we cannot.
> lodash is a peer dependency for react-diagrams (and some existing modules
> in pgAdmin) so it will come to package.json without choice. We cannot
> remove underscore because it is a dependency of backbone. Underscore is
> outdated, and I cannot migrate the complete pgAdmin code. So, I decided
> to go with 100/0 method. All the new codes will use lodash only as we'll
> phase out underscore with time. Just like jQuery vs ReactJS.
>
>>
>>
>> Table dialog code is duplicate of the table node, as it was difficult to
>> extend it because it was attached to the tree.
>> So, we need to keep in mind that while implementing React in pgAdmin, the
>> nodes should be properly detached from the tree itself, so we can reuse it.
>>
>> Yes. I agree. We need to separate out data source from UI going forward with
> React.
>
>>
>> Thanks,
>> Khushboo
>>
>>
>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>> [email protected]> wrote:
>>
>>>
>>>
>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Hi Khushboo,
>>>>
>>>> Can you please review it?
>>>>
>>>> On it.
>>>
>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>>
>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>>> details:
>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>> 2) Generate "Create" DDL from the diagram.
>>>>> 3) Save the diagram and resume it later.
>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>> many-to-many relationships, adding notes.
>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>
>>>>> Please review.
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 05:03  Aditya Toshniwal <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-18 05:03 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Hi Akshay,

I forgot to remove few of the dependencies which are not required as of now
(may be in future). Attached patch removes those dependencies from
package.json.

On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <[email protected]>
wrote:

> Thanks, patch applied.
>
> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi,
>>
>> I've fixed the issues. You can find the comments inline.
>> I've also added PropTypes for the components for increased validation.
>>
>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>> [email protected]> wrote:
>>
>>> Hi Aditya,
>>>
>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>
>>>
>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>
>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>
>>> be great help as we all are new to React.
>>>
>>> Done. Added comments to the components.
>>
>>>
>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>
>>> Removed.
>>
>>>
>>>    - Remove commented code
>>>
>>> # req_args = request.args
>>> # if ('recreate' in req_args and
>>> #     req_args['recreate'] == '1'):
>>> #     connect = False
>>>
>>> Removed.
>>
>>>
>>>    - TableNode.jsx, below two lines can be combined.
>>>
>>> import { PortModelAlignment, DefaultNodeModel } from
>>> '@projectstorm/react-diagrams';
>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>
>>> Done.
>>
>>>
>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>
>>> I wanted to keep the code as it will be used in future. Anyway, I've
>> removed the code.
>>
>>>
>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>
>>> I tried but I didn't get any. Looking at the screenshot, the error is
>> from the underlying library. Can't do much in this.
>>
>>>
>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>
>>> Fixed.
>>
>>>
>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>
>>> It will show connection lost error now. Fixed.
>>
>>>
>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>
>>> Done.
>>
>>>
>>>    - For large data sets, generate ERD hangs.
>>>
>>> It shows the spinner and waits for the response to come from the back
>> end. I've used the existing table fetching code which is used at other
>> places. I'll create an RM to improve the back end code for fetching the
>> tables data which will help the schema diff tool as well.
>>
>>>
>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>
>>> Fixed. Added the setting in "Tab settings".
>>
>>>
>>>    - No shortcut is provided to open the ERD Tool.
>>>
>>> A shortcut is helpful if we are using it frequently. ERD tool won't be
>> used that frequently. We already have a limited number of keys available
>> for shortcuts. I think we should roll out without shortcut for now. If
>> there is a user demand for it then we can think of adding it.
>>
>>>
>>>    - SonarQube fixes required.
>>>
>>> Fixed.
>>
>>>
>>>    -
>>>
>>> *Suggestion:*
>>>
>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>
>>> I've added a confirmation dialog which will show the number of tables
>> and links selected. This way user will know what he has selected before
>> deleting.
>>
>>> *Observations:*
>>>
>>> Lodash has been used in this module in place of Underscore, though the
>>> dependency is already introduced by another module,
>>> but we have mentioned it in the package.json file, which is somewhat not
>>> convincing to me.
>>> Lodash is more advanced than Underscore but we should pick anyone as it
>>> will be easy to manage.
>>>
>>> TL;DR; we cannot.
>> lodash is a peer dependency for react-diagrams (and some existing modules
>> in pgAdmin) so it will come to package.json without choice. We cannot
>> remove underscore because it is a dependency of backbone. Underscore is
>> outdated, and I cannot migrate the complete pgAdmin code. So, I decided
>> to go with 100/0 method. All the new codes will use lodash only as we'll
>> phase out underscore with time. Just like jQuery vs ReactJS.
>>
>>>
>>>
>>> Table dialog code is duplicate of the table node, as it was difficult to
>>> extend it because it was attached to the tree.
>>> So, we need to keep in mind that while implementing React in pgAdmin,
>>> the nodes should be properly detached from the tree itself, so we can reuse
>>> it.
>>>
>>> Yes. I agree. We need to separate out data source from UI going forward with
>> React.
>>
>>>
>>> Thanks,
>>> Khushboo
>>>
>>>
>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>> [email protected]> wrote:
>>>
>>>>
>>>>
>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Khushboo,
>>>>>
>>>>> Can you please review it?
>>>>>
>>>>> On it.
>>>>
>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>>
>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>>>> details:
>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>> 3) Save the diagram and resume it later.
>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>> many-to-many relationships, adding notes.
>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>
>>>>>> Please review.
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> *Thanks & Regards*
>>>>> *Akshay Joshi*
>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>
>>>>> *Mobile: +91 976-788-8246*
>>>>>
>>>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802.removedeps.patch (7.4K, 3-RM1802.removedeps.patch)
  download | inline diff:
diff --git a/web/package.json b/web/package.json
index c1cc5c492..9d4c060dc 100644
--- a/web/package.json
+++ b/web/package.json
@@ -79,7 +79,6 @@
     "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",
@@ -88,8 +87,6 @@
     "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",
diff --git a/web/yarn.lock b/web/yarn.lock
index ada962e94..551213a27 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -825,14 +825,6 @@
     "@babel/plugin-transform-react-jsx-development" "^7.12.7"
     "@babel/plugin-transform-react-pure-annotations" "^7.12.1"
 
-"@babel/runtime-corejs3@^7.9.6":
-  version "7.12.5"
-  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4"
-  integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==
-  dependencies:
-    core-js-pure "^3.0.0"
-    regenerator-runtime "^0.13.4"
-
 "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
   version "7.12.5"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
@@ -1080,11 +1072,6 @@
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
-"@types/raf@^3.4.0":
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
-  integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
-
 "@webassemblyjs/[email protected]":
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@@ -1826,11 +1813,6 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
-base64-arraybuffer@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
-  integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
-
 base64-js@^1.0.2, base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -2480,18 +2462,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7"
   integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==
 
-canvg@^3.0.7:
-  version "3.0.7"
-  resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.7.tgz#e45b87a64116af906917f7cad57d370ea372d682"
-  integrity sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==
-  dependencies:
-    "@babel/runtime-corejs3" "^7.9.6"
-    "@types/raf" "^3.4.0"
-    raf "^3.4.1"
-    rgbcolor "^1.0.1"
-    stackblur-canvas "^2.0.0"
-    svg-pathdata "^5.0.5"
-
 caw@^2.0.0, caw@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95"
@@ -2957,11 +2927,6 @@ core-js-compat@^3.8.0:
     browserslist "^4.15.0"
     semver "7.0.0"
 
-core-js-pure@^3.0.0:
-  version "3.8.1"
-  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119"
-  integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==
-
 core-js@^2.4.0:
   version "2.6.12"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
@@ -3086,13 +3051,6 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
[email protected]:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
-  integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
-  dependencies:
-    base64-arraybuffer "^0.2.0"
-
 [email protected]:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@@ -5024,18 +4982,6 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
-html-to-image@^0.1.1:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-0.1.3.tgz#9661a54c30ed9c4b269f4612416b1c7e887de3a7"
-  integrity sha512-8JTGEAAdJGL/nlp3wb/WI8fLMx2dHKOFZMdsvdon23D45ZdtsXDeRm39Wddf04ludQe3OPmvjMJ9nPjI/7hPlg==
-
-html2canvas@^1.0.0-rc.7:
-  version "1.0.0-rc.7"
-  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
-  integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
-  dependencies:
-    css-line-break "1.1.1"
-
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
@@ -8481,11 +8427,6 @@ rgba-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
 
-rgbcolor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
-  integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0=
-
 [email protected]:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@@ -9070,11 +9011,6 @@ stable@^0.1.8:
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
-stackblur-canvas@^2.0.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz#2b2eba910cb46f6feae918e1c402f863d602c01b"
-  integrity sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==
-
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -9355,7 +9291,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
-svg-pathdata@^5.0.0, svg-pathdata@^5.0.5:
+svg-pathdata@^5.0.0:
   version "5.0.5"
   resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-5.0.5.tgz#65e8d765642ba15fe15434444087d082bc526b29"
   integrity sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 07:39  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2021-01-18 07:39 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Thanks, patch applied.

On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
[email protected]> wrote:

> Hi Akshay,
>
> I forgot to remove few of the dependencies which are not required as of
> now (may be in future). Attached patch removes those dependencies from
> package.json.
>
> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, patch applied.
>>
>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi,
>>>
>>> I've fixed the issues. You can find the comments inline.
>>> I've also added PropTypes for the components for increased validation.
>>>
>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>> [email protected]> wrote:
>>>
>>>> Hi Aditya,
>>>>
>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>
>>>>
>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>
>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>
>>>> be great help as we all are new to React.
>>>>
>>>> Done. Added comments to the components.
>>>
>>>>
>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>
>>>> Removed.
>>>
>>>>
>>>>    - Remove commented code
>>>>
>>>> # req_args = request.args
>>>> # if ('recreate' in req_args and
>>>> #     req_args['recreate'] == '1'):
>>>> #     connect = False
>>>>
>>>> Removed.
>>>
>>>>
>>>>    - TableNode.jsx, below two lines can be combined.
>>>>
>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>> '@projectstorm/react-diagrams';
>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>
>>>> Done.
>>>
>>>>
>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>
>>>> I wanted to keep the code as it will be used in future. Anyway, I've
>>> removed the code.
>>>
>>>>
>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>
>>>> I tried but I didn't get any. Looking at the screenshot, the error is
>>> from the underlying library. Can't do much in this.
>>>
>>>>
>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>
>>>> It will show connection lost error now. Fixed.
>>>
>>>>
>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>
>>>> Done.
>>>
>>>>
>>>>    - For large data sets, generate ERD hangs.
>>>>
>>>> It shows the spinner and waits for the response to come from the back
>>> end. I've used the existing table fetching code which is used at other
>>> places. I'll create an RM to improve the back end code for fetching the
>>> tables data which will help the schema diff tool as well.
>>>
>>>>
>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>
>>>> Fixed. Added the setting in "Tab settings".
>>>
>>>>
>>>>    - No shortcut is provided to open the ERD Tool.
>>>>
>>>> A shortcut is helpful if we are using it frequently. ERD tool won't be
>>> used that frequently. We already have a limited number of keys
>>> available for shortcuts. I think we should roll out without shortcut for
>>> now. If there is a user demand for it then we can think of adding it.
>>>
>>>>
>>>>    - SonarQube fixes required.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -
>>>>
>>>> *Suggestion:*
>>>>
>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>
>>>> I've added a confirmation dialog which will show the number of tables
>>> and links selected. This way user will know what he has selected before
>>> deleting.
>>>
>>>> *Observations:*
>>>>
>>>> Lodash has been used in this module in place of Underscore, though the
>>>> dependency is already introduced by another module,
>>>> but we have mentioned it in the package.json file, which is somewhat
>>>> not convincing to me.
>>>> Lodash is more advanced than Underscore but we should pick anyone as it
>>>> will be easy to manage.
>>>>
>>>> TL;DR; we cannot.
>>> lodash is a peer dependency for react-diagrams (and some existing
>>> modules in pgAdmin) so it will come to package.json without choice. We
>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>
>>>>
>>>>
>>>> Table dialog code is duplicate of the table node, as it was difficult
>>>> to extend it because it was attached to the tree.
>>>> So, we need to keep in mind that while implementing React in pgAdmin,
>>>> the nodes should be properly detached from the tree itself, so we can reuse
>>>> it.
>>>>
>>>> Yes. I agree. We need to separate out data source from UI going forward
>>>  with React.
>>>
>>>>
>>>> Thanks,
>>>> Khushboo
>>>>
>>>>
>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>>
>>>>>
>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Khushboo,
>>>>>>
>>>>>> Can you please review it?
>>>>>>
>>>>>> On it.
>>>>>
>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>>
>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>>>>> details:
>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>> 3) Save the diagram and resume it later.
>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>> many-to-many relationships, adding notes.
>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>
>>>>>>> Please review.
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> *Thanks & Regards*
>>>>>> *Akshay Joshi*
>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>
>>>>>> *Mobile: +91 976-788-8246*
>>>>>>
>>>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 09:27  Aditya Toshniwal <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-18 09:27 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Hi,

The jasmine test cases are working fine on my local machine. The test cases
are successful on jenkins other than on linux, not sure why.
I have made some fixes by looking at the log. Please review and try.

On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <[email protected]>
wrote:

> Thanks, patch applied.
>
> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi Akshay,
>>
>> I forgot to remove few of the dependencies which are not required as of
>> now (may be in future). Attached patch removes those dependencies from
>> package.json.
>>
>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Thanks, patch applied.
>>>
>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi,
>>>>
>>>> I've fixed the issues. You can find the comments inline.
>>>> I've also added PropTypes for the components for increased validation.
>>>>
>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Aditya,
>>>>>
>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>
>>>>>
>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>
>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>
>>>>> be great help as we all are new to React.
>>>>>
>>>>> Done. Added comments to the components.
>>>>
>>>>>
>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>
>>>>> Removed.
>>>>
>>>>>
>>>>>    - Remove commented code
>>>>>
>>>>> # req_args = request.args
>>>>> # if ('recreate' in req_args and
>>>>> #     req_args['recreate'] == '1'):
>>>>> #     connect = False
>>>>>
>>>>> Removed.
>>>>
>>>>>
>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>
>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>> '@projectstorm/react-diagrams';
>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>
>>>>> Done.
>>>>
>>>>>
>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>
>>>>> I wanted to keep the code as it will be used in future. Anyway, I've
>>>> removed the code.
>>>>
>>>>>
>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>
>>>>> I tried but I didn't get any. Looking at the screenshot, the error is
>>>> from the underlying library. Can't do much in this.
>>>>
>>>>>
>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>
>>>>> Fixed.
>>>>
>>>>>
>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>
>>>>> It will show connection lost error now. Fixed.
>>>>
>>>>>
>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>
>>>>> Done.
>>>>
>>>>>
>>>>>    - For large data sets, generate ERD hangs.
>>>>>
>>>>> It shows the spinner and waits for the response to come from the back
>>>> end. I've used the existing table fetching code which is used at other
>>>> places. I'll create an RM to improve the back end code for fetching the
>>>> tables data which will help the schema diff tool as well.
>>>>
>>>>>
>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>
>>>>> Fixed. Added the setting in "Tab settings".
>>>>
>>>>>
>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>
>>>>> A shortcut is helpful if we are using it frequently. ERD tool won't be
>>>> used that frequently. We already have a limited number of keys
>>>> available for shortcuts. I think we should roll out without shortcut for
>>>> now. If there is a user demand for it then we can think of adding it.
>>>>
>>>>>
>>>>>    - SonarQube fixes required.
>>>>>
>>>>> Fixed.
>>>>
>>>>>
>>>>>    -
>>>>>
>>>>> *Suggestion:*
>>>>>
>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>
>>>>> I've added a confirmation dialog which will show the number of tables
>>>> and links selected. This way user will know what he has selected before
>>>> deleting.
>>>>
>>>>> *Observations:*
>>>>>
>>>>> Lodash has been used in this module in place of Underscore, though the
>>>>> dependency is already introduced by another module,
>>>>> but we have mentioned it in the package.json file, which is somewhat
>>>>> not convincing to me.
>>>>> Lodash is more advanced than Underscore but we should pick anyone as
>>>>> it will be easy to manage.
>>>>>
>>>>> TL;DR; we cannot.
>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>
>>>>>
>>>>>
>>>>> Table dialog code is duplicate of the table node, as it was difficult
>>>>> to extend it because it was attached to the tree.
>>>>> So, we need to keep in mind that while implementing React in pgAdmin,
>>>>> the nodes should be properly detached from the tree itself, so we can reuse
>>>>> it.
>>>>>
>>>>> Yes. I agree. We need to separate out data source from UI going forward
>>>>  with React.
>>>>
>>>>>
>>>>> Thanks,
>>>>> Khushboo
>>>>>
>>>>>
>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>>
>>>>>>
>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Khushboo,
>>>>>>>
>>>>>>> Can you please review it?
>>>>>>>
>>>>>>> On it.
>>>>>>
>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>>
>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>>>>>> details:
>>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>
>>>>>>>> Please review.
>>>>>>>>
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Principal Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1801.jasmine.patch (1.5K, 3-RM1801.jasmine.patch)
  download | inline diff:
diff --git a/web/regression/javascript/erd/onetomany_port_spec.js b/web/regression/javascript/erd/onetomany_port_spec.js
index 0ad4831ef..25c25937e 100644
--- a/web/regression/javascript/erd/onetomany_port_spec.js
+++ b/web/regression/javascript/erd/onetomany_port_spec.js
@@ -16,6 +16,6 @@ describe('ERD OneToManyPortModel', ()=>{
 
   it('createLinkModel', ()=>{
     let portObj = new OneToManyPortModel({options: {}});
-    expect(portObj.createLinkModel()).toBeInstanceOf(OneToManyLinkModel);
+    expect(portObj.createLinkModel() instanceof OneToManyLinkModel).toBeTruthy();
   });
 });
diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js
index d8edcdb44..1c8bf74ee 100644
--- a/web/regression/javascript/erd/table_node_spec.js
+++ b/web/regression/javascript/erd/table_node_spec.js
@@ -271,9 +271,9 @@ describe('ERD TableNodeWidget', ()=>{
 
     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();
+      expect(cols.at(0).find('.wcTabIcon').hasClass('icon-primary_key')).toBeTruthy();
+      expect(cols.at(1).find('.wcTabIcon').hasClass('icon-column')).toBeTruthy();
+      expect(cols.at(2).find('.wcTabIcon').hasClass('icon-column')).toBeTruthy();
     });
 
     it('column names', ()=>{


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 11:10  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2021-01-18 11:10 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Thanks, patch applied.

On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
> The jasmine test cases are working fine on my local machine. The test
> cases are successful on jenkins other than on linux, not sure why.
> I have made some fixes by looking at the log. Please review and try.
>
> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, patch applied.
>>
>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Akshay,
>>>
>>> I forgot to remove few of the dependencies which are not required as of
>>> now (may be in future). Attached patch removes those dependencies from
>>> package.json.
>>>
>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Thanks, patch applied.
>>>>
>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> I've fixed the issues. You can find the comments inline.
>>>>> I've also added PropTypes for the components for increased validation.
>>>>>
>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Aditya,
>>>>>>
>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>
>>>>>>
>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>
>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>
>>>>>> be great help as we all are new to React.
>>>>>>
>>>>>> Done. Added comments to the components.
>>>>>
>>>>>>
>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>
>>>>>> Removed.
>>>>>
>>>>>>
>>>>>>    - Remove commented code
>>>>>>
>>>>>> # req_args = request.args
>>>>>> # if ('recreate' in req_args and
>>>>>> #     req_args['recreate'] == '1'):
>>>>>> #     connect = False
>>>>>>
>>>>>> Removed.
>>>>>
>>>>>>
>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>
>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>> '@projectstorm/react-diagrams';
>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>
>>>>>> Done.
>>>>>
>>>>>>
>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>
>>>>>> I wanted to keep the code as it will be used in future. Anyway, I've
>>>>> removed the code.
>>>>>
>>>>>>
>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>
>>>>>> I tried but I didn't get any. Looking at the screenshot, the error is
>>>>> from the underlying library. Can't do much in this.
>>>>>
>>>>>>
>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>
>>>>>> Fixed.
>>>>>
>>>>>>
>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>
>>>>>> It will show connection lost error now. Fixed.
>>>>>
>>>>>>
>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>
>>>>>> Done.
>>>>>
>>>>>>
>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>
>>>>>> It shows the spinner and waits for the response to come from the back
>>>>> end. I've used the existing table fetching code which is used at other
>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>> tables data which will help the schema diff tool as well.
>>>>>
>>>>>>
>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>
>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>
>>>>>>
>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>
>>>>>> A shortcut is helpful if we are using it frequently. ERD tool won't
>>>>> be used that frequently. We already have a limited number of keys
>>>>> available for shortcuts. I think we should roll out without shortcut for
>>>>> now. If there is a user demand for it then we can think of adding it.
>>>>>
>>>>>>
>>>>>>    - SonarQube fixes required.
>>>>>>
>>>>>> Fixed.
>>>>>
>>>>>>
>>>>>>    -
>>>>>>
>>>>>> *Suggestion:*
>>>>>>
>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>
>>>>>> I've added a confirmation dialog which will show the number of tables
>>>>> and links selected. This way user will know what he has selected before
>>>>> deleting.
>>>>>
>>>>>> *Observations:*
>>>>>>
>>>>>> Lodash has been used in this module in place of Underscore, though
>>>>>> the dependency is already introduced by another module,
>>>>>> but we have mentioned it in the package.json file, which is somewhat
>>>>>> not convincing to me.
>>>>>> Lodash is more advanced than Underscore but we should pick anyone as
>>>>>> it will be easy to manage.
>>>>>>
>>>>>> TL;DR; we cannot.
>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>
>>>>>>
>>>>>>
>>>>>> Table dialog code is duplicate of the table node, as it was difficult
>>>>>> to extend it because it was attached to the tree.
>>>>>> So, we need to keep in mind that while implementing React in pgAdmin,
>>>>>> the nodes should be properly detached from the tree itself, so we can reuse
>>>>>> it.
>>>>>>
>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>> forward with React.
>>>>>
>>>>>>
>>>>>> Thanks,
>>>>>> Khushboo
>>>>>>
>>>>>>
>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Khushboo,
>>>>>>>>
>>>>>>>> Can you please review it?
>>>>>>>>
>>>>>>>> On it.
>>>>>>>
>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>>
>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are the
>>>>>>>>> details:
>>>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>
>>>>>>>>> Please review.
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Thanks,
>>>>>>>>> Aditya Toshniwal
>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> *Thanks & Regards*
>>>>>>>> *Akshay Joshi*
>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>
>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>
>>>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 11:37  Aditya Toshniwal <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-18 11:37 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

OK, So the changes have worked. But still failing at one more place.
Attached the patch fixes it.

On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <[email protected]>
wrote:

> Thanks, patch applied.
>
> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi,
>>
>> The jasmine test cases are working fine on my local machine. The test
>> cases are successful on jenkins other than on linux, not sure why.
>> I have made some fixes by looking at the log. Please review and try.
>>
>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Thanks, patch applied.
>>>
>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi Akshay,
>>>>
>>>> I forgot to remove few of the dependencies which are not required as of
>>>> now (may be in future). Attached patch removes those dependencies from
>>>> package.json.
>>>>
>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>> [email protected]> wrote:
>>>>
>>>>> Thanks, patch applied.
>>>>>
>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi,
>>>>>>
>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>> I've also added PropTypes for the components for increased validation.
>>>>>>
>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Aditya,
>>>>>>>
>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>
>>>>>>>
>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>
>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>
>>>>>>> be great help as we all are new to React.
>>>>>>>
>>>>>>> Done. Added comments to the components.
>>>>>>
>>>>>>>
>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>
>>>>>>> Removed.
>>>>>>
>>>>>>>
>>>>>>>    - Remove commented code
>>>>>>>
>>>>>>> # req_args = request.args
>>>>>>> # if ('recreate' in req_args and
>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>> #     connect = False
>>>>>>>
>>>>>>> Removed.
>>>>>>
>>>>>>>
>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>
>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>> '@projectstorm/react-diagrams';
>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>
>>>>>>> Done.
>>>>>>
>>>>>>>
>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>
>>>>>>> I wanted to keep the code as it will be used in future. Anyway, I've
>>>>>> removed the code.
>>>>>>
>>>>>>>
>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>
>>>>>>> I tried but I didn't get any. Looking at the screenshot, the error
>>>>>> is from the underlying library. Can't do much in this.
>>>>>>
>>>>>>>
>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>
>>>>>>> Fixed.
>>>>>>
>>>>>>>
>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>
>>>>>>> It will show connection lost error now. Fixed.
>>>>>>
>>>>>>>
>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>
>>>>>>> Done.
>>>>>>
>>>>>>>
>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>
>>>>>>> It shows the spinner and waits for the response to come from the
>>>>>> back end. I've used the existing table fetching code which is used at other
>>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>>> tables data which will help the schema diff tool as well.
>>>>>>
>>>>>>>
>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>
>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>
>>>>>>>
>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>
>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool won't
>>>>>> be used that frequently. We already have a limited number of keys
>>>>>> available for shortcuts. I think we should roll out without shortcut for
>>>>>> now. If there is a user demand for it then we can think of adding it.
>>>>>>
>>>>>>>
>>>>>>>    - SonarQube fixes required.
>>>>>>>
>>>>>>> Fixed.
>>>>>>
>>>>>>>
>>>>>>>    -
>>>>>>>
>>>>>>> *Suggestion:*
>>>>>>>
>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>
>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>> tables and links selected. This way user will know what he has selected
>>>>>> before deleting.
>>>>>>
>>>>>>> *Observations:*
>>>>>>>
>>>>>>> Lodash has been used in this module in place of Underscore, though
>>>>>>> the dependency is already introduced by another module,
>>>>>>> but we have mentioned it in the package.json file, which is somewhat
>>>>>>> not convincing to me.
>>>>>>> Lodash is more advanced than Underscore but we should pick anyone as
>>>>>>> it will be easy to manage.
>>>>>>>
>>>>>>> TL;DR; we cannot.
>>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>>>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>>>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>> can reuse it.
>>>>>>>
>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>> forward with React.
>>>>>>
>>>>>>>
>>>>>>> Thanks,
>>>>>>> Khushboo
>>>>>>>
>>>>>>>
>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Khushboo,
>>>>>>>>>
>>>>>>>>> Can you please review it?
>>>>>>>>>
>>>>>>>>> On it.
>>>>>>>>
>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Hackers,
>>>>>>>>>>
>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are
>>>>>>>>>> the details:
>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>
>>>>>>>>>> Please review.
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Thanks,
>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> *Thanks & Regards*
>>>>>>>>> *Akshay Joshi*
>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>
>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>
>>>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> *Thanks & Regards*
>>>>> *Akshay Joshi*
>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>
>>>>> *Mobile: +91 976-788-8246*
>>>>>
>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Principal Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1801.jasmine_v2.patch (654B, 3-RM1801.jasmine_v2.patch)
  download | inline diff:
diff --git a/web/regression/javascript/erd/ui_components/loader_spec.js b/web/regression/javascript/erd/ui_components/loader_spec.js
index b14ed30aa..b73b69098 100644
--- a/web/regression/javascript/erd/ui_components/loader_spec.js
+++ b/web/regression/javascript/erd/ui_components/loader_spec.js
@@ -12,7 +12,7 @@ describe('ERD Loader', ()=>{
 
   it('<Loader /> comp', ()=>{
     let loaderComp = shallow(<Loader />);
-    expect(loaderComp.isEmptyRender()).toBeTrue();
+    expect(loaderComp.isEmptyRender()).toBeTruthy();
 
     loaderComp.setProps({message: 'test message'});
     expect(loaderComp.find('.pg-sp-text').text()).toBe('test message');


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-18 11:45  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2021-01-18 11:45 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers

Thanks, patch applied.

On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
[email protected]> wrote:

> OK, So the changes have worked. But still failing at one more place.
> Attached the patch fixes it.
>
> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, patch applied.
>>
>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi,
>>>
>>> The jasmine test cases are working fine on my local machine. The test
>>> cases are successful on jenkins other than on linux, not sure why.
>>> I have made some fixes by looking at the log. Please review and try.
>>>
>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Thanks, patch applied.
>>>>
>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Akshay,
>>>>>
>>>>> I forgot to remove few of the dependencies which are not required as
>>>>> of now (may be in future). Attached patch removes those dependencies from
>>>>> package.json.
>>>>>
>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Thanks, patch applied.
>>>>>>
>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi,
>>>>>>>
>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>> I've also added PropTypes for the components for increased
>>>>>>> validation.
>>>>>>>
>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Aditya,
>>>>>>>>
>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>
>>>>>>>>
>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>
>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>
>>>>>>>> be great help as we all are new to React.
>>>>>>>>
>>>>>>>> Done. Added comments to the components.
>>>>>>>
>>>>>>>>
>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>
>>>>>>>> Removed.
>>>>>>>
>>>>>>>>
>>>>>>>>    - Remove commented code
>>>>>>>>
>>>>>>>> # req_args = request.args
>>>>>>>> # if ('recreate' in req_args and
>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>> #     connect = False
>>>>>>>>
>>>>>>>> Removed.
>>>>>>>
>>>>>>>>
>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>
>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>
>>>>>>>> Done.
>>>>>>>
>>>>>>>>
>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>
>>>>>>>> I wanted to keep the code as it will be used in future. Anyway,
>>>>>>> I've removed the code.
>>>>>>>
>>>>>>>>
>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>
>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the error
>>>>>>> is from the underlying library. Can't do much in this.
>>>>>>>
>>>>>>>>
>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>
>>>>>>>> Fixed.
>>>>>>>
>>>>>>>>
>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>
>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>
>>>>>>>>
>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>
>>>>>>>> Done.
>>>>>>>
>>>>>>>>
>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>
>>>>>>>> It shows the spinner and waits for the response to come from the
>>>>>>> back end. I've used the existing table fetching code which is used at other
>>>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>>>> tables data which will help the schema diff tool as well.
>>>>>>>
>>>>>>>>
>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>
>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>
>>>>>>>>
>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>
>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool won't
>>>>>>> be used that frequently. We already have a limited number of keys
>>>>>>> available for shortcuts. I think we should roll out without shortcut for
>>>>>>> now. If there is a user demand for it then we can think of adding it.
>>>>>>>
>>>>>>>>
>>>>>>>>    - SonarQube fixes required.
>>>>>>>>
>>>>>>>> Fixed.
>>>>>>>
>>>>>>>>
>>>>>>>>    -
>>>>>>>>
>>>>>>>> *Suggestion:*
>>>>>>>>
>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>
>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>> before deleting.
>>>>>>>
>>>>>>>> *Observations:*
>>>>>>>>
>>>>>>>> Lodash has been used in this module in place of Underscore, though
>>>>>>>> the dependency is already introduced by another module,
>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>> somewhat not convincing to me.
>>>>>>>> Lodash is more advanced than Underscore but we should pick anyone
>>>>>>>> as it will be easy to manage.
>>>>>>>>
>>>>>>>> TL;DR; we cannot.
>>>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>>>>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>>>>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>> can reuse it.
>>>>>>>>
>>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>>> forward with React.
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks,
>>>>>>>> Khushboo
>>>>>>>>
>>>>>>>>
>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>
>>>>>>>>>> Can you please review it?
>>>>>>>>>>
>>>>>>>>>> On it.
>>>>>>>>>
>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>
>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are
>>>>>>>>>>> the details:
>>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>
>>>>>>>>>>> Please review.
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>
>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>
>>>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> *Thanks & Regards*
>>>>>> *Akshay Joshi*
>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>
>>>>>> *Mobile: +91 976-788-8246*
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-20 15:50  Dave Page <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Dave Page @ 2021-01-20 15:50 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Aditya Toshniwal <[email protected]>; Khushboo Vashi <[email protected]>; pgadmin-hackers

Hi

Where's the Save Image button gone? I know Aditya was removing it whilst
working on other things, but it's still required for phase 1 release.

On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <[email protected]>
wrote:

> Thanks, patch applied.
>
> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> OK, So the changes have worked. But still failing at one more place.
>> Attached the patch fixes it.
>>
>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Thanks, patch applied.
>>>
>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi,
>>>>
>>>> The jasmine test cases are working fine on my local machine. The test
>>>> cases are successful on jenkins other than on linux, not sure why.
>>>> I have made some fixes by looking at the log. Please review and try.
>>>>
>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>> [email protected]> wrote:
>>>>
>>>>> Thanks, patch applied.
>>>>>
>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Akshay,
>>>>>>
>>>>>> I forgot to remove few of the dependencies which are not required as
>>>>>> of now (may be in future). Attached patch removes those dependencies from
>>>>>> package.json.
>>>>>>
>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Thanks, patch applied.
>>>>>>>
>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi,
>>>>>>>>
>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>> validation.
>>>>>>>>
>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Aditya,
>>>>>>>>>
>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>
>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>
>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>
>>>>>>>>> Done. Added comments to the components.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>
>>>>>>>>> Removed.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - Remove commented code
>>>>>>>>>
>>>>>>>>> # req_args = request.args
>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>> #     connect = False
>>>>>>>>>
>>>>>>>>> Removed.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>
>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>
>>>>>>>>> Done.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>
>>>>>>>>> I wanted to keep the code as it will be used in future. Anyway,
>>>>>>>> I've removed the code.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>
>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the error
>>>>>>>> is from the underlying library. Can't do much in this.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>
>>>>>>>>> Fixed.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>
>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>
>>>>>>>>> Done.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>
>>>>>>>>> It shows the spinner and waits for the response to come from the
>>>>>>>> back end. I've used the existing table fetching code which is used at other
>>>>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>>>>> tables data which will help the schema diff tool as well.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>
>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>
>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>> won't be used that frequently. We already have a limited number of
>>>>>>>> keys available for shortcuts. I think we should roll out without shortcut
>>>>>>>> for now. If there is a user demand for it then we can think of adding it.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>
>>>>>>>>> Fixed.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>    -
>>>>>>>>>
>>>>>>>>> *Suggestion:*
>>>>>>>>>
>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>
>>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>>> before deleting.
>>>>>>>>
>>>>>>>>> *Observations:*
>>>>>>>>>
>>>>>>>>> Lodash has been used in this module in place of Underscore, though
>>>>>>>>> the dependency is already introduced by another module,
>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>> somewhat not convincing to me.
>>>>>>>>> Lodash is more advanced than Underscore but we should pick anyone
>>>>>>>>> as it will be easy to manage.
>>>>>>>>>
>>>>>>>>> TL;DR; we cannot.
>>>>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So, I
>>>>>>>> decided to go with 100/0 method. All the new codes will use lodash only as
>>>>>>>> we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>> can reuse it.
>>>>>>>>>
>>>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>>>> forward with React.
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks,
>>>>>>>>> Khushboo
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>
>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>
>>>>>>>>>>> On it.
>>>>>>>>>>
>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>
>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are
>>>>>>>>>>>> the details:
>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing DB.
>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>
>>>>>>>>>>>> Please review.
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>
>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> *Thanks & Regards*
>>>>> *Akshay Joshi*
>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>
>>>>> *Mobile: +91 976-788-8246*
>>>>>
>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Principal Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EDB: http://www.enterprisedb.com


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-21 04:47  Aditya Toshniwal <[email protected]>
  parent: Dave Page <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-21 04:47 UTC (permalink / raw)
  To: Dave Page <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Khushboo Vashi <[email protected]>; pgadmin-hackers

Hi Dave,

On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:

> Hi
>
> Where's the Save Image button gone? I know Aditya was removing it whilst
> working on other things, but it's still required for phase 1 release.
>
It was not working 100% right. :(
So I've removed it for the time being. I'm still working on it.

>
> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, patch applied.
>>
>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> OK, So the changes have worked. But still failing at one more place.
>>> Attached the patch fixes it.
>>>
>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Thanks, patch applied.
>>>>
>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> The jasmine test cases are working fine on my local machine. The test
>>>>> cases are successful on jenkins other than on linux, not sure why.
>>>>> I have made some fixes by looking at the log. Please review and try.
>>>>>
>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Thanks, patch applied.
>>>>>>
>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Akshay,
>>>>>>>
>>>>>>> I forgot to remove few of the dependencies which are not required as
>>>>>>> of now (may be in future). Attached patch removes those dependencies from
>>>>>>> package.json.
>>>>>>>
>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Thanks, patch applied.
>>>>>>>>
>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi,
>>>>>>>>>
>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>> validation.
>>>>>>>>>
>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Aditya,
>>>>>>>>>>
>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>
>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>
>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>
>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>
>>>>>>>>>> Removed.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - Remove commented code
>>>>>>>>>>
>>>>>>>>>> # req_args = request.args
>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>> #     connect = False
>>>>>>>>>>
>>>>>>>>>> Removed.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>
>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>
>>>>>>>>>> Done.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>
>>>>>>>>>> I wanted to keep the code as it will be used in future. Anyway,
>>>>>>>>> I've removed the code.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>
>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>
>>>>>>>>>> Fixed.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>
>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>
>>>>>>>>>> Done.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>
>>>>>>>>>> It shows the spinner and waits for the response to come from the
>>>>>>>>> back end. I've used the existing table fetching code which is used at other
>>>>>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>>>>>> tables data which will help the schema diff tool as well.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>
>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>
>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>>> won't be used that frequently. We already have a limited number
>>>>>>>>> of keys available for shortcuts. I think we should roll out without
>>>>>>>>> shortcut for now. If there is a user demand for it then we can think of
>>>>>>>>> adding it.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>
>>>>>>>>>> Fixed.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>    -
>>>>>>>>>>
>>>>>>>>>> *Suggestion:*
>>>>>>>>>>
>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>
>>>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>>>> before deleting.
>>>>>>>>>
>>>>>>>>>> *Observations:*
>>>>>>>>>>
>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>> Lodash is more advanced than Underscore but we should pick anyone
>>>>>>>>>> as it will be easy to manage.
>>>>>>>>>>
>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>> can reuse it.
>>>>>>>>>>
>>>>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>>>>> forward with React.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks,
>>>>>>>>>> Khushboo
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>
>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>
>>>>>>>>>>>> On it.
>>>>>>>>>>>
>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below are
>>>>>>>>>>>>> the details:
>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing
>>>>>>>>>>>>> DB.
>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>
>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Thanks,
>>>>>>>>> Aditya Toshniwal
>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> *Thanks & Regards*
>>>>>>>> *Akshay Joshi*
>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>
>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> *Thanks & Regards*
>>>>>> *Akshay Joshi*
>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>
>>>>>> *Mobile: +91 976-788-8246*
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EDB: http://www.enterprisedb.com
>
>

-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-21 09:38  Dave Page <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Dave Page @ 2021-01-21 09:38 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Khushboo Vashi <[email protected]>; pgadmin-hackers

On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
[email protected]> wrote:

> Hi Dave,
>
> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>
>> Hi
>>
>> Where's the Save Image button gone? I know Aditya was removing it whilst
>> working on other things, but it's still required for phase 1 release.
>>
> It was not working 100% right. :(
> So I've removed it for the time being. I'm still working on it.
>

OK, so that work will be completed in time for the build next week?


>
>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Thanks, patch applied.
>>>
>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> OK, So the changes have worked. But still failing at one more place.
>>>> Attached the patch fixes it.
>>>>
>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>> [email protected]> wrote:
>>>>
>>>>> Thanks, patch applied.
>>>>>
>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi,
>>>>>>
>>>>>> The jasmine test cases are working fine on my local machine. The test
>>>>>> cases are successful on jenkins other than on linux, not sure why.
>>>>>> I have made some fixes by looking at the log. Please review and try.
>>>>>>
>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Thanks, patch applied.
>>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Akshay,
>>>>>>>>
>>>>>>>> I forgot to remove few of the dependencies which are not required
>>>>>>>> as of now (may be in future). Attached patch removes those dependencies
>>>>>>>> from package.json.
>>>>>>>>
>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Thanks, patch applied.
>>>>>>>>>
>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi,
>>>>>>>>>>
>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>> validation.
>>>>>>>>>>
>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>
>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>
>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>
>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>
>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>
>>>>>>>>>>> Removed.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>
>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>> #     connect = False
>>>>>>>>>>>
>>>>>>>>>>> Removed.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>
>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>
>>>>>>>>>>> Done.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>
>>>>>>>>>>> I wanted to keep the code as it will be used in future. Anyway,
>>>>>>>>>> I've removed the code.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>
>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>
>>>>>>>>>>> Fixed.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>
>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>
>>>>>>>>>>> Done.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>
>>>>>>>>>>> It shows the spinner and waits for the response to come from the
>>>>>>>>>> back end. I've used the existing table fetching code which is used at other
>>>>>>>>>> places. I'll create an RM to improve the back end code for fetching the
>>>>>>>>>> tables data which will help the schema diff tool as well.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>
>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>
>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>>>> won't be used that frequently. We already have a limited number
>>>>>>>>>> of keys available for shortcuts. I think we should roll out without
>>>>>>>>>> shortcut for now. If there is a user demand for it then we can think of
>>>>>>>>>> adding it.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>
>>>>>>>>>>> Fixed.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>    -
>>>>>>>>>>>
>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>
>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>
>>>>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>>>>> before deleting.
>>>>>>>>>>
>>>>>>>>>>> *Observations:*
>>>>>>>>>>>
>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>
>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some existing
>>>>>>>>>> modules in pgAdmin) so it will come to package.json without choice. We
>>>>>>>>>> cannot remove underscore because it is a dependency of backbone. Underscore
>>>>>>>>>> is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>>> can reuse it.
>>>>>>>>>>>
>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>>>>>> forward with React.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Khushboo
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>
>>>>>>>>>>>>> On it.
>>>>>>>>>>>>
>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below
>>>>>>>>>>>>>> are the details:
>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing
>>>>>>>>>>>>>> DB.
>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Thanks,
>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> *Thanks & Regards*
>>>>>>>>> *Akshay Joshi*
>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>
>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> *Thanks & Regards*
>>>>> *Akshay Joshi*
>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>
>>>>> *Mobile: +91 976-788-8246*
>>>>>
>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Principal Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EDB: http://www.enterprisedb.com
>>
>>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EDB: http://www.enterprisedb.com


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-21 11:02  Aditya Toshniwal <[email protected]>
  parent: Dave Page <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-21 11:02 UTC (permalink / raw)
  To: Dave Page <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Khushboo Vashi <[email protected]>; pgadmin-hackers

Hi,


On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:

>
>
> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi Dave,
>>
>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>
>>> Hi
>>>
>>> Where's the Save Image button gone? I know Aditya was removing it whilst
>>> working on other things, but it's still required for phase 1 release.
>>>
>> It was not working 100% right. :(
>> So I've removed it for the time being. I'm still working on it.
>>
>
> OK, so that work will be completed in time for the build next week?
>
I'm trying my best to make it available before release. I'm struggling to
make it work perfectly.

>
>
>>
>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Thanks, patch applied.
>>>>
>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> OK, So the changes have worked. But still failing at one more place.
>>>>> Attached the patch fixes it.
>>>>>
>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Thanks, patch applied.
>>>>>>
>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi,
>>>>>>>
>>>>>>> The jasmine test cases are working fine on my local machine. The
>>>>>>> test cases are successful on jenkins other than on linux, not sure why.
>>>>>>> I have made some fixes by looking at the log. Please review and try.
>>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Thanks, patch applied.
>>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Akshay,
>>>>>>>>>
>>>>>>>>> I forgot to remove few of the dependencies which are not required
>>>>>>>>> as of now (may be in future). Attached patch removes those dependencies
>>>>>>>>> from package.json.
>>>>>>>>>
>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>
>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi,
>>>>>>>>>>>
>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>> validation.
>>>>>>>>>>>
>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>
>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>
>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>
>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>
>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>
>>>>>>>>>>>> Removed.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>
>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>
>>>>>>>>>>>> Removed.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>
>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>
>>>>>>>>>>>> Done.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>
>>>>>>>>>>>> I wanted to keep the code as it will be used in future. Anyway,
>>>>>>>>>>> I've removed the code.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>
>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>
>>>>>>>>>>>> Fixed.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>
>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>
>>>>>>>>>>>> Done.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>
>>>>>>>>>>>> It shows the spinner and waits for the response to come from
>>>>>>>>>>> the back end. I've used the existing table fetching code which is used at
>>>>>>>>>>> other places. I'll create an RM to improve the back end code for fetching
>>>>>>>>>>> the tables data which will help the schema diff tool as well.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>
>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>
>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>>>>> won't be used that frequently. We already have a limited number
>>>>>>>>>>> of keys available for shortcuts. I think we should roll out without
>>>>>>>>>>> shortcut for now. If there is a user demand for it then we can think of
>>>>>>>>>>> adding it.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>
>>>>>>>>>>>> Fixed.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>    -
>>>>>>>>>>>>
>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>
>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>
>>>>>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>>>>>> before deleting.
>>>>>>>>>>>
>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>
>>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>
>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>>>> can reuse it.
>>>>>>>>>>>>
>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI going
>>>>>>>>>>> forward with React.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below
>>>>>>>>>>>>>>> are the details:
>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an existing
>>>>>>>>>>>>>>> DB.
>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>
>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Thanks,
>>>>>>>>> Aditya Toshniwal
>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> *Thanks & Regards*
>>>>>>>> *Akshay Joshi*
>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>
>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> *Thanks & Regards*
>>>>>> *Akshay Joshi*
>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>
>>>>>> *Mobile: +91 976-788-8246*
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EDB: http://www.enterprisedb.com
>>>
>>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EDB: http://www.enterprisedb.com
>
>

-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-25 11:42  Aditya Toshniwal <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-25 11:42 UTC (permalink / raw)
  To: pgadmin-hackers; +Cc: Akshay Joshi <[email protected]>; Khushboo Vashi <[email protected]>; Dave Page <[email protected]>

Hi Hackers,

Attached is the patch to fix below issues in ERD:

   1. After opening an existing project, the first table is already
   selected but edit, clone, delete buttons are disable. Fixed.
   2. ERD project title gets changed when 2 ERD projects are open & anyone
   of it edited. Fixed.
   3. Closing ERD tab, does not ask for confirmation pop up. Added.
   4. Shortcut for 'Show more/Fewer details' is missing. Added.
   5. Deleting primary key does not delete associated links. Fixed.
   6. Long table & schema name are getting out of box. Fixed.
   7. Long table name in notes pop-up need re-alignment. Fixed.
   8. Same table name present in ERD/canvas is allowed in Add Table dialogue.
   Added validation in the dialog.
   9. Download image option is added, but it is not perfect yet. Image
   icons (table, schema, etc.) are not showing up.
   10. Rename panel option should be disabled by default. It should be
   enabled for the tools which implement rename functionality.
   11. The Toolbar is not visible in Safari for the ERD tool. Fixed.

Please review.

On Thu, Jan 21, 2021 at 4:32 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
>
> On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:
>
>>
>>
>> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Dave,
>>>
>>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>>
>>>> Hi
>>>>
>>>> Where's the Save Image button gone? I know Aditya was removing it
>>>> whilst working on other things, but it's still required for phase 1 release.
>>>>
>>> It was not working 100% right. :(
>>> So I've removed it for the time being. I'm still working on it.
>>>
>>
>> OK, so that work will be completed in time for the build next week?
>>
> I'm trying my best to make it available before release. I'm struggling to
> make it work perfectly.
>
>>
>>
>>>
>>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>>> [email protected]> wrote:
>>>>
>>>>> Thanks, patch applied.
>>>>>
>>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> OK, So the changes have worked. But still failing at one more place.
>>>>>> Attached the patch fixes it.
>>>>>>
>>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Thanks, patch applied.
>>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi,
>>>>>>>>
>>>>>>>> The jasmine test cases are working fine on my local machine. The
>>>>>>>> test cases are successful on jenkins other than on linux, not sure why.
>>>>>>>> I have made some fixes by looking at the log. Please review and try.
>>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Thanks, patch applied.
>>>>>>>>>
>>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Akshay,
>>>>>>>>>>
>>>>>>>>>> I forgot to remove few of the dependencies which are not required
>>>>>>>>>> as of now (may be in future). Attached patch removes those dependencies
>>>>>>>>>> from package.json.
>>>>>>>>>>
>>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>
>>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi,
>>>>>>>>>>>>
>>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>>> validation.
>>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>>
>>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>>
>>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>>
>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>>
>>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>>
>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>>
>>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>>
>>>>>>>>>>>>> Done.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I wanted to keep the code as it will be used in future.
>>>>>>>>>>>> Anyway, I've removed the code.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>>
>>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Done.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>>
>>>>>>>>>>>>> It shows the spinner and waits for the response to come from
>>>>>>>>>>>> the back end. I've used the existing table fetching code which is used at
>>>>>>>>>>>> other places. I'll create an RM to improve the back end code for fetching
>>>>>>>>>>>> the tables data which will help the schema diff tool as well.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>>
>>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>>>>>> won't be used that frequently. We already have a limited
>>>>>>>>>>>> number of keys available for shortcuts. I think we should roll out without
>>>>>>>>>>>> shortcut for now. If there is a user demand for it then we can think of
>>>>>>>>>>>> adding it.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>    -
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>>
>>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I've added a confirmation dialog which will show the number of
>>>>>>>>>>>> tables and links selected. This way user will know what he has selected
>>>>>>>>>>>> before deleting.
>>>>>>>>>>>>
>>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>>
>>>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>>
>>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>>>>> can reuse it.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI
>>>>>>>>>>>> going forward with React.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below
>>>>>>>>>>>>>>>> are the details:
>>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an
>>>>>>>>>>>>>>>> existing DB.
>>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>
>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Thanks,
>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> *Thanks & Regards*
>>>>>>>>> *Akshay Joshi*
>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>
>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> *Thanks & Regards*
>>>>> *Akshay Joshi*
>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>
>>>>> *Mobile: +91 976-788-8246*
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EDB: http://www.enterprisedb.com
>>>>
>>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EDB: http://www.enterprisedb.com
>>
>>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802.fixes_v1.patch (68.5K, 3-RM1802.fixes_v1.patch)
  download | inline diff:
diff --git a/web/package.json b/web/package.json
index 9d4c060dc..9e55eb674 100644
--- a/web/package.json
+++ b/web/package.json
@@ -46,6 +46,7 @@
     "prop-types": "^15.7.2",
     "raw-loader": "^3.1.0",
     "resize-observer-polyfill": "^1.5.1",
+    "resolve-url-loader": "^3.1.2",
     "sass": "^1.24.4",
     "sass-loader": "^7.1.0",
     "sass-resources-loader": "^2.0.0",
@@ -87,6 +88,7 @@
     "dagre": "^0.8.4",
     "dropzone": "^5.5.1",
     "exports-loader": "~0.7.0",
+    "html2canvas": "^1.0.0-rc.7",
     "immutability-helper": "^3.0.0",
     "imports-loader": "^0.8.0",
     "ip-address": "^5.8.9",
diff --git a/web/pgadmin/browser/static/js/frame.js b/web/pgadmin/browser/static/js/frame.js
index b72df7b0f..0d3b91273 100644
--- a/web/pgadmin/browser/static/js/frame.js
+++ b/web/pgadmin/browser/static/js/frame.js
@@ -30,7 +30,7 @@ define([
     height: 600,
     showTitle: true,
     isClosable: true,
-    isRenamable: true,
+    isRenamable: false,
     isPrivate: false,
     url: '',
     icon: '',
diff --git a/web/pgadmin/static/js/chartjs/index.jsx b/web/pgadmin/static/js/chartjs/index.jsx
index 52623bd24..022e63875 100644
--- a/web/pgadmin/static/js/chartjs/index.jsx
+++ b/web/pgadmin/static/js/chartjs/index.jsx
@@ -41,11 +41,11 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
       options: optionsMerged,
     });
     props.onInit && props.onInit(chartObj.current);
-  }
+  };
 
   const destroyChart = function() {
     chartObj.current && chartObj.current.destroy();
-  }
+  };
 
   useEffect(()=>{
     initChart();
@@ -72,7 +72,7 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
       destroyChart();
       initChart();
     }
-  }, [redraw])
+  }, [redraw]);
 
   return (
     <canvas id={id} ref={chartRef}></canvas>
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index a7d056683..c9b8b5b7e 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -180,7 +180,7 @@ define('pgadmin.datagrid', [
           name: 'frm_datagrid',
           showTitle: true,
           isCloseable: true,
-          isRenameable: true,
+          isRenamable: true,
           isPrivate: true,
           url: 'about:blank',
         });
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
index 46db6a70a..b3bd793e0 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
@@ -78,11 +78,11 @@ export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_fi
 
 export function set_renamable_option(panel, is_file) {
   if(is_file || is_file == 'true') {
-    panel._isRenamable = false;
+    panel.renamable(false);
     $('.conn-info-dd').hide();
     $('.connection-data').css({pointerEvents: 'none', cursor: 'arrow'});
   } else {
-    panel._isRenamable = true;
+    panel.renamable(true);
   }
 }
 
diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py
index 301aca9c9..1ce742186 100644
--- a/web/pgadmin/tools/erd/__init__.py
+++ b/web/pgadmin/tools/erd/__init__.py
@@ -288,6 +288,24 @@ class ERDModule(PgAdminModule):
             fields=shortcut_fields
         )
 
+        self.preference.register(
+            'keyboard_shortcuts',
+            'show_details',
+            gettext('Show more/fewer details'),
+            'keyboardshortcut',
+            {
+                'alt': True,
+                'shift': False,
+                'control': True,
+                'key': {
+                    'key_code': 84,
+                    'char': 't'
+                }
+            },
+            category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
+            fields=shortcut_fields
+        )
+
         self.preference.register(
             'keyboard_shortcuts',
             'zoom_to_fit',
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
index ea0e22a29..8faae4d01 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
@@ -212,14 +212,14 @@ export default class ERDCore {
     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}));
+      sourcePort = sourceNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'one', alignment:PortModelAlignment.RIGHT}));
     }
 
     portName = targetNode.getPortName(data.local_column_attnum);
     let targetPort = targetNode.getPort(portName);
     /* Create the port if not there */
     if(!targetPort) {
-      targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, alignment:PortModelAlignment.RIGHT}));
+      targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'many', alignment:PortModelAlignment.RIGHT}));
     }
 
     /* Link the ports */
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
index e1fee815f..2b56ea1c4 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
@@ -50,7 +50,7 @@ export default class TableDialog {
     return 'entity_dialog';
   }
 
-  getDataModel(attributes, colTypes, schemas, sVersion) {
+  getDataModel(attributes, existingTables, colTypes, schemas, sVersion) {
     let dialogObj = this;
     let columnsModel = this.pgBrowser.DataModel.extend({
       idAttribute: 'attnum',
@@ -694,6 +694,10 @@ export default class TableDialog {
           msg = gettext('Table name cannot be empty.');
           this.errorModel.set('name', msg);
           return msg;
+        } else if(_.findIndex(existingTables, (table)=>table[0]==schema&&table[1]==name) >= 0) {
+          msg = gettext('Table name already exists.');
+          this.errorModel.set('name', msg);
+          return msg;
         }
         this.errorModel.unset('name');
         if (
@@ -705,6 +709,8 @@ export default class TableDialog {
           return msg;
         }
         this.errorModel.unset('schema');
+
+
         return null;
       },
     });
@@ -731,9 +737,9 @@ export default class TableDialog {
     return Alertify[dialogName];
   }
 
-  show(title, attributes, colTypes, schemas, sVersion, callback) {
+  show(title, attributes, existingTables, 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);
+    dialog(dialogTitle, this.getDataModel(attributes, existingTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
   }
 }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
index 523ebd14f..e865c29bd 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/index.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
@@ -9,10 +9,13 @@
 
 import React from 'react';
 import ReactDOM from 'react-dom';
+import _ from 'lodash';
+
 import BodyWidget from './ui_components/BodyWidget';
 import getDialog, {transformToSupported} from './dialogs';
 import Alertify from 'pgadmin.alertifyjs';
 import pgWindow from 'sources/window';
+import pgAdmin from 'sources/pgadmin';
 
 export default class ERDTool {
   constructor(container, params) {
@@ -20,10 +23,28 @@ export default class ERDTool {
     this.params = params;
   }
 
+  getPreferencesForModule() {
+
+  }
+
   render() {
     /* Mount the React ERD tool to the container */
+    let panel = null;
+    _.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
+      if (p.isVisible()) {
+        panel = p;
+      }
+    });
+
     ReactDOM.render(
-      <BodyWidget params={this.params} getDialog={getDialog} transformToSupported={transformToSupported} pgAdmin={pgWindow.pgAdmin} alertify={Alertify} />,
+      <BodyWidget
+        params={this.params}
+        getDialog={getDialog}
+        transformToSupported={transformToSupported}
+        pgWindow={pgWindow}
+        pgAdmin={pgAdmin}
+        panel={panel}
+        alertify={Alertify} />,
       this.container
     );
   }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
index be985afde..37b456825 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
@@ -9,12 +9,12 @@
 
 import React from 'react';
 import {
-    RightAngleLinkModel,
-    RightAngleLinkWidget,
-    DefaultLinkFactory,
-    PortModelAlignment,
-    LinkWidget,
-    PointModel
+  RightAngleLinkModel,
+  RightAngleLinkWidget,
+  DefaultLinkFactory,
+  PortModelAlignment,
+  LinkWidget,
+  PointModel,
 } from '@projectstorm/react-diagrams';
 import {Point} from '@projectstorm/geometry';
 import _ from 'lodash';
@@ -24,7 +24,7 @@ export const OneToManyModel = {
   local_column_attnum: undefined,
   referenced_table_uid: undefined,
   referenced_column_attnum: undefined,
-}
+};
 
 export class OneToManyLinkModel extends RightAngleLinkModel {
   constructor({data, ...options}) {
@@ -33,7 +33,7 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
       width: 1,
       class: 'link-onetomany',
       locked: true,
-      ...options
+      ...options,
     });
 
     this._data = {
@@ -62,13 +62,13 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
         '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()
+      data: this.getData(),
     };
   }
 }
@@ -83,13 +83,13 @@ const CustomLinkEndWidget = props => {
           <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 + ')'}>
@@ -111,21 +111,21 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
     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;
+    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];
   }
@@ -146,8 +146,8 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
       point: point,
       rotation: rotation,
       tx: tx,
-      ty: ty
-    }
+      ty: ty,
+    };
   }
 
   generateCustomEndWidget({type, point, rotation, tx, ty}) {
@@ -183,7 +183,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
   }
 
   handleMove = function(event) {
-    this.props.link.getTargetPort()
+    this.props.link.getTargetPort();
     this.draggingEvent(event, this.dragging_index);
     this.props.link.fireEvent({}, 'positionChanged');
   }.bind(this);
@@ -220,7 +220,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
       this.props.link.addPoint(
         new PointModel({
           link: this.props.link,
-          position: new Point(onePoint.point.getX(), manyPoint.point.getY())
+          position: new Point(onePoint.point.getX(), manyPoint.point.getY()),
         })
       );
     }
@@ -246,7 +246,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
             onMouseEnter: (event) => {
               this.setState({ selected: true });
               this.props.link.lastHoverIndexOfPath = j;
-            }
+            },
           },
           j
         )
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
index 5e74d890b..383c621ce 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
@@ -19,7 +19,7 @@ export class TableNodeModel extends DefaultNodeModel {
   constructor({otherInfo, ...options}) {
     super({
       ...options,
-      type: TYPE
+      type: TYPE,
     });
 
     this._note = otherInfo.note || '';
@@ -60,22 +60,22 @@ export class TableNodeModel extends DefaultNodeModel {
 
   cloneData(name) {
     let newData = {
-      ...this.getData()
+      ...this.getData(),
     };
     if(name) {
-      newData['name'] = name
+      newData['name'] = name;
     }
     return newData;
   }
 
   setData(data) {
     let self = this;
-    /* Remove the links if column dropped */
+    /* Remove the links if column dropped or primary key removed */
     _.differenceWith(this._data.columns, data.columns, function(existing, incoming) {
-      return existing.attnum == incoming.attnum;
+      return existing.attnum == incoming.attnum && incoming.is_primary_key == true;
     }).forEach((col)=>{
       let existPort = self.getPort(self.getPortName(col.attnum));
-      if(existPort) {
+      if(existPort && existPort.getSubtype() == 'one') {
         existPort.removeAllLinks();
         self.removePort(existPort);
       }
@@ -109,7 +109,7 @@ export class TableNodeModel extends DefaultNodeModel {
       otherInfo: {
         data: this.getData(),
         note: this.getNote(),
-      }
+      },
     };
   }
 }
@@ -119,8 +119,8 @@ export class TableNodeWidget extends React.Component {
     super(props);
 
     this.state = {
-      show_details: true
-    }
+      show_details: true,
+    };
 
     this.props.node.registerListener({
       toggleDetails: (event) => {
@@ -143,13 +143,13 @@ export class TableNodeWidget extends React.Component {
         </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} />
+        <PortWidget engine={this.props.engine} port={port} key={port.getID()} className={'port-' + port.options.alignment} />
       );
     }
     return <></>;
@@ -163,21 +163,21 @@ export class TableNodeWidget extends React.Component {
   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-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')
+              this.props.node.fireEvent({}, 'showNote');
             }} title="Check note" />}
         </div>
-        <div className="table-schema">
-          <span className="wcTabIcon icon-schema"></span>
-          {node_data.schema}
+        <div className="d-flex table-schema-data">
+          <div className="table-icon-schema"></div>
+          <div className="table-schema">{node_data.schema}</div>
         </div>
-        <div className="table-name">
-          <span className="wcTabIcon icon-table"></span>
-          {node_data.name}
+        <div className="d-flex table-name-data">
+          <div><span className="wcTabIcon icon-table"></span></div>
+          <div className="table-name">{node_data.name}</div>
         </div>
         <div className="table-cols">
           {_.map(node_data.columns, (col)=>this.generateColumn(col))}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
index c412acd77..9bcbdc415 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
@@ -16,6 +16,7 @@ const TYPE = 'onetomany';
 export default class OneToManyPortModel extends PortModel {
   constructor({options}) {
     super({
+      subtype: 'notset',
       ...options,
       type: TYPE,
     });
@@ -30,6 +31,22 @@ export default class OneToManyPortModel extends PortModel {
   createLinkModel() {
     return new OneToManyLinkModel({});
   }
+
+  getSubtype() {
+    return this.options.subtype;
+  }
+
+  deserialize(event) {
+    super.deserialize(event);
+    this.options.subtype = event.data.subtype || 'notset';
+  }
+
+  serialize() {
+    return {
+      ...super.serialize(),
+      subtype: this.options.subtype,
+    };
+  }
 }
 
 export class OneToManyPortFactory extends AbstractModelFactory {
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
index 143654787..a85e008b6 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
@@ -12,6 +12,10 @@ 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 _ from 'lodash';
+import html2canvas from 'html2canvas';
+import * as htmlToImage from 'html-to-image';
+
 
 import ERDCore from '../ERDCore';
 import ToolBar, {IconButton, DetailsToggleButton, ButtonGroup} from './ToolBar';
@@ -22,22 +26,25 @@ 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';
+import 'wcdocker';
 
 /* 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 = {};
+    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];
+    for(let i=0; i<shortcut_handlers.length; i++){
+      let [key, handler] = shortcut_handlers[i];
+      if(key) {
         this.shortcuts[this.shortcutKey(key.alt, key.control, key.shift, false, key.key.key_code)] = handler;
       }
+    }
   }
 
   shortcutKey(altKey, ctrlKey, shiftKey, metaKey, keyCode) {
@@ -69,9 +76,12 @@ export default class BodyWidget extends React.Component {
       current_file: null,
       dirty: false,
       show_details: true,
+      is_new_tab: false,
       preferences: {},
-    }
+    };
     this.diagram = new ERDCore();
+    /* Flag for checking if user has opted for save before close */
+    this.closeOnSave = React.createRef();
     this.fileInputRef = React.createRef();
     this.diagramContainerRef = React.createRef();
     this.canvasEle = null;
@@ -83,6 +93,7 @@ export default class BodyWidget extends React.Component {
     this.onSaveDiagram = this.onSaveDiagram.bind(this);
     this.onSaveAsDiagram = this.onSaveAsDiagram.bind(this);
     this.onSQLClick = this.onSQLClick.bind(this);
+    this.onImageClick = this.onImageClick.bind(this);
     this.onAddNewNode = this.onAddNewNode.bind(this);
     this.onEditNode = this.onEditNode.bind(this);
     this.onCloneNode = this.onCloneNode.bind(this);
@@ -133,7 +144,7 @@ export default class BodyWidget extends React.Component {
       },
       'editNode': (event) => {
         this.addEditNode(event.node);
-      }
+      },
     };
     Object.keys(diagramEvents).forEach(eventName => {
       this.diagram.registerModelEvent(eventName, diagramEvents[eventName]);
@@ -157,9 +168,10 @@ export default class BodyWidget extends React.Component {
       [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.show_details, this.onDetailsToggle],
       [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.state.preferences.zoom_out, this.diagram.zoomOut],
     ]);
 
     this.diagram.registerKeyAction(this.keyboardActionObj);
@@ -182,11 +194,16 @@ export default class BodyWidget extends React.Component {
   }
 
   async componentDidMount() {
-    this.setLoading('Preparing');
-    this.setTitle(this.state.current_file);
+    this.setLoading(gettext('Preparing...'));
+
     this.setState({
-      preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
-    }, this.registerKeyboardShortcuts);
+      preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
+      is_new_tab: (this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('browser').new_browser_tab_open || '')
+        .includes('erd_tool'),
+    }, ()=>{
+      this.registerKeyboardShortcuts();
+      this.setTitle(this.state.current_file);
+    });
     this.registerModelEvents();
     this.realignGrid({
       backgroundSize: '45px 45px',
@@ -197,10 +214,19 @@ export default class BodyWidget extends React.Component {
     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')
+        preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
       }, ()=>this.registerKeyboardShortcuts());
     });
 
+    this.props.panel?.on(window.wcDocker?.EVENT.CLOSING, () => {
+      if(this.state.dirty) {
+        this.closeOnSave = false;
+        this.confirmBeforeClose();
+        return false;
+      }
+      return true;
+    });
+
     let done = await this.initConnection();
     if(!done) return;
 
@@ -218,18 +244,78 @@ export default class BodyWidget extends React.Component {
     }
   }
 
+  confirmBeforeClose() {
+    let bodyObj = this;
+    this.props.alertify.confirmSave || this.props.alertify.dialog('confirmSave', function() {
+      return {
+        main: function(title, message) {
+          this.setHeader(title);
+          this.setContent(message);
+        },
+        setup: function() {
+          return {
+            buttons: [{
+              text: gettext('Cancel'),
+              key: 27, // ESC
+              invokeOnClose: true,
+              className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
+            }, {
+              text: gettext('Don\'t save'),
+              className: 'btn btn-secondary fa fa-lg fa-trash-alt pg-alertify-button',
+            }, {
+              text: gettext('Save'),
+              className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+            }],
+            focus: {
+              element: 0,
+              select: false,
+            },
+            options: {
+              maximizable: false,
+              resizable: false,
+            },
+          };
+        },
+        callback: function(closeEvent) {
+          switch (closeEvent.index) {
+          case 0: // Cancel
+            //Do nothing.
+            break;
+          case 1: // Don't Save
+            bodyObj.closePanel();
+            break;
+          case 2: //Save
+            bodyObj.onSaveDiagram(false, true);
+            break;
+          }
+        },
+      };
+    });
+    this.props.alertify.confirmSave(gettext('Save changes?'), gettext('The diagram has changed. Do you want to save changes?'));
+    return false;
+  }
+
+  closePanel() {
+    window.onbeforeunload = null;
+    this.props.panel.off(wcDocker.EVENT.CLOSING);
+    this.props.pgWindow.pgAdmin.Browser.docker.removePanel(this.props.panel);
+  }
+
   getDialog(dialogName) {
     if(dialogName === 'entity_dialog') {
+      let existingTables = this.diagram.getModel().getNodes().map((node)=>{
+        return node.getSchemaTableName();
+      });
       return (title, attributes, callback)=>{
-          this.props.getDialog(dialogName).show(
-            title, attributes, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
-          );
+        this.props.getDialog(dialogName).show(
+          title, attributes, existingTables, 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
-          );
+        this.props.getDialog(dialogName).show(
+          title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
+        );
       };
     }
   }
@@ -289,20 +375,20 @@ export default class BodyWidget extends React.Component {
       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();
-        },
-        () => {}
+      () => {
+        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();
+      },
+      () => {}
     );
   }
 
@@ -312,11 +398,11 @@ export default class BodyWidget extends React.Component {
 
   onDetailsToggle() {
     this.setState((prevState)=>({
-      show_details: !prevState.show_details
+      show_details: !prevState.show_details,
     }), ()=>{
       this.diagram.getModel().getNodes().forEach((node)=>{
         node.fireEvent({show_details: this.state.show_details}, 'toggleDetails');
-      })
+      });
     });
   }
 
@@ -335,8 +421,9 @@ export default class BodyWidget extends React.Component {
   }
 
   openFile(fileName) {
+    this.setLoading(gettext('Loading project...'));
     axios.post(url_for('sqleditor.load_file'), {
-      'file_name': decodeURI(fileName)
+      'file_name': decodeURI(fileName),
     }).then((res)=>{
       this.setState({
         current_file: fileName,
@@ -344,13 +431,17 @@ export default class BodyWidget extends React.Component {
       });
       this.setTitle(fileName);
       this.diagram.deserialize(res.data);
+      this.diagram.clearSelection();
       this.registerModelEvents();
     }).catch((err)=>{
       this.handleAxiosCatch(err);
+    }).then(()=>{
+      this.setLoading(null);
     });
   }
 
-  onSaveDiagram(isSaveAs=false) {
+  onSaveDiagram(isSaveAs=false, closeOnSave=false) {
+    this.closeOnSave = closeOnSave;
     if(this.state.current_file && !isSaveAs) {
       this.saveFile(this.state.current_file);
     } else {
@@ -370,9 +461,10 @@ export default class BodyWidget extends React.Component {
   }
 
   saveFile(fileName) {
+    this.setLoading(gettext('Saving...'));
     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))
+      '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({
@@ -380,7 +472,12 @@ export default class BodyWidget extends React.Component {
         dirty: false,
       });
       this.setTitle(fileName);
+      this.setLoading(null);
+      if(this.closeOnSave) {
+        this.closePanel.call(this);
+      }
     }).catch((err)=>{
+      this.setLoading(null);
       this.handleAxiosCatch(err);
     });
   }
@@ -395,14 +492,10 @@ export default class BodyWidget extends React.Component {
       title = 'Untitled';
     }
     title = this.getCurrentProjectName(title) + (dirty ? '*': '');
-    if (this.new_browser_tab) {
+    if (this.state.is_new_tab) {
       window.document.title = title;
     } else {
-      _.each(this.props.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
-        if (p.isVisible()) {
-          setPanelTitle(p, title);
-        }
-      });
+      setPanelTitle(this.props.panel, title);
     }
   }
 
@@ -414,7 +507,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     this.setLoading(gettext('Preparing the SQL...'));
@@ -428,18 +521,53 @@ export default class BodyWidget extends React.Component {
           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);
+        showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.DataGrid, this.props.alertify);
       })
       .catch((error)=>{
         this.handleAxiosCatch(error);
       })
       .then(()=>{
         this.setLoading(null);
-      })
+      });
+  }
+
+  onImageClick() {
+    this.setLoading(gettext('Preparing the image...'));
+
+    /* Change the styles for suiting html2canvas */
+    this.canvasEle.classList.add('html2canvas-reset');
+    this.canvasEle.style.width = this.canvasEle.scrollWidth + 'px';
+    this.canvasEle.style.height = this.canvasEle.scrollHeight + 'px';
+
+    html2canvas(this.canvasEle, {
+      width: this.canvasEle.scrollWidth + 10,
+      height: this.canvasEle.scrollHeight + 10,
+      scrollX: 0,
+      scrollY: 0,
+      useCORS: true,
+      allowTaint: true,
+      backgroundColor: window.getComputedStyle(this.canvasEle).backgroundColor,
+    }).then((canvas)=>{
+      let link = document.createElement('a');
+      link.setAttribute('href', canvas.toDataURL('image/png'));
+      link.setAttribute('download', this.getCurrentProjectName() + '.png');
+      link.click();
+    }).catch((err)=>{
+      console.error(err);
+      this.props.alertify.alert()
+        .set('title', gettext('Error'))
+        .set('message', err).show();
+    }).then(()=>{
+      /* Revert back to the original CSS styles */
+      this.canvasEle.classList.remove('html2canvas-reset');
+      this.canvasEle.style.width = '';
+      this.canvasEle.style.height = '';
+      this.setLoading(null);
+    });
   }
 
   onOneToManyClick() {
@@ -473,8 +601,8 @@ export default class BodyWidget extends React.Component {
           '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);
@@ -484,7 +612,7 @@ export default class BodyWidget extends React.Component {
         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 = {
@@ -492,7 +620,7 @@ export default class BodyWidget extends React.Component {
         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');
 
@@ -505,7 +633,7 @@ export default class BodyWidget extends React.Component {
       this.noteRefEle = this.diagram.getEngine().getNodeElement(noteNode);
       this.setState({
         note_node: noteNode,
-        note_open: true
+        note_open: true,
       });
     }
   }
@@ -528,14 +656,14 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      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
+        server_version: response.data.data.serverVersion,
       });
       return true;
     } catch (error) {
@@ -556,7 +684,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     try {
@@ -579,7 +707,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     try {
@@ -604,7 +732,7 @@ export default class BodyWidget extends React.Component {
         <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')}
+          <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}/>
@@ -612,6 +740,8 @@ export default class BodyWidget extends React.Component {
         <ButtonGroup>
           <IconButton id="save-sql" icon="fa fa-file-code" onClick={this.onSQLClick} title={gettext('Generate SQL')}
             shortcut={this.state.preferences.generate_sql}/>
+          <IconButton id="save-image" icon="fa fa-file-image" onClick={this.onImageClick} 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')}
@@ -634,7 +764,8 @@ export default class BodyWidget extends React.Component {
         <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} />
+          <DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details}
+            shortcut={this.state.preferences.show_details} />
         </ButtonGroup>
         <ButtonGroup>
           <IconButton id="zoom-to-fit" icon="fa fa-compress" onClick={this.diagram.zoomToFit} title={gettext('Zoom to fit')}
@@ -654,7 +785,7 @@ export default class BodyWidget extends React.Component {
         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()} />
+        <CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
       </div>
       </>
     );
@@ -672,10 +803,11 @@ BodyWidget.propTypes = {
     title: PropTypes.string.isRequired,
     bgcolor: PropTypes.string,
     fgcolor: PropTypes.string,
-    gen: PropTypes.bool.isRequired
+    gen: PropTypes.bool.isRequired,
   }),
   getDialog: PropTypes.func.isRequired,
   transformToSupported: PropTypes.func.isRequired,
+  pgWindow: PropTypes.object.isRequired,
   pgAdmin: PropTypes.object.isRequired,
-  alertify: PropTypes.object.isRequired
+  alertify: PropTypes.object.isRequired,
 };
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
index f191dc85e..3b71dbcc0 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
@@ -16,7 +16,7 @@ export const STATUS = {
   DISCONNECTED: 2,
   CONNECTING: 3,
   FAILED: 4,
-}
+};
 
 /* The connection bar component */
 export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title}) {
@@ -33,19 +33,19 @@ export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title
             + (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">
+        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}
+          {status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
+          {status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
+          {title}
         </div>
       </div>
     </div>
-  )
+  );
 }
 
 ConnectionBar.propTypes = {
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
index 19050512c..052829f93 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
@@ -51,13 +51,13 @@ export default function FloatingNote({open, onClose, reference, rows, noteNode,
           </div>
         </div>
       </div>
-      )}
-      visible={open}
-      interactive={true}
-      animation={false}
-      reference={reference}
-      placement='auto-end'
-      {...tippyProps}
+    )}
+    visible={open}
+    interactive={true}
+    animation={false}
+    reference={reference}
+    placement='auto-end'
+    {...tippyProps}
     />
   );
 }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
index 2f376e4cb..fe4280af3 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
@@ -34,7 +34,7 @@ BaseIconButton.propTypes = {
   text: PropTypes.string,
   className: PropTypes.string,
   ref: CustomPropTypes.ref,
-}
+};
 
 
 /* The tooltip content to show shortcut details */
@@ -47,10 +47,10 @@ export function Shortcut({shortcut}) {
   return (
     <div style={{justifyContent: 'center', marginTop: '0.125rem'}} className="d-flex">
       {keys.map((key, i)=>{
-        return <div key={i} className="shortcut-key">{key}</div>
+        return <div key={i} className="shortcut-key">{key}</div>;
       })}
     </div>
-  )
+  );
 }
 
 const shortcutPropType = PropTypes.shape({
@@ -85,7 +85,7 @@ export const IconButton = forwardRef((props, ref) => {
       </Tippy>
     );
   } else {
-    return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>
+    return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>;
   }
 });
 
@@ -93,21 +93,21 @@ 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") }
+      title={showDetails ? gettext('Show fewer details') : gettext('Show more details') }
       {...props} />
   );
 }
 
 DetailsToggleButton.propTypes = {
   showDetails: PropTypes.bool,
-}
+};
 
 /* Button group container */
 export function ButtonGroup({className, children}) {
@@ -115,12 +115,12 @@ export function ButtonGroup({className, children}) {
     <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}) {
@@ -128,9 +128,9 @@ export default function ToolBar({id, children}) {
     <div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
       {children}
     </div>
-  )
+  );
 }
 
 ButtonGroup.propTypes = {
   id: PropTypes.string,
-}
+};
diff --git a/web/pgadmin/tools/erd/static/scss/_erd.scss b/web/pgadmin/tools/erd/static/scss/_erd.scss
index 733dd53c6..4fffcf4be 100644
--- a/web/pgadmin/tools/erd/static/scss/_erd.scss
+++ b/web/pgadmin/tools/erd/static/scss/_erd.scss
@@ -28,6 +28,7 @@
     position: relative;
     width: 100%;
     height: 100%;
+    min-height: 0;
   }
 
   .floating-note {
@@ -56,6 +57,7 @@
     }
 
     .note-body {
+      word-break: break-all;
       & textarea {
         width: 100%;
         border: none;
@@ -69,11 +71,21 @@
     }
   }
 
+  .html2canvas-reset {
+    background-image: none !important;
+    overflow: auto !important;
+
+    & > svg, & > div {
+      transform: none !important;
+    }
+  }
+
   .diagram-canvas{
     width: 100%;
     height: 100%;
     color: $color-fg;
     font-family: sans-serif;
+    background-color: $erd-canvas-bg;
     background-image: $erd-bg-grid;
     cursor: unset;
 
@@ -85,6 +97,22 @@
       width: 175px;
       font-size: 0.8em;
 
+      .table-icon-schema {
+        background-image: url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') !important;
+        // background-repeat: no-repeat;
+        // // background-size: 20px !important;
+        // align-content: center;
+        // vertical-align: middle;
+        // height: 100%;
+        // background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIwLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NCA2NCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNjQgNjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojOTk5OTk5O3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEwO30KPC9zdHlsZT4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIxLjksMjIuMmMwLDMuNS0yLjksNi40LTYuNCw2LjRzLTYuNC0yLjktNi40LTYuNHMyLjktNi40LDYuNC02LjRTMjEuOSwxOC43LDIxLjksMjIuMnogTTQuMiw1MWwyMi4xLTE5CgkgTTQ2LjUsNTEuMUwyNi40LDMyIE0zNy42LDQyLjZsMTEuNi0xMCBNNjIuNCw0NS4xTDQ5LjIsMzIuNyBNNjMsMUgxdjYyaDYyVjF6IE0xLDUxLjRoNjIiLz4KPC9zdmc+Cg==');
+
+        width: 20px;
+        height: 20px;
+        // background: transparent url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') no-repeat center center;
+        // height: 100%;
+        // width: 20px;
+      }
+
       &.selected {
         border-color: $input-focus-border-color;
         box-shadow: $input-btn-focus-box-shadow;
@@ -105,16 +133,24 @@
         }
       }
 
-      .table-schema {
+      .table-schema-data {
         border-bottom: $border-width solid $erd-node-border-color;
         padding: $erd-row-padding;
-        font-weight: bold;
+
+        & .table-schema {
+          font-weight: bold;
+          word-break: break-all;
+        }
       }
 
-      .table-name {
+      .table-name-data {
         border-bottom: $border-width*2 solid $erd-node-border-color;
         padding: $erd-row-padding;
-        font-weight: bold;
+
+        & .table-name {
+          font-weight: bold;
+          word-break: break-all;
+        }
       }
 
       .table-cols {
@@ -123,10 +159,7 @@
           .col-row-data {
             padding: $erd-row-padding;
             width: 100%;
-
-            .col-name {
-              word-break: break-all;
-            }
+            word-break: break-all;
           }
           .col-row-port {
             padding: 0;
diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js
index 1c8bf74ee..4818d2a7a 100644
--- a/web/regression/javascript/erd/table_node_spec.js
+++ b/web/regression/javascript/erd/table_node_spec.js
@@ -73,7 +73,10 @@ describe('ERD TableNodeModel', ()=>{
   });
 
   describe('setData', ()=>{
-    let existPort = jasmine.createSpyObj('port', ['removeAllLinks']);
+    let existPort = jasmine.createSpyObj('port', {
+      'removeAllLinks': jasmine.createSpy('removeAllLinks'),
+      'getSubtype': 'notset',
+    });
 
     beforeEach(()=>{
       modelObj._data.columns = [
@@ -93,6 +96,7 @@ describe('ERD TableNodeModel', ()=>{
     });
 
     it('add columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('many');
       existPort.removeAllLinks.calls.reset();
       modelObj.setData({
         name: 'noname',
@@ -118,29 +122,31 @@ describe('ERD TableNodeModel', ()=>{
     });
 
     it('update columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('many');
       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},
+          {name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
+          {name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
+          {name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
         ],
       });
       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},
+          {name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
+          {name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
+          {name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
         ],
       });
       expect(existPort.removeAllLinks).not.toHaveBeenCalled();
     });
 
     it('remove columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('one');
       existPort.removeAllLinks.calls.reset();
       modelObj.setData({
         name: 'noname',
diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js
index 16928980f..3114ba627 100644
--- a/web/regression/javascript/erd/ui_components/body_widget_spec.js
+++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js
@@ -41,6 +41,10 @@ let pgAdmin = {
   },
 };
 
+let pgWindow = {
+  pgAdmin: pgAdmin,
+};
+
 let alertify = jasmine.createSpyObj('alertify', {
   'success': null,
   'error': null,
@@ -124,7 +128,7 @@ describe('ERD BodyWidget', ()=>{
 
   beforeEach(()=>{
     jasmineEnzyme();
-    body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
+    body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
     bodyInstance = body.instance();
   });
 
@@ -248,7 +252,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode();
     expect(tableDialog.show).toHaveBeenCalled();
 
-    let saveCallback = tableDialog.show.calls.mostRecent().args[5];
+    let saveCallback = tableDialog.show.calls.mostRecent().args[6];
     let newData = {key: 'value'};
     saveCallback(newData);
     expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
@@ -263,7 +267,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode(node);
     expect(tableDialog.show).toHaveBeenCalled();
 
-    saveCallback = tableDialog.show.calls.mostRecent().args[5];
+    saveCallback = tableDialog.show.calls.mostRecent().args[6];
     newData = {key: 'value'};
     saveCallback(newData);
     expect(node.setData).toHaveBeenCalledWith(newData);
diff --git a/web/yarn.lock b/web/yarn.lock
index 551213a27..5106e84d1 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1281,6 +1281,14 @@ acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
[email protected]:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e"
+  integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==
+  dependencies:
+    loader-utils "^2.0.0"
+    regex-parser "^2.2.11"
+
 [email protected]:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -1432,6 +1440,11 @@ argparse@^1.0.6, argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
+arity-n@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745"
+  integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U=
+
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1813,6 +1826,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
+base64-arraybuffer@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+  integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
+
 base64-js@^1.0.2, base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -2437,16 +2455,16 @@ camelcase-keys@^2.0.0:
     camelcase "^2.0.0"
     map-obj "^1.0.0"
 
[email protected], camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
 camelcase@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
   integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
 
-camelcase@^5.0.0:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
-  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
 caniuse-api@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@@ -2797,6 +2815,13 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
   integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
 
[email protected]:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f"
+  integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=
+  dependencies:
+    arity-n "^1.0.4"
+
 [email protected]:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2857,13 +2882,18 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
[email protected], convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
+convert-source-map@^0.3.3:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
+  integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
+
 convert-source-map@~1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
@@ -3051,6 +3081,13 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
[email protected]:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+  integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
+  dependencies:
+    base64-arraybuffer "^0.2.0"
+
 [email protected]:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@@ -3119,6 +3156,16 @@ css-what@^4.0.0:
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
   integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
 
+css@^2.0.0:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
+  integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
+  dependencies:
+    inherits "^2.0.3"
+    source-map "^0.6.1"
+    source-map-resolve "^0.5.2"
+    urix "^0.1.0"
+
 cssesc@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -3235,6 +3282,14 @@ cyclist@^1.0.1:
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
   integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
 
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
 dagre@^0.8.4:
   version "0.8.5"
   resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
@@ -3664,6 +3719,11 @@ emoji-regex@^7.0.1:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
   integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
 
+emojis-list@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+  integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
 emojis-list@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@@ -3870,6 +3930,32 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es5-ext@^0.10.35, es5-ext@^0.10.50:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
+
[email protected], es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4176,6 +4262,13 @@ ext-name@^5.0.0:
     ext-list "^2.0.0"
     sort-keys-length "^1.0.0"
 
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+  dependencies:
+    type "^2.0.0"
+
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -4982,6 +5075,13 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+html2canvas@^1.0.0-rc.7:
+  version "1.0.0-rc.7"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+  integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
+  dependencies:
+    css-line-break "1.1.1"
+
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
@@ -6167,6 +6267,15 @@ loader-runner@^2.4.0:
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
   integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
 
[email protected]:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
+  integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^2.0.0"
+    json5 "^1.0.1"
+
 [email protected], loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.1, loader-utils@^1.2.3, loader-utils@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
@@ -6837,6 +6946,11 @@ neo-async@^2.5.0, neo-async@^2.6.1:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
   integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
 
+next-tick@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -7854,6 +7968,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
[email protected]:
+  version "7.0.21"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17"
+  integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
 [email protected]:
   version "7.0.27"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
@@ -8260,6 +8383,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
+regex-parser@^2.2.11:
+  version "2.2.11"
+  resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
+  integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
+
 regexp.prototype.flags@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
@@ -8374,6 +8502,22 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-url-loader@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08"
+  integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==
+  dependencies:
+    adjust-sourcemap-loader "3.0.0"
+    camelcase "5.3.1"
+    compose-function "3.0.3"
+    convert-source-map "1.7.0"
+    es6-iterator "2.0.3"
+    loader-utils "1.2.3"
+    postcss "7.0.21"
+    rework "1.0.1"
+    rework-visit "1.0.0"
+    source-map "0.6.1"
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -8412,6 +8556,19 @@ ret@~0.1.10:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
[email protected]:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a"
+  integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo=
+
[email protected]:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7"
+  integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=
+  dependencies:
+    convert-source-map "^0.3.3"
+    css "^2.0.0"
+
 rfdc@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
@@ -8890,7 +9047,7 @@ source-list-map@^2.0.0:
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
   integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
 
-source-map-resolve@^0.5.0:
+source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
   integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
@@ -8919,16 +9076,16 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
   integrity sha1-D+llA6yGpa213mP05BKuSHLNvoY=
 
[email protected], source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
 source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.3:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
-  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
 source-map@^0.7.3:
   version "0.7.3"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@@ -9653,6 +9810,16 @@ type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
+  integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-25 11:47  Aditya Toshniwal <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-25 11:47 UTC (permalink / raw)
  To: pgadmin-hackers; +Cc: Akshay Joshi <[email protected]>; Khushboo Vashi <[email protected]>; Dave Page <[email protected]>

Hi,

Please find the rebased patch from the latest pull.

On Mon, Jan 25, 2021 at 5:12 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi Hackers,
>
> Attached is the patch to fix below issues in ERD:
>
>    1. After opening an existing project, the first table is already
>    selected but edit, clone, delete buttons are disable. Fixed.
>    2. ERD project title gets changed when 2 ERD projects are open &
>    anyone of it edited. Fixed.
>    3. Closing ERD tab, does not ask for confirmation pop up. Added.
>    4. Shortcut for 'Show more/Fewer details' is missing. Added.
>    5. Deleting primary key does not delete associated links. Fixed.
>    6. Long table & schema name are getting out of box. Fixed.
>    7. Long table name in notes pop-up need re-alignment. Fixed.
>    8. Same table name present in ERD/canvas is allowed in Add Table
>    dialogue. Added validation in the dialog.
>    9. Download image option is added, but it is not perfect yet. Image
>    icons (table, schema, etc.) are not showing up.
>    10. Rename panel option should be disabled by default. It should be
>    enabled for the tools which implement rename functionality.
>    11. The Toolbar is not visible in Safari for the ERD tool. Fixed.
>
> Please review.
>
> On Thu, Jan 21, 2021 at 4:32 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi,
>>
>>
>> On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:
>>
>>>
>>>
>>> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi Dave,
>>>>
>>>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>>>
>>>>> Hi
>>>>>
>>>>> Where's the Save Image button gone? I know Aditya was removing it
>>>>> whilst working on other things, but it's still required for phase 1 release.
>>>>>
>>>> It was not working 100% right. :(
>>>> So I've removed it for the time being. I'm still working on it.
>>>>
>>>
>>> OK, so that work will be completed in time for the build next week?
>>>
>> I'm trying my best to make it available before release. I'm struggling to
>> make it work perfectly.
>>
>>>
>>>
>>>>
>>>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Thanks, patch applied.
>>>>>>
>>>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> OK, So the changes have worked. But still failing at one more place.
>>>>>>> Attached the patch fixes it.
>>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Thanks, patch applied.
>>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi,
>>>>>>>>>
>>>>>>>>> The jasmine test cases are working fine on my local machine. The
>>>>>>>>> test cases are successful on jenkins other than on linux, not sure why.
>>>>>>>>> I have made some fixes by looking at the log. Please review and
>>>>>>>>> try.
>>>>>>>>>
>>>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>
>>>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Akshay,
>>>>>>>>>>>
>>>>>>>>>>> I forgot to remove few of the dependencies which are not
>>>>>>>>>>> required as of now (may be in future). Attached patch removes those
>>>>>>>>>>> dependencies from package.json.
>>>>>>>>>>>
>>>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>
>>>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi,
>>>>>>>>>>>>>
>>>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>>>> validation.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I wanted to keep the code as it will be used in future.
>>>>>>>>>>>>> Anyway, I've removed the code.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> It shows the spinner and waits for the response to come from
>>>>>>>>>>>>> the back end. I've used the existing table fetching code which is used at
>>>>>>>>>>>>> other places. I'll create an RM to improve the back end code for fetching
>>>>>>>>>>>>> the tables data which will help the schema diff tool as well.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD tool
>>>>>>>>>>>>> won't be used that frequently. We already have a limited
>>>>>>>>>>>>> number of keys available for shortcuts. I think we should roll out without
>>>>>>>>>>>>> shortcut for now. If there is a user demand for it then we can think of
>>>>>>>>>>>>> adding it.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>    -
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I've added a confirmation dialog which will show the number
>>>>>>>>>>>>> of tables and links selected. This way user will know what he has selected
>>>>>>>>>>>>> before deleting.
>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>>>>>> can reuse it.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI
>>>>>>>>>>>>> going forward with React.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin. Below
>>>>>>>>>>>>>>>>> are the details:
>>>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an
>>>>>>>>>>>>>>>>> existing DB.
>>>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many relationships,
>>>>>>>>>>>>>>>>> many-to-many relationships, adding notes.
>>>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>
>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>
>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Thanks,
>>>>>>>>> Aditya Toshniwal
>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> *Thanks & Regards*
>>>>>>>> *Akshay Joshi*
>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>
>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> *Thanks & Regards*
>>>>>> *Akshay Joshi*
>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>
>>>>>> *Mobile: +91 976-788-8246*
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EDB: http://www.enterprisedb.com
>>>>>
>>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EDB: http://www.enterprisedb.com
>>>
>>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802.fixes_v2.patch (68.6K, 3-RM1802.fixes_v2.patch)
  download | inline diff:
diff --git a/web/package.json b/web/package.json
index 9d4c060dc..9e55eb674 100644
--- a/web/package.json
+++ b/web/package.json
@@ -46,6 +46,7 @@
     "prop-types": "^15.7.2",
     "raw-loader": "^3.1.0",
     "resize-observer-polyfill": "^1.5.1",
+    "resolve-url-loader": "^3.1.2",
     "sass": "^1.24.4",
     "sass-loader": "^7.1.0",
     "sass-resources-loader": "^2.0.0",
@@ -87,6 +88,7 @@
     "dagre": "^0.8.4",
     "dropzone": "^5.5.1",
     "exports-loader": "~0.7.0",
+    "html2canvas": "^1.0.0-rc.7",
     "immutability-helper": "^3.0.0",
     "imports-loader": "^0.8.0",
     "ip-address": "^5.8.9",
diff --git a/web/pgadmin/browser/static/js/frame.js b/web/pgadmin/browser/static/js/frame.js
index b72df7b0f..0d3b91273 100644
--- a/web/pgadmin/browser/static/js/frame.js
+++ b/web/pgadmin/browser/static/js/frame.js
@@ -30,7 +30,7 @@ define([
     height: 600,
     showTitle: true,
     isClosable: true,
-    isRenamable: true,
+    isRenamable: false,
     isPrivate: false,
     url: '',
     icon: '',
diff --git a/web/pgadmin/static/js/chartjs/index.jsx b/web/pgadmin/static/js/chartjs/index.jsx
index 52623bd24..022e63875 100644
--- a/web/pgadmin/static/js/chartjs/index.jsx
+++ b/web/pgadmin/static/js/chartjs/index.jsx
@@ -41,11 +41,11 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
       options: optionsMerged,
     });
     props.onInit && props.onInit(chartObj.current);
-  }
+  };
 
   const destroyChart = function() {
     chartObj.current && chartObj.current.destroy();
-  }
+  };
 
   useEffect(()=>{
     initChart();
@@ -72,7 +72,7 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
       destroyChart();
       initChart();
     }
-  }, [redraw])
+  }, [redraw]);
 
   return (
     <canvas id={id} ref={chartRef}></canvas>
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index a7d056683..c9b8b5b7e 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -180,7 +180,7 @@ define('pgadmin.datagrid', [
           name: 'frm_datagrid',
           showTitle: true,
           isCloseable: true,
-          isRenameable: true,
+          isRenamable: true,
           isPrivate: true,
           url: 'about:blank',
         });
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
index 46db6a70a..b3bd793e0 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
@@ -78,11 +78,11 @@ export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_fi
 
 export function set_renamable_option(panel, is_file) {
   if(is_file || is_file == 'true') {
-    panel._isRenamable = false;
+    panel.renamable(false);
     $('.conn-info-dd').hide();
     $('.connection-data').css({pointerEvents: 'none', cursor: 'arrow'});
   } else {
-    panel._isRenamable = true;
+    panel.renamable(true);
   }
 }
 
diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py
index 301aca9c9..1ce742186 100644
--- a/web/pgadmin/tools/erd/__init__.py
+++ b/web/pgadmin/tools/erd/__init__.py
@@ -288,6 +288,24 @@ class ERDModule(PgAdminModule):
             fields=shortcut_fields
         )
 
+        self.preference.register(
+            'keyboard_shortcuts',
+            'show_details',
+            gettext('Show more/fewer details'),
+            'keyboardshortcut',
+            {
+                'alt': True,
+                'shift': False,
+                'control': True,
+                'key': {
+                    'key_code': 84,
+                    'char': 't'
+                }
+            },
+            category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
+            fields=shortcut_fields
+        )
+
         self.preference.register(
             'keyboard_shortcuts',
             'zoom_to_fit',
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
index ea0e22a29..8faae4d01 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
@@ -212,14 +212,14 @@ export default class ERDCore {
     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}));
+      sourcePort = sourceNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'one', alignment:PortModelAlignment.RIGHT}));
     }
 
     portName = targetNode.getPortName(data.local_column_attnum);
     let targetPort = targetNode.getPort(portName);
     /* Create the port if not there */
     if(!targetPort) {
-      targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, alignment:PortModelAlignment.RIGHT}));
+      targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'many', alignment:PortModelAlignment.RIGHT}));
     }
 
     /* Link the ports */
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
index e1fee815f..2b56ea1c4 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
@@ -50,7 +50,7 @@ export default class TableDialog {
     return 'entity_dialog';
   }
 
-  getDataModel(attributes, colTypes, schemas, sVersion) {
+  getDataModel(attributes, existingTables, colTypes, schemas, sVersion) {
     let dialogObj = this;
     let columnsModel = this.pgBrowser.DataModel.extend({
       idAttribute: 'attnum',
@@ -694,6 +694,10 @@ export default class TableDialog {
           msg = gettext('Table name cannot be empty.');
           this.errorModel.set('name', msg);
           return msg;
+        } else if(_.findIndex(existingTables, (table)=>table[0]==schema&&table[1]==name) >= 0) {
+          msg = gettext('Table name already exists.');
+          this.errorModel.set('name', msg);
+          return msg;
         }
         this.errorModel.unset('name');
         if (
@@ -705,6 +709,8 @@ export default class TableDialog {
           return msg;
         }
         this.errorModel.unset('schema');
+
+
         return null;
       },
     });
@@ -731,9 +737,9 @@ export default class TableDialog {
     return Alertify[dialogName];
   }
 
-  show(title, attributes, colTypes, schemas, sVersion, callback) {
+  show(title, attributes, existingTables, 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);
+    dialog(dialogTitle, this.getDataModel(attributes, existingTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
   }
 }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/index.js b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
index 523ebd14f..e865c29bd 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/index.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/index.js
@@ -9,10 +9,13 @@
 
 import React from 'react';
 import ReactDOM from 'react-dom';
+import _ from 'lodash';
+
 import BodyWidget from './ui_components/BodyWidget';
 import getDialog, {transformToSupported} from './dialogs';
 import Alertify from 'pgadmin.alertifyjs';
 import pgWindow from 'sources/window';
+import pgAdmin from 'sources/pgadmin';
 
 export default class ERDTool {
   constructor(container, params) {
@@ -20,10 +23,28 @@ export default class ERDTool {
     this.params = params;
   }
 
+  getPreferencesForModule() {
+
+  }
+
   render() {
     /* Mount the React ERD tool to the container */
+    let panel = null;
+    _.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
+      if (p.isVisible()) {
+        panel = p;
+      }
+    });
+
     ReactDOM.render(
-      <BodyWidget params={this.params} getDialog={getDialog} transformToSupported={transformToSupported} pgAdmin={pgWindow.pgAdmin} alertify={Alertify} />,
+      <BodyWidget
+        params={this.params}
+        getDialog={getDialog}
+        transformToSupported={transformToSupported}
+        pgWindow={pgWindow}
+        pgAdmin={pgAdmin}
+        panel={panel}
+        alertify={Alertify} />,
       this.container
     );
   }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
index be985afde..37b456825 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx
@@ -9,12 +9,12 @@
 
 import React from 'react';
 import {
-    RightAngleLinkModel,
-    RightAngleLinkWidget,
-    DefaultLinkFactory,
-    PortModelAlignment,
-    LinkWidget,
-    PointModel
+  RightAngleLinkModel,
+  RightAngleLinkWidget,
+  DefaultLinkFactory,
+  PortModelAlignment,
+  LinkWidget,
+  PointModel,
 } from '@projectstorm/react-diagrams';
 import {Point} from '@projectstorm/geometry';
 import _ from 'lodash';
@@ -24,7 +24,7 @@ export const OneToManyModel = {
   local_column_attnum: undefined,
   referenced_table_uid: undefined,
   referenced_column_attnum: undefined,
-}
+};
 
 export class OneToManyLinkModel extends RightAngleLinkModel {
   constructor({data, ...options}) {
@@ -33,7 +33,7 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
       width: 1,
       class: 'link-onetomany',
       locked: true,
-      ...options
+      ...options,
     });
 
     this._data = {
@@ -62,13 +62,13 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
         '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()
+      data: this.getData(),
     };
   }
 }
@@ -83,13 +83,13 @@ const CustomLinkEndWidget = props => {
           <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 + ')'}>
@@ -111,21 +111,21 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
     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;
+    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];
   }
@@ -146,8 +146,8 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
       point: point,
       rotation: rotation,
       tx: tx,
-      ty: ty
-    }
+      ty: ty,
+    };
   }
 
   generateCustomEndWidget({type, point, rotation, tx, ty}) {
@@ -183,7 +183,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
   }
 
   handleMove = function(event) {
-    this.props.link.getTargetPort()
+    this.props.link.getTargetPort();
     this.draggingEvent(event, this.dragging_index);
     this.props.link.fireEvent({}, 'positionChanged');
   }.bind(this);
@@ -220,7 +220,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
       this.props.link.addPoint(
         new PointModel({
           link: this.props.link,
-          position: new Point(onePoint.point.getX(), manyPoint.point.getY())
+          position: new Point(onePoint.point.getX(), manyPoint.point.getY()),
         })
       );
     }
@@ -246,7 +246,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
             onMouseEnter: (event) => {
               this.setState({ selected: true });
               this.props.link.lastHoverIndexOfPath = j;
-            }
+            },
           },
           j
         )
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
index 5e74d890b..383c621ce 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx
@@ -19,7 +19,7 @@ export class TableNodeModel extends DefaultNodeModel {
   constructor({otherInfo, ...options}) {
     super({
       ...options,
-      type: TYPE
+      type: TYPE,
     });
 
     this._note = otherInfo.note || '';
@@ -60,22 +60,22 @@ export class TableNodeModel extends DefaultNodeModel {
 
   cloneData(name) {
     let newData = {
-      ...this.getData()
+      ...this.getData(),
     };
     if(name) {
-      newData['name'] = name
+      newData['name'] = name;
     }
     return newData;
   }
 
   setData(data) {
     let self = this;
-    /* Remove the links if column dropped */
+    /* Remove the links if column dropped or primary key removed */
     _.differenceWith(this._data.columns, data.columns, function(existing, incoming) {
-      return existing.attnum == incoming.attnum;
+      return existing.attnum == incoming.attnum && incoming.is_primary_key == true;
     }).forEach((col)=>{
       let existPort = self.getPort(self.getPortName(col.attnum));
-      if(existPort) {
+      if(existPort && existPort.getSubtype() == 'one') {
         existPort.removeAllLinks();
         self.removePort(existPort);
       }
@@ -109,7 +109,7 @@ export class TableNodeModel extends DefaultNodeModel {
       otherInfo: {
         data: this.getData(),
         note: this.getNote(),
-      }
+      },
     };
   }
 }
@@ -119,8 +119,8 @@ export class TableNodeWidget extends React.Component {
     super(props);
 
     this.state = {
-      show_details: true
-    }
+      show_details: true,
+    };
 
     this.props.node.registerListener({
       toggleDetails: (event) => {
@@ -143,13 +143,13 @@ export class TableNodeWidget extends React.Component {
         </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} />
+        <PortWidget engine={this.props.engine} port={port} key={port.getID()} className={'port-' + port.options.alignment} />
       );
     }
     return <></>;
@@ -163,21 +163,21 @@ export class TableNodeWidget extends React.Component {
   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-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')
+              this.props.node.fireEvent({}, 'showNote');
             }} title="Check note" />}
         </div>
-        <div className="table-schema">
-          <span className="wcTabIcon icon-schema"></span>
-          {node_data.schema}
+        <div className="d-flex table-schema-data">
+          <div className="table-icon-schema"></div>
+          <div className="table-schema">{node_data.schema}</div>
         </div>
-        <div className="table-name">
-          <span className="wcTabIcon icon-table"></span>
-          {node_data.name}
+        <div className="d-flex table-name-data">
+          <div><span className="wcTabIcon icon-table"></span></div>
+          <div className="table-name">{node_data.name}</div>
         </div>
         <div className="table-cols">
           {_.map(node_data.columns, (col)=>this.generateColumn(col))}
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
index c412acd77..9bcbdc415 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ports/OneToManyPort.js
@@ -16,6 +16,7 @@ const TYPE = 'onetomany';
 export default class OneToManyPortModel extends PortModel {
   constructor({options}) {
     super({
+      subtype: 'notset',
       ...options,
       type: TYPE,
     });
@@ -30,6 +31,22 @@ export default class OneToManyPortModel extends PortModel {
   createLinkModel() {
     return new OneToManyLinkModel({});
   }
+
+  getSubtype() {
+    return this.options.subtype;
+  }
+
+  deserialize(event) {
+    super.deserialize(event);
+    this.options.subtype = event.data.subtype || 'notset';
+  }
+
+  serialize() {
+    return {
+      ...super.serialize(),
+      subtype: this.options.subtype,
+    };
+  }
 }
 
 export class OneToManyPortFactory extends AbstractModelFactory {
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
index 22701bae2..e36aaf2f1 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
@@ -12,6 +12,10 @@ 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 _ from 'lodash';
+import html2canvas from 'html2canvas';
+import * as htmlToImage from 'html-to-image';
+
 
 import ERDCore from '../ERDCore';
 import ToolBar, {IconButton, DetailsToggleButton, ButtonGroup} from './ToolBar';
@@ -22,22 +26,25 @@ 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';
+import 'wcdocker';
 
 /* 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 = {};
+    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];
+    for(let i=0; i<shortcut_handlers.length; i++){
+      let [key, handler] = shortcut_handlers[i];
+      if(key) {
         this.shortcuts[this.shortcutKey(key.alt, key.control, key.shift, false, key.key.key_code)] = handler;
       }
+    }
   }
 
   shortcutKey(altKey, ctrlKey, shiftKey, metaKey, keyCode) {
@@ -69,9 +76,12 @@ export default class BodyWidget extends React.Component {
       current_file: null,
       dirty: false,
       show_details: true,
+      is_new_tab: false,
       preferences: {},
-    }
+    };
     this.diagram = new ERDCore();
+    /* Flag for checking if user has opted for save before close */
+    this.closeOnSave = React.createRef();
     this.fileInputRef = React.createRef();
     this.diagramContainerRef = React.createRef();
     this.canvasEle = null;
@@ -83,6 +93,7 @@ export default class BodyWidget extends React.Component {
     this.onSaveDiagram = this.onSaveDiagram.bind(this);
     this.onSaveAsDiagram = this.onSaveAsDiagram.bind(this);
     this.onSQLClick = this.onSQLClick.bind(this);
+    this.onImageClick = this.onImageClick.bind(this);
     this.onAddNewNode = this.onAddNewNode.bind(this);
     this.onEditNode = this.onEditNode.bind(this);
     this.onCloneNode = this.onCloneNode.bind(this);
@@ -133,7 +144,7 @@ export default class BodyWidget extends React.Component {
       },
       'editNode': (event) => {
         this.addEditNode(event.node);
-      }
+      },
     };
     Object.keys(diagramEvents).forEach(eventName => {
       this.diagram.registerModelEvent(eventName, diagramEvents[eventName]);
@@ -157,9 +168,10 @@ export default class BodyWidget extends React.Component {
       [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.show_details, this.onDetailsToggle],
       [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.state.preferences.zoom_out, this.diagram.zoomOut],
     ]);
 
     this.diagram.registerKeyAction(this.keyboardActionObj);
@@ -182,11 +194,16 @@ export default class BodyWidget extends React.Component {
   }
 
   async componentDidMount() {
-    this.setLoading('Preparing');
-    this.setTitle(this.state.current_file);
+    this.setLoading(gettext('Preparing...'));
+
     this.setState({
-      preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
-    }, this.registerKeyboardShortcuts);
+      preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
+      is_new_tab: (this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('browser').new_browser_tab_open || '')
+        .includes('erd_tool'),
+    }, ()=>{
+      this.registerKeyboardShortcuts();
+      this.setTitle(this.state.current_file);
+    });
     this.registerModelEvents();
     this.realignGrid({
       backgroundSize: '45px 45px',
@@ -197,10 +214,19 @@ export default class BodyWidget extends React.Component {
     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')
+        preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
       }, ()=>this.registerKeyboardShortcuts());
     });
 
+    this.props.panel?.on(window.wcDocker?.EVENT.CLOSING, () => {
+      if(this.state.dirty) {
+        this.closeOnSave = false;
+        this.confirmBeforeClose();
+        return false;
+      }
+      return true;
+    });
+
     let done = await this.initConnection();
     if(!done) return;
 
@@ -218,18 +244,78 @@ export default class BodyWidget extends React.Component {
     }
   }
 
+  confirmBeforeClose() {
+    let bodyObj = this;
+    this.props.alertify.confirmSave || this.props.alertify.dialog('confirmSave', function() {
+      return {
+        main: function(title, message) {
+          this.setHeader(title);
+          this.setContent(message);
+        },
+        setup: function() {
+          return {
+            buttons: [{
+              text: gettext('Cancel'),
+              key: 27, // ESC
+              invokeOnClose: true,
+              className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
+            }, {
+              text: gettext('Don\'t save'),
+              className: 'btn btn-secondary fa fa-lg fa-trash-alt pg-alertify-button',
+            }, {
+              text: gettext('Save'),
+              className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+            }],
+            focus: {
+              element: 0,
+              select: false,
+            },
+            options: {
+              maximizable: false,
+              resizable: false,
+            },
+          };
+        },
+        callback: function(closeEvent) {
+          switch (closeEvent.index) {
+          case 0: // Cancel
+            //Do nothing.
+            break;
+          case 1: // Don't Save
+            bodyObj.closePanel();
+            break;
+          case 2: //Save
+            bodyObj.onSaveDiagram(false, true);
+            break;
+          }
+        },
+      };
+    });
+    this.props.alertify.confirmSave(gettext('Save changes?'), gettext('The diagram has changed. Do you want to save changes?'));
+    return false;
+  }
+
+  closePanel() {
+    window.onbeforeunload = null;
+    this.props.panel.off(wcDocker.EVENT.CLOSING);
+    this.props.pgWindow.pgAdmin.Browser.docker.removePanel(this.props.panel);
+  }
+
   getDialog(dialogName) {
     if(dialogName === 'entity_dialog') {
+      let existingTables = this.diagram.getModel().getNodes().map((node)=>{
+        return node.getSchemaTableName();
+      });
       return (title, attributes, callback)=>{
-          this.props.getDialog(dialogName).show(
-            title, attributes, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
-          );
+        this.props.getDialog(dialogName).show(
+          title, attributes, existingTables, 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
-          );
+        this.props.getDialog(dialogName).show(
+          title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
+        );
       };
     }
   }
@@ -289,20 +375,20 @@ export default class BodyWidget extends React.Component {
       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();
-        },
-        () => {}
+      () => {
+        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();
+      },
+      () => {}
     );
   }
 
@@ -312,11 +398,11 @@ export default class BodyWidget extends React.Component {
 
   onDetailsToggle() {
     this.setState((prevState)=>({
-      show_details: !prevState.show_details
+      show_details: !prevState.show_details,
     }), ()=>{
       this.diagram.getModel().getNodes().forEach((node)=>{
         node.fireEvent({show_details: this.state.show_details}, 'toggleDetails');
-      })
+      });
     });
   }
 
@@ -335,8 +421,9 @@ export default class BodyWidget extends React.Component {
   }
 
   openFile(fileName) {
+    this.setLoading(gettext('Loading project...'));
     axios.post(url_for('sqleditor.load_file'), {
-      'file_name': decodeURI(fileName)
+      'file_name': decodeURI(fileName),
     }).then((res)=>{
       this.setState({
         current_file: fileName,
@@ -344,13 +431,17 @@ export default class BodyWidget extends React.Component {
       });
       this.setTitle(fileName);
       this.diagram.deserialize(res.data);
+      this.diagram.clearSelection();
       this.registerModelEvents();
     }).catch((err)=>{
       this.handleAxiosCatch(err);
+    }).then(()=>{
+      this.setLoading(null);
     });
   }
 
-  onSaveDiagram(isSaveAs=false) {
+  onSaveDiagram(isSaveAs=false, closeOnSave=false) {
+    this.closeOnSave = closeOnSave;
     if(this.state.current_file && !isSaveAs) {
       this.saveFile(this.state.current_file);
     } else {
@@ -370,9 +461,10 @@ export default class BodyWidget extends React.Component {
   }
 
   saveFile(fileName) {
+    this.setLoading(gettext('Saving...'));
     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))
+      '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({
@@ -380,7 +472,12 @@ export default class BodyWidget extends React.Component {
         dirty: false,
       });
       this.setTitle(fileName);
+      this.setLoading(null);
+      if(this.closeOnSave) {
+        this.closePanel.call(this);
+      }
     }).catch((err)=>{
+      this.setLoading(null);
       this.handleAxiosCatch(err);
     });
   }
@@ -395,14 +492,10 @@ export default class BodyWidget extends React.Component {
       title = 'Untitled';
     }
     title = this.getCurrentProjectName(title) + (dirty ? '*': '');
-    if (this.new_browser_tab) {
+    if (this.state.is_new_tab) {
       window.document.title = title;
     } else {
-      _.each(this.props.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
-        if (p.isVisible()) {
-          setPanelTitle(p, title);
-        }
-      });
+      setPanelTitle(this.props.panel, title);
     }
   }
 
@@ -414,7 +507,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     this.setLoading(gettext('Preparing the SQL...'));
@@ -428,18 +521,53 @@ export default class BodyWidget extends React.Component {
           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);
+        showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.DataGrid, this.props.alertify);
       })
       .catch((error)=>{
         this.handleAxiosCatch(error);
       })
       .then(()=>{
         this.setLoading(null);
-      })
+      });
+  }
+
+  onImageClick() {
+    this.setLoading(gettext('Preparing the image...'));
+
+    /* Change the styles for suiting html2canvas */
+    this.canvasEle.classList.add('html2canvas-reset');
+    this.canvasEle.style.width = this.canvasEle.scrollWidth + 'px';
+    this.canvasEle.style.height = this.canvasEle.scrollHeight + 'px';
+
+    html2canvas(this.canvasEle, {
+      width: this.canvasEle.scrollWidth + 10,
+      height: this.canvasEle.scrollHeight + 10,
+      scrollX: 0,
+      scrollY: 0,
+      useCORS: true,
+      allowTaint: true,
+      backgroundColor: window.getComputedStyle(this.canvasEle).backgroundColor,
+    }).then((canvas)=>{
+      let link = document.createElement('a');
+      link.setAttribute('href', canvas.toDataURL('image/png'));
+      link.setAttribute('download', this.getCurrentProjectName() + '.png');
+      link.click();
+    }).catch((err)=>{
+      console.error(err);
+      this.props.alertify.alert()
+        .set('title', gettext('Error'))
+        .set('message', err).show();
+    }).then(()=>{
+      /* Revert back to the original CSS styles */
+      this.canvasEle.classList.remove('html2canvas-reset');
+      this.canvasEle.style.width = '';
+      this.canvasEle.style.height = '';
+      this.setLoading(null);
+    });
   }
 
   onOneToManyClick() {
@@ -473,8 +601,8 @@ export default class BodyWidget extends React.Component {
           '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);
@@ -484,7 +612,7 @@ export default class BodyWidget extends React.Component {
         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 = {
@@ -492,7 +620,7 @@ export default class BodyWidget extends React.Component {
         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');
 
@@ -505,7 +633,7 @@ export default class BodyWidget extends React.Component {
       this.noteRefEle = this.diagram.getEngine().getNodeElement(noteNode);
       this.setState({
         note_node: noteNode,
-        note_open: true
+        note_open: true,
       });
     }
   }
@@ -528,14 +656,14 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      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
+        server_version: response.data.data.serverVersion,
       });
       return true;
     } catch (error) {
@@ -556,7 +684,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     try {
@@ -579,7 +707,7 @@ export default class BodyWidget extends React.Component {
       trans_id: this.props.params.trans_id,
       sgid: this.props.params.sgid,
       sid: this.props.params.sid,
-      did: this.props.params.did
+      did: this.props.params.did,
     });
 
     try {
@@ -604,7 +732,7 @@ export default class BodyWidget extends React.Component {
         <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')}
+          <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}/>
@@ -612,6 +740,8 @@ export default class BodyWidget extends React.Component {
         <ButtonGroup>
           <IconButton id="save-sql" icon="fa fa-file-code" onClick={this.onSQLClick} title={gettext('Generate SQL')}
             shortcut={this.state.preferences.generate_sql}/>
+          <IconButton id="save-image" icon="fa fa-file-image" onClick={this.onImageClick} 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')}
@@ -634,7 +764,8 @@ export default class BodyWidget extends React.Component {
             shortcut={this.state.preferences.add_edit_note} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
           <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} />
+          <DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details}
+            shortcut={this.state.preferences.show_details} />
         </ButtonGroup>
         <ButtonGroup>
           <IconButton id="zoom-to-fit" icon="fa fa-compress" onClick={this.diagram.zoomToFit} title={gettext('Zoom to fit')}
@@ -654,7 +785,7 @@ export default class BodyWidget extends React.Component {
         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()} />
+        <CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
       </div>
       </>
     );
@@ -672,10 +803,11 @@ BodyWidget.propTypes = {
     title: PropTypes.string.isRequired,
     bgcolor: PropTypes.string,
     fgcolor: PropTypes.string,
-    gen: PropTypes.bool.isRequired
+    gen: PropTypes.bool.isRequired,
   }),
   getDialog: PropTypes.func.isRequired,
   transformToSupported: PropTypes.func.isRequired,
+  pgWindow: PropTypes.object.isRequired,
   pgAdmin: PropTypes.object.isRequired,
-  alertify: PropTypes.object.isRequired
+  alertify: PropTypes.object.isRequired,
 };
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
index f191dc85e..3b71dbcc0 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ConnectionBar.jsx
@@ -16,7 +16,7 @@ export const STATUS = {
   DISCONNECTED: 2,
   CONNECTING: 3,
   FAILED: 4,
-}
+};
 
 /* The connection bar component */
 export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title}) {
@@ -33,19 +33,19 @@ export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title
             + (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">
+        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}
+          {status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
+          {status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
+          {title}
         </div>
       </div>
     </div>
-  )
+  );
 }
 
 ConnectionBar.propTypes = {
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
index 19050512c..052829f93 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/FloatingNote.jsx
@@ -51,13 +51,13 @@ export default function FloatingNote({open, onClose, reference, rows, noteNode,
           </div>
         </div>
       </div>
-      )}
-      visible={open}
-      interactive={true}
-      animation={false}
-      reference={reference}
-      placement='auto-end'
-      {...tippyProps}
+    )}
+    visible={open}
+    interactive={true}
+    animation={false}
+    reference={reference}
+    placement='auto-end'
+    {...tippyProps}
     />
   );
 }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
index 2f376e4cb..fe4280af3 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/ToolBar.jsx
@@ -34,7 +34,7 @@ BaseIconButton.propTypes = {
   text: PropTypes.string,
   className: PropTypes.string,
   ref: CustomPropTypes.ref,
-}
+};
 
 
 /* The tooltip content to show shortcut details */
@@ -47,10 +47,10 @@ export function Shortcut({shortcut}) {
   return (
     <div style={{justifyContent: 'center', marginTop: '0.125rem'}} className="d-flex">
       {keys.map((key, i)=>{
-        return <div key={i} className="shortcut-key">{key}</div>
+        return <div key={i} className="shortcut-key">{key}</div>;
       })}
     </div>
-  )
+  );
 }
 
 const shortcutPropType = PropTypes.shape({
@@ -85,7 +85,7 @@ export const IconButton = forwardRef((props, ref) => {
       </Tippy>
     );
   } else {
-    return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>
+    return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>;
   }
 });
 
@@ -93,21 +93,21 @@ 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") }
+      title={showDetails ? gettext('Show fewer details') : gettext('Show more details') }
       {...props} />
   );
 }
 
 DetailsToggleButton.propTypes = {
   showDetails: PropTypes.bool,
-}
+};
 
 /* Button group container */
 export function ButtonGroup({className, children}) {
@@ -115,12 +115,12 @@ export function ButtonGroup({className, children}) {
     <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}) {
@@ -128,9 +128,9 @@ export default function ToolBar({id, children}) {
     <div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
       {children}
     </div>
-  )
+  );
 }
 
 ButtonGroup.propTypes = {
   id: PropTypes.string,
-}
+};
diff --git a/web/pgadmin/tools/erd/static/scss/_erd.scss b/web/pgadmin/tools/erd/static/scss/_erd.scss
index 733dd53c6..4fffcf4be 100644
--- a/web/pgadmin/tools/erd/static/scss/_erd.scss
+++ b/web/pgadmin/tools/erd/static/scss/_erd.scss
@@ -28,6 +28,7 @@
     position: relative;
     width: 100%;
     height: 100%;
+    min-height: 0;
   }
 
   .floating-note {
@@ -56,6 +57,7 @@
     }
 
     .note-body {
+      word-break: break-all;
       & textarea {
         width: 100%;
         border: none;
@@ -69,11 +71,21 @@
     }
   }
 
+  .html2canvas-reset {
+    background-image: none !important;
+    overflow: auto !important;
+
+    & > svg, & > div {
+      transform: none !important;
+    }
+  }
+
   .diagram-canvas{
     width: 100%;
     height: 100%;
     color: $color-fg;
     font-family: sans-serif;
+    background-color: $erd-canvas-bg;
     background-image: $erd-bg-grid;
     cursor: unset;
 
@@ -85,6 +97,22 @@
       width: 175px;
       font-size: 0.8em;
 
+      .table-icon-schema {
+        background-image: url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') !important;
+        // background-repeat: no-repeat;
+        // // background-size: 20px !important;
+        // align-content: center;
+        // vertical-align: middle;
+        // height: 100%;
+        // background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIwLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NCA2NCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNjQgNjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojOTk5OTk5O3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEwO30KPC9zdHlsZT4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIxLjksMjIuMmMwLDMuNS0yLjksNi40LTYuNCw2LjRzLTYuNC0yLjktNi40LTYuNHMyLjktNi40LDYuNC02LjRTMjEuOSwxOC43LDIxLjksMjIuMnogTTQuMiw1MWwyMi4xLTE5CgkgTTQ2LjUsNTEuMUwyNi40LDMyIE0zNy42LDQyLjZsMTEuNi0xMCBNNjIuNCw0NS4xTDQ5LjIsMzIuNyBNNjMsMUgxdjYyaDYyVjF6IE0xLDUxLjRoNjIiLz4KPC9zdmc+Cg==');
+
+        width: 20px;
+        height: 20px;
+        // background: transparent url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') no-repeat center center;
+        // height: 100%;
+        // width: 20px;
+      }
+
       &.selected {
         border-color: $input-focus-border-color;
         box-shadow: $input-btn-focus-box-shadow;
@@ -105,16 +133,24 @@
         }
       }
 
-      .table-schema {
+      .table-schema-data {
         border-bottom: $border-width solid $erd-node-border-color;
         padding: $erd-row-padding;
-        font-weight: bold;
+
+        & .table-schema {
+          font-weight: bold;
+          word-break: break-all;
+        }
       }
 
-      .table-name {
+      .table-name-data {
         border-bottom: $border-width*2 solid $erd-node-border-color;
         padding: $erd-row-padding;
-        font-weight: bold;
+
+        & .table-name {
+          font-weight: bold;
+          word-break: break-all;
+        }
       }
 
       .table-cols {
@@ -123,10 +159,7 @@
           .col-row-data {
             padding: $erd-row-padding;
             width: 100%;
-
-            .col-name {
-              word-break: break-all;
-            }
+            word-break: break-all;
           }
           .col-row-port {
             padding: 0;
diff --git a/web/regression/javascript/erd/table_node_spec.js b/web/regression/javascript/erd/table_node_spec.js
index 1c8bf74ee..4818d2a7a 100644
--- a/web/regression/javascript/erd/table_node_spec.js
+++ b/web/regression/javascript/erd/table_node_spec.js
@@ -73,7 +73,10 @@ describe('ERD TableNodeModel', ()=>{
   });
 
   describe('setData', ()=>{
-    let existPort = jasmine.createSpyObj('port', ['removeAllLinks']);
+    let existPort = jasmine.createSpyObj('port', {
+      'removeAllLinks': jasmine.createSpy('removeAllLinks'),
+      'getSubtype': 'notset',
+    });
 
     beforeEach(()=>{
       modelObj._data.columns = [
@@ -93,6 +96,7 @@ describe('ERD TableNodeModel', ()=>{
     });
 
     it('add columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('many');
       existPort.removeAllLinks.calls.reset();
       modelObj.setData({
         name: 'noname',
@@ -118,29 +122,31 @@ describe('ERD TableNodeModel', ()=>{
     });
 
     it('update columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('many');
       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},
+          {name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
+          {name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
+          {name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
         ],
       });
       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},
+          {name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
+          {name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
+          {name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
         ],
       });
       expect(existPort.removeAllLinks).not.toHaveBeenCalled();
     });
 
     it('remove columns', ()=>{
+      spyOn(existPort, 'getSubtype').and.returnValue('one');
       existPort.removeAllLinks.calls.reset();
       modelObj.setData({
         name: 'noname',
diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js
index 16928980f..3114ba627 100644
--- a/web/regression/javascript/erd/ui_components/body_widget_spec.js
+++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js
@@ -41,6 +41,10 @@ let pgAdmin = {
   },
 };
 
+let pgWindow = {
+  pgAdmin: pgAdmin,
+};
+
 let alertify = jasmine.createSpyObj('alertify', {
   'success': null,
   'error': null,
@@ -124,7 +128,7 @@ describe('ERD BodyWidget', ()=>{
 
   beforeEach(()=>{
     jasmineEnzyme();
-    body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
+    body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
     bodyInstance = body.instance();
   });
 
@@ -248,7 +252,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode();
     expect(tableDialog.show).toHaveBeenCalled();
 
-    let saveCallback = tableDialog.show.calls.mostRecent().args[5];
+    let saveCallback = tableDialog.show.calls.mostRecent().args[6];
     let newData = {key: 'value'};
     saveCallback(newData);
     expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
@@ -263,7 +267,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode(node);
     expect(tableDialog.show).toHaveBeenCalled();
 
-    saveCallback = tableDialog.show.calls.mostRecent().args[5];
+    saveCallback = tableDialog.show.calls.mostRecent().args[6];
     newData = {key: 'value'};
     saveCallback(newData);
     expect(node.setData).toHaveBeenCalledWith(newData);
diff --git a/web/yarn.lock b/web/yarn.lock
index 551213a27..5106e84d1 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1281,6 +1281,14 @@ acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
[email protected]:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e"
+  integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==
+  dependencies:
+    loader-utils "^2.0.0"
+    regex-parser "^2.2.11"
+
 [email protected]:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -1432,6 +1440,11 @@ argparse@^1.0.6, argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
+arity-n@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745"
+  integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U=
+
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1813,6 +1826,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
+base64-arraybuffer@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+  integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
+
 base64-js@^1.0.2, base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -2437,16 +2455,16 @@ camelcase-keys@^2.0.0:
     camelcase "^2.0.0"
     map-obj "^1.0.0"
 
[email protected], camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
 camelcase@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
   integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
 
-camelcase@^5.0.0:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
-  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
 caniuse-api@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@@ -2797,6 +2815,13 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
   integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
 
[email protected]:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f"
+  integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=
+  dependencies:
+    arity-n "^1.0.4"
+
 [email protected]:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2857,13 +2882,18 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
[email protected], convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
+convert-source-map@^0.3.3:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
+  integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
+
 convert-source-map@~1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
@@ -3051,6 +3081,13 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
[email protected]:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+  integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
+  dependencies:
+    base64-arraybuffer "^0.2.0"
+
 [email protected]:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@@ -3119,6 +3156,16 @@ css-what@^4.0.0:
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
   integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
 
+css@^2.0.0:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
+  integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
+  dependencies:
+    inherits "^2.0.3"
+    source-map "^0.6.1"
+    source-map-resolve "^0.5.2"
+    urix "^0.1.0"
+
 cssesc@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -3235,6 +3282,14 @@ cyclist@^1.0.1:
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
   integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
 
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
 dagre@^0.8.4:
   version "0.8.5"
   resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
@@ -3664,6 +3719,11 @@ emoji-regex@^7.0.1:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
   integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
 
+emojis-list@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+  integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
 emojis-list@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@@ -3870,6 +3930,32 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es5-ext@^0.10.35, es5-ext@^0.10.50:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
+
[email protected], es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4176,6 +4262,13 @@ ext-name@^5.0.0:
     ext-list "^2.0.0"
     sort-keys-length "^1.0.0"
 
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+  dependencies:
+    type "^2.0.0"
+
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -4982,6 +5075,13 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+html2canvas@^1.0.0-rc.7:
+  version "1.0.0-rc.7"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+  integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
+  dependencies:
+    css-line-break "1.1.1"
+
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
@@ -6167,6 +6267,15 @@ loader-runner@^2.4.0:
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
   integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
 
[email protected]:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
+  integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^2.0.0"
+    json5 "^1.0.1"
+
 [email protected], loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.1, loader-utils@^1.2.3, loader-utils@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
@@ -6837,6 +6946,11 @@ neo-async@^2.5.0, neo-async@^2.6.1:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
   integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
 
+next-tick@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -7854,6 +7968,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
[email protected]:
+  version "7.0.21"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17"
+  integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
 [email protected]:
   version "7.0.27"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
@@ -8260,6 +8383,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
+regex-parser@^2.2.11:
+  version "2.2.11"
+  resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
+  integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
+
 regexp.prototype.flags@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
@@ -8374,6 +8502,22 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-url-loader@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08"
+  integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==
+  dependencies:
+    adjust-sourcemap-loader "3.0.0"
+    camelcase "5.3.1"
+    compose-function "3.0.3"
+    convert-source-map "1.7.0"
+    es6-iterator "2.0.3"
+    loader-utils "1.2.3"
+    postcss "7.0.21"
+    rework "1.0.1"
+    rework-visit "1.0.0"
+    source-map "0.6.1"
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -8412,6 +8556,19 @@ ret@~0.1.10:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
[email protected]:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a"
+  integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo=
+
[email protected]:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7"
+  integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=
+  dependencies:
+    convert-source-map "^0.3.3"
+    css "^2.0.0"
+
 rfdc@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
@@ -8890,7 +9047,7 @@ source-list-map@^2.0.0:
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
   integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
 
-source-map-resolve@^0.5.0:
+source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
   integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
@@ -8919,16 +9076,16 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
   integrity sha1-D+llA6yGpa213mP05BKuSHLNvoY=
 
[email protected], source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
 source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.3:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
-  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
 source-map@^0.7.3:
   version "0.7.3"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@@ -9653,6 +9810,16 @@ type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
+  integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-25 12:03  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2021-01-25 12:03 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: pgadmin-hackers; Khushboo Vashi <[email protected]>; Dave Page <[email protected]>

Thanks, patch applied.

On Mon, Jan 25, 2021 at 5:18 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
> Please find the rebased patch from the latest pull.
>
> On Mon, Jan 25, 2021 at 5:12 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Attached is the patch to fix below issues in ERD:
>>
>>    1. After opening an existing project, the first table is already
>>    selected but edit, clone, delete buttons are disable. Fixed.
>>    2. ERD project title gets changed when 2 ERD projects are open &
>>    anyone of it edited. Fixed.
>>    3. Closing ERD tab, does not ask for confirmation pop up. Added.
>>    4. Shortcut for 'Show more/Fewer details' is missing. Added.
>>    5. Deleting primary key does not delete associated links. Fixed.
>>    6. Long table & schema name are getting out of box. Fixed.
>>    7. Long table name in notes pop-up need re-alignment. Fixed.
>>    8. Same table name present in ERD/canvas is allowed in Add Table
>>    dialogue. Added validation in the dialog.
>>    9. Download image option is added, but it is not perfect yet. Image
>>    icons (table, schema, etc.) are not showing up.
>>    10. Rename panel option should be disabled by default. It should be
>>    enabled for the tools which implement rename functionality.
>>    11. The Toolbar is not visible in Safari for the ERD tool. Fixed.
>>
>> Please review.
>>
>> On Thu, Jan 21, 2021 at 4:32 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi,
>>>
>>>
>>> On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:
>>>
>>>>
>>>>
>>>> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave,
>>>>>
>>>>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>>>>
>>>>>> Hi
>>>>>>
>>>>>> Where's the Save Image button gone? I know Aditya was removing it
>>>>>> whilst working on other things, but it's still required for phase 1 release.
>>>>>>
>>>>> It was not working 100% right. :(
>>>>> So I've removed it for the time being. I'm still working on it.
>>>>>
>>>>
>>>> OK, so that work will be completed in time for the build next week?
>>>>
>>> I'm trying my best to make it available before release. I'm struggling
>>> to make it work perfectly.
>>>
>>>>
>>>>
>>>>>
>>>>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Thanks, patch applied.
>>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> OK, So the changes have worked. But still failing at one more place.
>>>>>>>> Attached the patch fixes it.
>>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Thanks, patch applied.
>>>>>>>>>
>>>>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi,
>>>>>>>>>>
>>>>>>>>>> The jasmine test cases are working fine on my local machine. The
>>>>>>>>>> test cases are successful on jenkins other than on linux, not sure why.
>>>>>>>>>> I have made some fixes by looking at the log. Please review and
>>>>>>>>>> try.
>>>>>>>>>>
>>>>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>
>>>>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Akshay,
>>>>>>>>>>>>
>>>>>>>>>>>> I forgot to remove few of the dependencies which are not
>>>>>>>>>>>> required as of now (may be in future). Attached patch removes those
>>>>>>>>>>>> dependencies from package.json.
>>>>>>>>>>>>
>>>>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>>>>> validation.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I wanted to keep the code as it will be used in future.
>>>>>>>>>>>>>> Anyway, I've removed the code.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot, the
>>>>>>>>>>>>>> error is from the underlying library. Can't do much in this.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> It shows the spinner and waits for the response to come from
>>>>>>>>>>>>>> the back end. I've used the existing table fetching code which is used at
>>>>>>>>>>>>>> other places. I'll create an RM to improve the back end code for fetching
>>>>>>>>>>>>>> the tables data which will help the schema diff tool as well.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD
>>>>>>>>>>>>>> tool won't be used that frequently. We already have a
>>>>>>>>>>>>>> limited number of keys available for shortcuts. I think we should roll out
>>>>>>>>>>>>>> without shortcut for now. If there is a user demand for it then we can
>>>>>>>>>>>>>> think of adding it.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>    -
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I've added a confirmation dialog which will show the number
>>>>>>>>>>>>>> of tables and links selected. This way user will know what he has selected
>>>>>>>>>>>>>> before deleting.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>>>>>> So, we need to keep in mind that while implementing React in
>>>>>>>>>>>>>>> pgAdmin, the nodes should be properly detached from the tree itself, so we
>>>>>>>>>>>>>>> can reuse it.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI
>>>>>>>>>>>>>> going forward with React.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin.
>>>>>>>>>>>>>>>>>> Below are the details:
>>>>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an
>>>>>>>>>>>>>>>>>> existing DB.
>>>>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many
>>>>>>>>>>>>>>>>>> relationships, many-to-many relationships, adding notes.
>>>>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer |
>>>>>>>>>>>>>>>>>> *edbpostgres.com* <http://edbpostgres.com;
>>>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>
>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Thanks,
>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> *Thanks & Regards*
>>>>>>>>> *Akshay Joshi*
>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>
>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EDB: http://www.enterprisedb.com
>>>>>>
>>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EDB: http://www.enterprisedb.com
>>>>
>>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-25 14:08  Aditya Toshniwal <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Aditya Toshniwal @ 2021-01-25 14:08 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgadmin-hackers; Khushboo Vashi <[email protected]>; Dave Page <[email protected]>

Hi,

The attached patch fixes an issue created by existing table name check.
Please review.

On Mon, Jan 25, 2021 at 5:34 PM Akshay Joshi <[email protected]>
wrote:

> Thanks, patch applied.
>
> On Mon, Jan 25, 2021 at 5:18 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi,
>>
>> Please find the rebased patch from the latest pull.
>>
>> On Mon, Jan 25, 2021 at 5:12 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> Attached is the patch to fix below issues in ERD:
>>>
>>>    1. After opening an existing project, the first table is already
>>>    selected but edit, clone, delete buttons are disable. Fixed.
>>>    2. ERD project title gets changed when 2 ERD projects are open &
>>>    anyone of it edited. Fixed.
>>>    3. Closing ERD tab, does not ask for confirmation pop up. Added.
>>>    4. Shortcut for 'Show more/Fewer details' is missing. Added.
>>>    5. Deleting primary key does not delete associated links. Fixed.
>>>    6. Long table & schema name are getting out of box. Fixed.
>>>    7. Long table name in notes pop-up need re-alignment. Fixed.
>>>    8. Same table name present in ERD/canvas is allowed in Add Table
>>>    dialogue. Added validation in the dialog.
>>>    9. Download image option is added, but it is not perfect yet. Image
>>>    icons (table, schema, etc.) are not showing up.
>>>    10. Rename panel option should be disabled by default. It should be
>>>    enabled for the tools which implement rename functionality.
>>>    11. The Toolbar is not visible in Safari for the ERD tool. Fixed.
>>>
>>> Please review.
>>>
>>> On Thu, Jan 21, 2021 at 4:32 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi,
>>>>
>>>>
>>>> On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:
>>>>
>>>>>
>>>>>
>>>>> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Dave,
>>>>>>
>>>>>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> Hi
>>>>>>>
>>>>>>> Where's the Save Image button gone? I know Aditya was removing it
>>>>>>> whilst working on other things, but it's still required for phase 1 release.
>>>>>>>
>>>>>> It was not working 100% right. :(
>>>>>> So I've removed it for the time being. I'm still working on it.
>>>>>>
>>>>>
>>>>> OK, so that work will be completed in time for the build next week?
>>>>>
>>>> I'm trying my best to make it available before release. I'm struggling
>>>> to make it work perfectly.
>>>>
>>>>>
>>>>>
>>>>>>
>>>>>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Thanks, patch applied.
>>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> OK, So the changes have worked. But still failing at one more
>>>>>>>>> place.
>>>>>>>>> Attached the patch fixes it.
>>>>>>>>>
>>>>>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>
>>>>>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi,
>>>>>>>>>>>
>>>>>>>>>>> The jasmine test cases are working fine on my local machine. The
>>>>>>>>>>> test cases are successful on jenkins other than on linux, not sure why.
>>>>>>>>>>> I have made some fixes by looking at the log. Please review and
>>>>>>>>>>> try.
>>>>>>>>>>>
>>>>>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>
>>>>>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Akshay,
>>>>>>>>>>>>>
>>>>>>>>>>>>> I forgot to remove few of the dependencies which are not
>>>>>>>>>>>>> required as of now (may be in future). Attached patch removes those
>>>>>>>>>>>>> dependencies from package.json.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>>>>>> validation.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I wanted to keep the code as it will be used in future.
>>>>>>>>>>>>>>> Anyway, I've removed the code.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot,
>>>>>>>>>>>>>>> the error is from the underlying library. Can't do much in this.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> It shows the spinner and waits for the response to come
>>>>>>>>>>>>>>> from the back end. I've used the existing table fetching code which is used
>>>>>>>>>>>>>>> at other places. I'll create an RM to improve the back end code for
>>>>>>>>>>>>>>> fetching the tables data which will help the schema diff tool as well.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD
>>>>>>>>>>>>>>> tool won't be used that frequently. We already have a
>>>>>>>>>>>>>>> limited number of keys available for shortcuts. I think we should roll out
>>>>>>>>>>>>>>> without shortcut for now. If there is a user demand for it then we can
>>>>>>>>>>>>>>> think of adding it.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>    -
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I've added a confirmation dialog which will show the number
>>>>>>>>>>>>>>> of tables and links selected. This way user will know what he has selected
>>>>>>>>>>>>>>> before deleting.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Lodash has been used in this module in place of Underscore,
>>>>>>>>>>>>>>>> though the dependency is already introduced by another module,
>>>>>>>>>>>>>>>> but we have mentioned it in the package.json file, which is
>>>>>>>>>>>>>>>> somewhat not convincing to me.
>>>>>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it was
>>>>>>>>>>>>>>>> difficult to extend it because it was attached to the tree.
>>>>>>>>>>>>>>>> So, we need to keep in mind that while implementing React
>>>>>>>>>>>>>>>> in pgAdmin, the nodes should be properly detached from the tree itself, so
>>>>>>>>>>>>>>>> we can reuse it.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI
>>>>>>>>>>>>>>> going forward with React.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin.
>>>>>>>>>>>>>>>>>>> Below are the details:
>>>>>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an
>>>>>>>>>>>>>>>>>>> existing DB.
>>>>>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many
>>>>>>>>>>>>>>>>>>> relationships, many-to-many relationships, adding notes.
>>>>>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer |
>>>>>>>>>>>>>>>>>>> *edbpostgres.com* <http://edbpostgres.com;
>>>>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>
>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>
>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Thanks,
>>>>>>>>> Aditya Toshniwal
>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> *Thanks & Regards*
>>>>>>>> *Akshay Joshi*
>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>
>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EDB: http://www.enterprisedb.com
>>>>>>>
>>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EDB: http://www.enterprisedb.com
>>>>>
>>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Principal Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [application/octet-stream] RM1802.fixes_v3.patch (5.0K, 3-RM1802.fixes_v3.patch)
  download | inline diff:
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
index 2b56ea1c4..3375fdc6f 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/TableDialog.js
@@ -50,7 +50,7 @@ export default class TableDialog {
     return 'entity_dialog';
   }
 
-  getDataModel(attributes, existingTables, colTypes, schemas, sVersion) {
+  getDataModel(attributes, isNew, allTables, colTypes, schemas, sVersion) {
     let dialogObj = this;
     let columnsModel = this.pgBrowser.DataModel.extend({
       idAttribute: 'attnum',
@@ -694,7 +694,11 @@ export default class TableDialog {
           msg = gettext('Table name cannot be empty.');
           this.errorModel.set('name', msg);
           return msg;
-        } else if(_.findIndex(existingTables, (table)=>table[0]==schema&&table[1]==name) >= 0) {
+        }
+
+        /* Check existing table names */
+        let sameNameCount = _.filter(allTables, (table)=>table[0]==schema&&table[1]==name).length;
+        if(isNew && this.sessAttrs['name'] && sameNameCount > 0 || isNew && sameNameCount > 0) {
           msg = gettext('Table name already exists.');
           this.errorModel.set('name', msg);
           return msg;
@@ -737,9 +741,9 @@ export default class TableDialog {
     return Alertify[dialogName];
   }
 
-  show(title, attributes, existingTables, colTypes, schemas, sVersion, callback) {
+  show(title, attributes, isNew, allTables, colTypes, schemas, sVersion, callback) {
     let dialogTitle = title || gettext('Unknown');
     const dialog = this.createOrGetDialog('table_dialog');
-    dialog(dialogTitle, this.getDataModel(attributes, existingTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
+    dialog(dialogTitle, this.getDataModel(attributes, isNew, allTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
   }
 }
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
index 83f0491d6..1e3eff031 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/ui_components/BodyWidget.jsx
@@ -301,12 +301,12 @@ export default class BodyWidget extends React.Component {
 
   getDialog(dialogName) {
     if(dialogName === 'entity_dialog') {
-      let existingTables = this.diagram.getModel().getNodes().map((node)=>{
+      let allTables = this.diagram.getModel().getNodes().map((node)=>{
         return node.getSchemaTableName();
       });
-      return (title, attributes, callback)=>{
+      return (title, attributes, isNew, callback)=>{
         this.props.getDialog(dialogName).show(
-          title, attributes, existingTables, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
+          title, attributes, isNew, allTables, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
         );
       };
     } else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
@@ -335,12 +335,12 @@ export default class BodyWidget extends React.Component {
     let dialog = this.getDialog('entity_dialog');
     if(node) {
       let [schema, table] = node.getSchemaTableName();
-      dialog(_.escape(`Table: ${table} (${schema})`), node.getData(), (newData)=>{
+      dialog(_.escape(`Table: ${table} (${schema})`), node.getData(), false, (newData)=>{
         node.setData(newData);
         this.diagram.repaint();
       });
     } else {
-      dialog('New table', {name: this.diagram.getNextTableName()}, (newData)=>{
+      dialog('New table', {name: this.diagram.getNextTableName()}, true, (newData)=>{
         let newNode = this.diagram.addNode(newData);
         newNode.setSelected(true);
       });
diff --git a/web/regression/javascript/erd/ui_components/body_widget_spec.js b/web/regression/javascript/erd/ui_components/body_widget_spec.js
index 3114ba627..2095ecd90 100644
--- a/web/regression/javascript/erd/ui_components/body_widget_spec.js
+++ b/web/regression/javascript/erd/ui_components/body_widget_spec.js
@@ -252,7 +252,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode();
     expect(tableDialog.show).toHaveBeenCalled();
 
-    let saveCallback = tableDialog.show.calls.mostRecent().args[6];
+    let saveCallback = tableDialog.show.calls.mostRecent().args[7];
     let newData = {key: 'value'};
     saveCallback(newData);
     expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
@@ -267,7 +267,7 @@ describe('ERD BodyWidget', ()=>{
     bodyInstance.addEditNode(node);
     expect(tableDialog.show).toHaveBeenCalled();
 
-    saveCallback = tableDialog.show.calls.mostRecent().args[6];
+    saveCallback = tableDialog.show.calls.mostRecent().args[7];
     newData = {key: 'value'};
     saveCallback(newData);
     expect(node.setData).toHaveBeenCalledWith(newData);


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [pgAdmin][RM1802] ERD Tool (Beta)
@ 2021-01-25 14:17  Akshay Joshi <[email protected]>
  parent: Aditya Toshniwal <[email protected]>
  0 siblings, 0 replies; 21+ messages in thread

From: Akshay Joshi @ 2021-01-25 14:17 UTC (permalink / raw)
  To: Aditya Toshniwal <[email protected]>; +Cc: pgadmin-hackers; Khushboo Vashi <[email protected]>; Dave Page <[email protected]>

Thanks, patch applied.

On Mon, Jan 25, 2021 at 7:39 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
> The attached patch fixes an issue created by existing table name check.
> Please review.
>
> On Mon, Jan 25, 2021 at 5:34 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, patch applied.
>>
>> On Mon, Jan 25, 2021 at 5:18 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi,
>>>
>>> Please find the rebased patch from the latest pull.
>>>
>>> On Mon, Jan 25, 2021 at 5:12 PM Aditya Toshniwal <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> Attached is the patch to fix below issues in ERD:
>>>>
>>>>    1. After opening an existing project, the first table is already
>>>>    selected but edit, clone, delete buttons are disable. Fixed.
>>>>    2. ERD project title gets changed when 2 ERD projects are open &
>>>>    anyone of it edited. Fixed.
>>>>    3. Closing ERD tab, does not ask for confirmation pop up. Added.
>>>>    4. Shortcut for 'Show more/Fewer details' is missing. Added.
>>>>    5. Deleting primary key does not delete associated links. Fixed.
>>>>    6. Long table & schema name are getting out of box. Fixed.
>>>>    7. Long table name in notes pop-up need re-alignment. Fixed.
>>>>    8. Same table name present in ERD/canvas is allowed in Add Table
>>>>    dialogue. Added validation in the dialog.
>>>>    9. Download image option is added, but it is not perfect yet. Image
>>>>    icons (table, schema, etc.) are not showing up.
>>>>    10. Rename panel option should be disabled by default. It should be
>>>>    enabled for the tools which implement rename functionality.
>>>>    11. The Toolbar is not visible in Safari for the ERD tool. Fixed.
>>>>
>>>> Please review.
>>>>
>>>> On Thu, Jan 21, 2021 at 4:32 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>>
>>>>> On Thu, Jan 21, 2021 at 3:08 PM Dave Page <[email protected]> wrote:
>>>>>
>>>>>>
>>>>>>
>>>>>> On Thu, Jan 21, 2021 at 4:48 AM Aditya Toshniwal <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Dave,
>>>>>>>
>>>>>>> On Wed, Jan 20, 2021 at 9:20 PM Dave Page <[email protected]> wrote:
>>>>>>>
>>>>>>>> Hi
>>>>>>>>
>>>>>>>> Where's the Save Image button gone? I know Aditya was removing it
>>>>>>>> whilst working on other things, but it's still required for phase 1 release.
>>>>>>>>
>>>>>>> It was not working 100% right. :(
>>>>>>> So I've removed it for the time being. I'm still working on it.
>>>>>>>
>>>>>>
>>>>>> OK, so that work will be completed in time for the build next week?
>>>>>>
>>>>> I'm trying my best to make it available before release. I'm struggling
>>>>> to make it work perfectly.
>>>>>
>>>>>>
>>>>>>
>>>>>>>
>>>>>>>> On Mon, Jan 18, 2021 at 11:45 AM Akshay Joshi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Thanks, patch applied.
>>>>>>>>>
>>>>>>>>> On Mon, Jan 18, 2021 at 5:08 PM Aditya Toshniwal <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> OK, So the changes have worked. But still failing at one more
>>>>>>>>>> place.
>>>>>>>>>> Attached the patch fixes it.
>>>>>>>>>>
>>>>>>>>>> On Mon, Jan 18, 2021 at 4:40 PM Akshay Joshi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>
>>>>>>>>>>> On Mon, Jan 18, 2021 at 2:58 PM Aditya Toshniwal <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi,
>>>>>>>>>>>>
>>>>>>>>>>>> The jasmine test cases are working fine on my local machine.
>>>>>>>>>>>> The test cases are successful on jenkins other than on linux, not sure why.
>>>>>>>>>>>> I have made some fixes by looking at the log. Please review and
>>>>>>>>>>>> try.
>>>>>>>>>>>>
>>>>>>>>>>>> On Mon, Jan 18, 2021 at 1:10 PM Akshay Joshi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Mon, Jan 18, 2021 at 10:34 AM Aditya Toshniwal <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Akshay,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I forgot to remove few of the dependencies which are not
>>>>>>>>>>>>>> required as of now (may be in future). Attached patch removes those
>>>>>>>>>>>>>> dependencies from package.json.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Sat, Jan 16, 2021 at 5:08 PM Akshay Joshi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks, patch applied.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Fri, Jan 15, 2021 at 7:01 PM Aditya Toshniwal <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I've fixed the issues. You can find the comments inline.
>>>>>>>>>>>>>>>> I've also added PropTypes for the components for increased
>>>>>>>>>>>>>>>> validation.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Tue, Jan 12, 2021 at 12:18 PM Khushboo Vashi <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Aditya,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> The functionalities and the code looks good to me, however some of the comments as below:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - Correct the comments at some places (3 occurrences found  in /erd/__init__.py)  which mention Schema diff instead of ERD.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Some comments in the JS/JSX file regarding components/functions (For example, IconButton (forwardRef), Bodywidget etc.) would
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> be great help as we all are new to React.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Done. Added comments to the components.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - Remove the unused imports (for ex bad_request) in /erd/__init__.py
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - Remove commented code
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> # req_args = request.args
>>>>>>>>>>>>>>>>> # if ('recreate' in req_args and
>>>>>>>>>>>>>>>>> #     req_args['recreate'] == '1'):
>>>>>>>>>>>>>>>>> #     connect = False
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Removed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - TableNode.jsx, below two lines can be combined.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> import { PortModelAlignment, DefaultNodeModel } from
>>>>>>>>>>>>>>>>> '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>>>> import { PortWidget } from '@projectstorm/react-diagrams';
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - onImageClick function in BodyWidget.jsx is no use I think, so it should be removed.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I wanted to keep the code as it will be used in future.
>>>>>>>>>>>>>>>> Anyway, I've removed the code.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - I got some console errors while adding/editing tables. Refer to the attached screenshot.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I tried but I didn't get any. Looking at the screenshot,
>>>>>>>>>>>>>>>> the error is from the underlying library. Can't do much in this.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - In the column Edit Mode, while deleting the primary key, it gives the error, which does not go away with any further modifications.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - While generating the SQL, if the server is disconnected, a proper error message should be thrown, right now some server side error is coming.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> It will show connection lost error now. Fixed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - Please remove ... from the menu title (New ERD Project(Beta)...) as it is not opening a dialog.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Done.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - For large data sets, generate ERD hangs.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> It shows the spinner and waits for the response to come
>>>>>>>>>>>>>>>> from the back end. I've used the existing table fetching code which is used
>>>>>>>>>>>>>>>> at other places. I'll create an RM to improve the back end code for
>>>>>>>>>>>>>>>> fetching the tables data which will help the schema diff tool as well.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - Opening the ERD panel in a new window is not working, it opens in the same tab even if you have set the Preference "Open in new browser tab" to True.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Fixed. Added the setting in "Tab settings".
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - No shortcut is provided to open the ERD Tool.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> A shortcut is helpful if we are using it frequently. ERD
>>>>>>>>>>>>>>>> tool won't be used that frequently. We already have a
>>>>>>>>>>>>>>>> limited number of keys available for shortcuts. I think we should roll out
>>>>>>>>>>>>>>>> without shortcut for now. If there is a user demand for it then we can
>>>>>>>>>>>>>>>> think of adding it.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    - SonarQube fixes required.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Fixed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>    -
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *Suggestion:*
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> While removal of the FK link, If any of the table is selected, it is being deleted with FK link.
>>>>>>>>>>>>>>>>> Either we should warn the user OR make 2 different buttons for FK removal and table removal as the user may be confused if the selected table is also removed with the FK.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I've added a confirmation dialog which will show the
>>>>>>>>>>>>>>>> number of tables and links selected. This way user will know what he has
>>>>>>>>>>>>>>>> selected before deleting.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *Observations:*
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Lodash has been used in this module in place of
>>>>>>>>>>>>>>>>> Underscore, though the dependency is already introduced by another module,
>>>>>>>>>>>>>>>>> but we have mentioned it in the package.json file, which
>>>>>>>>>>>>>>>>> is somewhat not convincing to me.
>>>>>>>>>>>>>>>>> Lodash is more advanced than Underscore but we should pick
>>>>>>>>>>>>>>>>> anyone as it will be easy to manage.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> TL;DR; we cannot.
>>>>>>>>>>>>>>>> lodash is a peer dependency for react-diagrams (and some
>>>>>>>>>>>>>>>> existing modules in pgAdmin) so it will come to package.json without
>>>>>>>>>>>>>>>> choice. We cannot remove underscore because it is a dependency of backbone.
>>>>>>>>>>>>>>>> Underscore is outdated, and I cannot migrate the complete pgAdmin code. So,
>>>>>>>>>>>>>>>> I decided to go with 100/0 method. All the new codes will use lodash only
>>>>>>>>>>>>>>>> as we'll phase out underscore with time. Just like jQuery vs ReactJS.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Table dialog code is duplicate of the table node, as it
>>>>>>>>>>>>>>>>> was difficult to extend it because it was attached to the tree.
>>>>>>>>>>>>>>>>> So, we need to keep in mind that while implementing React
>>>>>>>>>>>>>>>>> in pgAdmin, the nodes should be properly detached from the tree itself, so
>>>>>>>>>>>>>>>>> we can reuse it.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Yes. I agree. We need to separate out data source from UI
>>>>>>>>>>>>>>>> going forward with React.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Mon, Dec 28, 2020 at 10:53 AM Khushboo Vashi <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 4:34 PM Akshay Joshi <
>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Can you please review it?
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> On it.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> On Fri, Dec 25, 2020 at 3:31 PM Aditya Toshniwal <
>>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Attached patch introduces ERD Tool(Beta) to pgAdmin.
>>>>>>>>>>>>>>>>>>>> Below are the details:
>>>>>>>>>>>>>>>>>>>> 1) Create a diagram from scratch or generate for an
>>>>>>>>>>>>>>>>>>>> existing DB.
>>>>>>>>>>>>>>>>>>>> 2) Generate "Create" DDL from the diagram.
>>>>>>>>>>>>>>>>>>>> 3) Save the diagram and resume it later.
>>>>>>>>>>>>>>>>>>>> 4) Supports basic table fields, one-to-many
>>>>>>>>>>>>>>>>>>>> relationships, many-to-many relationships, adding notes.
>>>>>>>>>>>>>>>>>>>> 5) Test cases added with 75-80% test coverage.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Please review.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer |
>>>>>>>>>>>>>>>>>>>> *edbpostgres.com* <http://edbpostgres.com;
>>>>>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> *Thanks & Regards*
>>>>>>>>>>> *Akshay Joshi*
>>>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>>>
>>>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Thanks,
>>>>>>>>>> Aditya Toshniwal
>>>>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>>>>> <http://edbpostgres.com;
>>>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> *Thanks & Regards*
>>>>>>>>> *Akshay Joshi*
>>>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>>>
>>>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Dave Page
>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>> Twitter: @pgsnake
>>>>>>>>
>>>>>>>> EDB: http://www.enterprisedb.com
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Thanks,
>>>>>>> Aditya Toshniwal
>>>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>>>> <http://edbpostgres.com;
>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EDB: http://www.enterprisedb.com
>>>>>>
>>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> Thanks,
>>>> Aditya Toshniwal
>>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>>> <http://edbpostgres.com;
>>>> "Don't Complain about Heat, Plant a TREE"
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> *Thanks & Regards*
>> *Akshay Joshi*
>> *pgAdmin Hacker | Principal Software Architect*
>> *EDB Postgres <http://edbpostgres.com>*
>>
>> *Mobile: +91 976-788-8246*
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin hacker | Sr. Software Engineer | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Principal Software Architect*
*EDB Postgres <http://edbpostgres.com>*

*Mobile: +91 976-788-8246*


^ permalink  raw  reply  [nested|flat] 21+ messages in thread


end of thread, other threads:[~2021-01-25 14:17 UTC | newest]

Thread overview: 21+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-12-25 10:01 [pgAdmin][RM1802] ERD Tool (Beta) Aditya Toshniwal <[email protected]>
2020-12-25 11:03 ` Akshay Joshi <[email protected]>
2020-12-28 05:23   ` Khushboo Vashi <[email protected]>
2021-01-12 06:48     ` Khushboo Vashi <[email protected]>
2021-01-15 13:30       ` Aditya Toshniwal <[email protected]>
2021-01-16 11:37         ` Akshay Joshi <[email protected]>
2021-01-18 05:03           ` Aditya Toshniwal <[email protected]>
2021-01-18 07:39             ` Akshay Joshi <[email protected]>
2021-01-18 09:27               ` Aditya Toshniwal <[email protected]>
2021-01-18 11:10                 ` Akshay Joshi <[email protected]>
2021-01-18 11:37                   ` Aditya Toshniwal <[email protected]>
2021-01-18 11:45                     ` Akshay Joshi <[email protected]>
2021-01-20 15:50                       ` Dave Page <[email protected]>
2021-01-21 04:47                         ` Aditya Toshniwal <[email protected]>
2021-01-21 09:38                           ` Dave Page <[email protected]>
2021-01-21 11:02                             ` Aditya Toshniwal <[email protected]>
2021-01-25 11:42                               ` Aditya Toshniwal <[email protected]>
2021-01-25 11:47                                 ` Aditya Toshniwal <[email protected]>
2021-01-25 12:03                                   ` Akshay Joshi <[email protected]>
2021-01-25 14:08                                     ` Aditya Toshniwal <[email protected]>
2021-01-25 14:17                                       ` Akshay Joshi <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox