Added support for viewing Log Based Clusters. #7216
Co-authored-by: Akshay Joshi <akshay.joshi@enterprisedb.com>pull/7325/head
parent
5931162556
commit
ace73ebb60
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
|
@ -18,4 +18,5 @@ database, right-click on the *Databases* node, and select *Create Database...*
|
|||
resource_group_dialog
|
||||
role_dialog
|
||||
tablespace_dialog
|
||||
replica_nodes_dialog
|
||||
role_reassign_dialog
|
|
@ -0,0 +1,46 @@
|
|||
.. _replica_nodes_dialog:
|
||||
|
||||
****************************
|
||||
`Replica Node Dialog`:index:
|
||||
****************************
|
||||
|
||||
Use The *Replica Node* dialog to view a standby instance being replicated
|
||||
using log based streaming replication. Streaming replication allows a standby
|
||||
server to stay more up-to-date than is possible with file-based log shipping.
|
||||
The standby connects to the primary, which streams WAL records to the standby as
|
||||
they're generated, without waiting for the WAL file to be filled.
|
||||
|
||||
The *Replica Node* dialog organizes the information through the following tabs:
|
||||
*General*, *Replication Slot*
|
||||
|
||||
.. image:: images/replica_nodes_general.png
|
||||
:alt: Replica Node dialog general tab
|
||||
:align: center
|
||||
|
||||
* The *PID* field is the process ID of a WAL sender process.
|
||||
* The *Username* field is the name of the user logged into this WAL sender process.
|
||||
* The *App Name* field is the name of the application that is connected to this WAL sender.
|
||||
* The *Client Address* field is the IP address of the client connected to this WAL sender.
|
||||
If this field is null, it indicates that the client is connected via a Unix socket on the server machine.
|
||||
* The *Client Hostname* field is the host name of the connected client, as reported by a reverse DNS lookup
|
||||
of client_addr.This field will only be non-null for IP connections, and only when log_hostname is enabled.
|
||||
* The *Client Port* field is the TCP port number that the client is using for communication with
|
||||
this WAL sender, or -1 if a Unix socket is used.
|
||||
* The *State* field is the current WAL sender state.
|
||||
|
||||
Click the *Replication Slot* tab to continue.
|
||||
|
||||
.. image:: images/replica_nodes_replication.png
|
||||
:alt: Replica Node dialog replication slot tab
|
||||
:align: center
|
||||
|
||||
* The *Slot Name* field is a unique, cluster-wide identifier for the replication slot.
|
||||
* The *Slot Type* field is the slot type - physical or logical
|
||||
* The *Active* field is True if this slot is currently actively being used.
|
||||
|
||||
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.
|
|
@ -15,6 +15,7 @@ from flask import render_template, request, make_response, jsonify, \
|
|||
from flask_babel import gettext
|
||||
from flask_security import current_user, login_required
|
||||
from psycopg.conninfo import make_conninfo, conninfo_to_dict
|
||||
|
||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||
from pgadmin.browser.utils import PGChildNodeView
|
||||
from pgadmin.utils.ajax import make_json_response, bad_request, forbidden, \
|
||||
|
@ -30,7 +31,8 @@ from pgadmin.utils.driver import get_driver
|
|||
from pgadmin.utils.master_password import get_crypt_key
|
||||
from pgadmin.utils.exception import CryptKeyMissing
|
||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
from pgadmin.browser.server_groups.servers.utils import is_valid_ipaddress
|
||||
from pgadmin.browser.server_groups.servers.utils import \
|
||||
is_valid_ipaddress, get_replication_type
|
||||
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
||||
SERVER_CONNECTION_CLOSED
|
||||
from sqlalchemy import or_
|
||||
|
@ -343,6 +345,9 @@ class ServerModule(sg.ServerGroupPluginModule):
|
|||
from .tablespaces import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .replica_nodes import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super().register(app, options)
|
||||
|
||||
# We do not have any preferences for server node.
|
||||
|
@ -469,7 +474,7 @@ class ServerNode(PGChildNodeView):
|
|||
}],
|
||||
'check_pgpass': [{'get': 'check_pgpass'}],
|
||||
'clear_saved_password': [{'put': 'clear_saved_password'}],
|
||||
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}]
|
||||
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}],
|
||||
})
|
||||
SSL_MODES = ['prefer', 'require', 'verify-ca', 'verify-full']
|
||||
|
||||
|
@ -1247,6 +1252,7 @@ class ServerNode(PGChildNodeView):
|
|||
connected = False
|
||||
user = None
|
||||
manager = None
|
||||
replication_type = None
|
||||
|
||||
if 'connect_now' in data and data['connect_now']:
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
|
||||
|
@ -1324,6 +1330,8 @@ class ServerNode(PGChildNodeView):
|
|||
server.id),
|
||||
tunnel_password)
|
||||
|
||||
replication_type = get_replication_type(conn,
|
||||
manager.version)
|
||||
user = manager.user_info
|
||||
connected = True
|
||||
|
||||
|
@ -1337,6 +1345,7 @@ class ServerNode(PGChildNodeView):
|
|||
username=server.username,
|
||||
user=user,
|
||||
connected=connected,
|
||||
replication_type=replication_type,
|
||||
shared=server.shared,
|
||||
server_type=manager.server_type
|
||||
if manager and manager.server_type
|
||||
|
@ -1427,6 +1436,7 @@ class ServerNode(PGChildNodeView):
|
|||
in_recovery = None
|
||||
wal_paused = None
|
||||
errmsg = None
|
||||
replication_type = None
|
||||
if connected:
|
||||
status, result, in_recovery, wal_paused =\
|
||||
recovery_state(conn, manager.version)
|
||||
|
@ -1436,10 +1446,13 @@ class ServerNode(PGChildNodeView):
|
|||
manager.release()
|
||||
errmsg = "{0} : {1}".format(server.name, result)
|
||||
|
||||
replication_type = get_replication_type(conn, manager.version)
|
||||
|
||||
return make_json_response(
|
||||
data={
|
||||
'icon': server_icon_and_background(connected, manager, server),
|
||||
'connected': connected,
|
||||
'replication_type': replication_type,
|
||||
'in_recovery': in_recovery,
|
||||
'wal_pause': wal_paused,
|
||||
'server_type': manager.server_type if connected else "pg",
|
||||
|
@ -1709,6 +1722,8 @@ class ServerNode(PGChildNodeView):
|
|||
_, _, in_recovery, wal_paused =\
|
||||
recovery_state(conn, manager.version)
|
||||
|
||||
replication_type = get_replication_type(conn, manager.version)
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=gettext("Server connected."),
|
||||
|
@ -1716,6 +1731,7 @@ class ServerNode(PGChildNodeView):
|
|||
'icon': server_icon_and_background(True, manager, server),
|
||||
'connected': True,
|
||||
'server_type': manager.server_type,
|
||||
'replication_type': replication_type,
|
||||
'type': manager.server_type,
|
||||
'version': manager.version,
|
||||
'db': manager.db,
|
||||
|
|
|
@ -0,0 +1,285 @@
|
|||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Implements Replication Nodes for PG/PPAS 9.4 and above"""
|
||||
|
||||
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 ReplicationNodesModule(CollectionNodeModule):
|
||||
"""
|
||||
class ReplicationNodesModule(CollectionNodeModule)
|
||||
|
||||
A module class for Replication Nodes node derived from
|
||||
CollectionNodeModule.
|
||||
|
||||
Methods:
|
||||
-------
|
||||
* __init__(*args, **kwargs)
|
||||
- Method is used to initialize the ReplicationNodesModule and it's
|
||||
base module.
|
||||
|
||||
* backend_supported(manager, **kwargs)
|
||||
- This function is used to check the database server type and version.
|
||||
Replication Nodes only supported in PG/PPAS 9.4 and above.
|
||||
|
||||
* 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.
|
||||
|
||||
* script_load()
|
||||
- Load the module script for Replication Nodes, when any of the server
|
||||
node is initialized.
|
||||
"""
|
||||
|
||||
_NODE_TYPE = 'replica_nodes'
|
||||
_COLLECTION_LABEL = gettext("Replica Nodes")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Method is used to initialize the ReplicationNodesModule and
|
||||
it's base module.
|
||||
|
||||
Args:
|
||||
*args:
|
||||
**kwargs:
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def script_load(self):
|
||||
"""
|
||||
Load the module script for Replication Nodes, when any of the server
|
||||
node is initialized.
|
||||
|
||||
Returns: node type of the server module.
|
||||
"""
|
||||
return servers.ServerModule.NODE_TYPE
|
||||
|
||||
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 bool(replication_type)
|
||||
|
||||
|
||||
blueprint = ReplicationNodesModule(__name__)
|
||||
|
||||
|
||||
class ReplicationNodesView(PGChildNodeView):
|
||||
"""
|
||||
class ReplicationNodesView(NodeView)
|
||||
|
||||
A view class for Replication Nodes node derived from NodeView.
|
||||
This class is responsible for all the stuff related to view like
|
||||
showing properties/list of Replication Nodes nodes
|
||||
|
||||
Methods:
|
||||
-------
|
||||
* __init__(**kwargs)
|
||||
- Method is used to initialize the ReplicationNodesView,
|
||||
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 Replication Nodes within
|
||||
that collection.
|
||||
|
||||
* nodes()
|
||||
- This function will used to create all the child node within that
|
||||
collection. Here it will create all the Replication Nodes node.
|
||||
|
||||
* properties(gid, sid, did, pid)
|
||||
- This function will show the properties of the selected node
|
||||
"""
|
||||
|
||||
node_type = blueprint.node_type
|
||||
BASE_TEMPLATE_PATH = 'replica_nodes/sql/#{0}#'
|
||||
|
||||
parent_ids = [
|
||||
{'type': 'int', 'id': 'gid'},
|
||||
{'type': 'int', 'id': 'sid'}
|
||||
]
|
||||
ids = [
|
||||
{'type': 'int', 'id': 'pid'}
|
||||
]
|
||||
|
||||
operations = dict({
|
||||
'obj': [
|
||||
{'get': 'properties'},
|
||||
{'get': 'list'}
|
||||
],
|
||||
'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
|
||||
'replication_slots': [{'get': 'replication_slots'},
|
||||
{'get': 'replication_slots'}],
|
||||
})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Method is used to initialize the ReplicationNodesView 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 Replication 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 Replication Nodes node.
|
||||
|
||||
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['pid'],
|
||||
sid,
|
||||
row['name'],
|
||||
icon="icon-replica_nodes"
|
||||
))
|
||||
|
||||
return make_json_response(
|
||||
data=res,
|
||||
status=200
|
||||
)
|
||||
|
||||
@check_precondition
|
||||
def properties(self, gid, sid, pid):
|
||||
"""
|
||||
This function will show the properties of the selected node.
|
||||
|
||||
Args:
|
||||
gid: Server Group ID
|
||||
sid: Server ID
|
||||
pid: Replication Nodes ID
|
||||
"""
|
||||
sql = render_template(
|
||||
"/".join([self.template_path, self._PROPERTIES_SQL]), pid=pid)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
ReplicationNodesView.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,61 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import ReplicaNodeSchema from './replica_node.ui';
|
||||
|
||||
define('pgadmin.node.replica_nodes', [
|
||||
'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'] =
|
||||
pgBrowser.Collection.extend({
|
||||
node: 'replica_nodes',
|
||||
label: gettext('Replica Nodes'),
|
||||
type: 'coll-replica_nodes',
|
||||
columns: ['pid', 'name', 'usename', 'state'],
|
||||
canEdit: false,
|
||||
canDrop: false,
|
||||
canDropCascade: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Extend the browser's node class for replica nodes node
|
||||
if (!pgBrowser.Nodes['replica_nodes']) {
|
||||
pgBrowser.Nodes['replica_nodes'] = pgBrowser.Node.extend({
|
||||
parent_type: 'server',
|
||||
type: 'replica_nodes',
|
||||
epasHelp: false,
|
||||
sqlAlterHelp: '',
|
||||
sqlCreateHelp: '',
|
||||
dialogHelp: url_for('help.static', {'filename': 'replica_nodes_dialog.html'}),
|
||||
label: gettext('Replica Nodes'),
|
||||
hasSQL: false,
|
||||
hasScriptTypes: false,
|
||||
canDrop: false,
|
||||
Init: function() {
|
||||
|
||||
// Avoid multiple registration of menus
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
getSchema: ()=>{
|
||||
return new ReplicaNodeSchema();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return pgBrowser.Nodes['coll-replica_nodes'];
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 ReplicaNodeSchema extends BaseUISchema {
|
||||
get idAttribute() {
|
||||
return 'pid';
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'pid', label: gettext('PID'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'usename', label: gettext('Username'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'application_name', label: gettext('App Name'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'client_addr', label: gettext('Client Address'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'client_hostname', label: gettext('Client Hostname'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'client_port', label: gettext('Client Port'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'state', label: gettext('State'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
},
|
||||
{
|
||||
id: 'sent_lsn', label: gettext('Sent LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'write_lsn', label: gettext('Write LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'flush_lsn', label: gettext('Flush LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'replay_lsn', label: gettext('Replay LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'write_lag', label: gettext('Write Lag'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'flush_lag', label: gettext('Flush Lag'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'replay_lag', label: gettext('Replay Lag'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('WAL Details')
|
||||
},
|
||||
{
|
||||
id: 'slot_name', label: gettext('Slot Name'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
group: gettext('Replication Slot')
|
||||
},
|
||||
{
|
||||
id: 'slot_type', label: gettext('Slot Type'), type: 'text', mode:['properties', 'edit'], readonly: true,
|
||||
group: gettext('Replication Slot')
|
||||
},
|
||||
{
|
||||
id: 'active', label: gettext('Active'), type: 'switch', mode:['properties', 'edit'], readonly: true,
|
||||
group: gettext('Replication Slot')
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
SELECT count(*)
|
||||
FROM pg_stat_replication
|
|
@ -0,0 +1,3 @@
|
|||
SELECT pid, 'Standby ['||COALESCE(client_addr::text, client_hostname,'Socket')||']' as name
|
||||
FROM pg_stat_replication
|
||||
ORDER BY pid
|
|
@ -0,0 +1,8 @@
|
|||
SELECT st.*, 'Standby ['||COALESCE(client_addr::text, 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
|
||||
{% if pid %}
|
||||
WHERE st.pid={{pid}}
|
||||
{% endif %}
|
||||
ORDER BY st.pid
|
|
@ -0,0 +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
|
||||
THEN 'log'
|
||||
ELSE NULL
|
||||
END as type;
|
|
@ -158,7 +158,9 @@ class ServersConnectTestCase(BaseTestGenerator):
|
|||
self.manager.connection.connected.side_effect = True
|
||||
|
||||
connection_mock_result.execute_dict.side_effect = \
|
||||
[eval(self.mock_data["return_value"])]
|
||||
[eval(self.mock_data["return_value"]),
|
||||
# replication type mock
|
||||
(True, {'rows': [{'type': None}]})]
|
||||
|
||||
response = self.get_server_connection(server_id)
|
||||
self.assertEqual(response.status_code,
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
"""Server helper utilities"""
|
||||
from ipaddress import ip_address
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from flask import render_template
|
||||
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
import config
|
||||
|
@ -277,3 +279,14 @@ def remove_saved_passwords(user_id):
|
|||
except Exception:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def get_replication_type(conn, sversion):
|
||||
status, res = conn.execute_dict(render_template(
|
||||
"/servers/sql/#{0}#/replication_type.sql".format(sversion)
|
||||
))
|
||||
|
||||
if not status:
|
||||
raise InternalServerError(res)
|
||||
|
||||
return res['rows'][0]['type']
|
||||
|
|
|
@ -79,7 +79,8 @@ define('pgadmin.browser.utils',
|
|||
'server_group', 'server', 'coll-tablespace', 'tablespace',
|
||||
'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'
|
||||
'pga_job', 'pga_schedule', 'pga_jobstep',
|
||||
'coll-replica_nodes', 'replica_nodes'
|
||||
];
|
||||
|
||||
pgBrowser.utils = {
|
||||
|
|
|
@ -245,6 +245,8 @@ class DashboardModule(PgAdminModule):
|
|||
'dashboard.system_statistics',
|
||||
'dashboard.system_statistics_sid',
|
||||
'dashboard.system_statistics_did',
|
||||
'dashboard.replication_slots',
|
||||
'dashboard.replication_stats',
|
||||
]
|
||||
|
||||
|
||||
|
@ -646,3 +648,51 @@ def system_statistics(sid=None, did=None):
|
|||
response=resp_data,
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route('/replication_stats/<int:sid>',
|
||||
endpoint='replication_stats', methods=['GET'])
|
||||
@login_required
|
||||
@check_precondition
|
||||
def replication_stats(sid=None):
|
||||
"""
|
||||
This function is used to list all the Replication slots of the cluster
|
||||
"""
|
||||
|
||||
if not sid:
|
||||
return internal_server_error(errormsg='Server ID not specified.')
|
||||
|
||||
sql = render_template("/".join([g.template_path, 'replication_stats.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('/replication_slots/<int:sid>',
|
||||
endpoint='replication_slots', methods=['GET'])
|
||||
@login_required
|
||||
@check_precondition
|
||||
def replication_slots(sid=None):
|
||||
"""
|
||||
This function is used to list all the Replication slots of the cluster
|
||||
"""
|
||||
|
||||
if not sid:
|
||||
return internal_server_error(errormsg='Server ID not specified.')
|
||||
|
||||
sql = render_template("/".join([g.template_path, 'replication_slots.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
|
||||
)
|
||||
|
|
|
@ -19,12 +19,9 @@ import { Box, Tab, Tabs } from '@material-ui/core';
|
|||
import { PgIconButton } from '../../../static/js/components/Buttons';
|
||||
import CancelIcon from '@material-ui/icons/Cancel';
|
||||
import StopSharpIcon from '@material-ui/icons/StopSharp';
|
||||
import ArrowRightOutlinedIcon from '@material-ui/icons/ArrowRightOutlined';
|
||||
import ArrowDropDownOutlinedIcon from '@material-ui/icons/ArrowDropDownOutlined';
|
||||
import WelcomeDashboard from './WelcomeDashboard';
|
||||
import ActiveQuery from './ActiveQuery.ui';
|
||||
import _ from 'lodash';
|
||||
import CachedOutlinedIcon from '@material-ui/icons/CachedOutlined';
|
||||
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
|
||||
import TabPanel from '../../../static/js/components/TabPanel';
|
||||
import Summary from './SystemStats/Summary';
|
||||
|
@ -37,6 +34,10 @@ import { usePgAdmin } from '../../../static/js/BrowserComponent';
|
|||
import usePreferences from '../../../preferences/static/js/store';
|
||||
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
|
||||
import { parseApiError } from '../../../static/js/api_instance';
|
||||
import SectionContainer from './components/SectionContainer';
|
||||
import Replication from './Replication';
|
||||
import RefreshButton from './components/RefreshButtons';
|
||||
import {getExpandCell } from '../../../static/js/components/PgTable';
|
||||
|
||||
function parseData(data) {
|
||||
let res = [];
|
||||
|
@ -55,11 +56,6 @@ const useStyles = makeStyles((theme) => ({
|
|||
padding: '8px',
|
||||
display: 'flex',
|
||||
},
|
||||
fixedSizeList: {
|
||||
overflowX: 'hidden !important',
|
||||
overflow: 'overlay !important',
|
||||
height: 'auto !important',
|
||||
},
|
||||
dashboardPanel: {
|
||||
height: '100%',
|
||||
background: theme.palette.grey[400],
|
||||
|
@ -108,22 +104,9 @@ const useStyles = makeStyles((theme) => ({
|
|||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
arrowButton: {
|
||||
fontSize: '2rem !important',
|
||||
margin: '-7px'
|
||||
},
|
||||
terminateButton: {
|
||||
color: theme.palette.error.main
|
||||
},
|
||||
buttonClick: {
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
},
|
||||
refreshButton: {
|
||||
marginLeft: 'auto',
|
||||
height: '1.9rem',
|
||||
width: '2.2rem',
|
||||
...theme.mixins.panelBorder,
|
||||
},
|
||||
chartCard: {
|
||||
border: '1px solid '+theme.otherVars.borderColor,
|
||||
},
|
||||
|
@ -156,6 +139,9 @@ function Dashboard({
|
|||
const classes = useStyles();
|
||||
let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')];
|
||||
let mainTabs = [gettext('General'), gettext('System Statistics')];
|
||||
if(treeNodeInfo?.server?.replication_type) {
|
||||
mainTabs.push(gettext('Replication'));
|
||||
}
|
||||
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')];
|
||||
const [dashData, setdashData] = useState([]);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
@ -247,8 +233,10 @@ function Dashboard({
|
|||
sortable: true,
|
||||
resizable: false,
|
||||
disableGlobalFilter: false,
|
||||
disableResizing: true,
|
||||
width: 35,
|
||||
minWidth: 0,
|
||||
maxWidth: 35,
|
||||
minWidth: 35,
|
||||
id: 'btn-terminate',
|
||||
// eslint-disable-next-line react/display-name
|
||||
Cell: ({ row }) => {
|
||||
|
@ -391,40 +379,21 @@ function Dashboard({
|
|||
width: 35,
|
||||
minWidth: 0,
|
||||
id: 'btn-edit',
|
||||
Cell: ({ row }) => {
|
||||
let canEditRow = true;
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
className={row.isExpanded ?classes.buttonClick : ''}
|
||||
icon={
|
||||
row.isExpanded ? (
|
||||
<ArrowDropDownOutlinedIcon className={classes.arrowButton}/>
|
||||
) : (
|
||||
<ArrowRightOutlinedIcon className={classes.arrowButton}/>
|
||||
)
|
||||
}
|
||||
noBorder
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
row.toggleRowExpanded(!row.isExpanded);
|
||||
let schema = new ActiveQuery({
|
||||
query: row.original.query,
|
||||
backend_type: row.original.backend_type,
|
||||
state_change: row.original.state_change,
|
||||
query_start: row.original.query_start,
|
||||
});
|
||||
setSchemaDict(prevState => ({
|
||||
...prevState,
|
||||
[row.id]: schema
|
||||
}));
|
||||
}}
|
||||
disabled={!canEditRow}
|
||||
aria-label="View the active session details"
|
||||
title={gettext('View the active session details')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: getExpandCell({
|
||||
onClick: (row) => {
|
||||
let schema = new ActiveQuery({
|
||||
query: row.original.query,
|
||||
backend_type: row.original.backend_type,
|
||||
state_change: row.original.state_change,
|
||||
query_start: row.original.query_start,
|
||||
});
|
||||
setSchemaDict(prevState => ({
|
||||
...prevState,
|
||||
[row.id]: schema
|
||||
}));
|
||||
},
|
||||
title: gettext('View the active session details')
|
||||
}),
|
||||
},
|
||||
{
|
||||
accessor: 'pid',
|
||||
|
@ -740,6 +709,11 @@ function Dashboard({
|
|||
},[nodeData]);
|
||||
|
||||
useEffect(() => {
|
||||
// disable replication tab
|
||||
if(!treeNodeInfo?.server?.replication_type && mainTabVal == 2) {
|
||||
setMainTabVal(0);
|
||||
}
|
||||
|
||||
let url,
|
||||
ssExtensionCheckUrl = url_for('dashboard.check_system_statistics'),
|
||||
message = gettext(
|
||||
|
@ -829,24 +803,6 @@ function Dashboard({
|
|||
return dashData;
|
||||
}, [dashData, activeOnly, tabVal]);
|
||||
|
||||
const RefreshButton = () =>{
|
||||
return(
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
className={classes.refreshButton}
|
||||
icon={<CachedOutlinedIcon />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
color="default"
|
||||
aria-label="Refresh"
|
||||
title={gettext('Refresh')}
|
||||
></PgIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const showDefaultContents = () => {
|
||||
return (
|
||||
sid && !serverConnected ? (
|
||||
|
@ -915,57 +871,52 @@ function Dashboard({
|
|||
></Graphs>
|
||||
)}
|
||||
{!_.isUndefined(preferences) && preferences.show_activity && (
|
||||
<Box className={classes.panelContent}>
|
||||
<Box
|
||||
className={classes.cardHeader}
|
||||
title={dbConnected ? gettext('Database activity') : gettext('Server activity')}
|
||||
>
|
||||
{dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '}
|
||||
<SectionContainer title={dbConnected ? gettext('Database activity') : gettext('Server activity')}>
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
onChange={tabChanged}
|
||||
>
|
||||
{tabs.map((tabValue) => {
|
||||
return <Tab key={tabValue} label={tabValue} />;
|
||||
})}
|
||||
<RefreshButton onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setRefresh(!refresh);
|
||||
}}/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box height="100%" display="flex" flexDirection="column">
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
onChange={tabChanged}
|
||||
>
|
||||
{tabs.map((tabValue) => {
|
||||
return <Tab key={tabValue} label={tabValue} />;
|
||||
})}
|
||||
<RefreshButton/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
CustomHeader={CustomActiveOnlyHeader}
|
||||
columns={activityColumns}
|
||||
data={filteredDashData}
|
||||
schema={schemaDict}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={databaseLocksColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={databasePreparedColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={3} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={serverConfigColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Box>
|
||||
<TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
CustomHeader={CustomActiveOnlyHeader}
|
||||
columns={activityColumns}
|
||||
data={filteredDashData}
|
||||
schema={schemaDict}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={databaseLocksColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={databasePreparedColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={3} classNameRoot={classes.tabPanel}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={serverConfigColumns}
|
||||
data={dashData}
|
||||
></PgTable>
|
||||
</TabPanel>
|
||||
</SectionContainer>
|
||||
)}
|
||||
</TabPanel>
|
||||
{/* System Statistics */}
|
||||
|
@ -1031,6 +982,10 @@ function Dashboard({
|
|||
}
|
||||
</Box>
|
||||
</TabPanel>
|
||||
{/* Replication */}
|
||||
<TabPanel value={mainTabVal} index={2} classNameRoot={classes.tabPanel}>
|
||||
<Replication key={mainTabVal} sid={sid} node={node} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
//////////////////////////////////////////////////////////////
|
||||
import React, { useEffect, useRef, useState, useReducer, useMemo } from 'react';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import ChartContainer from './ChartContainer';
|
||||
import ChartContainer from './components/ChartContainer';
|
||||
import url_for from 'sources/url_for';
|
||||
import axios from 'axios';
|
||||
import gettext from 'sources/gettext';
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
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/PgTable';
|
||||
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
|
||||
import url_for from 'sources/url_for';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
const replicationStatsColumns = [{
|
||||
accessor: 'view_details',
|
||||
Header: () => null,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
disableGlobalFilter: false,
|
||||
disableResizing: true,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
minWidth: 35,
|
||||
id: 'btn-edit',
|
||||
Cell: getExpandCell({
|
||||
title: gettext('View details')
|
||||
}),
|
||||
},
|
||||
{
|
||||
accessor: 'pid',
|
||||
Header: gettext('PID'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
accessor: 'client_addr',
|
||||
Header: gettext('Client Addr'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
accessor:'state',
|
||||
Header: gettext('State'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
accessor:'write_lag',
|
||||
Header: gettext('Write Lag'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
accessor:'flush_lag',
|
||||
Header: gettext('Flush Lag'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
accessor:'replay_lag',
|
||||
Header: gettext('Replay Lag'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
accessor:'reply_time',
|
||||
Header: gettext('Reply Time'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 80
|
||||
}
|
||||
];
|
||||
|
||||
const replicationSlotsColumns = [{
|
||||
accessor: 'view_details',
|
||||
Header: () => null,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
disableGlobalFilter: false,
|
||||
disableResizing: true,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
minWidth: 35,
|
||||
id: 'btn-details',
|
||||
Cell: getExpandCell({
|
||||
title: gettext('View details')
|
||||
}),
|
||||
},
|
||||
{
|
||||
accessor: 'active_pid',
|
||||
Header: gettext('Active PID'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
accessor: 'slot_name',
|
||||
Header: gettext('Slot Name'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
accessor:'active',
|
||||
Header: gettext('Active'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 26,
|
||||
width: 60,
|
||||
Cell: getSwitchCell(),
|
||||
}
|
||||
];
|
||||
|
||||
const replSchemaObj = new ReplicationSlotsSchema();
|
||||
const replStatObj = new ReplicationStatsSchema();
|
||||
|
||||
export default function Replication({treeNodeInfo, pageVisible}) {
|
||||
const [replicationSlots, setReplicationSlots] = useState([{
|
||||
}]);
|
||||
const [replicationStats, setReplicationStats] = useState([{
|
||||
}]);
|
||||
const pgAdmin = usePgAdmin();
|
||||
|
||||
const getReplicationData = (endpoint, setter)=>{
|
||||
const api = getApiInstance();
|
||||
const url = url_for(`dashboard.${endpoint}`, {sid: treeNodeInfo.server._id});
|
||||
api.get(url)
|
||||
.then((res)=>{
|
||||
setter(res.data);
|
||||
})
|
||||
.catch((error)=>{
|
||||
console.error(error);
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
if(pageVisible) {
|
||||
getReplicationData('replication_stats', setReplicationStats);
|
||||
getReplicationData('replication_slots', setReplicationSlots);
|
||||
}
|
||||
}, [pageVisible ]);
|
||||
|
||||
return (
|
||||
<Box height="100%" display="flex" flexDirection="column">
|
||||
<SectionContainer
|
||||
titleExtras={<RefreshButton onClick={()=>{
|
||||
getReplicationData('replication_stats', setReplicationStats);
|
||||
}}/>}
|
||||
title={gettext('Replication Stats')} style={{minHeight: '300px'}}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={replicationStatsColumns}
|
||||
data={replicationStats}
|
||||
schema={replStatObj}
|
||||
></PgTable>
|
||||
</SectionContainer>
|
||||
<SectionContainer
|
||||
titleExtras={<RefreshButton onClick={()=>{
|
||||
getReplicationData('replication_slots', setReplicationSlots);
|
||||
}}/>}
|
||||
title={gettext('Replication Slots')} style={{minHeight: '300px', marginTop: '4px'}}>
|
||||
<PgTable
|
||||
caveTable={false}
|
||||
columns={replicationSlotsColumns}
|
||||
data={replicationSlots}
|
||||
schema={replSchemaObj}
|
||||
></PgTable>
|
||||
</SectionContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Replication.propTypes = {
|
||||
treeNodeInfo: PropTypes.object.isRequired,
|
||||
pageVisible: PropTypes.bool,
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 ReplicationSlotsSchema extends BaseUISchema {
|
||||
constructor(initValues) {
|
||||
super({
|
||||
...initValues,
|
||||
});
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'slot_name', label: gettext('Slot Name'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'slot_type', label: gettext('Slot Type'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'active', label: gettext('Active'), type: 'switch', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'active_pid', label: gettext('Active PID'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'restart_lsn', label: gettext('Restart LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'confirmed_flush_lsn', label: gettext('Confirmed Flush LSN'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
{
|
||||
id: 'wal_status', label: gettext('WAL Status'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 ReplicationStatsSchema extends BaseUISchema {
|
||||
constructor(initValues) {
|
||||
super({
|
||||
...initValues,
|
||||
});
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'pid', label: gettext('PID'), 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')
|
||||
},
|
||||
{
|
||||
id: 'reply_time', label: gettext('Reply Time'), type: 'text', mode:['properties'], readonly: true,
|
||||
group: gettext('Details')
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import gettext from 'sources/gettext';
|
|||
import PropTypes from 'prop-types';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import ChartContainer from '../ChartContainer';
|
||||
import ChartContainer from '../components/ChartContainer';
|
||||
import { Box, Grid } from '@material-ui/core';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||
|
|
|
@ -12,7 +12,7 @@ import gettext from 'sources/gettext';
|
|||
import PropTypes from 'prop-types';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import ChartContainer from '../ChartContainer';
|
||||
import ChartContainer from '../components/ChartContainer';
|
||||
import { Box, Grid } from '@material-ui/core';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||
|
|
|
@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
|
|||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import url_for from 'sources/url_for';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import ChartContainer from '../ChartContainer';
|
||||
import ChartContainer from '../components/ChartContainer';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||
import url_for from 'sources/url_for';
|
||||
import getApiInstance from 'sources/api_instance';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import ChartContainer from '../ChartContainer';
|
||||
import ChartContainer from '../components/ChartContainer';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Box, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core';
|
||||
|
||||
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
|
||||
import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessage';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
|
@ -0,0 +1,46 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 gettext from 'sources/gettext';
|
||||
import CachedOutlinedIcon from '@material-ui/icons/CachedOutlined';
|
||||
import { PgIconButton } from '../../../../static/js/components/Buttons';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
refreshButton: {
|
||||
marginLeft: 'auto',
|
||||
height: '1.9rem',
|
||||
width: '2.2rem',
|
||||
...theme.mixins.panelBorder,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
export default function RefreshButton({onClick}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return(
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
className={classes.refreshButton}
|
||||
icon={<CachedOutlinedIcon />}
|
||||
onClick={onClick}
|
||||
color="default"
|
||||
aria-label="Refresh"
|
||||
title={gettext('Refresh')}
|
||||
></PgIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
RefreshButton.propTypes = {
|
||||
onClick: PropTypes.func
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 PropTypes from 'prop-types';
|
||||
import { Box, makeStyles } from '@material-ui/core';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
...theme.mixins.panelBorder.all,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden !important',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minHeight: '400px',
|
||||
},
|
||||
cardHeader: {
|
||||
backgroundColor: theme.otherVars.tableBg,
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: theme.otherVars.borderColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cardTitle: {
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontWeight: 'bold',
|
||||
}
|
||||
}));
|
||||
|
||||
export default function SectionContainer({title, titleExtras, children, style}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.root} style={style}>
|
||||
<Box className={classes.cardHeader} title={title}>
|
||||
<div className={classes.cardTitle}>{title}</div>
|
||||
<div style={{marginLeft: 'auto'}}>
|
||||
{titleExtras}
|
||||
</div>
|
||||
</Box>
|
||||
<Box height="100%" display="flex" flexDirection="column">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SectionContainer.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
titleExtras: PropTypes.node,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
select * from pg_replication_slots
|
|
@ -0,0 +1 @@
|
|||
select * from pg_stat_replication
|
|
@ -0,0 +1,50 @@
|
|||
##########################################################################
|
||||
#
|
||||
# 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
|
||||
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 replication slots', dict(
|
||||
endpoint='/dashboard/replication_slots',
|
||||
data=[],
|
||||
)), (
|
||||
'TestCase for replication stats', dict(
|
||||
endpoint='/dashboard/replication_stats',
|
||||
data=[],
|
||||
)),
|
||||
]
|
||||
|
||||
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.":
|
||||
|
||||
url = self.endpoint + '/{0}'.format(self.server_id)
|
||||
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
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import getApiInstance from 'sources/api_instance';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Box, Switch } from '@material-ui/core';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { generateCollectionURL } from '../../browser/static/js/node_ajax';
|
||||
import gettext from 'sources/gettext';
|
||||
import PgTable from 'sources/components/PgTable';
|
||||
|
@ -23,6 +23,7 @@ import EmptyPanelMessage from '../../static/js/components/EmptyPanelMessage';
|
|||
import Loader from 'sources/components/Loader';
|
||||
import { evalFunc } from '../../static/js/utils';
|
||||
import { usePgAdmin } from '../../static/js/BrowserComponent';
|
||||
import { getSwitchCell } from '../../static/js/components/PgTable';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
emptyPanel: {
|
||||
|
@ -64,12 +65,6 @@ const useStyles = makeStyles((theme) => ({
|
|||
overflow: 'hidden !important',
|
||||
overflowX: 'auto !important'
|
||||
},
|
||||
readOnlySwitch: {
|
||||
opacity: 0.75,
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: theme.palette.action.disabledOpacity,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default function CollectionNodeProperties({
|
||||
|
@ -215,14 +210,6 @@ export default function CollectionNodeProperties({
|
|||
schemaRef.current?.fields.forEach((field) => {
|
||||
if (node.columns.indexOf(field.id) > -1) {
|
||||
if (field.label.indexOf('?') > -1) {
|
||||
const Cell = ({value})=>{
|
||||
return <Switch color="primary" checked={value} className={classes.readOnlySwitch} value={value} readOnly title={String(value)} />;
|
||||
};
|
||||
Cell.displayName = 'StatusCell';
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
column = {
|
||||
Header: field.label,
|
||||
accessor: field.id,
|
||||
|
@ -230,7 +217,7 @@ export default function CollectionNodeProperties({
|
|||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
minWidth: 0,
|
||||
Cell: Cell
|
||||
Cell: getSwitchCell()
|
||||
};
|
||||
} else {
|
||||
column = {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Checkbox, Box } from '@material-ui/core';
|
||||
import { Checkbox, Box, Switch } from '@material-ui/core';
|
||||
import { InputText } from './FormComponents';
|
||||
import _ from 'lodash';
|
||||
import gettext from 'sources/gettext';
|
||||
|
@ -30,6 +30,8 @@ import SchemaView from '../SchemaView';
|
|||
import EmptyPanelMessage from './EmptyPanelMessage';
|
||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||
import { PgIconButton } from './Buttons';
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
@ -123,6 +125,9 @@ const useStyles = makeStyles((theme) => ({
|
|||
textAlign: 'center',
|
||||
minWidth: 20
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: theme.otherVars.tableBg,
|
||||
},
|
||||
tableCellHeader: {
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
padding: theme.spacing(1, 0.5),
|
||||
|
@ -182,6 +187,15 @@ const useStyles = makeStyles((theme) => ({
|
|||
padding: theme.spacing(0.5, 0),
|
||||
textAlign: 'center',
|
||||
},
|
||||
btnExpanded: {
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
},
|
||||
readOnlySwitch: {
|
||||
opacity: 0.75,
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: theme.palette.action.disabledOpacity,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(
|
||||
|
@ -280,9 +294,9 @@ function RenderRow({ index, style, schema, row, prepareRow, setRowHeight, Expand
|
|||
{!_.isUndefined(row) && row.isExpanded && (
|
||||
<Box key={row.id} className={classes.expandedForm}>
|
||||
{schema && <SchemaView
|
||||
getInitData={()=>Promise.resolve({})}
|
||||
getInitData={()=>Promise.resolve(row.original)}
|
||||
viewHelperProps={{ mode: 'properties' }}
|
||||
schema={schema[row.id]}
|
||||
schema={schema[row.id]??schema}
|
||||
showFooter={false}
|
||||
onDataChange={()=>{setExpandComplete(true);}}
|
||||
/>}
|
||||
|
@ -307,7 +321,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
|||
// Use the state and functions returned from useTable to build your UI
|
||||
const classes = useStyles();
|
||||
const [searchVal, setSearchVal] = React.useState('');
|
||||
const tableRef = React.useRef();
|
||||
const windowTableRef = React.useRef();
|
||||
const rowHeights = React.useRef({});
|
||||
|
||||
// Reset Search value on tab changes.
|
||||
|
@ -316,7 +330,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
|||
setSearchVal(prevState => (prevState));
|
||||
setGlobalFilter(searchVal || undefined);
|
||||
rowHeights.current = {};
|
||||
tableRef.current?.resetAfterIndex(0);
|
||||
windowTableRef.current?.resetAfterIndex(0);
|
||||
}, [data]);
|
||||
|
||||
function getRowHeight(index) {
|
||||
|
@ -324,13 +338,13 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
|||
}
|
||||
|
||||
const setRowHeight = (index, size) => {
|
||||
if(tableRef.current) {
|
||||
if(windowTableRef.current) {
|
||||
if(size == ROW_HEIGHT) {
|
||||
delete rowHeights.current[index];
|
||||
} else {
|
||||
rowHeights.current[index] = size;
|
||||
}
|
||||
tableRef.current.resetAfterIndex(index);
|
||||
windowTableRef.current.resetAfterIndex(index);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -422,9 +436,10 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
|||
</div>
|
||||
),
|
||||
sortable: false,
|
||||
disableResizing: true,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
minWidth: 0
|
||||
minWidth: 35
|
||||
},
|
||||
...CLOUMNS,
|
||||
];
|
||||
|
@ -522,7 +537,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
|||
>
|
||||
{({ height }) => (
|
||||
<VariableSizeList
|
||||
ref={tableRef}
|
||||
ref={windowTableRef}
|
||||
className={classes.fixedSizeList}
|
||||
height={isNaN(height) ? 100 : height}
|
||||
itemCount={rows.length}
|
||||
|
@ -574,3 +589,54 @@ PgTable.propTypes = {
|
|||
tableProps: PropTypes.object,
|
||||
'data-test': PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
export function getExpandCell({onClick, ...props}) {
|
||||
const Cell = ({ row }) => {
|
||||
const classes = useStyles();
|
||||
const onClickFinal = (e)=>{
|
||||
e.preventDefault();
|
||||
row.toggleRowExpanded(!row.isExpanded);
|
||||
onClick?.(row, e);
|
||||
};
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
className={row.isExpanded ? classes.btnExpanded : ''}
|
||||
icon={
|
||||
row.isExpanded ? (
|
||||
<KeyboardArrowDownIcon />
|
||||
) : (
|
||||
<ChevronRightIcon />
|
||||
)
|
||||
}
|
||||
noBorder
|
||||
{...props}
|
||||
onClick={onClickFinal}
|
||||
aria-label={props.title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'ExpandCell';
|
||||
Cell.propTypes = {
|
||||
title: PropTypes.string,
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function getSwitchCell() {
|
||||
const Cell = ({value})=>{
|
||||
const classes = useStyles();
|
||||
return <Switch color="primary" checked={value} className={classes.readOnlySwitch} value={value} readOnly title={String(value)} />;
|
||||
};
|
||||
|
||||
Cell.displayName = 'SwitchCell';
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
|
@ -633,3 +633,14 @@ export function requestAnimationAndFocus(ele) {
|
|||
cancelAnimationFrame(animateId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function scrollbarWidth() {
|
||||
// thanks too https://davidwalsh.name/detect-scrollbar-width
|
||||
const scrollDiv = document.createElement('div');
|
||||
scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;');
|
||||
document.body.appendChild(scrollDiv);
|
||||
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
|
||||
document.body.removeChild(scrollDiv);
|
||||
return scrollbarWidth;
|
||||
}
|
|
@ -503,6 +503,7 @@ module.exports = [{
|
|||
'pure|pgadmin.node.aggregate',
|
||||
'pure|pgadmin.node.operator',
|
||||
'pure|pgadmin.node.dbms_job_scheduler',
|
||||
'pure|pgadmin.node.replica_node'
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -121,6 +121,7 @@ let webpackShimConfig = {
|
|||
'pgadmin.node.primary_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key'),
|
||||
'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.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'),
|
||||
|
|
Loading…
Reference in New Issue