diff --git a/docs/en_US/images/pgd_group_node_dialog.png b/docs/en_US/images/pgd_group_node_dialog.png new file mode 100644 index 00000000000..7309ced011b Binary files /dev/null and b/docs/en_US/images/pgd_group_node_dialog.png differ diff --git a/docs/en_US/images/pgd_server_node_dialog.png b/docs/en_US/images/pgd_server_node_dialog.png new file mode 100644 index 00000000000..4a53efd6ff8 Binary files /dev/null and b/docs/en_US/images/pgd_server_node_dialog.png differ diff --git a/docs/en_US/images/replica_nodes_general.png b/docs/en_US/images/replica_nodes_general.png index 37da75efd1f..5ba48994b4d 100644 Binary files a/docs/en_US/images/replica_nodes_general.png and b/docs/en_US/images/replica_nodes_general.png differ diff --git a/docs/en_US/managing_cluster_objects.rst b/docs/en_US/managing_cluster_objects.rst index 0bbe5c03963..55ca3c5ec75 100644 --- a/docs/en_US/managing_cluster_objects.rst +++ b/docs/en_US/managing_cluster_objects.rst @@ -3,7 +3,7 @@ ********************************* `Managing Cluster Objects`:index: ********************************* - + Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs that allow you to create these objects, manage them, and control their relationships to each other. To access a dialog that allows you to create a @@ -12,11 +12,12 @@ and select the *Create* option for that object. For example, to create a new database, right-click on the *Databases* node, and select *Create Database...* .. toctree:: - :maxdepth: 1 + :maxdepth: 3 database_dialog resource_group_dialog role_dialog tablespace_dialog replica_nodes_dialog + pgd_replication_group_dialog role_reassign_dialog \ No newline at end of file diff --git a/docs/en_US/pgd_replication_group_dialog.rst b/docs/en_US/pgd_replication_group_dialog.rst new file mode 100644 index 00000000000..ae4cce41708 --- /dev/null +++ b/docs/en_US/pgd_replication_group_dialog.rst @@ -0,0 +1,40 @@ +.. _pgd_replication_group_dialog: + +****************************************** +`PGD Replication Group Node Dialog`:index: +****************************************** + +Use the *Replication Group Node* dialog to view a PGD group/sub-group. +A PGD cluster's nodes are gathered in groups. A group can also contain zero or more subgroups. +Subgroups can be used to represent data centers or locations allowing commit scopes to refer to +nodes in a particular region as a whole. + +The dialog organizes the information through the following tabs: +*General* + +.. image:: images/pgd_group_node_dialog.png + :alt: Replication Group Node Dialog general tab + :align: center + +* The *ID* field is the ID of the node group. +* The *Name* field is the name of the node group. +* The *Location* field is the name of the location associated with the node group. +* The *Type* field is the type of the node group, one of "global", "data", "shard" or "subscriber-only". +* The *Streaming Mode* field is the transaction streaming setting of the node group, one of "off", "file", "writer", + "auto" or "default" +* The *Enable Proxy Routing?* field tells whether the node group allows routing from pgd-proxy. +* The *Enable Raft?* field tells whether the node group allows Raft Consensus. + + +Other buttons: + +* Click the *Info* button (i) to access online help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. + + +A group can have multiple servers and will be visible in the object explorer tree. + +.. toctree:: + pgd_replication_server_dialog \ No newline at end of file diff --git a/docs/en_US/pgd_replication_server_dialog.rst b/docs/en_US/pgd_replication_server_dialog.rst new file mode 100644 index 00000000000..eb8cf14e518 --- /dev/null +++ b/docs/en_US/pgd_replication_server_dialog.rst @@ -0,0 +1,30 @@ +.. _pgd_replication_server_dialog: + +******************************************* +`PGD Replication Server Node Dialog`:index: +******************************************* + +Use The *Replication Server Node* dialog to view an element that run +databases and participate in the PGD cluster. A typical PGD node runs a Postgres +database, the BDR extension, and optionally a PGD Proxy service. + +The dialog organizes the information through the following tabs: +*General* + +.. image:: images/pgd_server_node_dialog.png + :alt: Replication Server Node Dialog general tab + :align: center + +* The *Sequence ID* field is the identifier of the node used for generating unique sequence numbers. +* The *ID* field is the OID of the node. +* The *Name* field is the name of the node. +* The *Kind* field is the node kind name. +* The *Group Name* field is the PGD group the node is part of. +* The *Local DB Name* field is the database name of the node. + +Other buttons: + +* Click the *Info* button (i) to access online help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. diff --git a/web/pgadmin/browser/collection.py b/web/pgadmin/browser/collection.py index 1ef888b9fc7..3beb1a2b313 100644 --- a/web/pgadmin/browser/collection.py +++ b/web/pgadmin/browser/collection.py @@ -61,7 +61,7 @@ def module_use_template_javascript(self): return True def generate_browser_node( - self, node_id, parent_id, label, icon, **kwargs + self, node_id, parent_id, label, icon=None, **kwargs ): obj = { "id": "%s_%s" % (self.node_type, node_id), diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 9e65e244866..e1903499d97 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -349,6 +349,9 @@ def register(self, app, options): from .replica_nodes import blueprint as module self.submodules.append(module) + from .pgd_replication_groups import blueprint as module + self.submodules.append(module) + super().register(app, options) # We do not have any preferences for server node. diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/__init__.py b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/__init__.py new file mode 100644 index 00000000000..b7a3945e94a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/__init__.py @@ -0,0 +1,284 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from functools import wraps + +from pgadmin.browser.server_groups import servers +from flask import render_template +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, \ + make_response as ajax_response, internal_server_error, gone +from pgadmin.utils.ajax import precondition_required +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.server_groups.servers.utils import get_replication_type + + +class PGDReplicationGroupsModule(CollectionNodeModule): + """ + class PGDReplicationGroupsModule(CollectionNodeModule) + + A module class for PGD Replication Group Nodes derived from + CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the PGDReplicationGroupsModule and it's + base module. + + * backend_supported(manager, **kwargs) + - This function is used to check the database server type and version. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * node_inode() + - Method is overridden from its base class to make the node as leaf node. + """ + + _NODE_TYPE = 'pgd_replication_groups' + _COLLECTION_LABEL = gettext("PGD Replication Groups") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the PGDReplicationGroupsModule and + it's base module. + + Args: + *args: + **kwargs: + """ + super().__init__(*args, **kwargs) + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-ppas' + + def get_nodes(self, gid, sid): + """ + Method is used to generate the browser collection node + + Args: + gid: Server Group ID + sid: Server ID + """ + yield self.generate_browser_collection_node(sid) + + @property + def node_inode(self): + """ + Override this property to make the node as leaf node. + + Returns: False as this is the leaf node + """ + return False + + def backend_supported(self, manager, **kwargs): + """ + Load this module if replication type exists + """ + if super().backend_supported(manager, **kwargs): + conn = manager.connection(sid=kwargs['sid']) + + replication_type = get_replication_type(conn, manager.version) + return replication_type == 'pgd' + + def register(self, app, options): + """ + Override the default register function to automagically register + sub-modules at once. + """ + from .pgd_replication_servers import blueprint as module + self.submodules.append(module) + + super().register(app, options) + + +blueprint = PGDReplicationGroupsModule(__name__) + + +class PGDReplicationGroupsView(PGChildNodeView): + """ + class PGDReplicationGroupsView(NodeView) + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the PGDReplicationGroupsView, + and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the PGD Replication Group Nodes + within that collection. + + * nodes() + - This function will used to create all the child node within that + collection. Here it will create all the PGD Replication Group Nodes. + + * properties(gid, sid, did, pid) + - This function will show the properties of the selected node + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'pgd_replication_groups/sql/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'} + ] + ids = [ + {'type': 'int', 'id': 'node_group_id'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties'}, + {'get': 'list'} + ], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'children': [ + {'get': 'children'} + ], + }) + + def __init__(self, **kwargs): + """ + Method is used to initialize the PGDReplicationGroupsView and, + it's base view. + Also initialize all the variables create/used dynamically like conn, + template_path. + + Args: + **kwargs: + """ + self.conn = None + self.template_path = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection() + + if not self.conn.connected(): + return precondition_required( + gettext( + "Connection to the server has been lost." + ) + ) + + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid): + """ + This function is used to list all the PGD Replication Group Nodes + within that collection. + + Args: + gid: Server Group ID + sid: Server ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid): + """ + This function will used to create all the child node within that + collection. Here it will create all the PGD Replication Group Nodes. + + Args: + gid: Server Group ID + sid: Server ID + """ + res = [] + sql = render_template("/".join([self.template_path, self._NODES_SQL])) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['node_group_id'], + sid, + row['node_group_name'], + icon='icon-server_group', + inode=True + )) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def properties(self, gid, sid, node_group_id): + """ + This function will show the properties of the selected node. + + Args: + gid: Server Group ID + sid: Server ID + node_group_id: PGD Replication Group Nodes ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + node_group_id=node_group_id) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(gettext("""Could not find the Replication Node.""")) + + return ajax_response( + response=res['rows'][0], + status=200 + ) + + +PGDReplicationGroupsView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/__init__.py b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/__init__.py new file mode 100644 index 00000000000..8551e59115d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/__init__.py @@ -0,0 +1,276 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from functools import wraps + +from pgadmin.browser.server_groups import servers +from flask import render_template +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, \ + make_response as ajax_response, internal_server_error, gone +from pgadmin.utils.ajax import precondition_required +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.server_groups.servers.utils import get_replication_type + + +class PGDReplicationServersModule(CollectionNodeModule): + """ + class PGDReplicationServersModule(CollectionNodeModule) + + A module class for PGD Replication Server Nodes derived from + CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the PGDReplicationServersModule and it's + base module. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * node_inode() + - Method is overridden from its base class to make the node as leaf node. + """ + + _NODE_TYPE = 'pgd_replication_servers' + _COLLECTION_LABEL = gettext("Servers") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the PGDReplicationServersModule and + it's base module. + + Args: + *args: + **kwargs: + """ + super().__init__(*args, **kwargs) + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-server_group' + + def get_nodes(self, gid, sid, node_group_id): + """ + Method is used to generate the browser collection node + + Args: + gid: Server Group ID + sid: Server ID + """ + yield self.generate_browser_collection_node(node_group_id) + + @property + def node_inode(self): + """ + Override this property to make the node as leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def csssnippets(self): + """ + Returns a snippet of css to include in the page + """ + return [ + render_template( + "pgd_replication_servers/css/pgd_replication_servers.css", + node_type=self.node_type + ) + ] + + +blueprint = PGDReplicationServersModule(__name__) + + +class PGDReplicationServersView(PGChildNodeView): + """ + class PGDReplicationServersView(NodeView) + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the PGDReplicationServersView, + and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the PGD Replication Server Nodes + within that collection. + + * nodes() + - This function will used to create all the child node within that + collection. Here it will create all the PGD Replication Server Nodes. + + * properties(gid, sid, did, pid) + - This function will show the properties of the selected node + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'pgd_replication_servers/sql/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'node_group_id'} + ] + ids = [ + {'type': 'int', 'id': 'node_id'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties'}, + {'get': 'list'} + ], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'children': [ + {'get': 'children'} + ], + }) + + def __init__(self, **kwargs): + """ + Method is used to initialize the PGDReplicationServersView and, + it's base view. + Also initialize all the variables create/used dynamically like conn, + template_path. + + Args: + **kwargs: + """ + self.conn = None + self.template_path = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection() + + if not self.conn.connected(): + return precondition_required( + gettext( + "Connection to the server has been lost." + ) + ) + + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid, node_group_id): + """ + This function is used to list all the PGD Replication Server Nodes + within that collection. + + Args: + gid: Server Group ID + sid: Server ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + node_group_id=node_group_id) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, node_group_id): + """ + This function will used to create all the child node within that + collection. Here it will create all the PGD Replication Server Nodes. + + Args: + gid: Server Group ID + sid: Server ID + """ + res = [] + sql = render_template("/".join([self.template_path, self._NODES_SQL]), + node_group_id=node_group_id) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['node_id'], + sid, + row['node_name'], + icon='icon-pgd_node_{0}'.format(row['node_kind_name']), + )) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def properties(self, gid, sid, node_group_id, node_id): + """ + This function will show the properties of the selected node. + + Args: + gid: Server Group ID + sid: Server ID + node_id: PGD Replication Server Nodes ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + node_group_id=node_group_id, + node_id=node_id) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(gettext("""Could not find the Replication Node.""")) + + return ajax_response( + response=res['rows'][0], + status=200 + ) + + +PGDReplicationServersView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_data.svg b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_data.svg new file mode 100644 index 00000000000..176a1517b67 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_data.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_standby.svg b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_standby.svg new file mode 100644 index 00000000000..176a1517b67 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_standby.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_subscriber-only.svg b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_subscriber-only.svg new file mode 100644 index 00000000000..176a1517b67 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_subscriber-only.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_witness.svg b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_witness.svg new file mode 100644 index 00000000000..176a1517b67 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/img/pgd_node_witness.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_server_node.ui.js b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_server_node.ui.js new file mode 100644 index 00000000000..d0f4b551f96 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_server_node.ui.js @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + +export default class PgdReplicationServerNodeSchema extends BaseUISchema { + get idAttribute() { + return 'node_id'; + } + + get baseFields() { + return [ + { + id: 'node_seq_id', label: gettext('Sequence ID'), type: 'numeric', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_id', label: gettext('ID'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_name', label: gettext('Name'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_kind_name', label: gettext('Kind'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_name', label: gettext('Group Name'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_local_dbname', label: gettext('Local DB Name'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + ]; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers.js b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers.js new file mode 100644 index 00000000000..0ce0dc1894d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers.js @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import PgdReplicationServerNodeSchema from './pgd_replication_server_node.ui'; + +define('pgadmin.node.pgd_replication_servers', [ + 'sources/gettext', 'sources/url_for', 'pgadmin.browser', + 'pgadmin.browser.collection', +], function(gettext, url_for, pgBrowser) { + + // Extend the browser's collection class for replica nodes collection + if (!pgBrowser.Nodes['coll-pgd_replication_servers']) { + pgBrowser.Nodes['coll-pgd_replication_servers'] = + pgBrowser.Collection.extend({ + node: 'pgd_replication_servers', + label: gettext('Servers'), + type: 'coll-pgd_replication_servers', + columns: ['node_seq_id', 'node_id', 'node_name', 'node_kind_name', 'node_group_name'], + canEdit: false, + canDrop: false, + canDropCascade: false, + canSelect: false, + }); + } + + // Extend the browser's node class for replica nodes node + if (!pgBrowser.Nodes['pgd_replication_servers']) { + pgBrowser.Nodes['pgd_replication_servers'] = pgBrowser.Node.extend({ + parent_type: 'pgd_replication_groups', + type: 'pgd_replication_servers', + epasHelp: false, + sqlAlterHelp: '', + sqlCreateHelp: '', + dialogHelp: url_for('help.static', {'filename': 'pgd_replication_server_dialog.html'}), + label: gettext('Server'), + hasSQL: false, + hasScriptTypes: false, + canDrop: false, + node_image: function(r) { + if(r.icon) { + return r.icon; + } + return 'icon-server-not-connected'; + }, + Init: function() { + + // Avoid multiple registration of menus + if (this.initialized) { + return; + } + + this.initialized = true; + }, + + getSchema: ()=>{ + return new PgdReplicationServerNodeSchema(); + }, + }); + } + + return pgBrowser.Nodes['coll-pgd_replication_servers']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/css/pgd_replication_servers.css b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/css/pgd_replication_servers.css new file mode 100644 index 00000000000..293765d3eed --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/css/pgd_replication_servers.css @@ -0,0 +1,35 @@ +.icon-pgd_node_data { + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/pgd_node_data.svg' )}}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} + +.icon-pgd_node_standby { + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/pgd_node_standby.svg' )}}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} + +.icon-pgd_node_witness { + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/pgd_node_witness.svg' )}}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} + +.icon-pgd_node_subscriber-only { + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/pgd_node_subscriber-only.svg' )}}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/count.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/count.sql new file mode 100644 index 00000000000..75e1139153f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/count.sql @@ -0,0 +1,3 @@ +SELECT count(*) +FROM bdr.node_summary +WHERE node_group_id = {{node_group_id}} diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/nodes.sql new file mode 100644 index 00000000000..25f9272b7e2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/nodes.sql @@ -0,0 +1,4 @@ +SELECT node_id, node_name, node_group_name, node_kind_name +FROM bdr.node_summary +WHERE node_group_id = {{node_group_id}} +ORDER BY node_seq_id; diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/properties.sql new file mode 100644 index 00000000000..0ac04370c2a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/templates/pgd_replication_servers/sql/default/properties.sql @@ -0,0 +1,10 @@ +SELECT node_name, node_group_name, interface_connstr, + peer_state_name, peer_target_state_name, node_seq_id, node_local_dbname, + node_id, node_group_id, node_kind_name +FROM + bdr.node_summary +WHERE node_group_id = {{node_group_id}} +{% if node_id %} +AND node_id={{node_id}} +{% endif %} +ORDER BY node_seq_id; diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/img/pgd_node_group.svg b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/img/pgd_node_group.svg new file mode 100644 index 00000000000..176a1517b67 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/img/pgd_node_group.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_group_node.ui.js b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_group_node.ui.js new file mode 100644 index 00000000000..de2eb209ede --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_group_node.ui.js @@ -0,0 +1,43 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + +export default class PgdReplicationGroupNodeSchema extends BaseUISchema { + get idAttribute() { + return 'node_group_id'; + } + + get baseFields() { + return [ + { + id: 'node_group_id', label: gettext('ID'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_name', label: gettext('Name'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_location', label: gettext('Location'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_type', label: gettext('Type'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'streaming_mode_name', label: gettext('Streaming Mode'), type: 'text', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_enable_proxy_routing', label: gettext('Enable Proxy Routing?'), type: 'switch', mode:['properties', 'edit'], readonly: true, + }, + { + id: 'node_group_enable_raft', label: gettext('Enable Raft?'), type: 'switch', mode:['properties', 'edit'], readonly: true, + }, + ]; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups.js b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups.js new file mode 100644 index 00000000000..9d709562576 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups.js @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import PgdReplicationGroupNodeSchema from './pgd_replication_group_node.ui'; + +define('pgadmin.node.pgd_replication_groups', [ + 'sources/gettext', 'sources/url_for', 'pgadmin.browser', + 'pgadmin.browser.collection', +], function(gettext, url_for, pgBrowser) { + + // Extend the browser's collection class for replica nodes collection + if (!pgBrowser.Nodes['coll-pgd_replication_groups']) { + pgBrowser.Nodes['coll-pgd_replication_groups'] = + pgBrowser.Collection.extend({ + node: 'pgd_replication_groups', + label: gettext('PGD Replication Groups'), + type: 'coll-pgd_replication_groups', + columns: ['node_group_id', 'node_group_name', 'node_group_location'], + canEdit: false, + canDrop: false, + canDropCascade: false, + canSelect: false, + }); + } + + // Extend the browser's node class for replica nodes node + if (!pgBrowser.Nodes['pgd_replication_groups']) { + pgBrowser.Nodes['pgd_replication_groups'] = pgBrowser.Node.extend({ + parent_type: 'server', + type: 'pgd_replication_groups', + epasHelp: false, + sqlAlterHelp: '', + sqlCreateHelp: '', + dialogHelp: url_for('help.static', {'filename': 'pgd_replication_group_dialog.html'}), + label: gettext('PGD Replication Group'), + hasSQL: false, + hasScriptTypes: false, + canDrop: false, + node_image: function(r) { + if(r.icon) { + return r.icon; + } + return 'icon-server-not-connected'; + }, + Init: function() { + + // Avoid multiple registration of menus + if (this.initialized) { + return; + } + + this.initialized = true; + }, + + getSchema: ()=>{ + return new PgdReplicationGroupNodeSchema(); + }, + }); + } + + return pgBrowser.Nodes['coll-pgd_replication_groups']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/count.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/count.sql new file mode 100644 index 00000000000..f58e32db87f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/count.sql @@ -0,0 +1,3 @@ +SELECT COUNT(*) +FROM bdr.node_group +WHERE node_group_parent_id != 0; diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/nodes.sql new file mode 100644 index 00000000000..ac9b97d4261 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/nodes.sql @@ -0,0 +1,4 @@ +SELECT node_group_id, node_group_name +FROM bdr.node_group +WHERE node_group_parent_id != 0 +ORDER BY node_group_name diff --git a/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/properties.sql new file mode 100644 index 00000000000..63c6365e980 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/pgd_replication_groups/templates/pgd_replication_groups/sql/default/properties.sql @@ -0,0 +1,11 @@ +SELECT ng.node_group_id, ng.node_group_name, + (SELECT node_group_name FROM bdr.node_group WHERE node_group_id = ng.node_group_parent_id) AS node_group_parent, + bdr.node_group_type(node_group_name::text) AS node_group_type, + bdr.streaming_mode_name(ng.node_group_streaming_mode) AS streaming_mode_name, + ng.node_group_location, ng.node_group_enable_proxy_routing, ng.node_group_enable_raft +FROM bdr.node_group ng +WHERE ng.node_group_parent_id != 0 +{% if node_group_id %} +AND ng.node_group_id={{node_group_id}} +{% endif %} +ORDER BY ng.node_group_name diff --git a/web/pgadmin/browser/server_groups/servers/replica_nodes/__init__.py b/web/pgadmin/browser/server_groups/servers/replica_nodes/__init__.py index 17103c86d92..70ee5294854 100644 --- a/web/pgadmin/browser/server_groups/servers/replica_nodes/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/replica_nodes/__init__.py @@ -103,7 +103,7 @@ def backend_supported(self, manager, **kwargs): conn = manager.connection(sid=kwargs['sid']) replication_type = get_replication_type(conn, manager.version) - return bool(replication_type) + return replication_type == 'log' blueprint = ReplicationNodesModule(__name__) @@ -248,7 +248,6 @@ def nodes(self, gid, sid): row['pid'], sid, row['name'], - icon="icon-replica_nodes" )) return make_json_response( diff --git a/web/pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node.js b/web/pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node.js index 8203884979f..109ac3d604c 100644 --- a/web/pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node.js +++ b/web/pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node.js @@ -9,18 +9,18 @@ import ReplicaNodeSchema from './replica_node.ui'; -define('pgadmin.node.replica_nodes', [ +define('pgadmin.node.replica_node', [ 'sources/gettext', 'sources/url_for', 'pgadmin.browser', 'pgadmin.browser.collection', ], function(gettext, url_for, pgBrowser) { // Extend the browser's collection class for replica nodes collection - if (!pgBrowser.Nodes['coll-replica_nodes']) { - pgBrowser.Nodes['coll-replica_nodes'] = + if (!pgBrowser.Nodes['coll-replica_node']) { + pgBrowser.Nodes['coll-replica_node'] = pgBrowser.Collection.extend({ - node: 'replica_nodes', + node: 'replica_node', label: gettext('Replica Nodes'), - type: 'coll-replica_nodes', + type: 'coll-replica_node', columns: ['pid', 'name', 'usename', 'state'], canEdit: false, canDrop: false, @@ -29,14 +29,14 @@ define('pgadmin.node.replica_nodes', [ } // Extend the browser's node class for replica nodes node - if (!pgBrowser.Nodes['replica_nodes']) { - pgBrowser.Nodes['replica_nodes'] = pgBrowser.Node.extend({ + if (!pgBrowser.Nodes['replica_node']) { + pgBrowser.Nodes['replica_node'] = pgBrowser.Node.extend({ parent_type: 'server', - type: 'replica_nodes', + type: 'replica_node', epasHelp: false, sqlAlterHelp: '', sqlCreateHelp: '', - dialogHelp: url_for('help.static', {'filename': 'replica_nodes_dialog.html'}), + dialogHelp: url_for('help.static', {'filename': 'replica_node_dialog.html'}), label: gettext('Replica Nodes'), hasSQL: false, hasScriptTypes: false, @@ -57,5 +57,5 @@ define('pgadmin.node.replica_nodes', [ }); } - return pgBrowser.Nodes['coll-replica_nodes']; + return pgBrowser.Nodes['coll-replica_node']; }); diff --git a/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/nodes.sql index 0c658bf823c..34219a67eb8 100644 --- a/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/nodes.sql +++ b/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/nodes.sql @@ -1,3 +1,3 @@ -SELECT pid, 'Standby ['||COALESCE(client_addr::text, client_hostname,'Socket')||']' as name +SELECT pid, 'Standby ['||COALESCE(host(client_addr), client_hostname, 'Socket')||']' as name FROM pg_stat_replication ORDER BY pid diff --git a/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/properties.sql index c9a533a0175..02dd66ebc98 100644 --- a/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/replica_nodes/templates/replica_nodes/sql/default/properties.sql @@ -1,4 +1,4 @@ -SELECT st.*, 'Standby ['||COALESCE(client_addr::text, client_hostname,'Socket')||']' as name, +SELECT st.*, 'Standby ['||COALESCE(host(client_addr), client_hostname, 'Socket')||']' as name, sl.slot_name, sl.slot_type, sl.active FROM pg_stat_replication st JOIN pg_replication_slots sl ON st.pid = sl.active_pid diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/sql/default/replication_type.sql b/web/pgadmin/browser/server_groups/servers/templates/servers/sql/default/replication_type.sql index 4c644e4cceb..50bbc585098 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/servers/sql/default/replication_type.sql +++ b/web/pgadmin/browser/server_groups/servers/templates/servers/sql/default/replication_type.sql @@ -1,7 +1,7 @@ SELECT CASE WHEN (SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname='bdr') > 0 THEN 'pgd' - WHEN (SELECT COUNT(*) FROM pg_stat_replication) > 0 + WHEN (SELECT COUNT(*) FROM pg_replication_slots) > 0 THEN 'log' ELSE NULL END as type; diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index ab1d10aedfb..c0435b3ac32 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.js @@ -81,7 +81,7 @@ define('pgadmin.browser.utils', 'coll-role', 'role', 'coll-resource_group', 'resource_group', 'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep', 'pga_job', 'pga_schedule', 'pga_jobstep', - 'coll-replica_nodes', 'replica_nodes' + 'coll-replica_node', 'replica_node' ]; pgBrowser.utils = { diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index c5e3fdf7fa0..7e6eb8ab16c 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -9,21 +9,22 @@ """A blueprint module implementing the dashboard frame.""" import math -from functools import wraps -from flask import render_template, url_for, Response, g, request + +from flask import render_template, Response, g, request from flask_babel import gettext from pgadmin.user_login_check import pga_login_required import json from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_response as ajax_response,\ internal_server_error -from pgadmin.utils.ajax import precondition_required + from pgadmin.utils.driver import get_driver -from pgadmin.utils.menu import Panel from pgadmin.utils.preferences import Preferences from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \ - PREF_LABEL_REFRESH_RATES + PREF_LABEL_REFRESH_RATES, ERROR_SERVER_ID_NOT_SPECIFIED +from .precondition import check_precondition +from .pgd_replication import blueprint as pgd_replication from config import PG_DEFAULT_DRIVER MODULE_NAME = 'dashboard' @@ -217,6 +218,8 @@ def register_preferences(self): help_str=gettext('Set the width of the lines on the line chart.') ) + pgd_replication.register_preferences(self) + def get_exposed_url_endpoints(self): """ Returns: @@ -247,70 +250,11 @@ def get_exposed_url_endpoints(self): 'dashboard.system_statistics_did', 'dashboard.replication_slots', 'dashboard.replication_stats', - ] + ] + pgd_replication.get_exposed_url_endpoints() blueprint = DashboardModule(MODULE_NAME, __name__) - - -def check_precondition(f): - """ - This function will behave as a decorator which will check - database connection before running view, it also adds - manager, conn & template_path properties to self - """ - - @wraps(f) - def wrap(*args, **kwargs): - # Here args[0] will hold self & kwargs will hold gid,sid,did - - g.manager = get_driver( - PG_DEFAULT_DRIVER).connection_manager( - kwargs['sid'] - ) - - def get_error(i_node_type): - stats_type = ('activity', 'prepared', 'locks', 'config') - if f.__name__ in stats_type: - return precondition_required( - gettext("Please connect to the selected {0}" - " to view the table.".format(i_node_type)) - ) - else: - return precondition_required( - gettext("Please connect to the selected {0}" - " to view the graph.".format(i_node_type)) - ) - - # Below check handle the case where existing server is deleted - # by user and python server will raise exception if this check - # is not introduce. - if g.manager is None: - return get_error('server') - - if 'did' in kwargs: - g.conn = g.manager.connection(did=kwargs['did']) - node_type = 'database' - else: - g.conn = g.manager.connection() - node_type = 'server' - - # If not connected then return error to browser - if not g.conn.connected(): - return get_error(node_type) - - # Set template path for sql scripts - g.server_type = g.manager.server_type - g.version = g.manager.version - - # Include server_type in template_path - g.template_path = 'dashboard/sql/' + ( - '#{0}#'.format(g.version) - ) - - return f(*args, **kwargs) - - return wrap +blueprint.register_blueprint(pgd_replication) @blueprint.route("/dashboard.js") @@ -389,7 +333,7 @@ def get_data(sid, did, template, check_long_running_query=False): # Allow no server ID to be specified (so we can generate a route in JS) # but throw an error if it's actually called. if not sid: - return internal_server_error(errormsg='Server ID not specified.') + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) sql = render_template( "/".join([g.template_path, template]), did=did @@ -455,7 +399,8 @@ def dashboard_stats(sid=None, did=None): chart_names = request.args['chart_names'].split(',') if not sid: - return internal_server_error(errormsg='Server ID not specified.') + return internal_server_error( + errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) sql = render_template( "/".join([g.template_path, 'dashboard_stats.sql']), did=did, @@ -629,7 +574,8 @@ def system_statistics(sid=None, did=None): chart_names = request.args['chart_names'].split(',') if not sid: - return internal_server_error(errormsg='Server ID not specified.') + return internal_server_error( + errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) sql = render_template( "/".join([g.template_path, 'system_statistics.sql']), did=did, @@ -660,7 +606,7 @@ def replication_stats(sid=None): """ if not sid: - return internal_server_error(errormsg='Server ID not specified.') + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) sql = render_template("/".join([g.template_path, 'replication_stats.sql'])) status, res = g.conn.execute_dict(sql) @@ -684,7 +630,7 @@ def replication_slots(sid=None): """ if not sid: - return internal_server_error(errormsg='Server ID not specified.') + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) sql = render_template("/".join([g.template_path, 'replication_slots.sql'])) status, res = g.conn.execute_dict(sql) diff --git a/web/pgadmin/dashboard/pgd_replication.py b/web/pgadmin/dashboard/pgd_replication.py new file mode 100644 index 00000000000..9a985d4bbd1 --- /dev/null +++ b/web/pgadmin/dashboard/pgd_replication.py @@ -0,0 +1,172 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from flask import Blueprint, render_template, g, request +from flask_security import login_required +from flask_babel import gettext +import json + +from .precondition import check_precondition +from pgadmin.utils.ajax import make_response as ajax_response,\ + internal_server_error +from pgadmin.utils.constants import PREF_LABEL_REFRESH_RATES, \ + ERROR_SERVER_ID_NOT_SPECIFIED + + +class PGDReplicationDashboard(Blueprint): + @staticmethod + def register_preferences(self): + help_string = gettext('The number of seconds between graph samples.') + + self.pgd_replication_lag_refresh = self.dashboard_preference.register( + 'dashboards', 'pgd_replication_lag_refresh', + gettext("PGD replication lag refresh rate"), 'integer', + 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + @staticmethod + def get_exposed_url_endpoints(): + return [ + 'dashboard.pgd.outgoing', 'dashboard.pgd.incoming', + 'dashboard.pgd.cluster_nodes', 'dashboard.pgd.raft_status', + 'dashboard.pgd.charts' + ] + + +blueprint = PGDReplicationDashboard('pgd', __name__, url_prefix='/pgd') + + +@blueprint.route('/cluster_nodes/', endpoint='cluster_nodes') +@login_required +@check_precondition +def cluster_nodes(sid=None): + """ + This function is used to list all the Replication slots of the cluster + """ + + if not sid: + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) + + sql = render_template("/".join([g.template_path, 'pgd_cluster_nodes.sql'])) + status, res = g.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=str(res)) + + return ajax_response( + response=res['rows'], + status=200 + ) + + +@blueprint.route('/raft_status/', endpoint='raft_status') +@login_required +@check_precondition +def raft_status(sid=None): + """ + This function is used to list all the raft details of the cluster + """ + + if not sid: + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) + + sql = render_template("/".join([g.template_path, 'pgd_raft_status.sql'])) + status, res = g.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=str(res)) + + return ajax_response( + response=res['rows'], + status=200 + ) + + +@blueprint.route('/charts/', endpoint='charts') +@login_required +@check_precondition +def charts(sid=None): + """ + This function is used to get all the charts + """ + + resp_data = {} + + if request.args['chart_names'] != '': + chart_names = request.args['chart_names'].split(',') + + if not sid: + return internal_server_error( + errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) + + sql = render_template( + "/".join([g.template_path, 'pgd_charts.sql']), + chart_names=chart_names, + ) + status, res = g.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=str(res)) + + for chart_row in res['rows']: + resp_data[chart_row['chart_name']] = json.loads( + chart_row['chart_data']) + + return ajax_response( + response=resp_data, + status=200 + ) + + +@blueprint.route('/outgoing/', endpoint='outgoing') +@login_required +@check_precondition +def outgoing(sid=None): + """ + This function is used to list all the outgoing replications of the cluster + """ + + if not sid: + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) + + sql = render_template("/".join([g.template_path, 'pgd_outgoing.sql'])) + status, res = g.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=str(res)) + + return ajax_response( + response=res['rows'], + status=200 + ) + + +@blueprint.route('/incoming/', endpoint='incoming') +@login_required +@check_precondition +def incoming(sid=None): + """ + This function is used to list all the incoming replications of the cluster + """ + + if not sid: + return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED) + + sql = render_template("/".join([g.template_path, 'pgd_incoming.sql'])) + status, res = g.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=str(res)) + + return ajax_response( + response=res['rows'], + status=200 + ) diff --git a/web/pgadmin/dashboard/precondition.py b/web/pgadmin/dashboard/precondition.py new file mode 100644 index 00000000000..4f5144ce9bc --- /dev/null +++ b/web/pgadmin/dashboard/precondition.py @@ -0,0 +1,76 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from config import PG_DEFAULT_DRIVER +from pgadmin.utils.driver import get_driver +from pgadmin.utils.ajax import precondition_required + +from flask_babel import gettext +from flask import g +from functools import wraps + + +def check_precondition(f): + """ + This function will behave as a decorator which will check + database connection before running view, it also adds + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + + g.manager = get_driver( + PG_DEFAULT_DRIVER).connection_manager( + kwargs['sid'] + ) + + def get_error(i_node_type): + stats_type = ('activity', 'prepared', 'locks', 'config') + if f.__name__ in stats_type: + return precondition_required( + gettext("Please connect to the selected {0}" + " to view the table.".format(i_node_type)) + ) + else: + return precondition_required( + gettext("Please connect to the selected {0}" + " to view the graph.".format(i_node_type)) + ) + + # Below check handle the case where existing server is deleted + # by user and python server will raise exception if this check + # is not introduce. + if g.manager is None: + return get_error('server') + + if 'did' in kwargs: + g.conn = g.manager.connection(did=kwargs['did']) + node_type = 'database' + else: + g.conn = g.manager.connection() + node_type = 'server' + + # If not connected then return error to browser + if not g.conn.connected(): + return get_error(node_type) + + # Set template path for sql scripts + g.server_type = g.manager.server_type + g.version = g.manager.version + + # Include server_type in template_path + g.template_path = 'dashboard/sql/' + ( + '#{0}#'.format(g.version) + ) + + return f(*args, **kwargs) + + return wrap diff --git a/web/pgadmin/dashboard/static/css/dashboard.css b/web/pgadmin/dashboard/static/css/dashboard.css deleted file mode 100644 index 09c5ce47a56..00000000000 --- a/web/pgadmin/dashboard/static/css/dashboard.css +++ /dev/null @@ -1,40 +0,0 @@ -.dashboard-link { - text-align: center; -} - -.dashboard-link a { - cursor: pointer; -} - -.dashboard-tab-container { - padding: 0px; -} - -.dashboard-tab { - border: 0px; - border-radius: 4px 4px 0px 0px; - margin-right: 1px; - font-size: 13px; - line-height: 25px; - margin-top: 5px; -} - -.dashboard-tab.active { - margin-top: 0px; - line-height: 30px; -} - -.icon-postgres:before { - height: 43px; - margin-top: 13px; - display: block; -} - -/* CSS to make subnode control look pretty - START */ -.dashboard-tab-container .subnode-dialog .form-control { - font-size: inherit; -} - -.dashboard-hidden { - display: none; -} diff --git a/web/pgadmin/dashboard/static/js/ChartsDOM.jsx b/web/pgadmin/dashboard/static/js/ChartsDOM.jsx deleted file mode 100644 index c378b86f7f8..00000000000 --- a/web/pgadmin/dashboard/static/js/ChartsDOM.jsx +++ /dev/null @@ -1,42 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2024, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// -import React from 'react'; -import ReactDOM from 'react-dom'; -import Graphs from './Graphs'; - -export default class ChartsDOM { - constructor(container, preferences, sid, did, pageVisible=true) { - this.container = container; - this.preferences = preferences; - this.sid = sid; - this.did = did; - this.pageVisible = pageVisible; - } - - render() { - if(this.container && this.preferences.show_graphs) { - ReactDOM.render(, this.container); - } - } - - unmount() { - this.container && ReactDOM.unmountComponentAtNode(this.container); - } - - setSidDid(sid, did) { - this.sid = sid; - this.did = did; - this.render(); - } - - setPageVisible(visible) { - this.pageVisible = visible; - this.render(); - } -} diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx index bc64acb2d9a..406dcb386de 100644 --- a/web/pgadmin/dashboard/static/js/Dashboard.jsx +++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx @@ -978,7 +978,8 @@ function Dashboard({ {/* Replication */} - + diff --git a/web/pgadmin/dashboard/static/js/Graphs.jsx b/web/pgadmin/dashboard/static/js/Graphs.jsx index de85bfb4f7b..e6759b93f23 100644 --- a/web/pgadmin/dashboard/static/js/Graphs.jsx +++ b/web/pgadmin/dashboard/static/js/Graphs.jsx @@ -17,17 +17,17 @@ import {useInterval, usePrevious} from 'sources/custom_hooks'; import PropTypes from 'prop-types'; import StreamingChart from '../../../static/js/components/PgChart/StreamingChart'; import { Grid } from '@mui/material'; +import { getChartColor } from '../../../static/js/utils'; export const X_AXIS_LENGTH = 75; /* Transform the labels data to suit ChartJS */ -export function transformData(labels, refreshRate) { - const colors = ['#00BCD4', '#9CCC65', '#E64A19']; +export function transformData(labels, refreshRate, theme='standard') { let datasets = Object.keys(labels).map((label, i)=>{ return { label: label, data: labels[label] || [], - borderColor: colors[i], + borderColor: getChartColor(i, theme), pointHitRadius: DATA_POINT_SIZE, }; }) || []; diff --git a/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx b/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx new file mode 100644 index 00000000000..8dff0fc0620 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx @@ -0,0 +1,200 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { Box } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +import gettext from 'sources/gettext'; +import ReplicationSlotsSchema from './schema_ui/replication_slots.ui'; +import PgTable from 'sources/components/PgTable'; +import getApiInstance, { parseApiError } from '../../../../static/js/api_instance'; +import SectionContainer from '../components/SectionContainer'; +import ReplicationStatsSchema from './schema_ui/replication_stats.ui'; +import RefreshButton from '../components/RefreshButtons'; +import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled'; +import { usePgAdmin } from '../../../../static/js/BrowserComponent'; +import url_for from 'sources/url_for'; +import PropTypes from 'prop-types'; + + +const replicationStatsColumns = [{ + accessorKey: 'view_details', + header: () => null, + enableSorting: false, + enableResizing: false, + size: 35, + maxSize: 35, + minSize: 35, + id: 'btn-edit', + cell: getExpandCell({ + title: gettext('View details') + }), +}, +{ + accessorKey: 'pid', + header: gettext('PID'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 40, +}, +{ + accessorKey: 'client_addr', + header: gettext('Client Addr'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'state', + header: gettext('State'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'write_lag', + header: gettext('Write Lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'flush_lag', + header: gettext('Flush Lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'replay_lag', + header: gettext('Replay Lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'reply_time', + header: gettext('Reply Time'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 80 +} +]; + +const replicationSlotsColumns = [{ + accessorKey: 'view_details', + header: () => null, + enableSorting: false, + enableResizing: false, + size: 35, + maxSize: 35, + minSize: 35, + id: 'btn-details', + cell: getExpandCell({ + title: gettext('View details') + }), +}, +{ + accessorKey: 'active_pid', + header: gettext('Active PID'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 50, +}, +{ + accessorKey: 'slot_name', + header: gettext('Slot Name'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 200, +}, +{ + accessorKey: 'active', + header: gettext('Active'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, + cell: getSwitchCell(), +} +]; + +const replSchemaObj = new ReplicationSlotsSchema(); +const replStatObj = new ReplicationStatsSchema(); + +export default function LogReplication({treeNodeInfo, pageVisible}) { + const [replicationSlots, setReplicationSlots] = useState([{ + }]); + const [replicationStats, setReplicationStats] = useState([{ + }]); + const pgAdmin = usePgAdmin(); + + const getReplicationData = (endpoint, setter)=>{ + const api = getApiInstance(); + const url = url_for(`dashboard.${endpoint}`, {sid: treeNodeInfo.server._id}); + api.get(url) + .then((res)=>{ + setter(res.data); + }) + .catch((error)=>{ + console.error(error); + pgAdmin.Browser.notifier.error(parseApiError(error)); + }); + }; + + useEffect(()=>{ + if(pageVisible) { + getReplicationData('replication_stats', setReplicationStats); + getReplicationData('replication_slots', setReplicationSlots); + } + }, [pageVisible ]); + + return ( + + { + getReplicationData('replication_stats', setReplicationStats); + }}/>} + title={gettext('Replication Stats')} style={{minHeight: '300px'}}> + + + { + getReplicationData('replication_slots', setReplicationSlots); + }}/>} + title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}> + + + + ); +} + +LogReplication.propTypes = { + treeNodeInfo: PropTypes.object.isRequired, + pageVisible: PropTypes.bool, +}; diff --git a/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx b/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx new file mode 100644 index 00000000000..f22cb83879e --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx @@ -0,0 +1,482 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { Box, Grid } from '@mui/material'; +import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react'; + +import gettext from 'sources/gettext'; +import PgTable from 'sources/components/PgTable'; +import getApiInstance, { parseApiError } from '../../../../static/js/api_instance'; +import SectionContainer from '../components/SectionContainer'; +import RefreshButton from '../components/RefreshButtons'; +import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled'; +import { usePgAdmin } from '../../../../static/js/BrowserComponent'; +import url_for from 'sources/url_for'; +import PropTypes from 'prop-types'; +import PGDOutgoingSchema from './schema_ui/pgd_outgoing.ui'; +import PGDIncomingSchema from './schema_ui/pgd_incoming.ui'; +import ChartContainer from '../components/ChartContainer'; +import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart'; +import { DATA_POINT_SIZE } from '../../../../static/js/chartjs'; +import { X_AXIS_LENGTH, statsReducer, transformData } from '../Graphs'; +import { getEpoch, getGCD, toPrettySize } from '../../../../static/js/utils'; +import { useInterval, usePrevious } from '../../../../static/js/custom_hooks'; + + +const outgoingReplicationColumns = [{ + accessorKey: 'view_details', + header: () => null, + enableSorting: false, + enableResizing: false, + size: 35, + maxSize: 35, + minSize: 35, + id: 'btn-edit', + cell: getExpandCell({ + title: gettext('View details') + }), +}, +{ + accessorKey: 'active_pid', + header: gettext('Active PID'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 40, +}, +{ + accessorKey: 'state', + header: gettext('State'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'slot_name', + header: gettext('Slot'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'write_lag', + header: gettext('Write lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'flush_lag', + header: gettext('Flush lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +{ + accessorKey: 'replay_lag', + header: gettext('Replay lag'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60 +}, +]; + +const incomingReplicationColumns = [{ + accessorKey: 'view_details', + header: () => null, + enableSorting: false, + enableResizing: false, + size: 35, + maxSize: 35, + minSize: 35, + id: 'btn-details', + cell: getExpandCell({ + title: gettext('View details') + }), +}, +{ + accessorKey: 'sub_name', + header: gettext('Subscription'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 50, +}, +{ + accessorKey: 'sub_slot_name', + header: gettext('Slot name'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 200, +}, +{ + accessorKey: 'subscription_status', + header: gettext('Status'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'last_xact_replay_timestamp', + header: gettext('Replay timestamp'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +} +]; + + +const clusterNodeColumns = [{ + accessorKey: 'node_name', + header: gettext('Node'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'node_group_name', + header: gettext('Group'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'peer_state_name', + header: gettext('Peer state'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 40, +}, +{ + accessorKey: 'node_kind_name', + header: gettext('Kind'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 40, +}, +{ + accessorKey: 'pg_version', + header: gettext('PostgreSQL version'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 80, +}, +{ + accessorKey: 'bdr_version', + header: gettext('BDR version'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 30, +}, +{ + accessorKey: 'catchup_state_name', + header: gettext('Catchup state'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 90, +} +]; + +const raftStatusColumns = [{ + accessorKey: 'node_name', + header: gettext('Node'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'node_group_name', + header: gettext('Group'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'state', + header: gettext('State'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'leader_type', + header: gettext('Leader type'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'leader_name', + header: gettext('Leader'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +}, +{ + accessorKey: 'voting', + header: gettext('Voting?'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 40, + cell: getSwitchCell(), +}, +{ + accessorKey: 'voting_for', + header: gettext('Voting for'), + enableSorting: true, + enableResizing: true, + minSize: 26, + size: 60, +} +]; + +const outgoingSchemaObj = new PGDOutgoingSchema(); +const incomingSchemaObj = new PGDIncomingSchema(); + +function getChartsUrl(sid=-1, chart_names=[]) { + let base_url = url_for('dashboard.pgd.charts', {sid: sid}); + base_url += '?chart_names=' + chart_names.join(','); + return base_url; +} + +const chartsDefault = { + 'pgd_replication_lag': {}, +}; + +export default function PGDReplication({preferences, treeNodeInfo, pageVisible, enablePoll=true, ...props}) { + const api = getApiInstance(); + const refreshOn = useRef(null); + const prevPreferences = usePrevious(preferences); + const [pollDelay, setPollDelay] = useState(5000); + + const [replicationLagTime, replicationLagTimeReduce] = useReducer(statsReducer, chartsDefault['pgd_replication_lag']); + const [replicationLagBytes, replicationLagBytesReduce] = useReducer(statsReducer, chartsDefault['pgd_replication_lag']); + const [clusterNodes, setClusterNodes] = useState([]); + const [raftStatus, setRaftStatus] = useState([]); + const [outgoingReplication, setOutgoingReplication] = useState([]); + const [incomingReplication, setIncomingReplication] = useState([]); + const [errorMsg, setErrorMsg] = useState(null); + + const pgAdmin = usePgAdmin(); + + const sid = treeNodeInfo.server._id; + + const options = useMemo(()=>({ + showDataPoints: preferences['graph_data_points'], + showTooltip: preferences['graph_mouse_track'], + lineBorderSize: preferences['graph_line_border_width'], + }), [preferences]); + + const getReplicationData = (endpoint, setter)=>{ + const url = url_for(`dashboard.pgd.${endpoint}`, {sid: sid}); + api.get(url) + .then((res)=>{ + setter(res.data); + }) + .catch((error)=>{ + console.error(error); + pgAdmin.Browser.notifier.error(parseApiError(error)); + }); + }; + + useEffect(()=>{ + let calcPollDelay = false; + if(prevPreferences) { + if(prevPreferences['pgd_replication_lag_refresh'] != preferences['pgd_replication_lag_refresh']) { + replicationLagTimeReduce({reset: chartsDefault['pgd_replication_lag']}); + replicationLagBytesReduce({reset: chartsDefault['pgd_replication_lag']}); + calcPollDelay = true; + } + } else { + calcPollDelay = true; + } + if(calcPollDelay) { + const keys = Object.keys(chartsDefault); + const length = keys.length; + if(length == 1){ + setPollDelay( + preferences[keys[0]+'_refresh']*1000 + ); + } else { + setPollDelay( + getGCD(Object.keys(chartsDefault).map((name)=>preferences[name+'_refresh']))*1000 + ); + } + } + }, [preferences]); + + useEffect(()=>{ + if(pageVisible) { + getReplicationData('cluster_nodes', setClusterNodes); + getReplicationData('raft_status', setRaftStatus); + getReplicationData('outgoing', setOutgoingReplication); + getReplicationData('incoming', setIncomingReplication); + } + }, [pageVisible]); + + useInterval(()=>{ + const currEpoch = getEpoch(); + if(refreshOn.current === null) { + let tmpRef = {}; + Object.keys(chartsDefault).forEach((name)=>{ + tmpRef[name] = currEpoch; + }); + refreshOn.current = tmpRef; + } + + let getFor = []; + Object.keys(chartsDefault).forEach((name)=>{ + if(currEpoch >= refreshOn.current[name]) { + getFor.push(name); + refreshOn.current[name] = currEpoch + preferences[name+'_refresh']; + } + }); + + let path = getChartsUrl(sid, getFor); + if (!pageVisible){ + return; + } + + api.get(path) + .then((resp)=>{ + let data = resp.data; + setErrorMsg(null); + if(data.hasOwnProperty('pgd_replication_lag')){ + let newTime = {}; + let newBytes = {}; + for(const row of data['pgd_replication_lag']) { + newTime[row['name']] = row['replay_lag'] ?? 0; + newBytes[row['name']] = row['replay_lag_bytes'] ?? 0; + } + replicationLagTimeReduce({incoming: newTime}); + replicationLagBytesReduce({incoming: newBytes}); + } + }) + .catch((error)=>{ + if(!errorMsg) { + replicationLagTimeReduce({reset: chartsDefault['pgd_replication_lag']}); + replicationLagBytesReduce({reset: chartsDefault['pgd_replication_lag']}); + if(error.response) { + if (error.response.status === 428) { + setErrorMsg(gettext('Please connect to the selected server to view the graph.')); + } else { + setErrorMsg(gettext('An error occurred whilst rendering the graph.')); + } + } else if(error.request) { + setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.')); + return; + } else { + console.error(error); + } + } + }); + }, enablePoll ? pollDelay : -1); + + const replicationLagTimeData = useMemo(()=>transformData(replicationLagTime, preferences['pgd_replication_lag_refresh'], preferences.theme), [replicationLagTime, preferences.theme]); + const replicationLagBytesData = useMemo(()=>transformData(replicationLagBytes, preferences['pgd_replication_lag_refresh'], preferences.theme), [replicationLagBytes, preferences.theme]); + + return ( + + + + + toPrettySize(v, 's')} /> + + + + + + + + + { + getReplicationData('cluster_nodes', setClusterNodes); + }}/>} + title={gettext('Cluster nodes')} style={{minHeight: '300px', marginTop: '4px'}}> + + + { + getReplicationData('raft_status', setRaftStatus); + }}/>} + title={gettext('Raft status')} style={{minHeight: '300px', marginTop: '4px'}}> + + + { + getReplicationData('outgoing', setOutgoingReplication); + }}/>} + title={gettext('Outgoing Replication')} style={{minHeight: '300px', marginTop: '4px'}}> + + + { + getReplicationData('incoming', setIncomingReplication); + }}/>} + title={gettext('Incoming Replication')} style={{minHeight: '300px', marginTop: '4px'}}> + + + + ); +} + +PGDReplication.propTypes = { + preferences: PropTypes.object, + treeNodeInfo: PropTypes.object.isRequired, + pageVisible: PropTypes.bool, + enablePoll: PropTypes.bool, + isTest: PropTypes.bool, +}; diff --git a/web/pgadmin/dashboard/static/js/Replication/index.jsx b/web/pgadmin/dashboard/static/js/Replication/index.jsx index e13cd304735..5c05e7efc9a 100644 --- a/web/pgadmin/dashboard/static/js/Replication/index.jsx +++ b/web/pgadmin/dashboard/static/js/Replication/index.jsx @@ -7,206 +7,26 @@ // ////////////////////////////////////////////////////////////// -import { Box } from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import gettext from 'sources/gettext'; -import ReplicationSlotsSchema from './replication_slots.ui'; -import PgTable from 'sources/components/PgTable'; -import getApiInstance, { parseApiError } from '../../../../static/js/api_instance'; -import SectionContainer from '../components/SectionContainer'; -import ReplicationStatsSchema from './replication_stats.ui'; -import RefreshButton from '../components/RefreshButtons'; -import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled'; -import { usePgAdmin } from '../../../../static/js/BrowserComponent'; -import url_for from 'sources/url_for'; +import React from 'react'; import PropTypes from 'prop-types'; - - -const replicationStatsColumns = [{ - accessorKey: 'view_details', - header: () => null, - enableSorting: false, - enableResizing: false, - enableFilters: true, - size: 35, - maxSize: 35, - minSize: 35, - id: 'btn-edit', - cell: getExpandCell({ - title: gettext('View details') - }), -}, -{ - accessorKey: 'pid', - header: gettext('PID'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 40, - minSize: 40, -}, -{ - accessorKey: 'client_addr', - header: gettext('Client Addr'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -}, -{ - accessorKey:'state', - header: gettext('State'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -}, -{ - accessorKey:'write_lag', - header: gettext('Write Lag'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -}, -{ - accessorKey:'flush_lag', - header: gettext('Flush Lag'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -}, -{ - accessorKey:'replay_lag', - header: gettext('Replay Lag'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -}, -{ - accessorKey:'reply_time', - header: gettext('Reply Time'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 100, - minSize: 50, -} -]; - -const replicationSlotsColumns = [{ - accessorKey: 'view_details', - header: () => null, - enableSorting: false, - enableResizing: false, - enableFilters: true, - size: 35, - maxSize: 35, - minSize: 35, - id: 'btn-details', - cell: getExpandCell({ - title: gettext('View details') - }), -}, -{ - accessorKey: 'active_pid', - header: gettext('Active PID'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 50, - minSize: 50, -}, -{ - accessorKey: 'slot_name', - header: gettext('Slot Name'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 200, - minSize: 50, -}, -{ - accessorKey:'active', - header: gettext('Active'), - enableSorting: true, - enableResizing: true, - enableFilters: true, - size: 50, - minSize: 50, - cell: getSwitchCell(), -} -]; - -const replSchemaObj = new ReplicationSlotsSchema(); -const replStatObj = new ReplicationStatsSchema(); - -export default function Replication({treeNodeInfo, pageVisible}) { - const [replicationSlots, setReplicationSlots] = useState([{ - }]); - const [replicationStats, setReplicationStats] = useState([{ - }]); - const pgAdmin = usePgAdmin(); - - const getReplicationData = (endpoint, setter)=>{ - const api = getApiInstance(); - const url = url_for(`dashboard.${endpoint}`, {sid: treeNodeInfo.server._id}); - api.get(url) - .then((res)=>{ - setter(res.data); - }) - .catch((error)=>{ - console.error(error); - pgAdmin.Browser.notifier.error(parseApiError(error)); - }); - }; - - useEffect(()=>{ - if(pageVisible) { - getReplicationData('replication_stats', setReplicationStats); - getReplicationData('replication_slots', setReplicationSlots); - } - }, [pageVisible ]); - - return ( - - { - getReplicationData('replication_stats', setReplicationStats); - }}/>} - title={gettext('Replication Stats')} style={{minHeight: '300px'}}> - - - { - getReplicationData('replication_slots', setReplicationSlots); - }}/>} - title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}> - - - - ); +import LogReplication from './LogReplication'; +import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessage'; +import PGDReplication from './PGDReplication'; + +export default function Replication({preferences, treeNodeInfo, pageVisible}) { + const replicationType = treeNodeInfo?.server?.replication_type; + if(replicationType == 'log') { + return ; + } else if(replicationType == 'pgd') { + return ; + } else { + return ; + } } Replication.propTypes = { + preferences: PropTypes.object, treeNodeInfo: PropTypes.object.isRequired, pageVisible: PropTypes.bool, }; diff --git a/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_incoming.ui.js b/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_incoming.ui.js new file mode 100644 index 00000000000..85ac41559f8 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_incoming.ui.js @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + +export default class PGDIncomingSchema extends BaseUISchema { + constructor(initValues) { + super({ + ...initValues, + }); + } + + get baseFields() { + return [ + { + id: 'node_group_name', label: gettext('Group Name'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'sub_name', label: gettext('Subscription'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'origin_name', label: gettext('Origin'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'target_name', label: gettext('Target'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'sub_enabled', label: gettext('Enabled'), type: 'switch', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'subscription_status', label: gettext('Status'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'receive_lsn', label: gettext('Receive LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'receive_commit_lsn', label: gettext('Receive Commit LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'last_xact_replay_lsn', label: gettext('Last Replay LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'last_xact_flush_lsn', label: gettext('Last Flush LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'last_xact_replay_timestamp', label: gettext('Replay Timestamp'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + ]; + } +} diff --git a/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_outgoing.ui.js b/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_outgoing.ui.js new file mode 100644 index 00000000000..7e52a6777d8 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Replication/schema_ui/pgd_outgoing.ui.js @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + +export default class PGDOutgoingSchema extends BaseUISchema { + constructor(initValues) { + super({ + ...initValues, + }); + } + + get baseFields() { + return [ + { + id: 'active_pid', label: gettext('Active PID'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'target_dbname', label: gettext('Target DB'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'origin_name', label: gettext('Origin'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'target_name', label: gettext('Target'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'usename', label: gettext('Usename'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'application_name', label: gettext('App Name'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'client_addr', label: gettext('Client Addr'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'client_port', label: gettext('Client Port'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'state', label: gettext('State'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'sent_lsn', label: gettext('Sent LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'write_lsn', label: gettext('Write LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'flush_lsn', label: gettext('Flush LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'replay_lsn', label: gettext('Replay LSN'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'write_lag', label: gettext('Write Lag'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'flush_lag', label: gettext('Flush Lag'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + { + id: 'replay_lag', label: gettext('Replay Lag'), type: 'text', mode:['properties'], readonly: true, + group: gettext('Details') + }, + ]; + } +} diff --git a/web/pgadmin/dashboard/static/js/Replication/replication_slots.ui.js b/web/pgadmin/dashboard/static/js/Replication/schema_ui/replication_slots.ui.js similarity index 100% rename from web/pgadmin/dashboard/static/js/Replication/replication_slots.ui.js rename to web/pgadmin/dashboard/static/js/Replication/schema_ui/replication_slots.ui.js diff --git a/web/pgadmin/dashboard/static/js/Replication/replication_stats.ui.js b/web/pgadmin/dashboard/static/js/Replication/schema_ui/replication_stats.ui.js similarity index 100% rename from web/pgadmin/dashboard/static/js/Replication/replication_stats.ui.js rename to web/pgadmin/dashboard/static/js/Replication/schema_ui/replication_stats.ui.js diff --git a/web/pgadmin/dashboard/static/js/components/SectionContainer.jsx b/web/pgadmin/dashboard/static/js/components/SectionContainer.jsx index fd920c8d369..11d678b8715 100644 --- a/web/pgadmin/dashboard/static/js/components/SectionContainer.jsx +++ b/web/pgadmin/dashboard/static/js/components/SectionContainer.jsx @@ -21,6 +21,7 @@ const useStyles = makeStyles((theme) => ({ height: '100%', width: '100%', minHeight: '400px', + borderRadius: theme.shape.borderRadius, }, cardHeader: { backgroundColor: theme.otherVars.tableBg, diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_charts.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_charts.sql new file mode 100644 index 00000000000..d95d791cccc --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_charts.sql @@ -0,0 +1,10 @@ +{% set add_union = false %} +{% if 'pgd_replication_lag' in chart_names %} +{% set add_union = true %} + SELECT 'pgd_replication_lag' AS chart_name, pg_catalog.json_agg(t) AS chart_data + FROM ( + SELECT n.node_name || '-' || nr.target_name as name, + EXTRACT(epoch FROM nr.replay_lag)::bigint as replay_lag, nr.replay_lag_bytes + FROM bdr.node_replication_rates nr, bdr.local_node_summary n + ) t +{% endif %} diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_cluster_nodes.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_cluster_nodes.sql new file mode 100644 index 00000000000..802a0df3a67 --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_cluster_nodes.sql @@ -0,0 +1,12 @@ +WITH node_details AS ( + SELECT node_id, version() pg_version, bdr.bdr_version() bdr_version + FROM bdr.local_node +) +SELECT ns.*, nd.*, + ni.catchup_state_name +FROM bdr.node_summary ns + LEFT JOIN node_details nd + ON ns.node_id = nd.node_id + LEFT JOIN bdr.node_catchup_info_details ni + ON ns.node_id = ni.target_node_id + ORDER BY node_seq_id; diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_incoming.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_incoming.sql new file mode 100644 index 00000000000..1539fd831e4 --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_incoming.sql @@ -0,0 +1 @@ +SELECT * FROM bdr.subscription_summary; diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_outgoing.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_outgoing.sql new file mode 100644 index 00000000000..7601488f2c3 --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_outgoing.sql @@ -0,0 +1 @@ +SELECT * FROM bdr.node_slots; diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_raft_status.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_raft_status.sql new file mode 100644 index 00000000000..f9b0cf9dbb7 --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_raft_status.sql @@ -0,0 +1,14 @@ +WITH raft_status AS ( + SELECT * + FROM json_to_record((SELECT * FROM bdr.get_raft_status())) AS rs( + instance_id oid, server_id oid, state text, leader_type text, leader oid, + voting bool, voted_for_type text, voted_for_id oid + ) +) +SELECT n.node_name, n.node_group_name, rs.*, + (SELECT node_name FROM bdr.node_summary WHERE node_id = rs.leader) AS leader_name, + (SELECT node_name FROM bdr.node_summary WHERE node_id = rs.voted_for_id) AS voting_for +FROM bdr.node_summary n + LEFT JOIN raft_status rs + ON n.node_id = rs.server_id +ORDER BY n.node_seq_id; diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_replication_lag.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_replication_lag.sql new file mode 100644 index 00000000000..5786bc6b35b --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/pgd_replication_lag.sql @@ -0,0 +1,5 @@ +SELECT n.node_name as source_name, nr.target_name, + EXTRACT(epoch FROM nr.replay_lag)::bigint, nr.replay_lag_bytes +FROM bdr.node_replication_rates nr, bdr.local_node_summary n + + diff --git a/web/pgadmin/dashboard/tests/test_pgd_replication.py b/web/pgadmin/dashboard/tests/test_pgd_replication.py new file mode 100644 index 00000000000..cc56c143a4f --- /dev/null +++ b/web/pgadmin/dashboard/tests/test_pgd_replication.py @@ -0,0 +1,62 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.utils import server_utils +from regression import parent_node_dict +from regression.python_test_utils import test_utils +import json + + +class DashboardReplicationTestCase(BaseTestGenerator): + """ + This class validates the version in range functionality + by defining different version scenarios; where dict of + parameters describes the scenario appended by test name. + """ + + scenarios = [( + 'TestCase for cluster nodes', dict( + endpoint='/dashboard/pgd/cluster_nodes', + )), ( + 'TestCase for raft status', dict( + endpoint='/dashboard/pgd/raft_status', + )), ( + 'TestCase for charts', dict( + endpoint='/dashboard/pgd/charts', + query='chart_names=pgd_replication_lag', + )), ( + 'TestCase for incoming replication slots', dict( + endpoint='/dashboard/pgd/incoming', + )), ( + 'TestCase for outgoing replication slots', dict( + endpoint='/dashboard/pgd/outgoing', + )), + ] + + def setUp(self): + pass + + def runTest(self): + self.server_id = parent_node_dict["server"][-1]["server_id"] + server_response = server_utils.connect_server(self, self.server_id) + if server_response["info"] == "Server connected.": + if server_response['data']['replication_type'] != 'pgd': + self.skipTest('Not a PGD Cluster') + url = self.endpoint + '/{0}'.format(self.server_id) + if hasattr(self, 'query'): + url = '{0}?{1}'.format(url, self.query) + response = self.tester.get(url) + self.assertEqual(response.status_code, 200) + else: + raise Exception("Error while connecting server to add the" + " database.") + + def tearDown(self): + pass diff --git a/web/pgadmin/misc/statistics/static/js/Statistics.jsx b/web/pgadmin/misc/statistics/static/js/Statistics.jsx index 7eab4f84b3f..1555a28a4fb 100644 --- a/web/pgadmin/misc/statistics/static/js/Statistics.jsx +++ b/web/pgadmin/misc/statistics/static/js/Statistics.jsx @@ -88,10 +88,10 @@ function getColumn(data, singleLineStatistics, prettifyFields=[]) { } columns.forEach((c)=>{ // Prettify the cell view - if(prettifyFields.includes(c.Header)) { - c.Cell = ({value})=><>{toPrettySize(value)}; - c.Cell.displayName = 'Cell'; - c.Cell.propTypes = { + if(prettifyFields.includes(c.header)) { + c.cell = ({value})=><>{toPrettySize(value)}; + c.cell.displayName = 'Cell'; + c.cell.propTypes = { value: PropTypes.any, }; } diff --git a/web/pgadmin/static/js/chartjs/index.jsx b/web/pgadmin/static/js/chartjs/index.jsx index 68312584fe4..b4413a50b7c 100644 --- a/web/pgadmin/static/js/chartjs/index.jsx +++ b/web/pgadmin/static/js/chartjs/index.jsx @@ -16,19 +16,6 @@ import { useTheme } from '@mui/material'; export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle']; export const DATA_POINT_SIZE = 3; -export const CHART_THEME_COLORS_LENGTH = 20; -export const CHART_THEME_COLORS = { - 'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B', - '#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900', - '#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'], - 'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C', - '#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC', - '#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'], - 'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2', - '#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8', - '#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0', - '#52B788'] -}; const defaultOptions = { responsive: true, diff --git a/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx b/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx index 7b773c2a0dd..be876386d14 100644 --- a/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx +++ b/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx @@ -81,8 +81,8 @@ export default function ObjectBreadcrumbs() {
{ - objectData.path?.reduce((res, item)=>( - res.concat({item}, ) + objectData.path?.reduce((res, item, i)=>( + res.concat({item}, ) ), []).slice(0, -1) }
diff --git a/web/pgadmin/static/js/components/PgReactTableStyled.jsx b/web/pgadmin/static/js/components/PgReactTableStyled.jsx index 32ca616df28..c8f5f3a38a7 100644 --- a/web/pgadmin/static/js/components/PgReactTableStyled.jsx +++ b/web/pgadmin/static/js/components/PgReactTableStyled.jsx @@ -11,13 +11,13 @@ import React, { forwardRef, useEffect } from 'react'; import { flexRender } from '@tanstack/react-table'; import { styled } from '@mui/styles'; import PropTypes from 'prop-types'; -import { Switch } from '@mui/material'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { PgIconButton } from './Buttons'; import clsx from 'clsx'; import CustomPropTypes from '../custom_prop_types'; +import { InputSwitch } from './FormComponents'; const StyledDiv = styled('div')(({theme})=>({ @@ -360,20 +360,14 @@ export function getExpandCell({ onClick, ...props }) { return Cell; } -const ReadOnlySwitch = styled(Switch)(({theme})=>({ - opacity: 0.75, - '& .MuiSwitch-track': { - opacity: theme.palette.action.disabledOpacity, - } -})); export function getSwitchCell() { - const Cell = ({ value }) => { - return ; + const Cell = ({ getValue }) => { + return ; }; Cell.displayName = 'SwitchCell'; Cell.propTypes = { - value: PropTypes.any, + getValue: PropTypes.func, }; return Cell; diff --git a/web/pgadmin/static/js/components/PgTable.jsx b/web/pgadmin/static/js/components/PgTable.jsx index 2bb0586b6d8..be8fb0e58f8 100644 --- a/web/pgadmin/static/js/components/PgTable.jsx +++ b/web/pgadmin/static/js/components/PgTable.jsx @@ -42,7 +42,7 @@ function TableRow({ index, style, schema, row, measureElement }) { }, [row.getIsExpanded(), expandComplete]); return ( - + {row.getVisibleCells().map((cell) => { const content = flexRender(cell.column.columnDef.cell, cell.getContext()); @@ -115,7 +115,12 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP enableResizing: false, maxSize: 35, }] : []).concat( - columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility) + columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility).map((c)=>({ + ...c, + // if data is null then global search doesn't work + // Use accessorFn to return empty string if data is null. + accessorFn: c.accessorFn ?? (c.accessorKey ? (row)=>row[c.accessorKey] ?? '' : undefined), + })) ), [hasSelectRow, columns]); // Render the UI for your table @@ -264,4 +269,4 @@ PgTable.propTypes = { caveTable: PropTypes.bool, tableNoBorder: PropTypes.bool, 'data-test': PropTypes.string -}; \ No newline at end of file +}; diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 6396ed0ce4c..6fbb1e6ec3d 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -641,3 +641,22 @@ export function scrollbarWidth() { document.body.removeChild(scrollDiv); return scrollbarWidth; } + +const CHART_THEME_COLORS = { + 'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B', + '#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900', + '#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'], + 'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C', + '#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC', + '#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'], + 'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2', + '#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8', + '#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0', + '#52B788'] +}; + +export function getChartColor(index, theme='standard', colorPalette=CHART_THEME_COLORS) { + const palette = colorPalette[theme]; + // loop back if out of index; + return palette[index % palette.length]; +} \ No newline at end of file diff --git a/web/pgadmin/submodules.py b/web/pgadmin/submodules.py index 50becf985b4..e85183ee3b1 100644 --- a/web/pgadmin/submodules.py +++ b/web/pgadmin/submodules.py @@ -16,7 +16,6 @@ def get_submodules(): AuthenticateModule, BrowserModule, DashboardModule, - DashboardModule, HelpModule, MiscModule, PreferencesModule, diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx index 1951dafadfd..563c9229415 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx @@ -23,10 +23,11 @@ import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; import { InputSelect } from '../../../../../../static/js/components/FormComponents'; import { DefaultButton, PgButtonGroup, PgIconButton} from '../../../../../../static/js/components/Buttons'; import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE, - CHART_THEME_COLORS, CHART_THEME_COLORS_LENGTH, LightenDarkenColor} from 'sources/chartjs'; + LightenDarkenColor} from 'sources/chartjs'; import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent'; import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants'; import { useTheme } from '@mui/material'; +import { getChartColor } from '../../../../../../static/js/utils'; // Numeric data type used to separate out the options for Y axis. const NUMERIC_TYPES = ['oid', 'smallint', 'integer', 'bigint', 'decimal', 'numeric', @@ -163,7 +164,7 @@ GenerateGraph.propTypes = { }; // This function is used to get the dataset for Line Chart and Stacked Line Chart. -function getLineChartData(graphType, rows, colName, colPosition, color, colorIndex, styleIndex, queryToolCtx) { +function getLineChartData(graphType, rows, colName, colPosition, color, styleIndex, queryToolCtx) { return { label: colName, data: rows.map((r)=>r[colPosition]), @@ -188,28 +189,23 @@ function getBarChartData(rows, colName, colPosition, color) { // This function is used to get the dataset for Pie Chart. function getPieChartData(rows, colName, colPosition, queryToolCtx) { - let rowCount = -1; return { label: colName, data: rows.map((r)=>r[colPosition]), - backgroundColor: rows.map(()=> { - if (rowCount >= (CHART_THEME_COLORS_LENGTH - 1)) { - rowCount = -1; - } - rowCount = rowCount + 1; - return CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme][rowCount]; + backgroundColor: rows.map((_v, i)=> { + return getChartColor(i, queryToolCtx.preferences.misc.theme); }), }; } // This function is used to get the graph data set for the X axis and Y axis -function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, graphColors) { +function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, theme) { // Function is used to the find the position of the column function getColumnPosition(colName) { return _.find(columns, (c)=>(c.name==colName))?.pos; } - let styleIndex = -1, colorIndex = -1; + let styleIndex = -1; return { 'labels': rows.map((r, index)=>{ @@ -221,14 +217,8 @@ function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, g return r[colPosition]; }), - 'datasets': yaxis.map((colName)=>{ - // Loop is used to set the index for random color array - if (colorIndex >= (CHART_THEME_COLORS_LENGTH - 1)) { - colorIndex = -1; - } - colorIndex = colorIndex + 1; - - let color = graphColors[colorIndex]; + 'datasets': yaxis.map((colName, i)=>{ + let color = getChartColor(i, theme); let colPosition = getColumnPosition(colName); // Loop is used to set the index for DATA_POINT_STYLE array @@ -242,8 +232,7 @@ function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, g } else if (graphType === 'B' || graphType === 'SB') { return getBarChartData(rows, colName, colPosition, color); } else if (graphType === 'L' || graphType === 'SL') { - return getLineChartData(graphType, rows, colName, colPosition, color, - colorIndex, styleIndex, queryToolCtx); + return getLineChartData(graphType, rows, colName, colPosition, color, styleIndex, queryToolCtx); } }), }; @@ -263,7 +252,6 @@ export function GraphVisualiser({initColumns}) { const [columns, setColumns] = useState(initColumns); const [graphHeight, setGraphHeight] = useState(); const [expandedState, setExpandedState] = useState(true); - const [graphColor, setGraphColor] = useState([]); const theme = useTheme(); @@ -344,10 +332,6 @@ export function GraphVisualiser({initColumns}) { } }, [graphType, theme]); - useEffect(()=>{ - setGraphColor(CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme]); - }, [queryToolCtx.preferences.misc.theme, theme]); - const graphBackgroundColor = useMemo(() => { return theme.palette.background.default; },[theme]); @@ -380,7 +364,10 @@ export function GraphVisualiser({initColumns}) { setLoaderText(gettext('Rendering data points...')); // Set the Graph Data setGraphData( - (prev)=> [getGraphDataSet(graphType, res.data.data.result, columns, xAxis, _.isArray(yAxis) ? yAxis : [yAxis] , queryToolCtx, graphColor), prev[1] + 1] + (prev)=> [ + getGraphDataSet(graphType, res.data.data.result, columns, xAxis, _.isArray(yAxis) ? yAxis : [yAxis] , queryToolCtx, queryToolCtx.preferences.misc.theme), + prev[1] + 1 + ] ); setLoaderText(''); diff --git a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx index e0c307935a5..781a33333c4 100644 --- a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx +++ b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx @@ -294,7 +294,7 @@ class UserManagementSchema extends BaseUISchema { canSearch: true }, { - id: 'refreshBrowserTree', visible: false, type: 'boolean', + id: 'refreshBrowserTree', visible: false, type: 'switch', deps: ['userManagement'], depChange: ()=> { return { refreshBrowserTree: this.changeOwnership }; } diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 9ea96fb0359..d795e24dbc8 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -59,6 +59,8 @@ ERROR_FETCHING_DATA = gettext('Unable to fetch data.') +ERROR_SERVER_ID_NOT_SPECIFIED = gettext('Server ID not specified.') + # Authentication Sources INTERNAL = 'internal' LDAP = 'ldap' diff --git a/web/regression/javascript/dashboard/graphs_spec.js b/web/regression/javascript/dashboard/graphs_spec.js index 7a47cc4985c..a6b5d2412e7 100644 --- a/web/regression/javascript/dashboard/graphs_spec.js +++ b/web/regression/javascript/dashboard/graphs_spec.js @@ -10,16 +10,16 @@ import { render } from '@testing-library/react'; describe('Graphs.js', ()=>{ it('transformData', ()=>{ - expect(transformData({'Label1': [], 'Label2': []}, 1, false)).toEqual({ + expect(transformData({'Label1': [], 'Label2': []}, 1)).toEqual({ datasets: [{ label: 'Label1', data: [], - borderColor: '#00BCD4', + borderColor: '#1F77B4', pointHitRadius: DATA_POINT_SIZE, },{ label: 'Label2', data: [], - borderColor: '#9CCC65', + borderColor: '#FF7F0E', pointHitRadius: DATA_POINT_SIZE, }], refreshRate: 1, diff --git a/web/webpack.config.js b/web/webpack.config.js index 3bc905dc298..29fde3f3101 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -221,7 +221,9 @@ module.exports = [{ 'pure|pgadmin.node.aggregate', 'pure|pgadmin.node.operator', 'pure|pgadmin.node.dbms_job_scheduler', - 'pure|pgadmin.node.replica_node' + 'pure|pgadmin.node.replica_node', + 'pure|pgadmin.node.pgd_replication_groups', + 'pure|pgadmin.node.pgd_replication_servers', ], }, }, @@ -396,7 +398,7 @@ module.exports = [{ minChunks: 2, enforce: true, test(module) { - return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@material-ui']); + return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@mui']); }, }, vendor_others: { diff --git a/web/webpack.shim.js b/web/webpack.shim.js index b56ac2e4215..bb857d7a78c 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -122,6 +122,8 @@ let webpackShimConfig = { 'pgadmin.node.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'), 'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'), 'pgadmin.node.replica_node': path.join(__dirname, './pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node'), + 'pgadmin.node.pgd_replication_groups': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups'), + 'pgadmin.node.pgd_replication_servers': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers'), 'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'), 'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'), 'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),