mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-07-07 04:53:25 -05:00
Added support for viewing Log Based Clusters. #7216
Co-authored-by: Akshay Joshi <akshay.joshi@enterprisedb.com>
This commit is contained in:
parent
5931162556
commit
ace73ebb60
BIN
docs/en_US/images/replica_nodes_general.png
Normal file
BIN
docs/en_US/images/replica_nodes_general.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
BIN
docs/en_US/images/replica_nodes_replication.png
Normal file
BIN
docs/en_US/images/replica_nodes_replication.png
Normal file
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
|
resource_group_dialog
|
||||||
role_dialog
|
role_dialog
|
||||||
tablespace_dialog
|
tablespace_dialog
|
||||||
|
replica_nodes_dialog
|
||||||
role_reassign_dialog
|
role_reassign_dialog
|
46
docs/en_US/replica_nodes_dialog.rst
Normal file
46
docs/en_US/replica_nodes_dialog.rst
Normal file
|
@ -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_babel import gettext
|
||||||
from flask_security import current_user, login_required
|
from flask_security import current_user, login_required
|
||||||
from psycopg.conninfo import make_conninfo, conninfo_to_dict
|
from psycopg.conninfo import make_conninfo, conninfo_to_dict
|
||||||
|
|
||||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||||
from pgadmin.browser.utils import PGChildNodeView
|
from pgadmin.browser.utils import PGChildNodeView
|
||||||
from pgadmin.utils.ajax import make_json_response, bad_request, forbidden, \
|
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.master_password import get_crypt_key
|
||||||
from pgadmin.utils.exception import CryptKeyMissing
|
from pgadmin.utils.exception import CryptKeyMissing
|
||||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
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, \
|
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
||||||
SERVER_CONNECTION_CLOSED
|
SERVER_CONNECTION_CLOSED
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
@ -343,6 +345,9 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||||
from .tablespaces import blueprint as module
|
from .tablespaces import blueprint as module
|
||||||
self.submodules.append(module)
|
self.submodules.append(module)
|
||||||
|
|
||||||
|
from .replica_nodes import blueprint as module
|
||||||
|
self.submodules.append(module)
|
||||||
|
|
||||||
super().register(app, options)
|
super().register(app, options)
|
||||||
|
|
||||||
# We do not have any preferences for server node.
|
# We do not have any preferences for server node.
|
||||||
|
@ -469,7 +474,7 @@ class ServerNode(PGChildNodeView):
|
||||||
}],
|
}],
|
||||||
'check_pgpass': [{'get': 'check_pgpass'}],
|
'check_pgpass': [{'get': 'check_pgpass'}],
|
||||||
'clear_saved_password': [{'put': 'clear_saved_password'}],
|
'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']
|
SSL_MODES = ['prefer', 'require', 'verify-ca', 'verify-full']
|
||||||
|
|
||||||
|
@ -1247,6 +1252,7 @@ class ServerNode(PGChildNodeView):
|
||||||
connected = False
|
connected = False
|
||||||
user = None
|
user = None
|
||||||
manager = None
|
manager = None
|
||||||
|
replication_type = None
|
||||||
|
|
||||||
if 'connect_now' in data and data['connect_now']:
|
if 'connect_now' in data and data['connect_now']:
|
||||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
|
||||||
|
@ -1324,6 +1330,8 @@ class ServerNode(PGChildNodeView):
|
||||||
server.id),
|
server.id),
|
||||||
tunnel_password)
|
tunnel_password)
|
||||||
|
|
||||||
|
replication_type = get_replication_type(conn,
|
||||||
|
manager.version)
|
||||||
user = manager.user_info
|
user = manager.user_info
|
||||||
connected = True
|
connected = True
|
||||||
|
|
||||||
|
@ -1337,6 +1345,7 @@ class ServerNode(PGChildNodeView):
|
||||||
username=server.username,
|
username=server.username,
|
||||||
user=user,
|
user=user,
|
||||||
connected=connected,
|
connected=connected,
|
||||||
|
replication_type=replication_type,
|
||||||
shared=server.shared,
|
shared=server.shared,
|
||||||
server_type=manager.server_type
|
server_type=manager.server_type
|
||||||
if manager and manager.server_type
|
if manager and manager.server_type
|
||||||
|
@ -1427,6 +1436,7 @@ class ServerNode(PGChildNodeView):
|
||||||
in_recovery = None
|
in_recovery = None
|
||||||
wal_paused = None
|
wal_paused = None
|
||||||
errmsg = None
|
errmsg = None
|
||||||
|
replication_type = None
|
||||||
if connected:
|
if connected:
|
||||||
status, result, in_recovery, wal_paused =\
|
status, result, in_recovery, wal_paused =\
|
||||||
recovery_state(conn, manager.version)
|
recovery_state(conn, manager.version)
|
||||||
|
@ -1436,10 +1446,13 @@ class ServerNode(PGChildNodeView):
|
||||||
manager.release()
|
manager.release()
|
||||||
errmsg = "{0} : {1}".format(server.name, result)
|
errmsg = "{0} : {1}".format(server.name, result)
|
||||||
|
|
||||||
|
replication_type = get_replication_type(conn, manager.version)
|
||||||
|
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
data={
|
data={
|
||||||
'icon': server_icon_and_background(connected, manager, server),
|
'icon': server_icon_and_background(connected, manager, server),
|
||||||
'connected': connected,
|
'connected': connected,
|
||||||
|
'replication_type': replication_type,
|
||||||
'in_recovery': in_recovery,
|
'in_recovery': in_recovery,
|
||||||
'wal_pause': wal_paused,
|
'wal_pause': wal_paused,
|
||||||
'server_type': manager.server_type if connected else "pg",
|
'server_type': manager.server_type if connected else "pg",
|
||||||
|
@ -1709,6 +1722,8 @@ class ServerNode(PGChildNodeView):
|
||||||
_, _, in_recovery, wal_paused =\
|
_, _, in_recovery, wal_paused =\
|
||||||
recovery_state(conn, manager.version)
|
recovery_state(conn, manager.version)
|
||||||
|
|
||||||
|
replication_type = get_replication_type(conn, manager.version)
|
||||||
|
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
success=1,
|
success=1,
|
||||||
info=gettext("Server connected."),
|
info=gettext("Server connected."),
|
||||||
|
@ -1716,6 +1731,7 @@ class ServerNode(PGChildNodeView):
|
||||||
'icon': server_icon_and_background(True, manager, server),
|
'icon': server_icon_and_background(True, manager, server),
|
||||||
'connected': True,
|
'connected': True,
|
||||||
'server_type': manager.server_type,
|
'server_type': manager.server_type,
|
||||||
|
'replication_type': replication_type,
|
||||||
'type': manager.server_type,
|
'type': manager.server_type,
|
||||||
'version': manager.version,
|
'version': manager.version,
|
||||||
'db': manager.db,
|
'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
|
self.manager.connection.connected.side_effect = True
|
||||||
|
|
||||||
connection_mock_result.execute_dict.side_effect = \
|
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)
|
response = self.get_server_connection(server_id)
|
||||||
self.assertEqual(response.status_code,
|
self.assertEqual(response.status_code,
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
"""Server helper utilities"""
|
"""Server helper utilities"""
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
from flask import render_template
|
||||||
|
|
||||||
from pgadmin.utils.crypto import encrypt, decrypt
|
from pgadmin.utils.crypto import encrypt, decrypt
|
||||||
import config
|
import config
|
||||||
|
@ -277,3 +279,14 @@ def remove_saved_passwords(user_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise
|
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',
|
'server_group', 'server', 'coll-tablespace', 'tablespace',
|
||||||
'coll-role', 'role', 'coll-resource_group', 'resource_group',
|
'coll-role', 'role', 'coll-resource_group', 'resource_group',
|
||||||
'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep',
|
'coll-database', 'coll-pga_job', 'coll-pga_schedule', 'coll-pga_jobstep',
|
||||||
'pga_job', 'pga_schedule', 'pga_jobstep'
|
'pga_job', 'pga_schedule', 'pga_jobstep',
|
||||||
|
'coll-replica_nodes', 'replica_nodes'
|
||||||
];
|
];
|
||||||
|
|
||||||
pgBrowser.utils = {
|
pgBrowser.utils = {
|
||||||
|
|
|
@ -245,6 +245,8 @@ class DashboardModule(PgAdminModule):
|
||||||
'dashboard.system_statistics',
|
'dashboard.system_statistics',
|
||||||
'dashboard.system_statistics_sid',
|
'dashboard.system_statistics_sid',
|
||||||
'dashboard.system_statistics_did',
|
'dashboard.system_statistics_did',
|
||||||
|
'dashboard.replication_slots',
|
||||||
|
'dashboard.replication_stats',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -646,3 +648,51 @@ def system_statistics(sid=None, did=None):
|
||||||
response=resp_data,
|
response=resp_data,
|
||||||
status=200
|
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 { PgIconButton } from '../../../static/js/components/Buttons';
|
||||||
import CancelIcon from '@material-ui/icons/Cancel';
|
import CancelIcon from '@material-ui/icons/Cancel';
|
||||||
import StopSharpIcon from '@material-ui/icons/StopSharp';
|
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 WelcomeDashboard from './WelcomeDashboard';
|
||||||
import ActiveQuery from './ActiveQuery.ui';
|
import ActiveQuery from './ActiveQuery.ui';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import CachedOutlinedIcon from '@material-ui/icons/CachedOutlined';
|
|
||||||
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
|
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
|
||||||
import TabPanel from '../../../static/js/components/TabPanel';
|
import TabPanel from '../../../static/js/components/TabPanel';
|
||||||
import Summary from './SystemStats/Summary';
|
import Summary from './SystemStats/Summary';
|
||||||
|
@ -37,6 +34,10 @@ import { usePgAdmin } from '../../../static/js/BrowserComponent';
|
||||||
import usePreferences from '../../../preferences/static/js/store';
|
import usePreferences from '../../../preferences/static/js/store';
|
||||||
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
|
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
|
||||||
import { parseApiError } from '../../../static/js/api_instance';
|
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) {
|
function parseData(data) {
|
||||||
let res = [];
|
let res = [];
|
||||||
|
@ -55,11 +56,6 @@ const useStyles = makeStyles((theme) => ({
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
fixedSizeList: {
|
|
||||||
overflowX: 'hidden !important',
|
|
||||||
overflow: 'overlay !important',
|
|
||||||
height: 'auto !important',
|
|
||||||
},
|
|
||||||
dashboardPanel: {
|
dashboardPanel: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: theme.palette.grey[400],
|
background: theme.palette.grey[400],
|
||||||
|
@ -108,22 +104,9 @@ const useStyles = makeStyles((theme) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
arrowButton: {
|
|
||||||
fontSize: '2rem !important',
|
|
||||||
margin: '-7px'
|
|
||||||
},
|
|
||||||
terminateButton: {
|
terminateButton: {
|
||||||
color: theme.palette.error.main
|
color: theme.palette.error.main
|
||||||
},
|
},
|
||||||
buttonClick: {
|
|
||||||
backgroundColor: theme.palette.grey[400]
|
|
||||||
},
|
|
||||||
refreshButton: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
height: '1.9rem',
|
|
||||||
width: '2.2rem',
|
|
||||||
...theme.mixins.panelBorder,
|
|
||||||
},
|
|
||||||
chartCard: {
|
chartCard: {
|
||||||
border: '1px solid '+theme.otherVars.borderColor,
|
border: '1px solid '+theme.otherVars.borderColor,
|
||||||
},
|
},
|
||||||
|
@ -156,6 +139,9 @@ function Dashboard({
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')];
|
let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')];
|
||||||
let mainTabs = [gettext('General'), gettext('System Statistics')];
|
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')];
|
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')];
|
||||||
const [dashData, setdashData] = useState([]);
|
const [dashData, setdashData] = useState([]);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
@ -247,8 +233,10 @@ function Dashboard({
|
||||||
sortable: true,
|
sortable: true,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
disableGlobalFilter: false,
|
disableGlobalFilter: false,
|
||||||
|
disableResizing: true,
|
||||||
width: 35,
|
width: 35,
|
||||||
minWidth: 0,
|
maxWidth: 35,
|
||||||
|
minWidth: 35,
|
||||||
id: 'btn-terminate',
|
id: 'btn-terminate',
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
Cell: ({ row }) => {
|
Cell: ({ row }) => {
|
||||||
|
@ -391,40 +379,21 @@ function Dashboard({
|
||||||
width: 35,
|
width: 35,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
id: 'btn-edit',
|
id: 'btn-edit',
|
||||||
Cell: ({ row }) => {
|
Cell: getExpandCell({
|
||||||
let canEditRow = true;
|
onClick: (row) => {
|
||||||
return (
|
let schema = new ActiveQuery({
|
||||||
<PgIconButton
|
query: row.original.query,
|
||||||
size="xs"
|
backend_type: row.original.backend_type,
|
||||||
className={row.isExpanded ?classes.buttonClick : ''}
|
state_change: row.original.state_change,
|
||||||
icon={
|
query_start: row.original.query_start,
|
||||||
row.isExpanded ? (
|
});
|
||||||
<ArrowDropDownOutlinedIcon className={classes.arrowButton}/>
|
setSchemaDict(prevState => ({
|
||||||
) : (
|
...prevState,
|
||||||
<ArrowRightOutlinedIcon className={classes.arrowButton}/>
|
[row.id]: schema
|
||||||
)
|
}));
|
||||||
}
|
},
|
||||||
noBorder
|
title: gettext('View the active session details')
|
||||||
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')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'pid',
|
accessor: 'pid',
|
||||||
|
@ -740,6 +709,11 @@ function Dashboard({
|
||||||
},[nodeData]);
|
},[nodeData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// disable replication tab
|
||||||
|
if(!treeNodeInfo?.server?.replication_type && mainTabVal == 2) {
|
||||||
|
setMainTabVal(0);
|
||||||
|
}
|
||||||
|
|
||||||
let url,
|
let url,
|
||||||
ssExtensionCheckUrl = url_for('dashboard.check_system_statistics'),
|
ssExtensionCheckUrl = url_for('dashboard.check_system_statistics'),
|
||||||
message = gettext(
|
message = gettext(
|
||||||
|
@ -829,24 +803,6 @@ function Dashboard({
|
||||||
return dashData;
|
return dashData;
|
||||||
}, [dashData, activeOnly, tabVal]);
|
}, [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 = () => {
|
const showDefaultContents = () => {
|
||||||
return (
|
return (
|
||||||
sid && !serverConnected ? (
|
sid && !serverConnected ? (
|
||||||
|
@ -915,57 +871,52 @@ function Dashboard({
|
||||||
></Graphs>
|
></Graphs>
|
||||||
)}
|
)}
|
||||||
{!_.isUndefined(preferences) && preferences.show_activity && (
|
{!_.isUndefined(preferences) && preferences.show_activity && (
|
||||||
<Box className={classes.panelContent}>
|
<SectionContainer title={dbConnected ? gettext('Database activity') : gettext('Server activity')}>
|
||||||
<Box
|
<Box>
|
||||||
className={classes.cardHeader}
|
<Tabs
|
||||||
title={dbConnected ? gettext('Database activity') : gettext('Server activity')}
|
value={tabVal}
|
||||||
>
|
onChange={tabChanged}
|
||||||
{dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '}
|
>
|
||||||
|
{tabs.map((tabValue) => {
|
||||||
|
return <Tab key={tabValue} label={tabValue} />;
|
||||||
|
})}
|
||||||
|
<RefreshButton onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setRefresh(!refresh);
|
||||||
|
}}/>
|
||||||
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<Box height="100%" display="flex" flexDirection="column">
|
<TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
|
||||||
<Box>
|
<PgTable
|
||||||
<Tabs
|
caveTable={false}
|
||||||
value={tabVal}
|
CustomHeader={CustomActiveOnlyHeader}
|
||||||
onChange={tabChanged}
|
columns={activityColumns}
|
||||||
>
|
data={filteredDashData}
|
||||||
{tabs.map((tabValue) => {
|
schema={schemaDict}
|
||||||
return <Tab key={tabValue} label={tabValue} />;
|
></PgTable>
|
||||||
})}
|
</TabPanel>
|
||||||
<RefreshButton/>
|
<TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
|
||||||
</Tabs>
|
<PgTable
|
||||||
</Box>
|
caveTable={false}
|
||||||
<TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
|
columns={databaseLocksColumns}
|
||||||
<PgTable
|
data={dashData}
|
||||||
caveTable={false}
|
></PgTable>
|
||||||
CustomHeader={CustomActiveOnlyHeader}
|
</TabPanel>
|
||||||
columns={activityColumns}
|
<TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
|
||||||
data={filteredDashData}
|
<PgTable
|
||||||
schema={schemaDict}
|
caveTable={false}
|
||||||
></PgTable>
|
columns={databasePreparedColumns}
|
||||||
</TabPanel>
|
data={dashData}
|
||||||
<TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
|
></PgTable>
|
||||||
<PgTable
|
</TabPanel>
|
||||||
caveTable={false}
|
<TabPanel value={tabVal} index={3} classNameRoot={classes.tabPanel}>
|
||||||
columns={databaseLocksColumns}
|
<PgTable
|
||||||
data={dashData}
|
caveTable={false}
|
||||||
></PgTable>
|
columns={serverConfigColumns}
|
||||||
</TabPanel>
|
data={dashData}
|
||||||
<TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
|
></PgTable>
|
||||||
<PgTable
|
</TabPanel>
|
||||||
caveTable={false}
|
</SectionContainer>
|
||||||
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>
|
</TabPanel>
|
||||||
{/* System Statistics */}
|
{/* System Statistics */}
|
||||||
|
@ -1031,6 +982,10 @@ function Dashboard({
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</TabPanel>
|
</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>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
import React, { useEffect, useRef, useState, useReducer, useMemo } from 'react';
|
import React, { useEffect, useRef, useState, useReducer, useMemo } from 'react';
|
||||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||||
import ChartContainer from './ChartContainer';
|
import ChartContainer from './components/ChartContainer';
|
||||||
import url_for from 'sources/url_for';
|
import url_for from 'sources/url_for';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
|
|
214
web/pgadmin/dashboard/static/js/Replication/index.jsx
Normal file
214
web/pgadmin/dashboard/static/js/Replication/index.jsx
Normal file
|
@ -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 PropTypes from 'prop-types';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import {getGCD, getEpoch} from 'sources/utils';
|
import {getGCD, getEpoch} from 'sources/utils';
|
||||||
import ChartContainer from '../ChartContainer';
|
import ChartContainer from '../components/ChartContainer';
|
||||||
import { Box, Grid } from '@material-ui/core';
|
import { Box, Grid } from '@material-ui/core';
|
||||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||||
|
|
|
@ -12,7 +12,7 @@ import gettext from 'sources/gettext';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import {getGCD, getEpoch} from 'sources/utils';
|
import {getGCD, getEpoch} from 'sources/utils';
|
||||||
import ChartContainer from '../ChartContainer';
|
import ChartContainer from '../components/ChartContainer';
|
||||||
import { Box, Grid } from '@material-ui/core';
|
import { Box, Grid } from '@material-ui/core';
|
||||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
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 { makeStyles } from '@material-ui/core/styles';
|
||||||
import url_for from 'sources/url_for';
|
import url_for from 'sources/url_for';
|
||||||
import {getGCD, getEpoch} from 'sources/utils';
|
import {getGCD, getEpoch} from 'sources/utils';
|
||||||
import ChartContainer from '../ChartContainer';
|
import ChartContainer from '../components/ChartContainer';
|
||||||
import { Grid } from '@material-ui/core';
|
import { Grid } from '@material-ui/core';
|
||||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
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 url_for from 'sources/url_for';
|
||||||
import getApiInstance from 'sources/api_instance';
|
import getApiInstance from 'sources/api_instance';
|
||||||
import {getGCD, getEpoch} from 'sources/utils';
|
import {getGCD, getEpoch} from 'sources/utils';
|
||||||
import ChartContainer from '../ChartContainer';
|
import ChartContainer from '../components/ChartContainer';
|
||||||
import { Grid } from '@material-ui/core';
|
import { Grid } from '@material-ui/core';
|
||||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Box, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core';
|
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) => ({
|
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
|
50
web/pgadmin/dashboard/tests/test_replication.py
Normal file
50
web/pgadmin/dashboard/tests/test_replication.py
Normal file
|
@ -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 React from 'react';
|
||||||
import getApiInstance from 'sources/api_instance';
|
import getApiInstance from 'sources/api_instance';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
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 { generateCollectionURL } from '../../browser/static/js/node_ajax';
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
import PgTable from 'sources/components/PgTable';
|
import PgTable from 'sources/components/PgTable';
|
||||||
|
@ -23,6 +23,7 @@ import EmptyPanelMessage from '../../static/js/components/EmptyPanelMessage';
|
||||||
import Loader from 'sources/components/Loader';
|
import Loader from 'sources/components/Loader';
|
||||||
import { evalFunc } from '../../static/js/utils';
|
import { evalFunc } from '../../static/js/utils';
|
||||||
import { usePgAdmin } from '../../static/js/BrowserComponent';
|
import { usePgAdmin } from '../../static/js/BrowserComponent';
|
||||||
|
import { getSwitchCell } from '../../static/js/components/PgTable';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
emptyPanel: {
|
emptyPanel: {
|
||||||
|
@ -64,12 +65,6 @@ const useStyles = makeStyles((theme) => ({
|
||||||
overflow: 'hidden !important',
|
overflow: 'hidden !important',
|
||||||
overflowX: 'auto !important'
|
overflowX: 'auto !important'
|
||||||
},
|
},
|
||||||
readOnlySwitch: {
|
|
||||||
opacity: 0.75,
|
|
||||||
'& .MuiSwitch-track': {
|
|
||||||
opacity: theme.palette.action.disabledOpacity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function CollectionNodeProperties({
|
export default function CollectionNodeProperties({
|
||||||
|
@ -215,14 +210,6 @@ export default function CollectionNodeProperties({
|
||||||
schemaRef.current?.fields.forEach((field) => {
|
schemaRef.current?.fields.forEach((field) => {
|
||||||
if (node.columns.indexOf(field.id) > -1) {
|
if (node.columns.indexOf(field.id) > -1) {
|
||||||
if (field.label.indexOf('?') > -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 = {
|
column = {
|
||||||
Header: field.label,
|
Header: field.label,
|
||||||
accessor: field.id,
|
accessor: field.id,
|
||||||
|
@ -230,7 +217,7 @@ export default function CollectionNodeProperties({
|
||||||
resizable: true,
|
resizable: true,
|
||||||
disableGlobalFilter: false,
|
disableGlobalFilter: false,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
Cell: Cell
|
Cell: getSwitchCell()
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
column = {
|
column = {
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
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 { InputText } from './FormComponents';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
|
@ -30,6 +30,8 @@ import SchemaView from '../SchemaView';
|
||||||
import EmptyPanelMessage from './EmptyPanelMessage';
|
import EmptyPanelMessage from './EmptyPanelMessage';
|
||||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||||
|
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||||
|
import { PgIconButton } from './Buttons';
|
||||||
|
|
||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
@ -123,6 +125,9 @@ const useStyles = makeStyles((theme) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
minWidth: 20
|
minWidth: 20
|
||||||
},
|
},
|
||||||
|
tableHeader: {
|
||||||
|
backgroundColor: theme.otherVars.tableBg,
|
||||||
|
},
|
||||||
tableCellHeader: {
|
tableCellHeader: {
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
padding: theme.spacing(1, 0.5),
|
padding: theme.spacing(1, 0.5),
|
||||||
|
@ -182,6 +187,15 @@ const useStyles = makeStyles((theme) => ({
|
||||||
padding: theme.spacing(0.5, 0),
|
padding: theme.spacing(0.5, 0),
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
btnExpanded: {
|
||||||
|
backgroundColor: theme.palette.grey[400]
|
||||||
|
},
|
||||||
|
readOnlySwitch: {
|
||||||
|
opacity: 0.75,
|
||||||
|
'& .MuiSwitch-track': {
|
||||||
|
opacity: theme.palette.action.disabledOpacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const IndeterminateCheckbox = React.forwardRef(
|
const IndeterminateCheckbox = React.forwardRef(
|
||||||
|
@ -280,9 +294,9 @@ function RenderRow({ index, style, schema, row, prepareRow, setRowHeight, Expand
|
||||||
{!_.isUndefined(row) && row.isExpanded && (
|
{!_.isUndefined(row) && row.isExpanded && (
|
||||||
<Box key={row.id} className={classes.expandedForm}>
|
<Box key={row.id} className={classes.expandedForm}>
|
||||||
{schema && <SchemaView
|
{schema && <SchemaView
|
||||||
getInitData={()=>Promise.resolve({})}
|
getInitData={()=>Promise.resolve(row.original)}
|
||||||
viewHelperProps={{ mode: 'properties' }}
|
viewHelperProps={{ mode: 'properties' }}
|
||||||
schema={schema[row.id]}
|
schema={schema[row.id]??schema}
|
||||||
showFooter={false}
|
showFooter={false}
|
||||||
onDataChange={()=>{setExpandComplete(true);}}
|
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
|
// Use the state and functions returned from useTable to build your UI
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [searchVal, setSearchVal] = React.useState('');
|
const [searchVal, setSearchVal] = React.useState('');
|
||||||
const tableRef = React.useRef();
|
const windowTableRef = React.useRef();
|
||||||
const rowHeights = React.useRef({});
|
const rowHeights = React.useRef({});
|
||||||
|
|
||||||
// Reset Search value on tab changes.
|
// Reset Search value on tab changes.
|
||||||
|
@ -316,7 +330,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
||||||
setSearchVal(prevState => (prevState));
|
setSearchVal(prevState => (prevState));
|
||||||
setGlobalFilter(searchVal || undefined);
|
setGlobalFilter(searchVal || undefined);
|
||||||
rowHeights.current = {};
|
rowHeights.current = {};
|
||||||
tableRef.current?.resetAfterIndex(0);
|
windowTableRef.current?.resetAfterIndex(0);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
function getRowHeight(index) {
|
function getRowHeight(index) {
|
||||||
|
@ -324,13 +338,13 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
||||||
}
|
}
|
||||||
|
|
||||||
const setRowHeight = (index, size) => {
|
const setRowHeight = (index, size) => {
|
||||||
if(tableRef.current) {
|
if(windowTableRef.current) {
|
||||||
if(size == ROW_HEIGHT) {
|
if(size == ROW_HEIGHT) {
|
||||||
delete rowHeights.current[index];
|
delete rowHeights.current[index];
|
||||||
} else {
|
} else {
|
||||||
rowHeights.current[index] = size;
|
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>
|
</div>
|
||||||
),
|
),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
|
disableResizing: true,
|
||||||
width: 35,
|
width: 35,
|
||||||
maxWidth: 35,
|
maxWidth: 35,
|
||||||
minWidth: 0
|
minWidth: 35
|
||||||
},
|
},
|
||||||
...CLOUMNS,
|
...CLOUMNS,
|
||||||
];
|
];
|
||||||
|
@ -522,7 +537,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, sc
|
||||||
>
|
>
|
||||||
{({ height }) => (
|
{({ height }) => (
|
||||||
<VariableSizeList
|
<VariableSizeList
|
||||||
ref={tableRef}
|
ref={windowTableRef}
|
||||||
className={classes.fixedSizeList}
|
className={classes.fixedSizeList}
|
||||||
height={isNaN(height) ? 100 : height}
|
height={isNaN(height) ? 100 : height}
|
||||||
itemCount={rows.length}
|
itemCount={rows.length}
|
||||||
|
@ -574,3 +589,54 @@ PgTable.propTypes = {
|
||||||
tableProps: PropTypes.object,
|
tableProps: PropTypes.object,
|
||||||
'data-test': PropTypes.string
|
'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);
|
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.aggregate',
|
||||||
'pure|pgadmin.node.operator',
|
'pure|pgadmin.node.operator',
|
||||||
'pure|pgadmin.node.dbms_job_scheduler',
|
'pure|pgadmin.node.dbms_job_scheduler',
|
||||||
|
'pure|pgadmin.node.replica_node'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.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.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'),
|
||||||
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
|
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
|
||||||
|
'pgadmin.node.replica_node': path.join(__dirname, './pgadmin/browser/server_groups/servers/replica_nodes/static/js/replica_node'),
|
||||||
'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
|
'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
|
||||||
'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
|
'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
|
||||||
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
|
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user