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