Added support for viewing PGD Clusters. #7215
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 79 KiB |
|
|
@ -3,7 +3,7 @@
|
||||||
*********************************
|
*********************************
|
||||||
`Managing Cluster Objects`:index:
|
`Managing Cluster Objects`:index:
|
||||||
*********************************
|
*********************************
|
||||||
|
|
||||||
Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs
|
Some object definitions reside at the cluster level; pgAdmin 4 provides dialogs
|
||||||
that allow you to create these objects, manage them, and control their
|
that allow you to create these objects, manage them, and control their
|
||||||
relationships to each other. To access a dialog that allows you to create a
|
relationships to each other. To access a dialog that allows you to create a
|
||||||
|
|
@ -12,11 +12,12 @@ and select the *Create* option for that object. For example, to create a new
|
||||||
database, right-click on the *Databases* node, and select *Create Database...*
|
database, right-click on the *Databases* node, and select *Create Database...*
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 3
|
||||||
|
|
||||||
database_dialog
|
database_dialog
|
||||||
resource_group_dialog
|
resource_group_dialog
|
||||||
role_dialog
|
role_dialog
|
||||||
tablespace_dialog
|
tablespace_dialog
|
||||||
replica_nodes_dialog
|
replica_nodes_dialog
|
||||||
|
pgd_replication_group_dialog
|
||||||
role_reassign_dialog
|
role_reassign_dialog
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -61,7 +61,7 @@ class CollectionNodeModule(PgAdminModule, PGChildModule, metaclass=ABCMeta):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def generate_browser_node(
|
def generate_browser_node(
|
||||||
self, node_id, parent_id, label, icon, **kwargs
|
self, node_id, parent_id, label, icon=None, **kwargs
|
||||||
):
|
):
|
||||||
obj = {
|
obj = {
|
||||||
"id": "%s_%s" % (self.node_type, node_id),
|
"id": "%s_%s" % (self.node_type, node_id),
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,9 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||||
from .replica_nodes import blueprint as module
|
from .replica_nodes import blueprint as module
|
||||||
self.submodules.append(module)
|
self.submodules.append(module)
|
||||||
|
|
||||||
|
from .pgd_replication_groups import blueprint as module
|
||||||
|
self.submodules.append(module)
|
||||||
|
|
||||||
super().register(app, options)
|
super().register(app, options)
|
||||||
|
|
||||||
# We do not have any preferences for server node.
|
# We do not have any preferences for server node.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'];
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
SELECT count(*)
|
||||||
|
FROM bdr.node_summary
|
||||||
|
WHERE node_group_id = {{node_group_id}}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 |
|
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'];
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM bdr.node_group
|
||||||
|
WHERE node_group_parent_id != 0;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -103,7 +103,7 @@ class ReplicationNodesModule(CollectionNodeModule):
|
||||||
conn = manager.connection(sid=kwargs['sid'])
|
conn = manager.connection(sid=kwargs['sid'])
|
||||||
|
|
||||||
replication_type = get_replication_type(conn, manager.version)
|
replication_type = get_replication_type(conn, manager.version)
|
||||||
return bool(replication_type)
|
return replication_type == 'log'
|
||||||
|
|
||||||
|
|
||||||
blueprint = ReplicationNodesModule(__name__)
|
blueprint = ReplicationNodesModule(__name__)
|
||||||
|
|
@ -248,7 +248,6 @@ class ReplicationNodesView(PGChildNodeView):
|
||||||
row['pid'],
|
row['pid'],
|
||||||
sid,
|
sid,
|
||||||
row['name'],
|
row['name'],
|
||||||
icon="icon-replica_nodes"
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@
|
||||||
|
|
||||||
import ReplicaNodeSchema from './replica_node.ui';
|
import ReplicaNodeSchema from './replica_node.ui';
|
||||||
|
|
||||||
define('pgadmin.node.replica_nodes', [
|
define('pgadmin.node.replica_node', [
|
||||||
'sources/gettext', 'sources/url_for', 'pgadmin.browser',
|
'sources/gettext', 'sources/url_for', 'pgadmin.browser',
|
||||||
'pgadmin.browser.collection',
|
'pgadmin.browser.collection',
|
||||||
], function(gettext, url_for, pgBrowser) {
|
], function(gettext, url_for, pgBrowser) {
|
||||||
|
|
||||||
// Extend the browser's collection class for replica nodes collection
|
// Extend the browser's collection class for replica nodes collection
|
||||||
if (!pgBrowser.Nodes['coll-replica_nodes']) {
|
if (!pgBrowser.Nodes['coll-replica_node']) {
|
||||||
pgBrowser.Nodes['coll-replica_nodes'] =
|
pgBrowser.Nodes['coll-replica_node'] =
|
||||||
pgBrowser.Collection.extend({
|
pgBrowser.Collection.extend({
|
||||||
node: 'replica_nodes',
|
node: 'replica_node',
|
||||||
label: gettext('Replica Nodes'),
|
label: gettext('Replica Nodes'),
|
||||||
type: 'coll-replica_nodes',
|
type: 'coll-replica_node',
|
||||||
columns: ['pid', 'name', 'usename', 'state'],
|
columns: ['pid', 'name', 'usename', 'state'],
|
||||||
canEdit: false,
|
canEdit: false,
|
||||||
canDrop: false,
|
canDrop: false,
|
||||||
|
|
@ -29,14 +29,14 @@ define('pgadmin.node.replica_nodes', [
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend the browser's node class for replica nodes node
|
// Extend the browser's node class for replica nodes node
|
||||||
if (!pgBrowser.Nodes['replica_nodes']) {
|
if (!pgBrowser.Nodes['replica_node']) {
|
||||||
pgBrowser.Nodes['replica_nodes'] = pgBrowser.Node.extend({
|
pgBrowser.Nodes['replica_node'] = pgBrowser.Node.extend({
|
||||||
parent_type: 'server',
|
parent_type: 'server',
|
||||||
type: 'replica_nodes',
|
type: 'replica_node',
|
||||||
epasHelp: false,
|
epasHelp: false,
|
||||||
sqlAlterHelp: '',
|
sqlAlterHelp: '',
|
||||||
sqlCreateHelp: '',
|
sqlCreateHelp: '',
|
||||||
dialogHelp: url_for('help.static', {'filename': 'replica_nodes_dialog.html'}),
|
dialogHelp: url_for('help.static', {'filename': 'replica_node_dialog.html'}),
|
||||||
label: gettext('Replica Nodes'),
|
label: gettext('Replica Nodes'),
|
||||||
hasSQL: false,
|
hasSQL: false,
|
||||||
hasScriptTypes: false,
|
hasScriptTypes: false,
|
||||||
|
|
@ -57,5 +57,5 @@ define('pgadmin.node.replica_nodes', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return pgBrowser.Nodes['coll-replica_nodes'];
|
return pgBrowser.Nodes['coll-replica_node'];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
SELECT pid, 'Standby ['||COALESCE(client_addr::text, client_hostname,'Socket')||']' as name
|
SELECT pid, 'Standby ['||COALESCE(host(client_addr), client_hostname, 'Socket')||']' as name
|
||||||
FROM pg_stat_replication
|
FROM pg_stat_replication
|
||||||
ORDER BY pid
|
ORDER BY pid
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
SELECT st.*, 'Standby ['||COALESCE(client_addr::text, client_hostname,'Socket')||']' as name,
|
SELECT st.*, 'Standby ['||COALESCE(host(client_addr), client_hostname, 'Socket')||']' as name,
|
||||||
sl.slot_name, sl.slot_type, sl.active
|
sl.slot_name, sl.slot_type, sl.active
|
||||||
FROM pg_stat_replication st JOIN pg_replication_slots sl
|
FROM pg_stat_replication st JOIN pg_replication_slots sl
|
||||||
ON st.pid = sl.active_pid
|
ON st.pid = sl.active_pid
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
SELECT CASE
|
SELECT CASE
|
||||||
WHEN (SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname='bdr') > 0
|
WHEN (SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname='bdr') > 0
|
||||||
THEN 'pgd'
|
THEN 'pgd'
|
||||||
WHEN (SELECT COUNT(*) FROM pg_stat_replication) > 0
|
WHEN (SELECT COUNT(*) FROM pg_replication_slots) > 0
|
||||||
THEN 'log'
|
THEN 'log'
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as type;
|
END as type;
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ define('pgadmin.browser.utils',
|
||||||
'coll-role', 'role', 'coll-resource_group', 'resource_group',
|
'coll-role', 'role', 'coll-resource_group', 'resource_group',
|
||||||
'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep',
|
'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep',
|
||||||
'pga_job', 'pga_schedule', 'pga_jobstep',
|
'pga_job', 'pga_schedule', 'pga_jobstep',
|
||||||
'coll-replica_nodes', 'replica_nodes'
|
'coll-replica_node', 'replica_node'
|
||||||
];
|
];
|
||||||
|
|
||||||
pgBrowser.utils = {
|
pgBrowser.utils = {
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,22 @@
|
||||||
|
|
||||||
"""A blueprint module implementing the dashboard frame."""
|
"""A blueprint module implementing the dashboard frame."""
|
||||||
import math
|
import math
|
||||||
from functools import wraps
|
|
||||||
from flask import render_template, url_for, Response, g, request
|
from flask import render_template, Response, g, request
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
from pgadmin.user_login_check import pga_login_required
|
from pgadmin.user_login_check import pga_login_required
|
||||||
import json
|
import json
|
||||||
from pgadmin.utils import PgAdminModule
|
from pgadmin.utils import PgAdminModule
|
||||||
from pgadmin.utils.ajax import make_response as ajax_response,\
|
from pgadmin.utils.ajax import make_response as ajax_response,\
|
||||||
internal_server_error
|
internal_server_error
|
||||||
from pgadmin.utils.ajax import precondition_required
|
|
||||||
from pgadmin.utils.driver import get_driver
|
from pgadmin.utils.driver import get_driver
|
||||||
from pgadmin.utils.menu import Panel
|
|
||||||
from pgadmin.utils.preferences import Preferences
|
from pgadmin.utils.preferences import Preferences
|
||||||
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
|
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
|
||||||
PREF_LABEL_REFRESH_RATES
|
PREF_LABEL_REFRESH_RATES, ERROR_SERVER_ID_NOT_SPECIFIED
|
||||||
|
|
||||||
|
from .precondition import check_precondition
|
||||||
|
from .pgd_replication import blueprint as pgd_replication
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
|
|
||||||
MODULE_NAME = 'dashboard'
|
MODULE_NAME = 'dashboard'
|
||||||
|
|
@ -217,6 +218,8 @@ class DashboardModule(PgAdminModule):
|
||||||
help_str=gettext('Set the width of the lines on the line chart.')
|
help_str=gettext('Set the width of the lines on the line chart.')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pgd_replication.register_preferences(self)
|
||||||
|
|
||||||
def get_exposed_url_endpoints(self):
|
def get_exposed_url_endpoints(self):
|
||||||
"""
|
"""
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -247,70 +250,11 @@ class DashboardModule(PgAdminModule):
|
||||||
'dashboard.system_statistics_did',
|
'dashboard.system_statistics_did',
|
||||||
'dashboard.replication_slots',
|
'dashboard.replication_slots',
|
||||||
'dashboard.replication_stats',
|
'dashboard.replication_stats',
|
||||||
]
|
] + pgd_replication.get_exposed_url_endpoints()
|
||||||
|
|
||||||
|
|
||||||
blueprint = DashboardModule(MODULE_NAME, __name__)
|
blueprint = DashboardModule(MODULE_NAME, __name__)
|
||||||
|
blueprint.register_blueprint(pgd_replication)
|
||||||
|
|
||||||
def check_precondition(f):
|
|
||||||
"""
|
|
||||||
This function will behave as a decorator which will check
|
|
||||||
database connection before running view, it also adds
|
|
||||||
manager, conn & template_path properties to self
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrap(*args, **kwargs):
|
|
||||||
# Here args[0] will hold self & kwargs will hold gid,sid,did
|
|
||||||
|
|
||||||
g.manager = get_driver(
|
|
||||||
PG_DEFAULT_DRIVER).connection_manager(
|
|
||||||
kwargs['sid']
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_error(i_node_type):
|
|
||||||
stats_type = ('activity', 'prepared', 'locks', 'config')
|
|
||||||
if f.__name__ in stats_type:
|
|
||||||
return precondition_required(
|
|
||||||
gettext("Please connect to the selected {0}"
|
|
||||||
" to view the table.".format(i_node_type))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return precondition_required(
|
|
||||||
gettext("Please connect to the selected {0}"
|
|
||||||
" to view the graph.".format(i_node_type))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Below check handle the case where existing server is deleted
|
|
||||||
# by user and python server will raise exception if this check
|
|
||||||
# is not introduce.
|
|
||||||
if g.manager is None:
|
|
||||||
return get_error('server')
|
|
||||||
|
|
||||||
if 'did' in kwargs:
|
|
||||||
g.conn = g.manager.connection(did=kwargs['did'])
|
|
||||||
node_type = 'database'
|
|
||||||
else:
|
|
||||||
g.conn = g.manager.connection()
|
|
||||||
node_type = 'server'
|
|
||||||
|
|
||||||
# If not connected then return error to browser
|
|
||||||
if not g.conn.connected():
|
|
||||||
return get_error(node_type)
|
|
||||||
|
|
||||||
# Set template path for sql scripts
|
|
||||||
g.server_type = g.manager.server_type
|
|
||||||
g.version = g.manager.version
|
|
||||||
|
|
||||||
# Include server_type in template_path
|
|
||||||
g.template_path = 'dashboard/sql/' + (
|
|
||||||
'#{0}#'.format(g.version)
|
|
||||||
)
|
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/dashboard.js")
|
@blueprint.route("/dashboard.js")
|
||||||
|
|
@ -389,7 +333,7 @@ def get_data(sid, did, template, check_long_running_query=False):
|
||||||
# Allow no server ID to be specified (so we can generate a route in JS)
|
# Allow no server ID to be specified (so we can generate a route in JS)
|
||||||
# but throw an error if it's actually called.
|
# but throw an error if it's actually called.
|
||||||
if not sid:
|
if not sid:
|
||||||
return internal_server_error(errormsg='Server ID not specified.')
|
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
|
||||||
|
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([g.template_path, template]), did=did
|
"/".join([g.template_path, template]), did=did
|
||||||
|
|
@ -455,7 +399,8 @@ def dashboard_stats(sid=None, did=None):
|
||||||
chart_names = request.args['chart_names'].split(',')
|
chart_names = request.args['chart_names'].split(',')
|
||||||
|
|
||||||
if not sid:
|
if not sid:
|
||||||
return internal_server_error(errormsg='Server ID not specified.')
|
return internal_server_error(
|
||||||
|
errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
|
||||||
|
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([g.template_path, 'dashboard_stats.sql']), did=did,
|
"/".join([g.template_path, 'dashboard_stats.sql']), did=did,
|
||||||
|
|
@ -629,7 +574,8 @@ def system_statistics(sid=None, did=None):
|
||||||
chart_names = request.args['chart_names'].split(',')
|
chart_names = request.args['chart_names'].split(',')
|
||||||
|
|
||||||
if not sid:
|
if not sid:
|
||||||
return internal_server_error(errormsg='Server ID not specified.')
|
return internal_server_error(
|
||||||
|
errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
|
||||||
|
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([g.template_path, 'system_statistics.sql']), did=did,
|
"/".join([g.template_path, 'system_statistics.sql']), did=did,
|
||||||
|
|
@ -660,7 +606,7 @@ def replication_stats(sid=None):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not sid:
|
if not sid:
|
||||||
return internal_server_error(errormsg='Server ID not specified.')
|
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
|
||||||
|
|
||||||
sql = render_template("/".join([g.template_path, 'replication_stats.sql']))
|
sql = render_template("/".join([g.template_path, 'replication_stats.sql']))
|
||||||
status, res = g.conn.execute_dict(sql)
|
status, res = g.conn.execute_dict(sql)
|
||||||
|
|
@ -684,7 +630,7 @@ def replication_slots(sid=None):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not sid:
|
if not sid:
|
||||||
return internal_server_error(errormsg='Server ID not specified.')
|
return internal_server_error(errormsg=ERROR_SERVER_ID_NOT_SPECIFIED)
|
||||||
|
|
||||||
sql = render_template("/".join([g.template_path, 'replication_slots.sql']))
|
sql = render_template("/".join([g.template_path, 'replication_slots.sql']))
|
||||||
status, res = g.conn.execute_dict(sql)
|
status, res = g.conn.execute_dict(sql)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -978,7 +978,8 @@ function Dashboard({
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
{/* Replication */}
|
{/* Replication */}
|
||||||
<TabPanel value={mainTabVal} index={2} classNameRoot={classes.tabPanel}>
|
<TabPanel value={mainTabVal} index={2} classNameRoot={classes.tabPanel}>
|
||||||
<Replication key={mainTabVal} sid={sid} node={node} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
|
<Replication key={sid} sid={sid} node={node}
|
||||||
|
preferences={preferences} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,17 @@ import {useInterval, usePrevious} from 'sources/custom_hooks';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
|
import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
|
||||||
import { Grid } from '@mui/material';
|
import { Grid } from '@mui/material';
|
||||||
|
import { getChartColor } from '../../../static/js/utils';
|
||||||
|
|
||||||
export const X_AXIS_LENGTH = 75;
|
export const X_AXIS_LENGTH = 75;
|
||||||
|
|
||||||
/* Transform the labels data to suit ChartJS */
|
/* Transform the labels data to suit ChartJS */
|
||||||
export function transformData(labels, refreshRate) {
|
export function transformData(labels, refreshRate, theme='standard') {
|
||||||
const colors = ['#00BCD4', '#9CCC65', '#E64A19'];
|
|
||||||
let datasets = Object.keys(labels).map((label, i)=>{
|
let datasets = Object.keys(labels).map((label, i)=>{
|
||||||
return {
|
return {
|
||||||
label: label,
|
label: label,
|
||||||
data: labels[label] || [],
|
data: labels[label] || [],
|
||||||
borderColor: colors[i],
|
borderColor: getChartColor(i, theme),
|
||||||
pointHitRadius: DATA_POINT_SIZE,
|
pointHitRadius: DATA_POINT_SIZE,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -7,206 +7,26 @@
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import gettext from 'sources/gettext';
|
import React from 'react';
|
||||||
import ReplicationSlotsSchema from './replication_slots.ui';
|
|
||||||
import PgTable from 'sources/components/PgTable';
|
|
||||||
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
|
||||||
import SectionContainer from '../components/SectionContainer';
|
|
||||||
import ReplicationStatsSchema from './replication_stats.ui';
|
|
||||||
import RefreshButton from '../components/RefreshButtons';
|
|
||||||
import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
|
|
||||||
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
|
|
||||||
import url_for from 'sources/url_for';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import LogReplication from './LogReplication';
|
||||||
|
import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessage';
|
||||||
|
import PGDReplication from './PGDReplication';
|
||||||
|
|
||||||
|
export default function Replication({preferences, treeNodeInfo, pageVisible}) {
|
||||||
const replicationStatsColumns = [{
|
const replicationType = treeNodeInfo?.server?.replication_type;
|
||||||
accessorKey: 'view_details',
|
if(replicationType == 'log') {
|
||||||
header: () => null,
|
return <LogReplication treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
|
||||||
enableSorting: false,
|
} else if(replicationType == 'pgd') {
|
||||||
enableResizing: false,
|
return <PGDReplication preferences={preferences} treeNodeInfo={treeNodeInfo} pageVisible={pageVisible} />;
|
||||||
enableFilters: true,
|
} else {
|
||||||
size: 35,
|
return <EmptyPanelMessage text='No replication' />;
|
||||||
maxSize: 35,
|
}
|
||||||
minSize: 35,
|
|
||||||
id: 'btn-edit',
|
|
||||||
cell: getExpandCell({
|
|
||||||
title: gettext('View details')
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'pid',
|
|
||||||
header: gettext('PID'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 40,
|
|
||||||
minSize: 40,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'client_addr',
|
|
||||||
header: gettext('Client Addr'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'state',
|
|
||||||
header: gettext('State'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'write_lag',
|
|
||||||
header: gettext('Write Lag'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'flush_lag',
|
|
||||||
header: gettext('Flush Lag'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'replay_lag',
|
|
||||||
header: gettext('Replay Lag'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'reply_time',
|
|
||||||
header: gettext('Reply Time'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 100,
|
|
||||||
minSize: 50,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const replicationSlotsColumns = [{
|
|
||||||
accessorKey: 'view_details',
|
|
||||||
header: () => null,
|
|
||||||
enableSorting: false,
|
|
||||||
enableResizing: false,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 35,
|
|
||||||
maxSize: 35,
|
|
||||||
minSize: 35,
|
|
||||||
id: 'btn-details',
|
|
||||||
cell: getExpandCell({
|
|
||||||
title: gettext('View details')
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'active_pid',
|
|
||||||
header: gettext('Active PID'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 50,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'slot_name',
|
|
||||||
header: gettext('Slot Name'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 200,
|
|
||||||
minSize: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey:'active',
|
|
||||||
header: gettext('Active'),
|
|
||||||
enableSorting: true,
|
|
||||||
enableResizing: true,
|
|
||||||
enableFilters: true,
|
|
||||||
size: 50,
|
|
||||||
minSize: 50,
|
|
||||||
cell: getSwitchCell(),
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const replSchemaObj = new ReplicationSlotsSchema();
|
|
||||||
const replStatObj = new ReplicationStatsSchema();
|
|
||||||
|
|
||||||
export default function Replication({treeNodeInfo, pageVisible}) {
|
|
||||||
const [replicationSlots, setReplicationSlots] = useState([{
|
|
||||||
}]);
|
|
||||||
const [replicationStats, setReplicationStats] = useState([{
|
|
||||||
}]);
|
|
||||||
const pgAdmin = usePgAdmin();
|
|
||||||
|
|
||||||
const getReplicationData = (endpoint, setter)=>{
|
|
||||||
const api = getApiInstance();
|
|
||||||
const url = url_for(`dashboard.${endpoint}`, {sid: treeNodeInfo.server._id});
|
|
||||||
api.get(url)
|
|
||||||
.then((res)=>{
|
|
||||||
setter(res.data);
|
|
||||||
})
|
|
||||||
.catch((error)=>{
|
|
||||||
console.error(error);
|
|
||||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
if(pageVisible) {
|
|
||||||
getReplicationData('replication_stats', setReplicationStats);
|
|
||||||
getReplicationData('replication_slots', setReplicationSlots);
|
|
||||||
}
|
|
||||||
}, [pageVisible ]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box height="100%" display="flex" flexDirection="column">
|
|
||||||
<SectionContainer
|
|
||||||
titleExtras={<RefreshButton onClick={()=>{
|
|
||||||
getReplicationData('replication_stats', setReplicationStats);
|
|
||||||
}}/>}
|
|
||||||
title={gettext('Replication Stats')} style={{minHeight: '300px'}}>
|
|
||||||
<PgTable
|
|
||||||
caveTable={false}
|
|
||||||
columns={replicationStatsColumns}
|
|
||||||
data={replicationStats}
|
|
||||||
schema={replStatObj}
|
|
||||||
></PgTable>
|
|
||||||
</SectionContainer>
|
|
||||||
<SectionContainer
|
|
||||||
titleExtras={<RefreshButton onClick={()=>{
|
|
||||||
getReplicationData('replication_slots', setReplicationSlots);
|
|
||||||
}}/>}
|
|
||||||
title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}>
|
|
||||||
<PgTable
|
|
||||||
caveTable={false}
|
|
||||||
columns={replicationSlotsColumns}
|
|
||||||
data={replicationSlots}
|
|
||||||
schema={replSchemaObj}
|
|
||||||
></PgTable>
|
|
||||||
</SectionContainer>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Replication.propTypes = {
|
Replication.propTypes = {
|
||||||
|
preferences: PropTypes.object,
|
||||||
treeNodeInfo: PropTypes.object.isRequired,
|
treeNodeInfo: PropTypes.object.isRequired,
|
||||||
pageVisible: PropTypes.bool,
|
pageVisible: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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')
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: '400px',
|
minHeight: '400px',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
},
|
},
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
backgroundColor: theme.otherVars.tableBg,
|
backgroundColor: theme.otherVars.tableBg,
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
SELECT * FROM bdr.subscription_summary;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
SELECT * FROM bdr.node_slots;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -88,10 +88,10 @@ function getColumn(data, singleLineStatistics, prettifyFields=[]) {
|
||||||
}
|
}
|
||||||
columns.forEach((c)=>{
|
columns.forEach((c)=>{
|
||||||
// Prettify the cell view
|
// Prettify the cell view
|
||||||
if(prettifyFields.includes(c.Header)) {
|
if(prettifyFields.includes(c.header)) {
|
||||||
c.Cell = ({value})=><>{toPrettySize(value)}</>;
|
c.cell = ({value})=><>{toPrettySize(value)}</>;
|
||||||
c.Cell.displayName = 'Cell';
|
c.cell.displayName = 'Cell';
|
||||||
c.Cell.propTypes = {
|
c.cell.propTypes = {
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,6 @@ import { useTheme } from '@mui/material';
|
||||||
export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect',
|
export const DATA_POINT_STYLE = ['circle', 'cross', 'crossRot', 'rect',
|
||||||
'rectRounded', 'rectRot', 'star', 'triangle'];
|
'rectRounded', 'rectRot', 'star', 'triangle'];
|
||||||
export const DATA_POINT_SIZE = 3;
|
export const DATA_POINT_SIZE = 3;
|
||||||
export const CHART_THEME_COLORS_LENGTH = 20;
|
|
||||||
export const CHART_THEME_COLORS = {
|
|
||||||
'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B',
|
|
||||||
'#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900',
|
|
||||||
'#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'],
|
|
||||||
'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C',
|
|
||||||
'#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC',
|
|
||||||
'#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'],
|
|
||||||
'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2',
|
|
||||||
'#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8',
|
|
||||||
'#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0',
|
|
||||||
'#52B788']
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,8 @@ export default function ObjectBreadcrumbs() {
|
||||||
<AccountTreeIcon style={{height: '1rem', marginRight: '0.125rem'}} data-label="AccountTreeIcon"/>
|
<AccountTreeIcon style={{height: '1rem', marginRight: '0.125rem'}} data-label="AccountTreeIcon"/>
|
||||||
<div className={classes.overflow}>
|
<div className={classes.overflow}>
|
||||||
{
|
{
|
||||||
objectData.path?.reduce((res, item)=>(
|
objectData.path?.reduce((res, item, i)=>(
|
||||||
res.concat(<span key={item}>{item}</span>, <ArrowForwardIosRoundedIcon key={item+'-arrow'} style={{height: '0.8rem', width: '1.25rem'}} />)
|
res.concat(<span key={item+i}>{item}</span>, <ArrowForwardIosRoundedIcon key={item+i+'-arrow'} style={{height: '0.8rem', width: '1.25rem'}} />)
|
||||||
), []).slice(0, -1)
|
), []).slice(0, -1)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import React, { forwardRef, useEffect } from 'react';
|
||||||
import { flexRender } from '@tanstack/react-table';
|
import { flexRender } from '@tanstack/react-table';
|
||||||
import { styled } from '@mui/styles';
|
import { styled } from '@mui/styles';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Switch } from '@mui/material';
|
|
||||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
import { PgIconButton } from './Buttons';
|
import { PgIconButton } from './Buttons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import CustomPropTypes from '../custom_prop_types';
|
import CustomPropTypes from '../custom_prop_types';
|
||||||
|
import { InputSwitch } from './FormComponents';
|
||||||
|
|
||||||
|
|
||||||
const StyledDiv = styled('div')(({theme})=>({
|
const StyledDiv = styled('div')(({theme})=>({
|
||||||
|
|
@ -360,20 +360,14 @@ export function getExpandCell({ onClick, ...props }) {
|
||||||
return Cell;
|
return Cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadOnlySwitch = styled(Switch)(({theme})=>({
|
|
||||||
opacity: 0.75,
|
|
||||||
'& .MuiSwitch-track': {
|
|
||||||
opacity: theme.palette.action.disabledOpacity,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
export function getSwitchCell() {
|
export function getSwitchCell() {
|
||||||
const Cell = ({ value }) => {
|
const Cell = ({ getValue }) => {
|
||||||
return <ReadOnlySwitch color="primary" checked={value} value={value} readOnly title={String(value)} />;
|
return <InputSwitch value={getValue()} readonly />;
|
||||||
};
|
};
|
||||||
|
|
||||||
Cell.displayName = 'SwitchCell';
|
Cell.displayName = 'SwitchCell';
|
||||||
Cell.propTypes = {
|
Cell.propTypes = {
|
||||||
value: PropTypes.any,
|
getValue: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Cell;
|
return Cell;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function TableRow({ index, style, schema, row, measureElement }) {
|
||||||
}, [row.getIsExpanded(), expandComplete]);
|
}, [row.getIsExpanded(), expandComplete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PgReactTableRow data-index={index} ref={rowRef} style={style} row={row}>
|
<PgReactTableRow data-index={index} ref={rowRef} style={style}>
|
||||||
<PgReactTableRowContent>
|
<PgReactTableRowContent>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
const content = flexRender(cell.column.columnDef.cell, cell.getContext());
|
const content = flexRender(cell.column.columnDef.cell, cell.getContext());
|
||||||
|
|
@ -115,7 +115,12 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
|
||||||
enableResizing: false,
|
enableResizing: false,
|
||||||
maxSize: 35,
|
maxSize: 35,
|
||||||
}] : []).concat(
|
}] : []).concat(
|
||||||
columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility)
|
columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility).map((c)=>({
|
||||||
|
...c,
|
||||||
|
// if data is null then global search doesn't work
|
||||||
|
// Use accessorFn to return empty string if data is null.
|
||||||
|
accessorFn: c.accessorFn ?? (c.accessorKey ? (row)=>row[c.accessorKey] ?? '' : undefined),
|
||||||
|
}))
|
||||||
), [hasSelectRow, columns]);
|
), [hasSelectRow, columns]);
|
||||||
|
|
||||||
// Render the UI for your table
|
// Render the UI for your table
|
||||||
|
|
@ -264,4 +269,4 @@ PgTable.propTypes = {
|
||||||
caveTable: PropTypes.bool,
|
caveTable: PropTypes.bool,
|
||||||
tableNoBorder: PropTypes.bool,
|
tableNoBorder: PropTypes.bool,
|
||||||
'data-test': PropTypes.string
|
'data-test': PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -641,3 +641,22 @@ export function scrollbarWidth() {
|
||||||
document.body.removeChild(scrollDiv);
|
document.body.removeChild(scrollDiv);
|
||||||
return scrollbarWidth;
|
return scrollbarWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CHART_THEME_COLORS = {
|
||||||
|
'standard':['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B',
|
||||||
|
'#E377C2', '#7F7F7F', '#BCBD22', '#17BECF', '#3366CC', '#DC3912', '#FF9900',
|
||||||
|
'#109618', '#990099', '#0099C6','#DD4477', '#66AA00', '#B82E2E', '#316395'],
|
||||||
|
'dark': ['#4878D0', '#EE854A', '#6ACC64', '#D65F5F', '#956CB4', '#8C613C',
|
||||||
|
'#DC7EC0', '#797979', '#D5BB67', '#82C6E2', '#7371FC', '#3A86FF', '#979DAC',
|
||||||
|
'#D4A276', '#2A9D8F', '#FFEE32', '#70E000', '#FF477E', '#7DC9F1', '#52B788'],
|
||||||
|
'high_contrast': ['#023EFF', '#FF7C00', '#1AC938', '#E8000B', '#8B2BE2',
|
||||||
|
'#9F4800', '#F14CC1', '#A3A3A3', '#FFC400', '#00D7FF', '#FF6C49', '#00B4D8',
|
||||||
|
'#45D48A', '#FFFB69', '#B388EB', '#D4A276', '#2EC4B6', '#7DC9F1', '#50B0F0',
|
||||||
|
'#52B788']
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getChartColor(index, theme='standard', colorPalette=CHART_THEME_COLORS) {
|
||||||
|
const palette = colorPalette[theme];
|
||||||
|
// loop back if out of index;
|
||||||
|
return palette[index % palette.length];
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,6 @@ def get_submodules():
|
||||||
AuthenticateModule,
|
AuthenticateModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
DashboardModule,
|
|
||||||
HelpModule,
|
HelpModule,
|
||||||
MiscModule,
|
MiscModule,
|
||||||
PreferencesModule,
|
PreferencesModule,
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,11 @@ import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
|
||||||
import { InputSelect } from '../../../../../../static/js/components/FormComponents';
|
import { InputSelect } from '../../../../../../static/js/components/FormComponents';
|
||||||
import { DefaultButton, PgButtonGroup, PgIconButton} from '../../../../../../static/js/components/Buttons';
|
import { DefaultButton, PgButtonGroup, PgIconButton} from '../../../../../../static/js/components/Buttons';
|
||||||
import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE,
|
import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE,
|
||||||
CHART_THEME_COLORS, CHART_THEME_COLORS_LENGTH, LightenDarkenColor} from 'sources/chartjs';
|
LightenDarkenColor} from 'sources/chartjs';
|
||||||
import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
|
import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
|
||||||
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
|
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
|
import { getChartColor } from '../../../../../../static/js/utils';
|
||||||
|
|
||||||
// Numeric data type used to separate out the options for Y axis.
|
// Numeric data type used to separate out the options for Y axis.
|
||||||
const NUMERIC_TYPES = ['oid', 'smallint', 'integer', 'bigint', 'decimal', 'numeric',
|
const NUMERIC_TYPES = ['oid', 'smallint', 'integer', 'bigint', 'decimal', 'numeric',
|
||||||
|
|
@ -163,7 +164,7 @@ GenerateGraph.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// This function is used to get the dataset for Line Chart and Stacked Line Chart.
|
// This function is used to get the dataset for Line Chart and Stacked Line Chart.
|
||||||
function getLineChartData(graphType, rows, colName, colPosition, color, colorIndex, styleIndex, queryToolCtx) {
|
function getLineChartData(graphType, rows, colName, colPosition, color, styleIndex, queryToolCtx) {
|
||||||
return {
|
return {
|
||||||
label: colName,
|
label: colName,
|
||||||
data: rows.map((r)=>r[colPosition]),
|
data: rows.map((r)=>r[colPosition]),
|
||||||
|
|
@ -188,28 +189,23 @@ function getBarChartData(rows, colName, colPosition, color) {
|
||||||
|
|
||||||
// This function is used to get the dataset for Pie Chart.
|
// This function is used to get the dataset for Pie Chart.
|
||||||
function getPieChartData(rows, colName, colPosition, queryToolCtx) {
|
function getPieChartData(rows, colName, colPosition, queryToolCtx) {
|
||||||
let rowCount = -1;
|
|
||||||
return {
|
return {
|
||||||
label: colName,
|
label: colName,
|
||||||
data: rows.map((r)=>r[colPosition]),
|
data: rows.map((r)=>r[colPosition]),
|
||||||
backgroundColor: rows.map(()=> {
|
backgroundColor: rows.map((_v, i)=> {
|
||||||
if (rowCount >= (CHART_THEME_COLORS_LENGTH - 1)) {
|
return getChartColor(i, queryToolCtx.preferences.misc.theme);
|
||||||
rowCount = -1;
|
|
||||||
}
|
|
||||||
rowCount = rowCount + 1;
|
|
||||||
return CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme][rowCount];
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is used to get the graph data set for the X axis and Y axis
|
// This function is used to get the graph data set for the X axis and Y axis
|
||||||
function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, graphColors) {
|
function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, theme) {
|
||||||
// Function is used to the find the position of the column
|
// Function is used to the find the position of the column
|
||||||
function getColumnPosition(colName) {
|
function getColumnPosition(colName) {
|
||||||
return _.find(columns, (c)=>(c.name==colName))?.pos;
|
return _.find(columns, (c)=>(c.name==colName))?.pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
let styleIndex = -1, colorIndex = -1;
|
let styleIndex = -1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'labels': rows.map((r, index)=>{
|
'labels': rows.map((r, index)=>{
|
||||||
|
|
@ -221,14 +217,8 @@ function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, g
|
||||||
return r[colPosition];
|
return r[colPosition];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
'datasets': yaxis.map((colName)=>{
|
'datasets': yaxis.map((colName, i)=>{
|
||||||
// Loop is used to set the index for random color array
|
let color = getChartColor(i, theme);
|
||||||
if (colorIndex >= (CHART_THEME_COLORS_LENGTH - 1)) {
|
|
||||||
colorIndex = -1;
|
|
||||||
}
|
|
||||||
colorIndex = colorIndex + 1;
|
|
||||||
|
|
||||||
let color = graphColors[colorIndex];
|
|
||||||
let colPosition = getColumnPosition(colName);
|
let colPosition = getColumnPosition(colName);
|
||||||
|
|
||||||
// Loop is used to set the index for DATA_POINT_STYLE array
|
// Loop is used to set the index for DATA_POINT_STYLE array
|
||||||
|
|
@ -242,8 +232,7 @@ function getGraphDataSet(graphType, rows, columns, xaxis, yaxis, queryToolCtx, g
|
||||||
} else if (graphType === 'B' || graphType === 'SB') {
|
} else if (graphType === 'B' || graphType === 'SB') {
|
||||||
return getBarChartData(rows, colName, colPosition, color);
|
return getBarChartData(rows, colName, colPosition, color);
|
||||||
} else if (graphType === 'L' || graphType === 'SL') {
|
} else if (graphType === 'L' || graphType === 'SL') {
|
||||||
return getLineChartData(graphType, rows, colName, colPosition, color,
|
return getLineChartData(graphType, rows, colName, colPosition, color, styleIndex, queryToolCtx);
|
||||||
colorIndex, styleIndex, queryToolCtx);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
@ -263,7 +252,6 @@ export function GraphVisualiser({initColumns}) {
|
||||||
const [columns, setColumns] = useState(initColumns);
|
const [columns, setColumns] = useState(initColumns);
|
||||||
const [graphHeight, setGraphHeight] = useState();
|
const [graphHeight, setGraphHeight] = useState();
|
||||||
const [expandedState, setExpandedState] = useState(true);
|
const [expandedState, setExpandedState] = useState(true);
|
||||||
const [graphColor, setGraphColor] = useState([]);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -344,10 +332,6 @@ export function GraphVisualiser({initColumns}) {
|
||||||
}
|
}
|
||||||
}, [graphType, theme]);
|
}, [graphType, theme]);
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
setGraphColor(CHART_THEME_COLORS[queryToolCtx.preferences.misc.theme]);
|
|
||||||
}, [queryToolCtx.preferences.misc.theme, theme]);
|
|
||||||
|
|
||||||
const graphBackgroundColor = useMemo(() => {
|
const graphBackgroundColor = useMemo(() => {
|
||||||
return theme.palette.background.default;
|
return theme.palette.background.default;
|
||||||
},[theme]);
|
},[theme]);
|
||||||
|
|
@ -380,7 +364,10 @@ export function GraphVisualiser({initColumns}) {
|
||||||
setLoaderText(gettext('Rendering data points...'));
|
setLoaderText(gettext('Rendering data points...'));
|
||||||
// Set the Graph Data
|
// Set the Graph Data
|
||||||
setGraphData(
|
setGraphData(
|
||||||
(prev)=> [getGraphDataSet(graphType, res.data.data.result, columns, xAxis, _.isArray(yAxis) ? yAxis : [yAxis] , queryToolCtx, graphColor), prev[1] + 1]
|
(prev)=> [
|
||||||
|
getGraphDataSet(graphType, res.data.data.result, columns, xAxis, _.isArray(yAxis) ? yAxis : [yAxis] , queryToolCtx, queryToolCtx.preferences.misc.theme),
|
||||||
|
prev[1] + 1
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
setLoaderText('');
|
setLoaderText('');
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ class UserManagementSchema extends BaseUISchema {
|
||||||
canSearch: true
|
canSearch: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'refreshBrowserTree', visible: false, type: 'boolean',
|
id: 'refreshBrowserTree', visible: false, type: 'switch',
|
||||||
deps: ['userManagement'], depChange: ()=> {
|
deps: ['userManagement'], depChange: ()=> {
|
||||||
return { refreshBrowserTree: this.changeOwnership };
|
return { refreshBrowserTree: this.changeOwnership };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ ERROR_FETCHING_ROLE_INFORMATION = gettext(
|
||||||
|
|
||||||
ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
|
ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
|
||||||
|
|
||||||
|
ERROR_SERVER_ID_NOT_SPECIFIED = gettext('Server ID not specified.')
|
||||||
|
|
||||||
# Authentication Sources
|
# Authentication Sources
|
||||||
INTERNAL = 'internal'
|
INTERNAL = 'internal'
|
||||||
LDAP = 'ldap'
|
LDAP = 'ldap'
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@ import { render } from '@testing-library/react';
|
||||||
|
|
||||||
describe('Graphs.js', ()=>{
|
describe('Graphs.js', ()=>{
|
||||||
it('transformData', ()=>{
|
it('transformData', ()=>{
|
||||||
expect(transformData({'Label1': [], 'Label2': []}, 1, false)).toEqual({
|
expect(transformData({'Label1': [], 'Label2': []}, 1)).toEqual({
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Label1',
|
label: 'Label1',
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: '#00BCD4',
|
borderColor: '#1F77B4',
|
||||||
pointHitRadius: DATA_POINT_SIZE,
|
pointHitRadius: DATA_POINT_SIZE,
|
||||||
},{
|
},{
|
||||||
label: 'Label2',
|
label: 'Label2',
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: '#9CCC65',
|
borderColor: '#FF7F0E',
|
||||||
pointHitRadius: DATA_POINT_SIZE,
|
pointHitRadius: DATA_POINT_SIZE,
|
||||||
}],
|
}],
|
||||||
refreshRate: 1,
|
refreshRate: 1,
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,9 @@ module.exports = [{
|
||||||
'pure|pgadmin.node.aggregate',
|
'pure|pgadmin.node.aggregate',
|
||||||
'pure|pgadmin.node.operator',
|
'pure|pgadmin.node.operator',
|
||||||
'pure|pgadmin.node.dbms_job_scheduler',
|
'pure|pgadmin.node.dbms_job_scheduler',
|
||||||
'pure|pgadmin.node.replica_node'
|
'pure|pgadmin.node.replica_node',
|
||||||
|
'pure|pgadmin.node.pgd_replication_groups',
|
||||||
|
'pure|pgadmin.node.pgd_replication_servers',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -396,7 +398,7 @@ module.exports = [{
|
||||||
minChunks: 2,
|
minChunks: 2,
|
||||||
enforce: true,
|
enforce: true,
|
||||||
test(module) {
|
test(module) {
|
||||||
return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@material-ui']);
|
return webpackShimConfig.matchModules(module, ['codemirror', 'rc-', '@mui']);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vendor_others: {
|
vendor_others: {
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,8 @@ let webpackShimConfig = {
|
||||||
'pgadmin.node.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'),
|
'pgadmin.node.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'),
|
||||||
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
|
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
|
||||||
'pgadmin.node.replica_node': path.join(__dirname, './pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node'),
|
'pgadmin.node.replica_node': path.join(__dirname, './pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node'),
|
||||||
|
'pgadmin.node.pgd_replication_groups': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/static/js/pgd_replication_groups'),
|
||||||
|
'pgadmin.node.pgd_replication_servers': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgd_replication_groups/pgd_replication_servers/static/js/pgd_replication_servers'),
|
||||||
'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
|
'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
|
||||||
'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
|
'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
|
||||||
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
|
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
|
||||||
|
|
|
||||||