Added support for viewing PGD Clusters. #7215

This commit is contained in:
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:
*********************************
Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs
that allow you to create these objects, manage them, and control their
relationships to each other. To access a dialog that allows you to create a
@ -12,11 +12,12 @@ and select the *Create* option for that object. For example, to create a new
database, right-click on the *Databases* node, and select *Create Database...*
.. toctree::
:maxdepth: 1
:maxdepth: 3
database_dialog
resource_group_dialog
role_dialog
tablespace_dialog
replica_nodes_dialog
pgd_replication_group_dialog
role_reassign_dialog

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
def generate_browser_node(
self, node_id, parent_id, label, icon, **kwargs
self, node_id, parent_id, label, icon=None, **kwargs
):
obj = {
"id": "%s_%s" % (self.node_type, node_id),

View File

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

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'])
replication_type = get_replication_type(conn, manager.version)
return bool(replication_type)
return replication_type == 'log'
blueprint = ReplicationNodesModule(__name__)
@ -248,7 +248,6 @@ class ReplicationNodesView(PGChildNodeView):
row['pid'],
sid,
row['name'],
icon="icon-replica_nodes"
))
return make_json_response(

View File

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

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
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
FROM pg_stat_replication st JOIN pg_replication_slots sl
ON st.pid = sl.active_pid

View File

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

View File

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

View File

@ -9,21 +9,22 @@
"""A blueprint module implementing the dashboard frame."""
import math
from functools import wraps
from flask import render_template, url_for, Response, g, request
from flask import render_template, Response, g, request
from flask_babel import gettext
from pgadmin.user_login_check import pga_login_required
import json
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response,\
internal_server_error
from pgadmin.utils.ajax import precondition_required
from pgadmin.utils.driver import get_driver
from pgadmin.utils.menu import Panel
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
PREF_LABEL_REFRESH_RATES
PREF_LABEL_REFRESH_RATES, ERROR_SERVER_ID_NOT_SPECIFIED
from .precondition import check_precondition
from .pgd_replication import blueprint as pgd_replication
from config import PG_DEFAULT_DRIVER
MODULE_NAME = 'dashboard'
@ -217,6 +218,8 @@ class DashboardModule(PgAdminModule):
help_str=gettext('Set the width of the lines on the line chart.')
)
pgd_replication.register_preferences(self)
def get_exposed_url_endpoints(self):
"""
Returns:
@ -247,70 +250,11 @@ class DashboardModule(PgAdminModule):
'dashboard.system_statistics_did',
'dashboard.replication_slots',
'dashboard.replication_stats',
]
] + pgd_replication.get_exposed_url_endpoints()
blueprint = DashboardModule(MODULE_NAME, __name__)
def check_precondition(f):
"""
This function will behave as a decorator which will check
database connection before running view, it also adds
manager, conn & template_path properties to self
"""
@wraps(f)
def wrap(*args, **kwargs):
# Here args[0] will hold self & kwargs will hold gid,sid,did
g.manager = get_driver(
PG_DEFAULT_DRIVER).connection_manager(
kwargs['sid']
)
def get_error(i_node_type):
stats_type = ('activity', 'prepared', 'locks', 'config')
if f.__name__ in stats_type:
return precondition_required(
gettext("Please connect to the selected {0}"
" to view the table.".format(i_node_type))
)
else:
return precondition_required(
gettext("Please connect to the selected {0}"
" to view the graph.".format(i_node_type))
)
# Below check handle the case where existing server is deleted
# by user and python server will raise exception if this check
# is not introduce.
if g.manager is None:
return get_error('server')
if 'did' in kwargs:
g.conn = g.manager.connection(did=kwargs['did'])
node_type = 'database'
else:
g.conn = g.manager.connection()
node_type = 'server'
# If not connected then return error to browser
if not g.conn.connected():
return get_error(node_type)
# Set template path for sql scripts
g.server_type = g.manager.server_type
g.version = g.manager.version
# Include server_type in template_path
g.template_path = 'dashboard/sql/' + (
'#{0}#'.format(g.version)
)
return f(*args, **kwargs)
return wrap
blueprint.register_blueprint(pgd_replication)
@blueprint.route("/dashboard.js")
@ -389,7 +333,7 @@ def get_data(sid, did, template, check_long_running_query=False):
# Allow no server ID to be specified (so we can generate a route in JS)
# but throw an error if it's actually called.
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
sql = render_template(
"/".join([g.template_path, template]), did=did
@ -455,7 +399,8 @@ def dashboard_stats(sid=None, did=None):
chart_names = request.args['chart_names'].split(',')
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
return internal_server_error(
errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
sql = render_template(
"/".join([g.template_path, 'dashboard_stats.sql']), did=did,
@ -629,7 +574,8 @@ def system_statistics(sid=None, did=None):
chart_names = request.args['chart_names'].split(',')
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
return internal_server_error(
errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
sql = render_template(
"/".join([g.template_path, 'system_statistics.sql']), did=did,
@ -660,7 +606,7 @@ def replication_stats(sid=None):
"""
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
sql = render_template("/".join([g.template_path, 'replication_stats.sql']))
status, res = g.conn.execute_dict(sql)
@ -684,7 +630,7 @@ def replication_slots(sid=None):
"""
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
sql = render_template("/".join([g.template_path, 'replication_slots.sql']))
status, res = g.conn.execute_dict(sql)

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>
{/* Replication */}
<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>
</Box>
</Box>

View File

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

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 ReplicationSlotsSchema from './replication_slots.ui';
import PgTable from 'sources/components/PgTable';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import SectionContainer from '../components/SectionContainer';
import ReplicationStatsSchema from './replication_stats.ui';
import RefreshButton from '../components/RefreshButtons';
import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import url_for from 'sources/url_for';
import React from 'react';
import PropTypes from 'prop-types';
import LogReplication from './LogReplication';
import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessage';
import PGDReplication from './PGDReplication';
const replicationStatsColumns = [{
accessorKey: 'view_details',
header: () => null,
enableSorting: false,
enableResizing: false,
enableFilters: true,
size: 35,
maxSize: 35,
minSize: 35,
id: 'btn-edit',
cell: getExpandCell({
title: gettext('View details')
}),
},
{
accessorKey: 'pid',
header: gettext('PID'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 40,
minSize: 40,
},
{
accessorKey: 'client_addr',
header: gettext('Client Addr'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
},
{
accessorKey:'state',
header: gettext('State'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
},
{
accessorKey:'write_lag',
header: gettext('Write Lag'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
},
{
accessorKey:'flush_lag',
header: gettext('Flush Lag'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
},
{
accessorKey:'replay_lag',
header: gettext('Replay Lag'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
},
{
accessorKey:'reply_time',
header: gettext('Reply Time'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 50,
}
];
const replicationSlotsColumns = [{
accessorKey: 'view_details',
header: () => null,
enableSorting: false,
enableResizing: false,
enableFilters: true,
size: 35,
maxSize: 35,
minSize: 35,
id: 'btn-details',
cell: getExpandCell({
title: gettext('View details')
}),
},
{
accessorKey: 'active_pid',
header: gettext('Active PID'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 50,
minSize: 50,
},
{
accessorKey: 'slot_name',
header: gettext('Slot Name'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 200,
minSize: 50,
},
{
accessorKey:'active',
header: gettext('Active'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 50,
minSize: 50,
cell: getSwitchCell(),
}
];
const replSchemaObj = new ReplicationSlotsSchema();
const replStatObj = new ReplicationStatsSchema();
export default function Replication({treeNodeInfo, pageVisible}) {
const [replicationSlots, setReplicationSlots] = useState([{
}]);
const [replicationStats, setReplicationStats] = useState([{
}]);
const pgAdmin = usePgAdmin();
const getReplicationData = (endpoint, setter)=>{
const api = getApiInstance();
const url = url_for(`dashboard.${endpoint}`, {sid: treeNodeInfo.server._id});
api.get(url)
.then((res)=>{
setter(res.data);
})
.catch((error)=>{
console.error(error);
pgAdmin.Browser.notifier.error(parseApiError(error));
});
};
useEffect(()=>{
if(pageVisible) {
getReplicationData('replication_stats', setReplicationStats);
getReplicationData('replication_slots', setReplicationSlots);
}
}, [pageVisible ]);
return (
<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>
);
export default function Replication({preferences, treeNodeInfo, pageVisible}) {
const replicationType = treeNodeInfo?.server?.replication_type;
if(replicationType == 'log') {
return <LogReplication treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
} else if(replicationType == 'pgd') {
return <PGDReplication preferences={preferences} treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
} else {
return <EmptyPanelMessage text='No replication' />;
}
}
Replication.propTypes = {
preferences: PropTypes.object,
treeNodeInfo: PropTypes.object.isRequired,
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%',
width: '100%',
minHeight: '400px',
borderRadius: theme.shape.borderRadius,
},
cardHeader: {
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)=>{
// Prettify the cell view
if(prettifyFields.includes(c.Header)) {
c.Cell = ({value})=><>{toPrettySize(value)}</>;
c.Cell.displayName = 'Cell';
c.Cell.propTypes = {
if(prettifyFields.includes(c.header)) {
c.cell = ({value})=><>{toPrettySize(value)}</>;
c.cell.displayName = 'Cell';
c.cell.propTypes = {
value: PropTypes.any,
};
}

View File

@ -16,19 +16,6 @@ import { useTheme } from '@mui/material';
export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect',
'rectRounded', 'rectRot', 'star', 'triangle'];
export const DATA_POINT_SIZE = 3;
export const CHART_THEME_COLORS_LENGTH = 20;
export const CHART_THEME_COLORS = {
'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B',
'#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900',
'#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'],
'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C',
'#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC',
'#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'],
'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2',
'#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8',
'#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0',
'#52B788']
};
const defaultOptions = {
responsive: true,

View File

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

View File

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

View File

@ -42,7 +42,7 @@ function TableRow({ index, style, schema, row, measureElement }) {
}, [row.getIsExpanded(), expandComplete]);
return (
<PgReactTableRow data-index={index} ref={rowRef} style={style} row={row}>
<PgReactTableRow data-index={index} ref={rowRef} style={style}>
<PgReactTableRowContent>
{row.getVisibleCells().map((cell) => {
const content = flexRender(cell.column.columnDef.cell, cell.getContext());
@ -115,7 +115,12 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
enableResizing: false,
maxSize: 35,
}] : []).concat(
columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility)
columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility).map((c)=>({
...c,
// if data is null then global search doesn't work
// Use accessorFn to return empty string if data is null.
accessorFn: c.accessorFn ?? (c.accessorKey ? (row)=>row[c.accessorKey] ?? '' : undefined),
}))
), [hasSelectRow, columns]);
// Render the UI for your table
@ -264,4 +269,4 @@ PgTable.propTypes = {
caveTable: PropTypes.bool,
tableNoBorder: PropTypes.bool,
'data-test': PropTypes.string
};
};

View File

@ -641,3 +641,22 @@ export function scrollbarWidth() {
document.body.removeChild(scrollDiv);
return scrollbarWidth;
}
const CHART_THEME_COLORS = {
'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B',
'#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900',
'#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'],
'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C',
'#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC',
'#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'],
'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2',
'#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8',
'#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0',
'#52B788']
};
export function getChartColor(index, theme='standard', colorPalette=CHART_THEME_COLORS) {
const palette = colorPalette[theme];
// loop back if out of index;
return palette[index % palette.length];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
'pgadmin.node.replica_node': path.join(__dirname, './pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node'),
'pgadmin.node.pgd_replication_groups': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups'),
'pgadmin.node.pgd_replication_servers': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers'),
'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),