Added support for viewing PostgreSQL Server Logs in Text, CSV and JSON formats. #3981

pull/7657/head
Khushboo Vashi 2024-07-03 16:17:29 +05:30 committed by GitHub
parent 93b5bc6bf8
commit 4f415f9768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1056 additions and 545 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities
New features New features
************ ************
| `Issue #3981 <https://github.com/pgadmin-org/pgadmin4/issues/3981>`_ - Add support for Postgres Server Logs for Text, CSV and JSON format in plain and tabular formats. Upgrade React to version 18.
Housekeeping Housekeeping
************ ************

View File

@ -9,14 +9,14 @@ display information about the object currently selected in the *pgAdmin* tree
control in the left window. Select a tab to access information about the control in the left window. Select a tab to access information about the
highlighted object in the tree control. highlighted object in the tree control.
.. image:: images/main_dashboard_general.png .. image:: images/dashboard_activity.png
:alt: Dashboard panel :alt: Dashboard Activity
:align: center :align: center
The graphs and tables on the *Dashboard* tab provides an active analysis of system statistics and the usage The graphs and tables on the *Dashboard* tab provides an active analysis of system statistics and the usage
statistics for the selected server or database. statistics for the selected server or database.
Click the *General* tab to get the usage statistics for the selected server or database: Click the *Activity* tab to get the usage statistics for the selected server or database:
* The *Server sessions* or *Database sessions* graph displays the interactions * The *Server sessions* or *Database sessions* graph displays the interactions
with the server or database. with the server or database.
@ -30,8 +30,14 @@ Click the *General* tab to get the usage statistics for the selected server or d
or fetched from the buffer cache (but not the operating system's file system or fetched from the buffer cache (but not the operating system's file system
cache) for the server or database. cache) for the server or database.
The *Server activity* panel displays information about sessions, locks, prepared .. image:: images/dashboard_stat.png
transactions, and server configuration (if applicable). The information is :alt: Dashboard Activity
:align: center
Click the *Stat* tab to get the usage statistics for the selected server or database:
The *Stat* panel displays information about sessions, locks, prepared
transactions. The information is
presented in context-sensitive tables. Use controls located above the table to: presented in context-sensitive tables. Use controls located above the table to:
* Click the *Refresh* button to update the information displayed in each table. * Click the *Refresh* button to update the information displayed in each table.
@ -56,6 +62,22 @@ session:
* Use the *Details* icon (located in the third column) to open the *Details* * Use the *Details* icon (located in the third column) to open the *Details*
tab; the tab displays information about the selected session. tab; the tab displays information about the selected session.
.. image:: images/dashboard_config.png
:alt: Dashboard Activity
:align: center
Click the *Configuration* tab to get the server configuration details.
.. image:: images/dashboard_logs.png
:alt: Dashboard Activity
:align: center
Click the *Logs* tab to get the server logs.
* Use the Log Format switch to select the format you want. Text/Plain, JSON and CSV are supported.
* Use the Logs in tabular format? switch if you want to see the logs in a tabular format.
Click the *System Statistics* tab to get the statistics for the system: Click the *System Statistics* tab to get the statistics for the system:
.. image:: images/main_dashboard_sys_statistics_summary.png .. image:: images/main_dashboard_sys_statistics_summary.png

View File

@ -929,6 +929,12 @@ SERVER_HEARTBEAT_TIMEOUT = 30 # In seconds
############################################################################# #############################################################################
ENABLE_SERVER_PASS_EXEC_CMD = False ENABLE_SERVER_PASS_EXEC_CMD = False
#############################################################################
# Number of records to fetch in one batch for server logs.
##############################################################################
ON_DEMAND_LOG_COUNT = 10000
############################################################################# #############################################################################
# Patch the default config with custom config and other manipulations # Patch the default config with custom config and other manipulations
############################################################################# #############################################################################

View File

@ -21,9 +21,10 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@emotion/utils": "^1.0.0", "@emotion/utils": "^1.0.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.1.2", "@testing-library/dom": "10.2.0",
"@testing-library/react": "12", "@testing-library/jest-dom": "^6.4.6",
"@testing-library/user-event": "^14.4.3", "@testing-library/react": "16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.4", "@types/jest": "^29.5.4",
"@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0", "@typescript-eslint/parser": "^7.12.0",
@ -72,6 +73,7 @@
"dependencies": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.12.13",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.6.5", "@codemirror/lang-sql": "^6.6.5",
"@date-io/core": "^3.0.0", "@date-io/core": "^3.0.0",
"@date-io/date-fns": "3.x", "@date-io/date-fns": "3.x",
@ -84,10 +86,12 @@
"@projectstorm/react-diagrams": "^6.6.1", "@projectstorm/react-diagrams": "^6.6.1",
"@simonwep/pickr": "^1.5.1", "@simonwep/pickr": "^1.5.1",
"@szhsin/react-menu": "^2.2.0", "@szhsin/react-menu": "^2.2.0",
"@tanstack/react-query": "5.37.1",
"@tanstack/react-table": "^8.16.0", "@tanstack/react-table": "^8.16.0",
"@tanstack/react-virtual": "^3.4.0", "@tanstack/react-virtual": "^3.4.0",
"@types/react": "^17.0.80", "@types/classnames": "^2.2.6",
"@types/react-dom": "^17.0.25", "@types/react": "^18.0.2",
"@types/react-dom": "^18.0.0",
"ajv": "^8.8.2", "ajv": "^8.8.2",
"anti-trojan-source": "^1.4.0", "anti-trojan-source": "^1.4.0",
"aspen-decorations": "^1.0.2", "aspen-decorations": "^1.0.2",
@ -125,14 +129,14 @@
"postcss": "^8.4.31", "postcss": "^8.4.31",
"raf": "^3.4.1", "raf": "^3.4.1",
"rc-dock": "^3.2.9", "rc-dock": "^3.2.9",
"react": "^17.0.1", "react": "^18.2.0",
"react-arborist": "^3.2.0", "react-arborist": "^3.2.0",
"react-aspen": "^1.1.0", "react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2", "react-checkbox-tree": "^1.7.2",
"react-data-grid": "https://github.com/pgadmin-org/react-data-grid.git#200d2f5e02de694e3e9ffbe177c279bc40240fb8", "react-data-grid": "https://github.com/pgadmin-org/react-data-grid.git#200d2f5e02de694e3e9ffbe177c279bc40240fb8",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^17.0.1", "react-dom": "^18.2.0",
"react-draggable": "^4.4.6", "react-draggable": "^4.4.6",
"react-dropzone": "^14.2.1", "react-dropzone": "^14.2.1",
"react-frame-component": "^5.2.6", "react-frame-component": "^5.2.6",

View File

@ -382,7 +382,7 @@ define('pgadmin.browser.node', [
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem); treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES); const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
const onClose = (force=false)=>pgBrowser.docker.close(panelId, force); const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onSave = (newNodeData)=>{ const onSave = (newNodeData)=>{
// Clear the cache for this node now. // Clear the cache for this node now.
setTimeout(()=>{ setTimeout(()=>{
@ -412,7 +412,7 @@ define('pgadmin.browser.node', [
// browser tree upon the 'Save' button click. // browser tree upon the 'Save' button click.
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem); treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES); const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
const onClose = (force=false)=>pgBrowser.docker.close(panelId, force); const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onSave = (newNodeData)=>{ const onSave = (newNodeData)=>{
// Clear the cache for this node now. // Clear the cache for this node now.
setTimeout(()=>{ setTimeout(()=>{
@ -438,7 +438,7 @@ define('pgadmin.browser.node', [
}); });
} else { } else {
const panelId = BROWSER_PANELS.EDIT_PROPERTIES+nodeData.id; const panelId = BROWSER_PANELS.EDIT_PROPERTIES+nodeData.id;
const onClose = (force=false)=>pgBrowser.docker.close(panelId, force); const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onSave = (newNodeData)=>{ const onSave = (newNodeData)=>{
let _old = nodeData, let _old = nodeData,
_new = newNodeData.node, _new = newNodeData.node,

View File

@ -9,6 +9,7 @@
"""A blueprint module implementing the dashboard frame.""" """A blueprint module implementing the dashboard frame."""
import math import math
import re
from flask import render_template, Response, g, request from flask import render_template, Response, g, request
from flask_babel import gettext from flask_babel import gettext
@ -16,8 +17,7 @@ from pgadmin.user_login_check import pga_login_required
import json import json
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response,\ from pgadmin.utils.ajax import make_response as ajax_response,\
internal_server_error internal_server_error, make_json_response, precondition_required
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \ from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
@ -25,7 +25,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
from .precondition import check_precondition from .precondition import check_precondition
from .pgd_replication import blueprint as pgd_replication from .pgd_replication import blueprint as pgd_replication
from config import PG_DEFAULT_DRIVER from config import PG_DEFAULT_DRIVER, ON_DEMAND_LOG_COUNT
MODULE_NAME = 'dashboard' MODULE_NAME = 'dashboard'
@ -157,17 +157,17 @@ class DashboardModule(PgAdminModule):
self.display_graphs = self.dashboard_preference.register( self.display_graphs = self.dashboard_preference.register(
'display', 'show_graphs', 'display', 'show_graphs',
gettext("Show graphs?"), 'boolean', True, gettext("Show activity?"), 'boolean', True,
category_label=PREF_LABEL_DISPLAY, category_label=PREF_LABEL_DISPLAY,
help_str=gettext('If set to True, graphs ' help_str=gettext('If set to True, activity '
'will be displayed on dashboards.') 'will be displayed on dashboards.')
) )
self.display_server_activity = self.dashboard_preference.register( self.display_server_activity = self.dashboard_preference.register(
'display', 'show_activity', 'display', 'show_activity',
gettext("Show activity?"), 'boolean', True, gettext("Show state?"), 'boolean', True,
category_label=PREF_LABEL_DISPLAY, category_label=PREF_LABEL_DISPLAY,
help_str=gettext('If set to True, activity tables ' help_str=gettext('If set to True, state tables '
'will be displayed on dashboards.') 'will be displayed on dashboards.')
) )
@ -241,7 +241,9 @@ class DashboardModule(PgAdminModule):
'dashboard.get_prepared_by_server_id', 'dashboard.get_prepared_by_server_id',
'dashboard.get_prepared_by_database_id', 'dashboard.get_prepared_by_database_id',
'dashboard.config', 'dashboard.config',
'dashboard.get_config_by_server_id', 'dashboard.log_formats',
'dashboard.logs',
'dashboard.get_logs_by_server_id',
'dashboard.check_system_statistics', 'dashboard.check_system_statistics',
'dashboard.check_system_statistics_sid', 'dashboard.check_system_statistics_sid',
'dashboard.check_system_statistics_did', 'dashboard.check_system_statistics_did',
@ -318,7 +320,8 @@ def index(sid=None, did=None):
) )
def get_data(sid, did, template, check_long_running_query=False): def get_data(sid, did, template, check_long_running_query=False,
only_data=False):
""" """
Generic function to get server stats based on an SQL template Generic function to get server stats based on an SQL template
Args: Args:
@ -347,6 +350,9 @@ def get_data(sid, did, template, check_long_running_query=False):
if check_long_running_query: if check_long_running_query:
get_long_running_query_status(res['rows']) get_long_running_query_status(res['rows'])
if only_data:
return res['rows']
return ajax_response( return ajax_response(
response=res['rows'], response=res['rows'],
status=200 status=200
@ -431,7 +437,15 @@ def activity(sid=None, did=None):
:param sid: server id :param sid: server id
:return: :return:
""" """
return get_data(sid, did, 'activity.sql', True) data = [{
'activity': get_data(sid, did, 'activity.sql', True, True),
'locks': get_data(sid, did, 'locks.sql', only_data=True),
'prepared': get_data(sid, did, 'prepared.sql', only_data=True)
}]
return ajax_response(
response=data,
status=200
)
@blueprint.route('/locks/', endpoint='locks') @blueprint.route('/locks/', endpoint='locks')
@ -466,8 +480,7 @@ def prepared(sid=None, did=None):
return get_data(sid, did, 'prepared.sql') return get_data(sid, did, 'prepared.sql')
@blueprint.route('/config/', endpoint='config') @blueprint.route('/config/<int:sid>', endpoint='config')
@blueprint.route('/config/<int:sid>', endpoint='get_config_by_server_id')
@pga_login_required @pga_login_required
@check_precondition @check_precondition
def config(sid=None): def config(sid=None):
@ -479,6 +492,145 @@ def config(sid=None):
return get_data(sid, None, 'config.sql') return get_data(sid, None, 'config.sql')
@blueprint.route('/log_formats', endpoint='log_formats')
@blueprint.route('/log_formats/<int:sid>', endpoint='log_formats')
@pga_login_required
@check_precondition
def log_formats(sid=None):
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
sql = render_template(
"/".join([g.template_path, 'log_format.sql'])
)
status, _format = g.conn.execute_scalar(sql)
if not status:
return internal_server_error(errormsg=_format)
return ajax_response(
response=_format,
status=200
)
@blueprint.route('/logs/<log_format>/<disp_format>/<int:sid>', endpoint='logs')
@blueprint.route('/logs/<log_format>/<disp_format>/<int:sid>/<int:page>',
endpoint='get_logs_by_server_id')
@pga_login_required
@check_precondition
def logs(log_format=None, disp_format=None, sid=None, page=0):
"""
This function returns server logs details
"""
LOG_STATEMENTS = 'DEBUG:|STATEMENT:|LOG:|WARNING:|NOTICE:|INFO:' \
'|ERROR:|FATAL:|PANIC:'
if not sid:
return internal_server_error(
errormsg=gettext('Server ID not specified.'))
sql = render_template(
"/".join([g.template_path, 'log_format.sql'])
)
status, _format = g.conn.execute_scalar(sql)
# Check the requested format is available or not
log_format = ''
if log_format == 'C' and 'csvlog' in _format:
log_format = 'csvlog'
elif log_format == 'J' and 'jsonlog' in _format:
log_format = 'jsonlog'
sql = render_template(
"/".join([g.template_path, 'log_stat.sql']),
log_format=log_format, conn=g.conn
)
status, res = g.conn.execute_scalar(sql)
if not status:
return internal_server_error(errormsg=res)
if not res or len(res) < 0:
return ajax_response(
response={'logs_disabled': True},
status=200
)
file_stat = json.loads(res[0])
_start = 0
_end = ON_DEMAND_LOG_COUNT
page = int(page)
final_cols = []
if page > 0:
_start = page * int(ON_DEMAND_LOG_COUNT)
_end = _start + int(ON_DEMAND_LOG_COUNT)
if _start < file_stat:
if disp_format == 'plain':
_end = file_stat
sql = render_template(
"/".join([g.template_path, 'logs.sql']), st=_start, ed=_end,
log_format=log_format, conn=g.conn
)
status, res = g.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
final_res = res['rows'][0]['pg_read_file'].split('\n')
# Json format
if log_format == 'J':
for f in final_res:
try:
_tmp_log = json.loads(f)
final_cols.append(
{"error_severity": _tmp_log['error_severity'],
"timestamp": _tmp_log['timestamp'],
"message": _tmp_log['message']})
except Exception as e:
pass
# CSV format
elif log_format == 'C':
for f in final_res:
try:
_tmp_log = f.split(',')
final_cols.append({"error_severity": _tmp_log[11],
"timestamp": _tmp_log[0],
"message": _tmp_log[13]})
except Exception as e:
pass
else:
col1 = []
col2 = []
_pattern = re.compile(LOG_STATEMENTS)
for f in final_res:
tmp = re.search(LOG_STATEMENTS, f)
if not tmp or tmp.group((0)) == 'STATEMENT:':
last_val = final_cols.pop() if len(final_cols) > 0\
else {'message': ''}
last_val['message'] += f
final_cols.append(last_val)
else:
_tmp = re.split(LOG_STATEMENTS, f)
final_cols.append({
"error_severity": tmp.group(0)[:len(tmp.group(0)) - 1],
"timestamp": _tmp[0],
"message": _tmp[1] if len(_tmp) > 1 else ''})
if disp_format == 'plain':
final_response = res['rows']
else:
final_response = final_cols
return ajax_response(
response=final_response,
status=200
)
@blueprint.route( @blueprint.route(
'/cancel_query/<int:sid>/<int:pid>', methods=['DELETE'] '/cancel_query/<int:sid>/<int:pid>', methods=['DELETE']
) )

View File

@ -6,13 +6,13 @@
// This software is released under the PostgreSQL Licence // This software is released under the PostgreSQL Licence
// //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState, Fragment } from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import getApiInstance from 'sources/api_instance'; import getApiInstance from 'sources/api_instance';
import PgTable from 'sources/components/PgTable'; import PgTable from 'sources/components/PgTable';
import { InputCheckbox } from '../../../static/js/components/FormComponents'; import { InputCheckbox, FormInputSwitch, FormInputToggle } from '../../../static/js/components/FormComponents';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
import Graphs from './Graphs'; import Graphs from './Graphs';
import { Box, Tab, Tabs } from '@mui/material'; import { Box, Tab, Tabs } from '@mui/material';
@ -21,6 +21,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
import StopSharpIcon from '@mui/icons-material/StopSharp'; import StopSharpIcon from '@mui/icons-material/StopSharp';
import WelcomeDashboard from './WelcomeDashboard'; import WelcomeDashboard from './WelcomeDashboard';
import ActiveQuery from './ActiveQuery.ui'; import ActiveQuery from './ActiveQuery.ui';
import ServerLog from './ServerLog.ui';
import _ from 'lodash'; import _ from 'lodash';
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';
@ -36,8 +37,11 @@ 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 SectionContainer from './components/SectionContainer';
import Replication from './Replication'; import Replication from './Replication';
import RefreshButton from './components/RefreshButtons';
import { getExpandCell } from '../../../static/js/components/PgReactTableStyled'; import { getExpandCell } from '../../../static/js/components/PgReactTableStyled';
import CodeMirror from '../../../static/js/components/ReactCodeMirror';
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
import { getBrowser } from '../../../static/js/utils';
import RefreshButton from './components/RefreshButtons';
function parseData(data) { function parseData(data) {
let res = []; let res = [];
@ -47,7 +51,6 @@ function parseData(data) {
}); });
return res; return res;
} }
const Root = styled('div')(({theme}) => ({ const Root = styled('div')(({theme}) => ({
height: '100%', height: '100%',
width: '100%', width: '100%',
@ -71,6 +74,16 @@ const Root = styled('div')(({theme}) => ({
'& .Dashboard-terminateButton': { '& .Dashboard-terminateButton': {
color: theme.palette.error.main color: theme.palette.error.main
}, },
'& .Dashboard-download': {
alignSelf: 'end',
'& .Dashboard-downloadButton': {
width: '35px',
height:'30px'
},
},
'& .Dashboard-textArea': {
height: '88%',
}
}, },
}, },
}, },
@ -85,6 +98,7 @@ const Root = styled('div')(({theme}) => ({
})); }));
let activeQSchemaObj = new ActiveQuery(); let activeQSchemaObj = new ActiveQuery();
let serverLogSchemaObj = new ServerLog();
const cellPropTypes = { const cellPropTypes = {
row: PropTypes.any, row: PropTypes.any,
@ -218,8 +232,25 @@ function getCancelCell(pgAdmin, sid, did, canTakeAction, onSuccess) {
return CancelCell; return CancelCell;
} }
function ActiveOnlyHeader({activeOnly, setActiveOnly}) { function CustomRefresh({refresh, setRefresh}) {
return ( return (
<RefreshButton onClick={(e) => {
e.preventDefault();
setRefresh(!refresh);
}}/>
);
}
CustomRefresh.propTypes = {
refresh: PropTypes.bool,
setRefresh: PropTypes.func,
};
function ActiveOnlyHeader({activeOnly, setActiveOnly, refresh, setRefresh}) {
return (<Fragment>
<RefreshButton onClick={(e) => {
e.preventDefault();
setRefresh(!refresh);
}}/>
<InputCheckbox <InputCheckbox
label={gettext('Active sessions only')} label={gettext('Active sessions only')}
labelPlacement="end" labelPlacement="end"
@ -232,35 +263,49 @@ function ActiveOnlyHeader({activeOnly, setActiveOnly}) {
controlProps={{ controlProps={{
label: gettext('Active sessions only'), label: gettext('Active sessions only'),
}} }}
/> /></Fragment>
); );
} }
ActiveOnlyHeader.propTypes = { ActiveOnlyHeader.propTypes = {
activeOnly: PropTypes.bool, activeOnly: PropTypes.bool,
setActiveOnly: PropTypes.func, setActiveOnly: PropTypes.func,
refresh: PropTypes.bool,
setRefresh: PropTypes.func,
}; };
function Dashboard({ function Dashboard({
nodeItem, nodeData, node, treeNodeInfo, nodeItem, nodeData, node, treeNodeInfo,
...props ...props
}) { }) {
const preferences = _.merge(
usePreferences().getPreferencesForModule('dashboards'),
usePreferences().getPreferencesForModule('graphs'),
usePreferences().getPreferencesForModule('misc')
);
let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')]; // Set Active tab depending on preferences setting
let mainTabs = [gettext('General'), gettext('System Statistics')]; let activeTab = 0;
if(treeNodeInfo?.server?.replication_type) { if (!_.isUndefined(preferences) && !preferences.show_graphs && preferences.show_activity) activeTab = 1;
mainTabs.push(gettext('Replication')); else if (!_.isUndefined(preferences) && !preferences.show_graphs && !preferences.show_activity) activeTab = 2;
}
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')]; const api = getApiInstance();
const [dashData, setDashData] = useState([]); const [dashData, setDashData] = useState([]);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const [ssMsg, setSsMsg] = useState(''); const [ssMsg, setSsMsg] = useState('');
const [tabVal, setTabVal] = useState(0); const [mainTabVal, setMainTabVal] = useState(activeTab);
const [mainTabVal, setMainTabVal] = useState(0);
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [activeOnly, setActiveOnly] = useState(false); const [activeOnly, setActiveOnly] = useState(false);
const [systemStatsTabVal, setSystemStatsTabVal] = useState(0); const [systemStatsTabVal, setSystemStatsTabVal] = useState(0);
const [ldid, setLdid] = useState(0); const [ldid, setLdid] = useState(0);
const [logCol, setLogCol] = useState(false);
const [logFormat, setLogFormat] = useState('T');
const [logConfigFormat, setLogConfigFormat] = useState([]);
const [nextPage, setNextPage] = useState(0);
const [hasNextPage, setHasNextPage] = useState(true);
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const systemStatsTabChanged = (e, tabVal) => { const systemStatsTabChanged = (e, tabVal) => {
setSystemStatsTabVal(tabVal); setSystemStatsTabVal(tabVal);
}; };
@ -270,19 +315,13 @@ function Dashboard({
const dbConnected = treeNodeInfo?.database?.connected ?? false; const dbConnected = treeNodeInfo?.database?.connected ?? false;
const serverConnected = treeNodeInfo?.server?.connected ?? false; const serverConnected = treeNodeInfo?.server?.connected ?? false;
const prefStore = usePreferences(); const prefStore = usePreferences();
const preferences = _.merge( let mainTabs = [gettext('Activity'), gettext('State')];
usePreferences().getPreferencesForModule('dashboards'),
usePreferences().getPreferencesForModule('graphs'),
usePreferences().getPreferencesForModule('misc')
);
if (!did) { mainTabs.push(gettext('Configuration'), gettext('Logs'), gettext('System'));
tabs.push(gettext('Configuration')); if(treeNodeInfo?.server?.replication_type) {
mainTabs.push(gettext('Replication'));
} }
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')];
const tabChanged = (e, tabVal) => {
setTabVal(tabVal);
};
const mainTabChanged = (e, tabVal) => { const mainTabChanged = (e, tabVal) => {
setMainTabVal(tabVal); setMainTabVal(tabVal);
@ -390,6 +429,97 @@ function Dashboard({
}, },
]; ];
const downloadServerLogs = async () => {
let extension = '.txt',
type = 'plain',
respData = '';
if(logCol === false) {
if (logFormat == 'C') {
extension = '.csv';
type = 'csv';
} else if (logFormat == 'J') {
extension = '.json';
type = 'json';
}
respData = dashData[0]['pg_read_file'];
} else if (logCol === true) {
extension = '.csv';
type = 'csv';
respData = dashData.map((d)=> {return Object.values(d).join(','); }).join('\n');
}
let fileName = 'data-' + new Date().getTime() + extension;
try {
let respBlob = new Blob([respData], {type : 'text/'+type}),
urlCreator = window.URL || window.webkitURL,
download_url = urlCreator.createObjectURL(respBlob),
link = document.createElement('a');
document.body.appendChild(link);
if (getBrowser() == 'IE' && window.navigator.msSaveBlob) {
// IE10: (has Blob, but not a[download] or URL)
window.navigator.msSaveBlob(respBlob, fileName);
} else {
link.setAttribute('href', download_url);
link.setAttribute('download', fileName);
link.click();
}
document.body.removeChild(link);
} catch (error) {
setSsMsg(gettext('Failed to download the logs.'));
}
};
const serverLogColumns = [
{
header: () => null,
enableSorting: false,
enableResizing: false,
enableFilters: false,
size: 35,
maxSize: 35,
minSize: 35,
id: 'btn-edit',
cell: getExpandCell({
title: gettext('View the log details')
}),
},
{
accessorKey: 'error_severity',
header: gettext('Error Severity'),
enableSorting: true,
enableResizing: true,
enableFilters: true,
size: 100,
minSize: 35,
filterFn: 'equalsString'
},
{
accessorKey: 'timestamp',
header: gettext('Log Prefix/Timestamp'),
sortable: true,
enableResizing: true,
enableSorting: false,
enableFilters: true,
size: 150,
minSize: 35,
filterFn: 'equalsString'
},
{
accessorKey: 'message',
header: gettext('Logs'),
enableResizing: true,
enableSorting: false,
enableFilters: false,
size: 35,
minSize: 200,
filterFn: 'equalsString'
},
];
const activityColumns = [ const activityColumns = [
{ {
header: () => null, header: () => null,
@ -682,15 +812,37 @@ function Dashboard({
enableFilters: true, enableFilters: true,
}, },
]; ];
useEffect(() => {
if (mainTabVal == 3) {
setLogFormat('T');
let url = url_for('dashboard.log_formats') + '/' + sid;
api({
url: url,
type: 'GET',
})
.then((res) => {
let _format = res.data;
let _frm = [
{'label': gettext('Text'), 'value': 'T', 'disabled': !_format.includes('stderr')},
{'label': gettext('JSON'), 'value': 'J', 'disabled': !_format.includes('jsonlog')},
{'label': gettext('CSV'), 'value': 'C', 'disabled': !_format.includes('csvlog')}
];
setLogConfigFormat(_frm);
})
.catch((error) => {
pgAdmin.Browser.notifier.alert(
gettext('Failed to retrieve data from the server.'),
_.isUndefined(error.response) ? error.message : error.response.data.errormsg
);
});
}
},[nodeData, mainTabVal]);
useEffect(() => { useEffect(() => {
// Reset Tab values to 0, so that it will select "Sessions" on node changed.
nodeData?._type === 'database' && setTabVal(0);
},[nodeData]);
useEffect(() => { if (mainTabVal == 0) return;
// disable replication tab // disable replication tab
if(!treeNodeInfo?.server?.replication_type && mainTabVal == 2) { if(!treeNodeInfo?.server?.replication_type && mainTabVal == 5) {
setMainTabVal(0); setMainTabVal(0);
} }
@ -700,39 +852,44 @@ function Dashboard({
'Please connect to the selected server to view the dashboard.' 'Please connect to the selected server to view the dashboard.'
); );
if(tabVal == 3 && did) {
setTabVal(0);
}
if (sid && serverConnected) { if (sid && serverConnected) {
if (tabVal === 0) {
url = url_for('dashboard.activity');
} else if (tabVal === 1) {
url = url_for('dashboard.locks');
} else if (tabVal === 2) {
url = url_for('dashboard.prepared');
} else {
url = url_for('dashboard.config');
}
message = gettext('Loading dashboard...'); message = gettext('Loading dashboard...');
if (did && !dbConnected) return; if (did && !dbConnected) return;
if (did) url += sid + '/' + did;
else url += sid;
if (did && !dbConnected) return; if (mainTabVal === 1) {
url = url_for('dashboard.activity');
if (did) url += sid + '/' + did;
else url += '/' + sid;
} else if (mainTabVal === 2) {
url = url_for('dashboard.config', {'sid': sid});
} else if (mainTabVal === 3) {
if(logCol === false) {
url = url_for('dashboard.logs', {'log_format': logFormat, 'disp_format': 'plain', 'sid': sid});
} else if (logCol === true) {
url = url_for('dashboard.logs', {'log_format': logFormat, 'disp_format': 'table', 'sid': sid});
setNextPage(0);
}
}
if (did && did > 0) ssExtensionCheckUrl += '/' + sid + '/' + did; if (did && did > 0) ssExtensionCheckUrl += '/' + sid + '/' + did;
else ssExtensionCheckUrl += '/' + sid; else ssExtensionCheckUrl += '/' + sid;
const api = getApiInstance();
if (node) { if (node) {
if (mainTabVal == 0) { setSsMsg(gettext('Loading logs...'));
setDashData([]);
if (mainTabVal != 4 && mainTabVal != 5) {
api({ api({
url: url, url: url,
type: 'GET', type: 'GET',
}) })
.then((res) => { .then((res) => {
if (res.data && res.data['logs_disabled']) {
setSsMsg(gettext('Please enable the logging to view the server logs.'));
} else {
setDashData(parseData(res.data)); setDashData(parseData(res.data));
}
}) })
.catch((error) => { .catch((error) => {
pgAdmin.Browser.notifier.alert( pgAdmin.Browser.notifier.alert(
@ -743,7 +900,7 @@ function Dashboard({
setMsg(gettext('Failed to retrieve data from the server.')); setMsg(gettext('Failed to retrieve data from the server.'));
}); });
} }
else if (mainTabVal == 1) { else if (mainTabVal == 4) {
api({ api({
url: ssExtensionCheckUrl, url: ssExtensionCheckUrl,
type: 'GET', type: 'GET',
@ -773,15 +930,15 @@ function Dashboard({
if (message != '') { if (message != '') {
setMsg(message); setMsg(message);
} }
}, [nodeData, tabVal, treeNodeInfo, prefStore, refresh, mainTabVal]); }, [nodeData, treeNodeInfo, prefStore, refresh, mainTabVal, logCol, logFormat]);
const filteredDashData = useMemo(()=>{ const filteredDashData = useMemo(()=>{
if (tabVal == 0 && activeOnly) { if (mainTabVal == 1 && activeOnly) {
// we want to show 'idle in transaction', 'active', 'active in transaction', and future non-blank, non-"idle" status values // we want to show 'idle in transaction', 'active', 'active in transaction', and future non-blank, non-"idle" status values
return dashData.filter((r)=>(r.state && r.state != '' && r.state != 'idle')); return dashData[0]['activity'].filter((r)=>(r.state && r.state != '' && r.state != 'idle'));
} }
return dashData; return dashData && dashData[0] && dashData[0]['activity'] || [];
}, [dashData, activeOnly, tabVal]); }, [dashData, activeOnly, mainTabVal]);
const showDefaultContents = () => { const showDefaultContents = () => {
return ( return (
@ -804,6 +961,83 @@ function Dashboard({
); );
}; };
const CustomLogHeaderLabel =
{
label: gettext('Table based logs'),
};
const CustomLogHeader = () => {
return ( <Box className='Dashboard-cardHeader' display="flex" flexDirection="column">
<FormInputToggle
label={gettext('Log Format')}
className='Dashboard-searchInput'
value={logFormat}
onChange={(val) => {
setLogFormat(val);
}}
options={logConfigFormat}
controlProps={CustomLogHeaderLabel}
labelGridBasis={3}
controlGridBasis={6}
></FormInputToggle>
<FormInputSwitch
label={gettext('Logs in tabular format ?')}
labelPlacement="end"
className='Dashboard-searchInput'
value={logCol}
onChange={(e) => {
setDashData([]);
setLogCol(e.target.checked);
}}
controlProps={CustomLogHeaderLabel}
labelGridBasis={3}
controlGridBasis={6}
></FormInputSwitch>
<div className='Dashboard-download'><PgIconButton
size="xs"
className='Dashboard-downloadButton'
icon={<GetAppRoundedIcon />}
onClick={downloadServerLogs}
aria-label="Download"
title={gettext('Download logs ')}
></PgIconButton></div>
</Box>);
};
const loadNextPage = () => {
setIsNextPageLoading(true);
setTimeout(() => {
setHasNextPage(true);
setIsNextPageLoading(false);
let _url = url_for('dashboard.logs', {'log_format': logFormat, 'disp_format': 'table', 'sid': sid});
_url += '/' + (nextPage +1);
const api = getApiInstance();
api({
url: _url,
type: 'GET',
})
.then((res) => {
console.warn(res.data.length);
if (res.data && res.data.length > 0) {
let _d = dashData.concat(parseData(res.data));
setDashData(_d);
setNextPage(nextPage + 1);
}
})
.catch((error) => {
pgAdmin.Browser.notifier.alert(
gettext('Failed to retrieve data from the server.'),
_.isUndefined(error.response) ? error.message : error.response.data.errormsg
);
// show failed message.
setMsg(gettext('Failed to retrieve data from the server.'));
});
}, 500);
};
return ( return (
(<Root> (<Root>
{sid && serverConnected ? ( {sid && serverConnected ? (
@ -815,14 +1049,25 @@ function Dashboard({
value={mainTabVal} value={mainTabVal}
onChange={mainTabChanged} onChange={mainTabChanged}
> >
{mainTabs.map((tabValue) => { {mainTabs.map((tabValue, i) => {
return <Tab key={tabValue} label={tabValue} />; if (tabValue == 'Activity') {
if (!_.isUndefined(preferences) && preferences.show_graphs) {
return <Tab key={tabValue} label={tabValue} value={i}/>;
}
} else if (tabValue == 'State') {
if (!_.isUndefined(preferences) && preferences.show_activity) {
return <Tab key={tabValue} label={tabValue} value={i}/>;
}
}
else {
return <Tab key={tabValue} label={tabValue} value={i}/>;
}
})} })}
</Tabs> </Tabs>
</Box> </Box>
{/* General Statistics */} {/* Server Activity */}
<TabPanel value={mainTabVal} index={0}>
{!_.isUndefined(preferences) && preferences.show_graphs && ( {!_.isUndefined(preferences) && preferences.show_graphs && (
<TabPanel value={mainTabVal} index={0}>
<Graphs <Graphs
key={sid + did} key={sid + did}
preferences={preferences} preferences={preferences}
@ -830,50 +1075,46 @@ function Dashboard({
did={did} did={did}
pageVisible={props.isActive} pageVisible={props.isActive}
></Graphs> ></Graphs>
</TabPanel>
)} )}
{/* Server Activity */}
<TabPanel value={mainTabVal} index={1} classNameRoot='Dashboard-tabPanel'>
{!_.isUndefined(preferences) && preferences.show_activity && ( {!_.isUndefined(preferences) && preferences.show_activity && (
<SectionContainer title={dbConnected ? gettext('Database activity') : gettext('Server activity')}> <Fragment>
<Box> <SectionContainer title={gettext('Sessions')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}
<Tabs
value={tabVal}
onChange={tabChanged}
> >
{tabs.map((tabValue) => {
return <Tab key={tabValue} label={tabValue} />;
})}
<RefreshButton onClick={(e) => {
e.preventDefault();
setRefresh(!refresh);
}}/>
</Tabs>
</Box>
<TabPanel value={tabVal} index={0}>
<PgTable <PgTable
caveTable={false} caveTable={false}
tableNoBorder={false} tableNoBorder={false}
customHeader={<ActiveOnlyHeader activeOnly={activeOnly} setActiveOnly={setActiveOnly} />} customHeader={<ActiveOnlyHeader activeOnly={activeOnly} setActiveOnly={setActiveOnly} refresh={refresh} setRefresh={setRefresh}/>}
columns={activityColumns} columns={activityColumns}
data={filteredDashData} data={(dashData !== undefined && dashData[0] && filteredDashData) || []}
schema={activeQSchemaObj} schema={activeQSchemaObj}
></PgTable> ></PgTable>
</TabPanel> </SectionContainer>
<TabPanel value={tabVal} index={1}> <SectionContainer title={gettext('Locks')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}>
<PgTable <PgTable
customHeader={<CustomRefresh refresh={refresh} setRefresh={setRefresh}/>}
caveTable={false} caveTable={false}
tableNoBorder={false} tableNoBorder={false}
columns={databaseLocksColumns} columns={databaseLocksColumns}
data={dashData} data={(dashData !== undefined && dashData[0] && dashData[0]['locks']) || []}
></PgTable> ></PgTable>
</TabPanel> </SectionContainer>
<TabPanel value={tabVal} index={2}> <SectionContainer title={gettext('Prepared Transactions')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}>
<PgTable <PgTable
customHeader={<CustomRefresh refresh={refresh} setRefresh={setRefresh}/>}
caveTable={false} caveTable={false}
tableNoBorder={false} tableNoBorder={false}
columns={databasePreparedColumns} columns={databasePreparedColumns}
data={dashData} data={(dashData !== undefined && dashData[0] && dashData[0]['prepared']) || []}
></PgTable> ></PgTable>
</SectionContainer>
</Fragment>
)}
</TabPanel> </TabPanel>
<TabPanel value={tabVal} index={3}> {/* Server Configuration */}
<TabPanel value={mainTabVal} index={2} classNameRoot='Dashboard-tabPanel'>
<PgTable <PgTable
caveTable={false} caveTable={false}
tableNoBorder={false} tableNoBorder={false}
@ -881,11 +1122,37 @@ function Dashboard({
data={dashData} data={dashData}
></PgTable> ></PgTable>
</TabPanel> </TabPanel>
</SectionContainer> {/* Server Logs */}
)} <TabPanel value={mainTabVal} index={3} classNameRoot='Dashboard-tabPanel'>
{dashData && dashData.length != 0 &&
<CustomLogHeader/>}
{dashData.length == 0 && <div className='Dashboard-emptyPanel'>
<EmptyPanelMessage text={ssMsg}/>
</div>}
{dashData && logCol === false && dashData.length == 1 && <CodeMirror
id='tests'
language={logFormat== 'J' ? 'json':'pgsql'}
className='Dashboard-textArea'
value={dashData[0]['pg_read_file']}
readonly={true}
options={{
lineNumbers: true,
mode: 'text/plain',
}}
/>}
{dashData && logCol === true && <PgTable
caveTable={false}
tableNoBorder={false}
columns={serverLogColumns}
data={dashData}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={loadNextPage}
schema={serverLogSchemaObj}
></PgTable>}
</TabPanel> </TabPanel>
{/* System Statistics */} {/* System Statistics */}
<TabPanel value={mainTabVal} index={1} classNameRoot='Dashboard-tabPanel'> <TabPanel value={mainTabVal} index={4} classNameRoot='Dashboard-tabPanel'>
<Box height="100%" display="flex" flexDirection="column"> <Box height="100%" display="flex" flexDirection="column">
{ssMsg === 'installed' && did === ldid ? {ssMsg === 'installed' && did === ldid ?
<ErrorBoundary> <ErrorBoundary>
@ -948,7 +1215,7 @@ function Dashboard({
</Box> </Box>
</TabPanel> </TabPanel>
{/* Replication */} {/* Replication */}
<TabPanel value={mainTabVal} index={2} classNameRoot='Dashboard-tabPanel'> <TabPanel value={mainTabVal} index={5} classNameRoot='Dashboard-tabPanel'>
<Replication key={sid} sid={sid} node={node} <Replication key={sid} sid={sid} node={node}
preferences={preferences} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} /> preferences={preferences} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
</TabPanel> </TabPanel>
@ -973,6 +1240,7 @@ Dashboard.propTypes = {
serverConnected: PropTypes.bool, serverConnected: PropTypes.bool,
dbConnected: PropTypes.bool, dbConnected: PropTypes.bool,
isActive: PropTypes.bool, isActive: PropTypes.bool,
column: PropTypes.object,
}; };
export default withStandardTabInfo(Dashboard, BROWSER_PANELS.DASHBOARD); export default withStandardTabInfo(Dashboard, BROWSER_PANELS.DASHBOARD);

View File

@ -0,0 +1,51 @@
/////////////////////////////////////////////////////////////
//
// 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 ServerLog extends BaseUISchema {
constructor(initValues) {
super({
...initValues,
});
}
get baseFields() {
return [
{
id: 'error_severity',
label: gettext('Error severity'),
type: 'text',
editable: false,
noEmpty: false,
readonly: true,
group: gettext('Details'),
},{
id: 'timestamp',
label: gettext('Log line prefix/timestamp'),
type: 'text',
editable: false,
noEmpty: false,
readonly: true,
group: gettext('Details'),
},{
id: 'message',
label: gettext('Log'),
type: 'text',
editable: false,
readonly: true,
group: gettext('Details'),
}
];
}
}

View File

@ -0,0 +1,6 @@
SELECT
setting
FROM
pg_show_all_settings()
WHERE
name='log_destination'

View File

@ -0,0 +1,6 @@
/*pga4dash*/
{% if log_format != '' %}
SELECT pg_stat_file(pg_current_logfile('{{log_format}}'));
{% else %}
SELECT pg_stat_file(pg_current_logfile());
{% endif %}

View File

@ -0,0 +1,8 @@
/*pga4dash*/
{% if log_format != '' %}
SELECT pg_read_file(pg_current_logfile('{{log_format}}'), {{ st }}, {{ ed }});
{% elif st !='' and ed != '' %}
SELECT pg_read_file(pg_current_logfile(), {{ st }}, {{ ed }});
{% else %}
SELECT pg_read_file(pg_current_logfile());
{% endif %}

View File

@ -228,6 +228,7 @@ def validate_binary_path():
running the utilities with their versions. running the utilities with their versions.
""" """
data = None data = None
return precondition_required(gettext('Invalid binary path.'))
if hasattr(request.data, 'decode'): if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8') data = request.data.decode('utf-8')

View File

@ -8,7 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import BrowserComponent from '../js/BrowserComponent'; import BrowserComponent from '../js/BrowserComponent';
import MainMenuFactory from '../../browser/static/js/MainMenuFactory'; import MainMenuFactory from '../../browser/static/js/MainMenuFactory';
import Theme from '../js/Theme'; import Theme from '../js/Theme';
@ -50,10 +50,10 @@ define('app', [
// Create menus after all modules are initialized. // Create menus after all modules are initialized.
MainMenuFactory.createMainMenus(); MainMenuFactory.createMainMenus();
ReactDOM.render( const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<Theme> <Theme>
<BrowserComponent pgAdmin={pgAdmin} /> <BrowserComponent pgAdmin={pgAdmin} />
</Theme>, </Theme>
document.querySelector('#root')
); );
}); });

View File

@ -7,7 +7,7 @@
// //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import HiddenIframe from './HiddenIframe'; import HiddenIframe from './HiddenIframe';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
@ -39,6 +39,8 @@ export function onlineHelpSearch(param, props) {
if(document.getElementById('hidden-quick-search-iframe')){ if(document.getElementById('hidden-quick-search-iframe')){
document.getElementById('hidden-quick-search-iframe').contentDocument.location.reload(true); document.getElementById('hidden-quick-search-iframe').contentDocument.location.reload(true);
} }
const root = ReactDOM.createRoot(document.getElementById('quick-search-iframe-container'));
// Below function will be called when the page will be loaded in Iframe // Below function will be called when the page will be loaded in Iframe
const _iframeLoaded = () => { const _iframeLoaded = () => {
@ -69,7 +71,7 @@ export function onlineHelpSearch(param, props) {
data: res, data: res,
})); }));
isIFrameLoaded = false; isIFrameLoaded = false;
ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container')); root.unmount();
} else { } else {
setState(state => ({ setState(state => ({
...state, ...state,
@ -87,7 +89,7 @@ export function onlineHelpSearch(param, props) {
url: srcURL, url: srcURL,
data: {}, data: {},
})); }));
ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container')); root.unmount();
isIFrameLoaded = false; isIFrameLoaded = false;
window.clearInterval(pooling); window.clearInterval(pooling);
} }
@ -98,8 +100,7 @@ export function onlineHelpSearch(param, props) {
}; };
// Render IFrame // Render IFrame
ReactDOM.render( root.render(
<HiddenIframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>, <HiddenIframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>
document.getElementById('quick-search-iframe-container'),
); );
} }

View File

@ -627,7 +627,6 @@ function SchemaDialogView({
type: SCHEMA_STATE_ACTIONS.INIT, type: SCHEMA_STATE_ACTIONS.INIT,
payload: schema.origData, payload: schema.origData,
}); });
return true;
}, [props.resetKey]); }, [props.resetKey]);
const onResetClick = ()=>{ const onResetClick = ()=>{

View File

@ -1,4 +1,4 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import React from 'react'; import React from 'react';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import Theme from '../Theme/index'; import Theme from '../Theme/index';
@ -18,20 +18,18 @@ window.renderSecurityPage = function(pageName, pageProps, otherProps) {
}; };
const Page = ComponentPageMap[pageName]; const Page = ComponentPageMap[pageName];
const root = ReactDOM.createRoot(document.querySelector('#root'));
if(Page) { if(Page) {
ReactDOM.render( root.render(<Theme>
<Theme>
<SnackbarProvider <SnackbarProvider
maxSnack={5} maxSnack={5}
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}> anchorOrigin={{ horizontal: 'right', vertical: 'top' }}>
<Page {...pageProps} {...otherProps} /> <Page {...pageProps} {...otherProps} />
</SnackbarProvider> </SnackbarProvider>
</Theme> </Theme>);
, document.querySelector('#root'));
} else { } else {
ReactDOM.render( root.render(
<h1>Invalid Page</h1> <h1>Invalid Page</h1>);
, document.querySelector('#root'));
} }
}; };

View File

@ -8,7 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React, { useEffect, useLayoutEffect, useRef } from 'react'; import React, { useEffect, useLayoutEffect, useRef } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import { usePgAdmin } from './BrowserComponent'; import { usePgAdmin } from './BrowserComponent';
import { BROWSER_PANELS } from '../../browser/static/js/constants'; import { BROWSER_PANELS } from '../../browser/static/js/constants';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -46,7 +46,8 @@ export default function ToolView() {
const newWin = window.open('', '_blank'); const newWin = window.open('', '_blank');
const div = newWin.document.createElement('div'); const div = newWin.document.createElement('div');
newWin.document.body.appendChild(div); newWin.document.body.appendChild(div);
ReactDOM.render( const root = ReactDOM.createRoot(div);
root.render(
<ToolForm actionUrl={window.location.origin+toolUrl} params={formParams}/>, div <ToolForm actionUrl={window.location.origin+toolUrl} params={formParams}/>, div
); );
} else { } else {

View File

@ -306,7 +306,7 @@ PgReactTableBody.propTypes = {
children: CustomPropTypes.children, children: CustomPropTypes.children,
}; };
export const PgReactTable = forwardRef(({children, table, rootClassName, tableClassName, ...props}, ref)=>{ export const PgReactTable = forwardRef(({children, table, rootClassName, tableClassName, onScrollFunc, ...props}, ref)=>{
const columns = table.getAllColumns(); const columns = table.getAllColumns();
useEffect(()=>{ useEffect(()=>{
@ -333,7 +333,7 @@ export const PgReactTable = forwardRef(({children, table, rootClassName, tableCl
}, [columns, table.getState().columnSizingInfo]); }, [columns, table.getState().columnSizingInfo]);
return ( return (
<StyledDiv className={['pgrt', rootClassName].join(' ')} ref={ref} > <StyledDiv className={['pgrt', rootClassName].join(' ')} ref={ref} onScroll={e => onScrollFunc?.(e.target)}>
<div className={['pgrt-table', tableClassName].join(' ')} style={{ ...columnSizeVars }} {...props}> <div className={['pgrt-table', tableClassName].join(' ')} style={{ ...columnSizeVars }} {...props}>
{children} {children}
</div> </div>
@ -346,6 +346,7 @@ PgReactTable.propTypes = {
rootClassName: PropTypes.any, rootClassName: PropTypes.any,
tableClassName: PropTypes.any, tableClassName: PropTypes.any,
children: CustomPropTypes.children, children: CustomPropTypes.children,
onScrollFunc: PropTypes.any,
}; };
export function getExpandCell({ onClick, title }) { export function getExpandCell({ onClick, title }) {

View File

@ -8,6 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React, { useMemo, useRef } from 'react'; import React, { useMemo, useRef } from 'react';
import { import {
useReactTable, useReactTable,
getCoreRowModel, getCoreRowModel,
@ -16,6 +17,12 @@ import {
getExpandedRowModel, getExpandedRowModel,
flexRender, flexRender,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
keepPreviousData,
} from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -74,7 +81,7 @@ TableRow.propTypes = {
measureElement: PropTypes.func, measureElement: PropTypes.func,
}; };
export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal, ...props }) { export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal, loadNextPage, ...props }) {
const defaultColumn = React.useMemo( const defaultColumn = React.useMemo(
() => ({ () => ({
size: 150, size: 150,
@ -106,10 +113,55 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
// Render the UI for your table // Render the UI for your table
const tableRef = useRef(); const tableRef = useRef();
let flatData = [];
let fetchMoreOnBottomReached = undefined;
let totalFetched = 0;
let totalDBRowCount = 0;
if (loadNextPage) {
//Infinite scrolling
const { _data, fetchNextPage, isFetching } =
useInfiniteQuery({
queryKey: ['logs'],
queryFn: async () => {
const fetchedData = await loadNextPage();
return fetchedData;
},
initialPageParam: 0,
getNextPageParam: (_lastGroup, groups) => groups.length,
refetchOnWindowFocus: false,
placeholderData: keepPreviousData,
});
flatData = _data || [];
totalFetched = flatData.length;
//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
fetchMoreOnBottomReached = React.useCallback(
(containerRefElement = HTMLDivElement | null) => {
if (containerRefElement) {
const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
//once the user has scrolled within 500px of the bottom of the table, fetch more data if we can
if (
scrollHeight - scrollTop - clientHeight < 500 &&
!isFetching
) {
fetchNextPage();
}
}
},
[fetchNextPage, isFetching, totalFetched, totalDBRowCount]
);
//a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
React.useEffect(() => {
fetchMoreOnBottomReached(tableRef.current);
}, [fetchMoreOnBottomReached]);
}
const table = useReactTable({ const table = useReactTable({
columns: finalColumns, columns: finalColumns,
data, data: flatData.length >0 ? flatData : data,
defaultColumn, defaultColumn,
autoResetAll: false, autoResetAll: false,
initialState: { initialState: {
@ -144,7 +196,7 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
}); });
return ( return (
<PgReactTable ref={tableRef} table={table}> <PgReactTable ref={tableRef} table={table} onScrollFunc={loadNextPage?fetchMoreOnBottomReached: null }>
<PgReactTableHeader table={table} /> <PgReactTableHeader table={table} />
{rows.length == 0 ? {rows.length == 0 ?
<EmptyPanelMessage text={gettext('No rows found')} /> : <EmptyPanelMessage text={gettext('No rows found')} /> :
@ -172,6 +224,7 @@ Table.propTypes = {
selectedRows: PropTypes.object, selectedRows: PropTypes.object,
setSelectedRows: PropTypes.func, setSelectedRows: PropTypes.func,
searchVal: PropTypes.string, searchVal: PropTypes.string,
loadNextPage: PropTypes.func,
}; };
const StyledPgTableRoot = styled('div')(({theme})=>({ const StyledPgTableRoot = styled('div')(({theme})=>({
@ -214,13 +267,16 @@ const StyledPgTableRoot = styled('div')(({theme})=>({
}, },
})); }));
const queryClient = new QueryClient();
export default function PgTable({ caveTable = true, tableNoBorder = true, ...props }) { export default function PgTable({ caveTable = true, tableNoBorder = true, ...props }) {
const [searchVal, setSearchVal] = React.useState(''); const [searchVal, setSearchVal] = React.useState('');
return ( return (
<QueryClientProvider client={queryClient}>
<StyledPgTableRoot className={[tableNoBorder ? '' : 'pgtable-pgrt-border', caveTable ? 'pgtable-pgrt-cave' : ''].join(' ')} data-test={props['data-test']}> <StyledPgTableRoot className={[tableNoBorder ? '' : 'pgtable-pgrt-border', caveTable ? 'pgtable-pgrt-cave' : ''].join(' ')} data-test={props['data-test']}>
<Box className='pgtable-header'> <Box className='pgtable-header'>
{props.customHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}>{props.customHeader}</Box>)} {props.customHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}> {props.customHeader }</Box>)}
<Box marginLeft="auto"> <Box marginLeft="auto">
<InputText <InputText
placeholder={gettext('Search')} placeholder={gettext('Search')}
@ -233,10 +289,11 @@ export default function PgTable({ caveTable = true, tableNoBorder = true, ...pro
/> />
</Box> </Box>
</Box> </Box>
<div className={'pgtable-body'}> <div className={'pgtable-body'} >
<Table {...props} searchVal={searchVal} /> <Table {...props} searchVal={searchVal}/>
</div> </div>
</StyledPgTableRoot> </StyledPgTableRoot>
</QueryClientProvider>
); );
} }

View File

@ -42,6 +42,7 @@ import {
import syntaxHighlighting from '../extensions/highlighting'; import syntaxHighlighting from '../extensions/highlighting';
import PgSQL from '../extensions/dialect'; import PgSQL from '../extensions/dialect';
import { sql } from '@codemirror/lang-sql'; import { sql } from '@codemirror/lang-sql';
import { json } from '@codemirror/lang-json';
import errorMarkerExtn from '../extensions/errorMarker'; import errorMarkerExtn from '../extensions/errorMarker';
import CustomEditorView from '../CustomEditorView'; import CustomEditorView from '../CustomEditorView';
import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter'; import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter';
@ -126,9 +127,6 @@ const defaultExtensions = [
preventDefault: true, preventDefault: true,
run: deleteCharBackwardStrict, run: deleteCharBackwardStrict,
}]), }]),
sql({
dialect: PgSQL,
}),
PgSQL.language.data.of({ PgSQL.language.data.of({
autocomplete: false, autocomplete: false,
}), }),
@ -151,7 +149,7 @@ const defaultExtensions = [
export default function Editor({ export default function Editor({
currEditor, name, value, options, onCursorActivity, onChange, readonly, disabled, autocomplete = false, currEditor, name, value, options, onCursorActivity, onChange, readonly, disabled, autocomplete = false,
breakpoint = false, onBreakPointChange, showActiveLine=false, breakpoint = false, onBreakPointChange, showActiveLine=false,
keepHistory = true, cid, helpid, labelledBy, customKeyMap}) { keepHistory = true, cid, helpid, labelledBy, customKeyMap, language='pgsql'}) {
const editorContainerRef = useRef(); const editorContainerRef = useRef();
const editor = useRef(); const editor = useRef();
@ -170,6 +168,7 @@ export default function Editor({
useEffect(() => { useEffect(() => {
const finalOptions = { ...defaultOptions, ...options }; const finalOptions = { ...defaultOptions, ...options };
const finalExtns = [ const finalExtns = [
(language == 'json') ? json() : sql({dialect: PgSQL}),
...defaultExtensions, ...defaultExtensions,
]; ];
if (finalOptions.lineNumbers) { if (finalOptions.lineNumbers) {
@ -399,4 +398,5 @@ Editor.propTypes = {
helpid: PropTypes.string, helpid: PropTypes.string,
labelledBy: PropTypes.string, labelledBy: PropTypes.string,
customKeyMap: PropTypes.array, customKeyMap: PropTypes.array,
language: PropTypes.string,
}; };

View File

@ -9,7 +9,7 @@
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { sprintf } from 'sources/utils'; import { sprintf } from 'sources/utils';
@ -570,7 +570,8 @@ export default class DebuggerModule {
); );
await listenPreferenceBroadcast(); await listenPreferenceBroadcast();
ReactDOM.render( const root = ReactDOM.createRoot(container);
root.render(
<Theme> <Theme>
<PgAdminContext.Provider value={pgAdmin}> <PgAdminContext.Provider value={pgAdmin}>
<ModalProvider> <ModalProvider>
@ -586,8 +587,7 @@ export default class DebuggerModule {
/> />
</ModalProvider> </ModalProvider>
</PgAdminContext.Provider> </PgAdminContext.Provider>
</Theme>, </Theme>
container
); );
} }

View File

@ -12,7 +12,7 @@ import {getRandomInt} from 'sources/utils';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import ERDTool from './erd_tool/components/ERDTool'; import ERDTool from './erd_tool/components/ERDTool';
import ModalProvider from '../../../../static/js/helpers/ModalProvider'; import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme'; import Theme from '../../../../static/js/Theme';
@ -144,7 +144,8 @@ export default class ERDModule {
async loadComponent(container, params) { async loadComponent(container, params) {
pgAdmin.Browser.keyboardNavigation.init(); pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast(); await listenPreferenceBroadcast();
ReactDOM.render( const root = ReactDOM.createRoot(container);
root.render(
<Theme> <Theme>
<PgAdminContext.Provider value={pgAdmin}> <PgAdminContext.Provider value={pgAdmin}>
<ModalProvider> <ModalProvider>
@ -158,8 +159,7 @@ export default class ERDModule {
/> />
</ModalProvider> </ModalProvider>
</PgAdminContext.Provider> </PgAdminContext.Provider>
</Theme>, </Theme>
container
); );
} }
} }

View File

@ -27,7 +27,7 @@ import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import * as csrfToken from 'sources/csrf'; import * as csrfToken from 'sources/csrf';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
export default class Psql { export default class Psql {
@ -184,7 +184,8 @@ export default class Psql {
async loadComponent(container, params) { async loadComponent(container, params) {
pgAdmin.Browser.keyboardNavigation.init(); pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast(); await listenPreferenceBroadcast();
ReactDOM.render( const root = ReactDOM.createRoot(container);
root.render(
<Theme> <Theme>
<PgAdminContext.Provider value={pgAdmin}> <PgAdminContext.Provider value={pgAdmin}>
<ModalProvider> <ModalProvider>
@ -192,8 +193,7 @@ export default class Psql {
<PsqlComponent params={params} pgAdmin={pgAdmin} /> <PsqlComponent params={params} pgAdmin={pgAdmin} />
</ModalProvider> </ModalProvider>
</PgAdminContext.Provider> </PgAdminContext.Provider>
</Theme>, </Theme>
container
); );
} }

View File

@ -8,7 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
@ -102,8 +102,8 @@ export default class SchemaDiff {
async load(container, trans_id) { async load(container, trans_id) {
pgAdmin.Browser.keyboardNavigation.init(); pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast(); await listenPreferenceBroadcast();
const root = ReactDOM.createRoot(container);
ReactDOM.render( root.render(
<Theme> <Theme>
<PgAdminContext.Provider value={pgAdmin}> <PgAdminContext.Provider value={pgAdmin}>
<ModalProvider> <ModalProvider>
@ -111,8 +111,7 @@ export default class SchemaDiff {
<SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin }}></SchemaDiffComponent> <SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin }}></SchemaDiffComponent>
</ModalProvider> </ModalProvider>
</PgAdminContext.Provider> </PgAdminContext.Provider>
</Theme>, </Theme>
container
); );
} }

View File

@ -19,7 +19,7 @@ import 'pgadmin.tools.user_management';
import 'pgadmin.tools.file_manager'; import 'pgadmin.tools.file_manager';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import QueryToolComponent from './components/QueryToolComponent'; import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider'; import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme'; import Theme from '../../../../static/js/Theme';
@ -228,7 +228,8 @@ export default class SQLEditor {
); );
pgAdmin.Browser.keyboardNavigation.init(); pgAdmin.Browser.keyboardNavigation.init();
await listenPreferenceBroadcast(); await listenPreferenceBroadcast();
ReactDOM.render( const root = ReactDOM.createRoot(container);
root.render(
<Theme> <Theme>
<PgAdminContext.Provider value={pgAdmin}> <PgAdminContext.Provider value={pgAdmin}>
<ModalProvider> <ModalProvider>
@ -237,8 +238,7 @@ export default class SQLEditor {
qtPanelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`} selectedNodeInfo={selectedNodeInfo}/> qtPanelId={`${BROWSER_PANELS.QUERY_TOOL}_${params.trans_id}`} selectedNodeInfo={selectedNodeInfo}/>
</ModalProvider> </ModalProvider>
</PgAdminContext.Provider> </PgAdminContext.Provider>
</Theme>, </Theme>
container
); );
} }
} }

View File

@ -388,7 +388,7 @@ export function QueryHistory() {
}); });
}, []); }, []);
React.useEffect(async ()=>{ const fetchQueryHistory = async() =>{
if(!queryToolConnCtx.connected) { if(!queryToolConnCtx.connected) {
return; return;
} }
@ -430,7 +430,11 @@ export function QueryHistory() {
listRef.current?.focus(); listRef.current?.focus();
eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory); eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
}, [queryToolConnCtx.connected]); };
React.useEffect(() =>{
fetchQueryHistory();
},[queryToolConnCtx.connected]);
const onRemove = async ()=>{ const onRemove = async ()=>{
setLoaderText(gettext('Removing history entry...')); setLoaderText(gettext('Removing history entry...'));

View File

@ -128,9 +128,6 @@ PSYCOPG_SUPPORTED_MULTIRANGE_ARRAY_TYPES = (6155, 6150, 6157, 6151, 6152, 6153)
def register_global_typecasters(): def register_global_typecasters():
# This registers a unicode type caster for datatype 'RECORD'.
psycopg.adapters.register_loader(
2249, TextLoaderpgAdmin)
# This registers a unicode type caster for datatype 'RECORD_ARRAY'. # This registers a unicode type caster for datatype 'RECORD_ARRAY'.
psycopg.adapters.register_loader( psycopg.adapters.register_loader(
2287, TextLoaderpgAdmin) 2287, TextLoaderpgAdmin)

View File

@ -282,6 +282,7 @@ class PgadminPage:
option_set_as_required = True option_set_as_required = True
break break
else: else:
self.driver.implicitly_wait(2)
menu_option.click() menu_option.click()
time.sleep(0.2) time.sleep(0.2)
if menu_option.get_attribute('data-checked') == is_selected: if menu_option.get_attribute('data-checked') == is_selected:

View File

@ -8,9 +8,9 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; import React, { act } from 'react';
import { act, render } from '@testing-library/react'; import { render } from '@testing-library/react';
import {TestSchema} from './TestSchema.ui'; import {TestSchema} from './TestSchema.ui';
import SchemaView from '../../../pgadmin/static/js/SchemaView'; import SchemaView from '../../../pgadmin/static/js/SchemaView';

View File

@ -8,7 +8,7 @@
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import React from 'react'; import React, { act} from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import Theme from '../../../pgadmin/static/js/Theme'; import Theme from '../../../pgadmin/static/js/Theme';
@ -18,7 +18,6 @@ import axios from 'axios';
import getApiInstance from '../../../pgadmin/static/js/api_instance'; import getApiInstance from '../../../pgadmin/static/js/api_instance';
import * as pgUtils from '../../../pgadmin/static/js/utils'; import * as pgUtils from '../../../pgadmin/static/js/utils';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
const files = [ const files = [
{ {

View File

@ -9,8 +9,8 @@
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import Theme from 'sources/Theme'; import Theme from 'sources/Theme';
import Wizard from '../../../pgadmin/static/js/helpers/wizard/Wizard'; import Wizard from '../../../pgadmin/static/js/helpers/wizard/Wizard';

View File

@ -1,4 +1,5 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
const { TextEncoder, TextDecoder } = require('util');
class BroadcastChannelMock { class BroadcastChannelMock {
onmessage() {/* mock */} onmessage() {/* mock */}
@ -93,5 +94,7 @@ Element.prototype.getBoundingClientRect = jest.fn(function () {
}; };
}); });
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
jest.setTimeout(18000); // 1 second jest.setTimeout(18000); // 1 second

File diff suppressed because it is too large Load Diff