Added support for viewing PGD Clusters. #7215

pull/7495/head
Aditya Toshniwal 2024-05-20 16:54:49 +05:30 committed by GitHub
parent d6a9f8a06c
commit dd45f06d50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 2237 additions and 436 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -3,7 +3,7 @@
********************************* *********************************
`Managing Cluster Objects`:index: `Managing Cluster Objects`:index:
********************************* *********************************
Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs
that allow you to create these objects, manage them, and control their 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 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...* database, right-click on the *Databases* node, and select *Create Database...*
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 3
database_dialog database_dialog
resource_group_dialog resource_group_dialog
role_dialog role_dialog
tablespace_dialog tablespace_dialog
replica_nodes_dialog replica_nodes_dialog
pgd_replication_group_dialog
role_reassign_dialog role_reassign_dialog

View File

@ -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

View File

@ -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.

View File

@ -61,7 +61,7 @@ class CollectionNodeModule(PgAdminModule, PGChildModule, metaclass=ABCMeta):
return True return True
def generate_browser_node( def generate_browser_node(
self, node_id, parent_id, label, icon, **kwargs self, node_id, parent_id, label, icon=None, **kwargs
): ):
obj = { obj = {
"id": "%s_%s" % (self.node_type, node_id), "id": "%s_%s" % (self.node_type, node_id),

View File

@ -349,6 +349,9 @@ class ServerModule(sg.ServerGroupPluginModule):
from .replica_nodes import blueprint as module from .replica_nodes import blueprint as module
self.submodules.append(module) self.submodules.append(module)
from .pgd_replication_groups import blueprint as module
self.submodules.append(module)
super().register(app, options) super().register(app, options)
# We do not have any preferences for server node. # We do not have any preferences for server node.

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-3{fill:#f2f2f2;}.cls-2{fill:#aaa;}.cls-3{stroke:#aaa;}.cls-3,.cls-5{stroke-miterlimit:1;stroke-width:0.75px;}.cls-4{fill:#7b7b97;}.cls-5{fill:#def4fd;stroke:#7b7b97;}</style></defs><title>server</title><g id="_3" data-name="3"><rect class="cls-1" x="3.08" y="2.6" width="9.85" height="3.1" rx="1.13" ry="1.13"/><path class="cls-2" d="M11.8,3a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V3.73A.75.75,0,0,1,4.2,3h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V3.73a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="4.15" x2="4.27" y2="4.15"/><path class="cls-1" d="M4.2,6.45h7.6a1.13,1.13,0,0,1,1.13,1.13v.85A1.12,1.12,0,0,1,11.8,9.55H4.2A1.12,1.12,0,0,1,3.08,8.42V7.58A1.13,1.13,0,0,1,4.2,6.45Z"/><path class="cls-4" d="M11.8,6.82a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V7.57a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V7.57a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-5" x1="6.37" y1="8" x2="4.27" y2="8"/><path class="cls-1" d="M4.2,10.3h7.6a1.12,1.12,0,0,1,1.12,1.12v.85A1.12,1.12,0,0,1,11.8,13.4H4.2a1.13,1.13,0,0,1-1.13-1.13v-.85A1.12,1.12,0,0,1,4.2,10.3Z"/><path class="cls-2" d="M11.8,10.68a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75v-.85a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5v-.85a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="11.85" x2="4.27" y2="11.85"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-3{fill:#f2f2f2;}.cls-2{fill:#aaa;}.cls-3{stroke:#aaa;}.cls-3,.cls-5{stroke-miterlimit:1;stroke-width:0.75px;}.cls-4{fill:#7b7b97;}.cls-5{fill:#def4fd;stroke:#7b7b97;}</style></defs><title>server</title><g id="_3" data-name="3"><rect class="cls-1" x="3.08" y="2.6" width="9.85" height="3.1" rx="1.13" ry="1.13"/><path class="cls-2" d="M11.8,3a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V3.73A.75.75,0,0,1,4.2,3h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V3.73a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="4.15" x2="4.27" y2="4.15"/><path class="cls-1" d="M4.2,6.45h7.6a1.13,1.13,0,0,1,1.13,1.13v.85A1.12,1.12,0,0,1,11.8,9.55H4.2A1.12,1.12,0,0,1,3.08,8.42V7.58A1.13,1.13,0,0,1,4.2,6.45Z"/><path class="cls-4" d="M11.8,6.82a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V7.57a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V7.57a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-5" x1="6.37" y1="8" x2="4.27" y2="8"/><path class="cls-1" d="M4.2,10.3h7.6a1.12,1.12,0,0,1,1.12,1.12v.85A1.12,1.12,0,0,1,11.8,13.4H4.2a1.13,1.13,0,0,1-1.13-1.13v-.85A1.12,1.12,0,0,1,4.2,10.3Z"/><path class="cls-2" d="M11.8,10.68a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75v-.85a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5v-.85a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="11.85" x2="4.27" y2="11.85"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-3{fill:#f2f2f2;}.cls-2{fill:#aaa;}.cls-3{stroke:#aaa;}.cls-3,.cls-5{stroke-miterlimit:1;stroke-width:0.75px;}.cls-4{fill:#7b7b97;}.cls-5{fill:#def4fd;stroke:#7b7b97;}</style></defs><title>server</title><g id="_3" data-name="3"><rect class="cls-1" x="3.08" y="2.6" width="9.85" height="3.1" rx="1.13" ry="1.13"/><path class="cls-2" d="M11.8,3a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V3.73A.75.75,0,0,1,4.2,3h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V3.73a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="4.15" x2="4.27" y2="4.15"/><path class="cls-1" d="M4.2,6.45h7.6a1.13,1.13,0,0,1,1.13,1.13v.85A1.12,1.12,0,0,1,11.8,9.55H4.2A1.12,1.12,0,0,1,3.08,8.42V7.58A1.13,1.13,0,0,1,4.2,6.45Z"/><path class="cls-4" d="M11.8,6.82a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V7.57a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V7.57a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-5" x1="6.37" y1="8" x2="4.27" y2="8"/><path class="cls-1" d="M4.2,10.3h7.6a1.12,1.12,0,0,1,1.12,1.12v.85A1.12,1.12,0,0,1,11.8,13.4H4.2a1.13,1.13,0,0,1-1.13-1.13v-.85A1.12,1.12,0,0,1,4.2,10.3Z"/><path class="cls-2" d="M11.8,10.68a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75v-.85a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5v-.85a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="11.85" x2="4.27" y2="11.85"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-3{fill:#f2f2f2;}.cls-2{fill:#aaa;}.cls-3{stroke:#aaa;}.cls-3,.cls-5{stroke-miterlimit:1;stroke-width:0.75px;}.cls-4{fill:#7b7b97;}.cls-5{fill:#def4fd;stroke:#7b7b97;}</style></defs><title>server</title><g id="_3" data-name="3"><rect class="cls-1" x="3.08" y="2.6" width="9.85" height="3.1" rx="1.13" ry="1.13"/><path class="cls-2" d="M11.8,3a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V3.73A.75.75,0,0,1,4.2,3h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V3.73a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="4.15" x2="4.27" y2="4.15"/><path class="cls-1" d="M4.2,6.45h7.6a1.13,1.13,0,0,1,1.13,1.13v.85A1.12,1.12,0,0,1,11.8,9.55H4.2A1.12,1.12,0,0,1,3.08,8.42V7.58A1.13,1.13,0,0,1,4.2,6.45Z"/><path class="cls-4" d="M11.8,6.82a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V7.57a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V7.57a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-5" x1="6.37" y1="8" x2="4.27" y2="8"/><path class="cls-1" d="M4.2,10.3h7.6a1.12,1.12,0,0,1,1.12,1.12v.85A1.12,1.12,0,0,1,11.8,13.4H4.2a1.13,1.13,0,0,1-1.13-1.13v-.85A1.12,1.12,0,0,1,4.2,10.3Z"/><path class="cls-2" d="M11.8,10.68a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75v-.85a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5v-.85a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="11.85" x2="4.27" y2="11.85"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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,
},
];
}
}

View File

@ -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'];
});

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
SELECT count(*)
FROM bdr.node_summary
WHERE node_group_id = {{node_group_id}}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-3{fill:#f2f2f2;}.cls-2{fill:#aaa;}.cls-3{stroke:#aaa;}.cls-3,.cls-5{stroke-miterlimit:1;stroke-width:0.75px;}.cls-4{fill:#7b7b97;}.cls-5{fill:#def4fd;stroke:#7b7b97;}</style></defs><title>server</title><g id="_3" data-name="3"><rect class="cls-1" x="3.08" y="2.6" width="9.85" height="3.1" rx="1.13" ry="1.13"/><path class="cls-2" d="M11.8,3a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V3.73A.75.75,0,0,1,4.2,3h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V3.73a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="4.15" x2="4.27" y2="4.15"/><path class="cls-1" d="M4.2,6.45h7.6a1.13,1.13,0,0,1,1.13,1.13v.85A1.12,1.12,0,0,1,11.8,9.55H4.2A1.12,1.12,0,0,1,3.08,8.42V7.58A1.13,1.13,0,0,1,4.2,6.45Z"/><path class="cls-4" d="M11.8,6.82a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75V7.57a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5V7.57a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-5" x1="6.37" y1="8" x2="4.27" y2="8"/><path class="cls-1" d="M4.2,10.3h7.6a1.12,1.12,0,0,1,1.12,1.12v.85A1.12,1.12,0,0,1,11.8,13.4H4.2a1.13,1.13,0,0,1-1.13-1.13v-.85A1.12,1.12,0,0,1,4.2,10.3Z"/><path class="cls-2" d="M11.8,10.68a.75.75,0,0,1,.75.75v.85a.75.75,0,0,1-.75.75H4.2a.75.75,0,0,1-.75-.75v-.85a.75.75,0,0,1,.75-.75h7.6m0-.75H4.2a1.5,1.5,0,0,0-1.5,1.5v.85a1.5,1.5,0,0,0,1.5,1.5h7.6a1.5,1.5,0,0,0,1.5-1.5v-.85a1.5,1.5,0,0,0-1.5-1.5Z"/><line class="cls-3" x1="6.37" y1="11.85" x2="4.27" y2="11.85"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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,
},
];
}
}

View File

@ -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'];
});

View File

@ -0,0 +1,3 @@
SELECT COUNT(*)
FROM bdr.node_group
WHERE node_group_parent_id != 0;

View File

@ -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

View File

@ -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

View File

@ -103,7 +103,7 @@ class ReplicationNodesModule(CollectionNodeModule):
conn = manager.connection(sid=kwargs['sid']) conn = manager.connection(sid=kwargs['sid'])
replication_type = get_replication_type(conn, manager.version) replication_type = get_replication_type(conn, manager.version)
return bool(replication_type) return replication_type == 'log'
blueprint = ReplicationNodesModule(__name__) blueprint = ReplicationNodesModule(__name__)
@ -248,7 +248,6 @@ class ReplicationNodesView(PGChildNodeView):
row['pid'], row['pid'],
sid, sid,
row['name'], row['name'],
icon="icon-replica_nodes"
)) ))
return make_json_response( return make_json_response(

View File

@ -9,18 +9,18 @@
import ReplicaNodeSchema from './replica_node.ui'; import ReplicaNodeSchema from './replica_node.ui';
define('pgadmin.node.replica_nodes', [ define('pgadmin.node.replica_node', [
'sources/gettext', 'sources/url_for', 'pgadmin.browser', 'sources/gettext', 'sources/url_for', 'pgadmin.browser',
'pgadmin.browser.collection', 'pgadmin.browser.collection',
], function(gettext, url_for, pgBrowser) { ], function(gettext, url_for, pgBrowser) {
// Extend the browser's collection class for replica nodes collection // Extend the browser's collection class for replica nodes collection
if (!pgBrowser.Nodes['coll-replica_nodes']) { if (!pgBrowser.Nodes['coll-replica_node']) {
pgBrowser.Nodes['coll-replica_nodes'] = pgBrowser.Nodes['coll-replica_node'] =
pgBrowser.Collection.extend({ pgBrowser.Collection.extend({
node: 'replica_nodes', node: 'replica_node',
label: gettext('Replica Nodes'), label: gettext('Replica Nodes'),
type: 'coll-replica_nodes', type: 'coll-replica_node',
columns: ['pid', 'name', 'usename', 'state'], columns: ['pid', 'name', 'usename', 'state'],
canEdit: false, canEdit: false,
canDrop: false, canDrop: false,
@ -29,14 +29,14 @@ define('pgadmin.node.replica_nodes', [
} }
// Extend the browser's node class for replica nodes node // Extend the browser's node class for replica nodes node
if (!pgBrowser.Nodes['replica_nodes']) { if (!pgBrowser.Nodes['replica_node']) {
pgBrowser.Nodes['replica_nodes'] = pgBrowser.Node.extend({ pgBrowser.Nodes['replica_node'] = pgBrowser.Node.extend({
parent_type: 'server', parent_type: 'server',
type: 'replica_nodes', type: 'replica_node',
epasHelp: false, epasHelp: false,
sqlAlterHelp: '', sqlAlterHelp: '',
sqlCreateHelp: '', 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'), label: gettext('Replica Nodes'),
hasSQL: false, hasSQL: false,
hasScriptTypes: false, hasScriptTypes: false,
@ -57,5 +57,5 @@ define('pgadmin.node.replica_nodes', [
}); });
} }
return pgBrowser.Nodes['coll-replica_nodes']; return pgBrowser.Nodes['coll-replica_node'];
}); });

View File

@ -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 FROM pg_stat_replication
ORDER BY pid ORDER BY pid

View File

@ -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 sl.slot_name, sl.slot_type, sl.active
FROM pg_stat_replication st JOIN pg_replication_slots sl FROM pg_stat_replication st JOIN pg_replication_slots sl
ON st.pid = sl.active_pid ON st.pid = sl.active_pid

View File

@ -1,7 +1,7 @@
SELECT CASE SELECT CASE
WHEN (SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname='bdr') > 0 WHEN (SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname='bdr') > 0
THEN 'pgd' THEN 'pgd'
WHEN (SELECT COUNT(*) FROM pg_stat_replication) > 0 WHEN (SELECT COUNT(*) FROM pg_replication_slots) > 0
THEN 'log' THEN 'log'
ELSE NULL ELSE NULL
END as type; END as type;

View File

@ -81,7 +81,7 @@ define('pgadmin.browser.utils',
'coll-role', 'role', 'coll-resource_group', 'resource_group', 'coll-role', 'role', 'coll-resource_group', 'resource_group',
'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep', 'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep',
'pga_job', 'pga_schedule', 'pga_jobstep', 'pga_job', 'pga_schedule', 'pga_jobstep',
'coll-replica_nodes', 'replica_nodes' 'coll-replica_node', 'replica_node'
]; ];
pgBrowser.utils = { pgBrowser.utils = {

View File

@ -9,21 +9,22 @@
"""A blueprint module implementing the dashboard frame.""" """A blueprint module implementing the dashboard frame."""
import math 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 flask_babel import gettext
from pgadmin.user_login_check import pga_login_required from pgadmin.user_login_check import pga_login_required
import json import json
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response,\ from pgadmin.utils.ajax import make_response as ajax_response,\
internal_server_error internal_server_error
from pgadmin.utils.ajax import precondition_required
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.utils.menu import Panel
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \ 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 from config import PG_DEFAULT_DRIVER
MODULE_NAME = 'dashboard' MODULE_NAME = 'dashboard'
@ -217,6 +218,8 @@ class DashboardModule(PgAdminModule):
help_str=gettext('Set the width of the lines on the line chart.') help_str=gettext('Set the width of the lines on the line chart.')
) )
pgd_replication.register_preferences(self)
def get_exposed_url_endpoints(self): def get_exposed_url_endpoints(self):
""" """
Returns: Returns:
@ -247,70 +250,11 @@ class DashboardModule(PgAdminModule):
'dashboard.system_statistics_did', 'dashboard.system_statistics_did',
'dashboard.replication_slots', 'dashboard.replication_slots',
'dashboard.replication_stats', 'dashboard.replication_stats',
] ] + pgd_replication.get_exposed_url_endpoints()
blueprint = DashboardModule(MODULE_NAME, __name__) blueprint = DashboardModule(MODULE_NAME, __name__)
blueprint.register_blueprint(pgd_replication)
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.route("/dashboard.js") @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) # Allow no server ID to be specified (so we can generate a route in JS)
# but throw an error if it's actually called. # but throw an error if it's actually called.
if not sid: 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( sql = render_template(
"/".join([g.template_path, template]), did=did "/".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(',') chart_names = request.args['chart_names'].split(',')
if not sid: 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( sql = render_template(
"/".join([g.template_path, 'dashboard_stats.sql']), did=did, "/".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(',') chart_names = request.args['chart_names'].split(',')
if not sid: 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( sql = render_template(
"/".join([g.template_path, 'system_statistics.sql']), did=did, "/".join([g.template_path, 'system_statistics.sql']), did=did,
@ -660,7 +606,7 @@ def replication_stats(sid=None):
""" """
if not sid: 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'])) sql = render_template("/".join([g.template_path, 'replication_stats.sql']))
status, res = g.conn.execute_dict(sql) status, res = g.conn.execute_dict(sql)
@ -684,7 +630,7 @@ def replication_slots(sid=None):
""" """
if not sid: 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'])) sql = render_template("/".join([g.template_path, 'replication_slots.sql']))
status, res = g.conn.execute_dict(sql) status, res = g.conn.execute_dict(sql)

View File

@ -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/<int:sid>', 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/<int:sid>', 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/<int:sid>', 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/<int:sid>', 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/<int:sid>', 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
)

View File

@ -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

View File

@ -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;
}

View File

@ -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(<Graphs sid={this.sid} did={this.did} preferences={this.preferences} pageVisible={this.pageVisible}/>, 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();
}
}

View File

@ -978,7 +978,8 @@ function Dashboard({
</TabPanel> </TabPanel>
{/* Replication */} {/* Replication */}
<TabPanel value={mainTabVal} index={2} classNameRoot={classes.tabPanel}> <TabPanel value={mainTabVal} index={2} classNameRoot={classes.tabPanel}>
<Replication key={mainTabVal} sid={sid} node={node} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} /> <Replication key={sid} sid={sid} node={node}
preferences={preferences} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
</TabPanel> </TabPanel>
</Box> </Box>
</Box> </Box>

View File

@ -17,17 +17,17 @@ import {useInterval, usePrevious} from 'sources/custom_hooks';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StreamingChart from '../../../static/js/components/PgChart/StreamingChart'; import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
import { Grid } from '@mui/material'; import { Grid } from '@mui/material';
import { getChartColor } from '../../../static/js/utils';
export const X_AXIS_LENGTH = 75; export const X_AXIS_LENGTH = 75;
/* Transform the labels data to suit ChartJS */ /* Transform the labels data to suit ChartJS */
export function transformData(labels, refreshRate) { export function transformData(labels, refreshRate, theme='standard') {
const colors = ['#00BCD4', '#9CCC65', '#E64A19'];
let datasets = Object.keys(labels).map((label, i)=>{ let datasets = Object.keys(labels).map((label, i)=>{
return { return {
label: label, label: label,
data: labels[label] || [], data: labels[label] || [],
borderColor: colors[i], borderColor: getChartColor(i, theme),
pointHitRadius: DATA_POINT_SIZE, pointHitRadius: DATA_POINT_SIZE,
}; };
}) || []; }) || [];

View File

@ -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 (
<Box height="100%" display="flex" flexDirection="column">
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('replication_stats', setReplicationStats);
}}/>}
title={gettext('Replication Stats')} style={{minHeight: '300px'}}>
<PgTable
caveTable={false}
columns={replicationStatsColumns}
data={replicationStats}
schema={replStatObj}
></PgTable>
</SectionContainer>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('replication_slots', setReplicationSlots);
}}/>}
title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
columns={replicationSlotsColumns}
data={replicationSlots}
schema={replSchemaObj}
></PgTable>
</SectionContainer>
</Box>
);
}
LogReplication.propTypes = {
treeNodeInfo: PropTypes.object.isRequired,
pageVisible: PropTypes.bool,
};

View File

@ -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 (
<Box height="100%" display="flex" flexDirection="column">
<Grid container spacing={0.5}>
<Grid item md={6}>
<ChartContainer id='sessions-graph' title={gettext('Replication lag (Time)')}
datasets={replicationLagTimeData.datasets} errorMsg={errorMsg} isTest={props.isTest}>
<StreamingChart data={replicationLagTimeData} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={(v)=>toPrettySize(v, 's')} />
</ChartContainer>
</Grid>
<Grid item md={6}>
<ChartContainer id='tps-graph' title={gettext('Replication lag (Size)')} datasets={replicationLagBytesData.datasets} errorMsg={errorMsg} isTest={props.isTest}>
<StreamingChart data={replicationLagBytesData} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={toPrettySize} />
</ChartContainer>
</Grid>
</Grid>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('cluster_nodes', setClusterNodes);
}}/>}
title={gettext('Cluster nodes')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={clusterNodeColumns}
data={clusterNodes}
></PgTable>
</SectionContainer>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('raft_status', setRaftStatus);
}}/>}
title={gettext('Raft status')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={raftStatusColumns}
data={raftStatus}
></PgTable>
</SectionContainer>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('outgoing', setOutgoingReplication);
}}/>}
title={gettext('Outgoing Replication')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={outgoingReplicationColumns}
data={outgoingReplication}
schema={outgoingSchemaObj}
></PgTable>
</SectionContainer>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('incoming', setIncomingReplication);
}}/>}
title={gettext('Incoming Replication')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={incomingReplicationColumns}
data={incomingReplication}
schema={incomingSchemaObj}
></PgTable>
</SectionContainer>
</Box>
);
}
PGDReplication.propTypes = {
preferences: PropTypes.object,
treeNodeInfo: PropTypes.object.isRequired,
pageVisible: PropTypes.bool,
enablePoll: PropTypes.bool,
isTest: PropTypes.bool,
};

View File

@ -7,206 +7,26 @@
// //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import { Box } from '@mui/material';
import React, { useEffect, useState } from 'react';
import gettext from 'sources/gettext'; import React from 'react';
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 PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LogReplication from './LogReplication';
import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessage';
import PGDReplication from './PGDReplication';
export default function Replication({preferences, treeNodeInfo, pageVisible}) {
const replicationStatsColumns = [{ const replicationType = treeNodeInfo?.server?.replication_type;
accessorKey: 'view_details', if(replicationType == 'log') {
header: () => null, return <LogReplication treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
enableSorting: false, } else if(replicationType == 'pgd') {
enableResizing: false, return <PGDReplication preferences={preferences} treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
enableFilters: true, } else {
size: 35, return <EmptyPanelMessage text='No replication' />;
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 (
<Box height="100%" display="flex" flexDirection="column">
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('replication_stats', setReplicationStats);
}}/>}
title={gettext('Replication Stats')} style={{minHeight: '300px'}}>
<PgTable
caveTable={false}
columns={replicationStatsColumns}
data={replicationStats}
schema={replStatObj}
></PgTable>
</SectionContainer>
<SectionContainer
titleExtras={<RefreshButton onClick={()=>{
getReplicationData('replication_slots', setReplicationSlots);
}}/>}
title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}>
<PgTable
caveTable={false}
columns={replicationSlotsColumns}
data={replicationSlots}
schema={replSchemaObj}
></PgTable>
</SectionContainer>
</Box>
);
} }
Replication.propTypes = { Replication.propTypes = {
preferences: PropTypes.object,
treeNodeInfo: PropTypes.object.isRequired, treeNodeInfo: PropTypes.object.isRequired,
pageVisible: PropTypes.bool, pageVisible: PropTypes.bool,
}; };

View File

@ -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')
},
];
}
}

View File

@ -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')
},
];
}
}

View File

@ -21,6 +21,7 @@ const useStyles = makeStyles((theme) => ({
height: '100%', height: '100%',
width: '100%', width: '100%',
minHeight: '400px', minHeight: '400px',
borderRadius: theme.shape.borderRadius,
}, },
cardHeader: { cardHeader: {
backgroundColor: theme.otherVars.tableBg, backgroundColor: theme.otherVars.tableBg,

View File

@ -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 %}

View File

@ -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;

View File

@ -0,0 +1 @@
SELECT * FROM bdr.subscription_summary;

View File

@ -0,0 +1 @@
SELECT * FROM bdr.node_slots;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -88,10 +88,10 @@ function getColumn(data, singleLineStatistics, prettifyFields=[]) {
} }
columns.forEach((c)=>{ columns.forEach((c)=>{
// Prettify the cell view // Prettify the cell view
if(prettifyFields.includes(c.Header)) { if(prettifyFields.includes(c.header)) {
c.Cell = ({value})=><>{toPrettySize(value)}</>; c.cell = ({value})=><>{toPrettySize(value)}</>;
c.Cell.displayName = 'Cell'; c.cell.displayName = 'Cell';
c.Cell.propTypes = { c.cell.propTypes = {
value: PropTypes.any, value: PropTypes.any,
}; };
} }

View File

@ -16,19 +16,6 @@ import { useTheme } from '@mui/material';
export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect', export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect',
'rectRounded', 'rectRot', 'star', 'triangle']; 'rectRounded', 'rectRot', 'star', 'triangle'];
export const DATA_POINT_SIZE = 3; 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 = { const defaultOptions = {
responsive: true, responsive: true,

View File

@ -81,8 +81,8 @@ export default function ObjectBreadcrumbs() {
<AccountTreeIcon style={{height: '1rem', marginRight: '0.125rem'}} data-label="AccountTreeIcon"/> <AccountTreeIcon style={{height: '1rem', marginRight: '0.125rem'}} data-label="AccountTreeIcon"/>
<div className={classes.overflow}> <div className={classes.overflow}>
{ {
objectData.path?.reduce((res, item)=>( objectData.path?.reduce((res, item, i)=>(
res.concat(<span key={item}>{item}</span>, <ArrowForwardIosRoundedIcon key={item+'-arrow'} style={{height: '0.8rem', width: '1.25rem'}} />) res.concat(<span key={item+i}>{item}</span>, <ArrowForwardIosRoundedIcon key={item+i+'-arrow'} style={{height: '0.8rem', width: '1.25rem'}} />)
), []).slice(0, -1) ), []).slice(0, -1)
} }
</div> </div>

View File

@ -11,13 +11,13 @@ import React, { forwardRef, useEffect } from 'react';
import { flexRender } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table';
import { styled } from '@mui/styles'; import { styled } from '@mui/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Switch } from '@mui/material';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { PgIconButton } from './Buttons'; import { PgIconButton } from './Buttons';
import clsx from 'clsx'; import clsx from 'clsx';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import { InputSwitch } from './FormComponents';
const StyledDiv = styled('div')(({theme})=>({ const StyledDiv = styled('div')(({theme})=>({
@ -360,20 +360,14 @@ export function getExpandCell({ onClick, ...props }) {
return Cell; return Cell;
} }
const ReadOnlySwitch = styled(Switch)(({theme})=>({
opacity: 0.75,
'& .MuiSwitch-track': {
opacity: theme.palette.action.disabledOpacity,
}
}));
export function getSwitchCell() { export function getSwitchCell() {
const Cell = ({ value }) => { const Cell = ({ getValue }) => {
return <ReadOnlySwitch color="primary" checked={value} value={value} readOnly title={String(value)} />; return <InputSwitch value={getValue()} readonly />;
}; };
Cell.displayName = 'SwitchCell'; Cell.displayName = 'SwitchCell';
Cell.propTypes = { Cell.propTypes = {
value: PropTypes.any, getValue: PropTypes.func,
}; };
return Cell; return Cell;

View File

@ -42,7 +42,7 @@ function TableRow({ index, style, schema, row, measureElement }) {
}, [row.getIsExpanded(), expandComplete]); }, [row.getIsExpanded(), expandComplete]);
return ( return (
<PgReactTableRow data-index={index} ref={rowRef} style={style} row={row}> <PgReactTableRow data-index={index} ref={rowRef} style={style}>
<PgReactTableRowContent> <PgReactTableRowContent>
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
const content = flexRender(cell.column.columnDef.cell, cell.getContext()); const content = flexRender(cell.column.columnDef.cell, cell.getContext());
@ -115,7 +115,12 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
enableResizing: false, enableResizing: false,
maxSize: 35, maxSize: 35,
}] : []).concat( }] : []).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]); ), [hasSelectRow, columns]);
// Render the UI for your table // Render the UI for your table
@ -264,4 +269,4 @@ PgTable.propTypes = {
caveTable: PropTypes.bool, caveTable: PropTypes.bool,
tableNoBorder: PropTypes.bool, tableNoBorder: PropTypes.bool,
'data-test': PropTypes.string 'data-test': PropTypes.string
}; };

View File

@ -641,3 +641,22 @@ export function scrollbarWidth() {
document.body.removeChild(scrollDiv); document.body.removeChild(scrollDiv);
return scrollbarWidth; 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];
}

View File

@ -16,7 +16,6 @@ def get_submodules():
AuthenticateModule, AuthenticateModule,
BrowserModule, BrowserModule,
DashboardModule, DashboardModule,
DashboardModule,
HelpModule, HelpModule,
MiscModule, MiscModule,
PreferencesModule, PreferencesModule,

View File

@ -23,10 +23,11 @@ import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
import { InputSelect } from '../../../../../../static/js/components/FormComponents'; import { InputSelect } from '../../../../../../static/js/components/FormComponents';
import { DefaultButton, PgButtonGroup, PgIconButton} from '../../../../../../static/js/components/Buttons'; import { DefaultButton, PgButtonGroup, PgIconButton} from '../../../../../../static/js/components/Buttons';
import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE, 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 { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants'; import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { getChartColor } from '../../../../../../static/js/utils';
// Numeric data type used to separate out the options for Y axis. // Numeric data type used to separate out the options for Y axis.
const NUMERIC_TYPES = ['oid', 'smallint', 'integer', 'bigint', 'decimal', 'numeric', 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. // 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 { return {
label: colName, label: colName,
data: rows.map((r)=>r[colPosition]), 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. // This function is used to get the dataset for Pie Chart.
function getPieChartData(rows, colName, colPosition, queryToolCtx) { function getPieChartData(rows, colName, colPosition, queryToolCtx) {
let rowCount = -1;
return { return {
label: colName, label: colName,
data: rows.map((r)=>r[colPosition]), data: rows.map((r)=>r[colPosition]),
backgroundColor: rows.map(()=> { backgroundColor: rows.map((_v, i)=> {
if (rowCount >= (CHART_THEME_COLORS_LENGTH - 1)) { return getChartColor(i, queryToolCtx.preferences.misc.theme);
rowCount = -1;
}
rowCount = rowCount + 1;
return CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme][rowCount];
}), }),
}; };
} }
// This function is used to get the graph data set for the X axis and Y axis // 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 is used to the find the position of the column
function getColumnPosition(colName) { function getColumnPosition(colName) {
return _.find(columns, (c)=>(c.name==colName))?.pos; return _.find(columns, (c)=>(c.name==colName))?.pos;
} }
let styleIndex = -1, colorIndex = -1; let styleIndex = -1;
return { return {
'labels': rows.map((r, index)=>{ 'labels': rows.map((r, index)=>{
@ -221,14 +217,8 @@ function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, g
return r[colPosition]; return r[colPosition];
}), }),
'datasets': yaxis.map((colName)=>{ 'datasets': yaxis.map((colName, i)=>{
// Loop is used to set the index for random color array let color = getChartColor(i, theme);
if (colorIndex >= (CHART_THEME_COLORS_LENGTH - 1)) {
colorIndex = -1;
}
colorIndex = colorIndex + 1;
let color = graphColors[colorIndex];
let colPosition = getColumnPosition(colName); let colPosition = getColumnPosition(colName);
// Loop is used to set the index for DATA_POINT_STYLE array // 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') { } else if (graphType === 'B' || graphType === 'SB') {
return getBarChartData(rows, colName, colPosition, color); return getBarChartData(rows, colName, colPosition, color);
} else if (graphType === 'L' || graphType === 'SL') { } else if (graphType === 'L' || graphType === 'SL') {
return getLineChartData(graphType, rows, colName, colPosition, color, return getLineChartData(graphType, rows, colName, colPosition, color, styleIndex, queryToolCtx);
colorIndex, styleIndex, queryToolCtx);
} }
}), }),
}; };
@ -263,7 +252,6 @@ export function GraphVisualiser({initColumns}) {
const [columns, setColumns] = useState(initColumns); const [columns, setColumns] = useState(initColumns);
const [graphHeight, setGraphHeight] = useState(); const [graphHeight, setGraphHeight] = useState();
const [expandedState, setExpandedState] = useState(true); const [expandedState, setExpandedState] = useState(true);
const [graphColor, setGraphColor] = useState([]);
const theme = useTheme(); const theme = useTheme();
@ -344,10 +332,6 @@ export function GraphVisualiser({initColumns}) {
} }
}, [graphType, theme]); }, [graphType, theme]);
useEffect(()=>{
setGraphColor(CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme]);
}, [queryToolCtx.preferences.misc.theme, theme]);
const graphBackgroundColor = useMemo(() => { const graphBackgroundColor = useMemo(() => {
return theme.palette.background.default; return theme.palette.background.default;
},[theme]); },[theme]);
@ -380,7 +364,10 @@ export function GraphVisualiser({initColumns}) {
setLoaderText(gettext('Rendering data points...')); setLoaderText(gettext('Rendering data points...'));
// Set the Graph Data // Set the Graph Data
setGraphData( 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(''); setLoaderText('');

View File

@ -294,7 +294,7 @@ class UserManagementSchema extends BaseUISchema {
canSearch: true canSearch: true
}, },
{ {
id: 'refreshBrowserTree', visible: false, type: 'boolean', id: 'refreshBrowserTree', visible: false, type: 'switch',
deps: ['userManagement'], depChange: ()=> { deps: ['userManagement'], depChange: ()=> {
return { refreshBrowserTree: this.changeOwnership }; return { refreshBrowserTree: this.changeOwnership };
} }

View File

@ -59,6 +59,8 @@ ERROR_FETCHING_ROLE_INFORMATION = gettext(
ERROR_FETCHING_DATA = gettext('Unable to fetch data.') ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
ERROR_SERVER_ID_NOT_SPECIFIED = gettext('Server ID not specified.')
# Authentication Sources # Authentication Sources
INTERNAL = 'internal' INTERNAL = 'internal'
LDAP = 'ldap' LDAP = 'ldap'

View File

@ -10,16 +10,16 @@ import { render } from '@testing-library/react';
describe('Graphs.js', ()=>{ describe('Graphs.js', ()=>{
it('transformData', ()=>{ it('transformData', ()=>{
expect(transformData({'Label1': [], 'Label2': []}, 1, false)).toEqual({ expect(transformData({'Label1': [], 'Label2': []}, 1)).toEqual({
datasets: [{ datasets: [{
label: 'Label1', label: 'Label1',
data: [], data: [],
borderColor: '#00BCD4', borderColor: '#1F77B4',
pointHitRadius: DATA_POINT_SIZE, pointHitRadius: DATA_POINT_SIZE,
},{ },{
label: 'Label2', label: 'Label2',
data: [], data: [],
borderColor: '#9CCC65', borderColor: '#FF7F0E',
pointHitRadius: DATA_POINT_SIZE, pointHitRadius: DATA_POINT_SIZE,
}], }],
refreshRate: 1, refreshRate: 1,

View File

@ -221,7 +221,9 @@ module.exports = [{
'pure|pgadmin.node.aggregate', 'pure|pgadmin.node.aggregate',
'pure|pgadmin.node.operator', 'pure|pgadmin.node.operator',
'pure|pgadmin.node.dbms_job_scheduler', '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, minChunks: 2,
enforce: true, enforce: true,
test(module) { test(module) {
return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@material-ui']); return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@mui']);
}, },
}, },
vendor_others: { vendor_others: {

View File

@ -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.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.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.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.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.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'), 'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),