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
************
| `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
************

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
highlighted object in the tree control.
.. image:: images/main_dashboard_general.png
:alt: Dashboard panel
.. image:: images/dashboard_activity.png
:alt: Dashboard Activity
:align: center
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.
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
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
cache) for the server or database.
The *Server activity* panel displays information about sessions, locks, prepared
transactions, and server configuration (if applicable). The information is
.. image:: images/dashboard_stat.png
: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:
* 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*
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:
.. 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
#############################################################################
# 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
#############################################################################

View File

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

View File

@ -382,7 +382,7 @@ define('pgadmin.browser.node', [
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
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)=>{
// Clear the cache for this node now.
setTimeout(()=>{
@ -412,7 +412,7 @@ define('pgadmin.browser.node', [
// browser tree upon the 'Save' button click.
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
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)=>{
// Clear the cache for this node now.
setTimeout(()=>{
@ -438,7 +438,7 @@ define('pgadmin.browser.node', [
});
} else {
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)=>{
let _old = nodeData,
_new = newNodeData.node,

View File

@ -9,6 +9,7 @@
"""A blueprint module implementing the dashboard frame."""
import math
import re
from flask import render_template, Response, g, request
from flask_babel import gettext
@ -16,8 +17,7 @@ from pgadmin.user_login_check import pga_login_required
import json
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response,\
internal_server_error
internal_server_error, make_json_response, precondition_required
from pgadmin.utils.driver import get_driver
from pgadmin.utils.preferences import Preferences
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 .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'
@ -157,17 +157,17 @@ class DashboardModule(PgAdminModule):
self.display_graphs = self.dashboard_preference.register(
'display', 'show_graphs',
gettext("Show graphs?"), 'boolean', True,
gettext("Show activity?"), 'boolean', True,
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.')
)
self.display_server_activity = self.dashboard_preference.register(
'display', 'show_activity',
gettext("Show activity?"), 'boolean', True,
gettext("Show state?"), 'boolean', True,
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.')
)
@ -241,7 +241,9 @@ class DashboardModule(PgAdminModule):
'dashboard.get_prepared_by_server_id',
'dashboard.get_prepared_by_database_id',
'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_sid',
'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
Args:
@ -347,6 +350,9 @@ def get_data(sid, did, template, check_long_running_query=False):
if check_long_running_query:
get_long_running_query_status(res['rows'])
if only_data:
return res['rows']
return ajax_response(
response=res['rows'],
status=200
@ -431,7 +437,15 @@ def activity(sid=None, did=None):
:param sid: server id
: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')
@ -466,8 +480,7 @@ def prepared(sid=None, did=None):
return get_data(sid, did, 'prepared.sql')
@blueprint.route('/config/', endpoint='config')
@blueprint.route('/config/<int:sid>', endpoint='get_config_by_server_id')
@blueprint.route('/config/<int:sid>', endpoint='config')
@pga_login_required
@check_precondition
def config(sid=None):
@ -479,6 +492,145 @@ def config(sid=None):
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(
'/cancel_query/<int:sid>/<int:pid>', methods=['DELETE']
)

View File

@ -6,13 +6,13 @@
// 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 gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import getApiInstance from 'sources/api_instance';
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 Graphs from './Graphs';
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 WelcomeDashboard from './WelcomeDashboard';
import ActiveQuery from './ActiveQuery.ui';
import ServerLog from './ServerLog.ui';
import _ from 'lodash';
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
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 SectionContainer from './components/SectionContainer';
import Replication from './Replication';
import RefreshButton from './components/RefreshButtons';
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) {
let res = [];
@ -47,7 +51,6 @@ function parseData(data) {
});
return res;
}
const Root = styled('div')(({theme}) => ({
height: '100%',
width: '100%',
@ -71,6 +74,16 @@ const Root = styled('div')(({theme}) => ({
'& .Dashboard-terminateButton': {
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 serverLogSchemaObj = new ServerLog();
const cellPropTypes = {
row: PropTypes.any,
@ -218,8 +232,25 @@ function getCancelCell(pgAdmin, sid, did, canTakeAction, onSuccess) {
return CancelCell;
}
function ActiveOnlyHeader({activeOnly, setActiveOnly}) {
function CustomRefresh({refresh, setRefresh}) {
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
label={gettext('Active sessions only')}
labelPlacement="end"
@ -232,35 +263,49 @@ function ActiveOnlyHeader({activeOnly, setActiveOnly}) {
controlProps={{
label: gettext('Active sessions only'),
}}
/>
/></Fragment>
);
}
ActiveOnlyHeader.propTypes = {
activeOnly: PropTypes.bool,
setActiveOnly: PropTypes.func,
refresh: PropTypes.bool,
setRefresh: PropTypes.func,
};
function Dashboard({
nodeItem, nodeData, node, treeNodeInfo,
...props
}) {
const preferences = _.merge(
usePreferences().getPreferencesForModule('dashboards'),
usePreferences().getPreferencesForModule('graphs'),
usePreferences().getPreferencesForModule('misc')
);
let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')];
let mainTabs = [gettext('General'), gettext('System Statistics')];
if(treeNodeInfo?.server?.replication_type) {
mainTabs.push(gettext('Replication'));
}
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')];
// Set Active tab depending on preferences setting
let activeTab = 0;
if (!_.isUndefined(preferences) && !preferences.show_graphs && preferences.show_activity) activeTab = 1;
else if (!_.isUndefined(preferences) && !preferences.show_graphs && !preferences.show_activity) activeTab = 2;
const api = getApiInstance();
const [dashData, setDashData] = useState([]);
const [msg, setMsg] = useState('');
const [ssMsg, setSsMsg] = useState('');
const [tabVal, setTabVal] = useState(0);
const [mainTabVal, setMainTabVal] = useState(0);
const [mainTabVal, setMainTabVal] = useState(activeTab);
const [refresh, setRefresh] = useState(false);
const [activeOnly, setActiveOnly] = useState(false);
const [systemStatsTabVal, setSystemStatsTabVal] = 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) => {
setSystemStatsTabVal(tabVal);
};
@ -270,19 +315,13 @@ function Dashboard({
const dbConnected = treeNodeInfo?.database?.connected ?? false;
const serverConnected = treeNodeInfo?.server?.connected ?? false;
const prefStore = usePreferences();
const preferences = _.merge(
usePreferences().getPreferencesForModule('dashboards'),
usePreferences().getPreferencesForModule('graphs'),
usePreferences().getPreferencesForModule('misc')
);
let mainTabs = [gettext('Activity'), gettext('State')];
if (!did) {
tabs.push(gettext('Configuration'));
mainTabs.push(gettext('Configuration'), gettext('Logs'), gettext('System'));
if(treeNodeInfo?.server?.replication_type) {
mainTabs.push(gettext('Replication'));
}
const tabChanged = (e, tabVal) => {
setTabVal(tabVal);
};
let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')];
const mainTabChanged = (e, 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 = [
{
header: () => null,
@ -682,15 +812,37 @@ function Dashboard({
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(() => {
// 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
if(!treeNodeInfo?.server?.replication_type && mainTabVal == 2) {
if(!treeNodeInfo?.server?.replication_type && mainTabVal == 5) {
setMainTabVal(0);
}
@ -700,39 +852,44 @@ function Dashboard({
'Please connect to the selected server to view the dashboard.'
);
if(tabVal == 3 && did) {
setTabVal(0);
}
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...');
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;
else ssExtensionCheckUrl += '/' + sid;
const api = getApiInstance();
if (node) {
if (mainTabVal == 0) {
setSsMsg(gettext('Loading logs...'));
setDashData([]);
if (mainTabVal != 4 && mainTabVal != 5) {
api({
url: url,
type: 'GET',
})
.then((res) => {
setDashData(parseData(res.data));
if (res.data && res.data['logs_disabled']) {
setSsMsg(gettext('Please enable the logging to view the server logs.'));
} else {
setDashData(parseData(res.data));
}
})
.catch((error) => {
pgAdmin.Browser.notifier.alert(
@ -743,7 +900,7 @@ function Dashboard({
setMsg(gettext('Failed to retrieve data from the server.'));
});
}
else if (mainTabVal == 1) {
else if (mainTabVal == 4) {
api({
url: ssExtensionCheckUrl,
type: 'GET',
@ -773,15 +930,15 @@ function Dashboard({
if (message != '') {
setMsg(message);
}
}, [nodeData, tabVal, treeNodeInfo, prefStore, refresh, mainTabVal]);
}, [nodeData, treeNodeInfo, prefStore, refresh, mainTabVal, logCol, logFormat]);
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
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;
}, [dashData, activeOnly, tabVal]);
return dashData && dashData[0] && dashData[0]['activity'] || [];
}, [dashData, activeOnly, mainTabVal]);
const showDefaultContents = () => {
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 (
(<Root>
{sid && serverConnected ? (
@ -815,14 +1049,25 @@ function Dashboard({
value={mainTabVal}
onChange={mainTabChanged}
>
{mainTabs.map((tabValue) => {
return <Tab key={tabValue} label={tabValue} />;
{mainTabs.map((tabValue, i) => {
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>
</Box>
{/* General Statistics */}
<TabPanel value={mainTabVal} index={0}>
{!_.isUndefined(preferences) && preferences.show_graphs && (
{/* Server Activity */}
{!_.isUndefined(preferences) && preferences.show_graphs && (
<TabPanel value={mainTabVal} index={0}>
<Graphs
key={sid + did}
preferences={preferences}
@ -830,62 +1075,84 @@ function Dashboard({
did={did}
pageVisible={props.isActive}
></Graphs>
)}
</TabPanel>
)}
{/* Server Activity */}
<TabPanel value={mainTabVal} index={1} classNameRoot='Dashboard-tabPanel'>
{!_.isUndefined(preferences) && preferences.show_activity && (
<SectionContainer title={dbConnected ? gettext('Database activity') : gettext('Server activity')}>
<Box>
<Tabs
value={tabVal}
onChange={tabChanged}
>
{tabs.map((tabValue) => {
return <Tab key={tabValue} label={tabValue} />;
})}
<RefreshButton onClick={(e) => {
e.preventDefault();
setRefresh(!refresh);
}}/>
</Tabs>
</Box>
<TabPanel value={tabVal} index={0}>
<Fragment>
<SectionContainer title={gettext('Sessions')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}
>
<PgTable
caveTable={false}
tableNoBorder={false}
customHeader={<ActiveOnlyHeader activeOnly={activeOnly} setActiveOnly={setActiveOnly} />}
customHeader={<ActiveOnlyHeader activeOnly={activeOnly} setActiveOnly={setActiveOnly} refresh={refresh} setRefresh={setRefresh}/>}
columns={activityColumns}
data={filteredDashData}
data={(dashData !== undefined && dashData[0] && filteredDashData) || []}
schema={activeQSchemaObj}
></PgTable>
</TabPanel>
<TabPanel value={tabVal} index={1}>
</SectionContainer>
<SectionContainer title={gettext('Locks')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}>
<PgTable
customHeader={<CustomRefresh refresh={refresh} setRefresh={setRefresh}/>}
caveTable={false}
tableNoBorder={false}
columns={databaseLocksColumns}
data={dashData}
data={(dashData !== undefined && dashData[0] && dashData[0]['locks']) || []}
></PgTable>
</TabPanel>
<TabPanel value={tabVal} index={2}>
</SectionContainer>
<SectionContainer title={gettext('Prepared Transactions')} style={{height: 'auto', minHeight: '200px', paddingBottom: '20px'}}>
<PgTable
customHeader={<CustomRefresh refresh={refresh} setRefresh={setRefresh}/>}
caveTable={false}
tableNoBorder={false}
columns={databasePreparedColumns}
data={dashData}
data={(dashData !== undefined && dashData[0] && dashData[0]['prepared']) || []}
></PgTable>
</TabPanel>
<TabPanel value={tabVal} index={3}>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={serverConfigColumns}
data={dashData}
></PgTable>
</TabPanel>
</SectionContainer>
</SectionContainer>
</Fragment>
)}
</TabPanel>
{/* Server Configuration */}
<TabPanel value={mainTabVal} index={2} classNameRoot='Dashboard-tabPanel'>
<PgTable
caveTable={false}
tableNoBorder={false}
columns={serverConfigColumns}
data={dashData}
></PgTable>
</TabPanel>
{/* 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>
{/* 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">
{ssMsg === 'installed' && did === ldid ?
<ErrorBoundary>
@ -948,7 +1215,7 @@ function Dashboard({
</Box>
</TabPanel>
{/* Replication */}
<TabPanel value={mainTabVal} index={2} classNameRoot='Dashboard-tabPanel'>
<TabPanel value={mainTabVal} index={5} classNameRoot='Dashboard-tabPanel'>
<Replication key={sid} sid={sid} node={node}
preferences={preferences} treeNodeInfo={treeNodeInfo} nodeData={nodeData} pageVisible={props.isActive} />
</TabPanel>
@ -973,6 +1240,7 @@ Dashboard.propTypes = {
serverConnected: PropTypes.bool,
dbConnected: PropTypes.bool,
isActive: PropTypes.bool,
column: PropTypes.object,
};
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.
"""
data = None
return precondition_required(gettext('Invalid binary path.'))
if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8')

View File

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

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom/client';
import HiddenIframe from './HiddenIframe';
import url_for from 'sources/url_for';
@ -39,6 +39,8 @@ export function onlineHelpSearch(param, props) {
if(document.getElementById('hidden-quick-search-iframe')){
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
const _iframeLoaded = () => {
@ -69,7 +71,7 @@ export function onlineHelpSearch(param, props) {
data: res,
}));
isIFrameLoaded = false;
ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
root.unmount();
} else {
setState(state => ({
...state,
@ -87,7 +89,7 @@ export function onlineHelpSearch(param, props) {
url: srcURL,
data: {},
}));
ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
root.unmount();
isIFrameLoaded = false;
window.clearInterval(pooling);
}
@ -98,8 +100,7 @@ export function onlineHelpSearch(param, props) {
};
// Render IFrame
ReactDOM.render(
<HiddenIframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>,
document.getElementById('quick-search-iframe-container'),
root.render(
<HiddenIframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>
);
}

View File

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

View File

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

View File

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

View File

@ -306,7 +306,7 @@ PgReactTableBody.propTypes = {
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();
useEffect(()=>{
@ -333,7 +333,7 @@ export const PgReactTable = forwardRef(({children, table, rootClassName, tableCl
}, [columns, table.getState().columnSizingInfo]);
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}>
{children}
</div>
@ -346,6 +346,7 @@ PgReactTable.propTypes = {
rootClassName: PropTypes.any,
tableClassName: PropTypes.any,
children: CustomPropTypes.children,
onScrollFunc: PropTypes.any,
};
export function getExpandCell({ onClick, title }) {

View File

@ -8,6 +8,7 @@
//////////////////////////////////////////////////////////////
import React, { useMemo, useRef } from 'react';
import {
useReactTable,
getCoreRowModel,
@ -16,6 +17,12 @@ import {
getExpandedRowModel,
flexRender,
} from '@tanstack/react-table';
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
keepPreviousData,
} from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
@ -74,7 +81,7 @@ TableRow.propTypes = {
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(
() => ({
size: 150,
@ -106,10 +113,55 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
// Render the UI for your table
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({
columns: finalColumns,
data,
data: flatData.length >0 ? flatData : data,
defaultColumn,
autoResetAll: false,
initialState: {
@ -144,7 +196,7 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
});
return (
<PgReactTable ref={tableRef} table={table}>
<PgReactTable ref={tableRef} table={table} onScrollFunc={loadNextPage?fetchMoreOnBottomReached: null }>
<PgReactTableHeader table={table} />
{rows.length == 0 ?
<EmptyPanelMessage text={gettext('No rows found')} /> :
@ -172,6 +224,7 @@ Table.propTypes = {
selectedRows: PropTypes.object,
setSelectedRows: PropTypes.func,
searchVal: PropTypes.string,
loadNextPage: PropTypes.func,
};
const StyledPgTableRoot = styled('div')(({theme})=>({
@ -214,29 +267,33 @@ const StyledPgTableRoot = styled('div')(({theme})=>({
},
}));
const queryClient = new QueryClient();
export default function PgTable({ caveTable = true, tableNoBorder = true, ...props }) {
const [searchVal, setSearchVal] = React.useState('');
return (
<StyledPgTableRoot className={[tableNoBorder ? '' : 'pgtable-pgrt-border', caveTable ? 'pgtable-pgrt-cave' : ''].join(' ')} data-test={props['data-test']}>
<Box className='pgtable-header'>
{props.customHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}>{props.customHeader}</Box>)}
<Box marginLeft="auto">
<InputText
placeholder={gettext('Search')}
controlProps={{ title: gettext('Search') }}
className='pgtable-search-input'
value={searchVal}
onChange={(val) => {
setSearchVal(val);
}}
/>
<QueryClientProvider client={queryClient}>
<StyledPgTableRoot className={[tableNoBorder ? '' : 'pgtable-pgrt-border', caveTable ? 'pgtable-pgrt-cave' : ''].join(' ')} data-test={props['data-test']}>
<Box className='pgtable-header'>
{props.customHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}> {props.customHeader }</Box>)}
<Box marginLeft="auto">
<InputText
placeholder={gettext('Search')}
controlProps={{ title: gettext('Search') }}
className='pgtable-search-input'
value={searchVal}
onChange={(val) => {
setSearchVal(val);
}}
/>
</Box>
</Box>
</Box>
<div className={'pgtable-body'}>
<Table {...props} searchVal={searchVal} />
</div>
</StyledPgTableRoot>
<div className={'pgtable-body'} >
<Table {...props} searchVal={searchVal}/>
</div>
</StyledPgTableRoot>
</QueryClientProvider>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -388,7 +388,7 @@ export function QueryHistory() {
});
}, []);
React.useEffect(async ()=>{
const fetchQueryHistory = async() =>{
if(!queryToolConnCtx.connected) {
return;
}
@ -430,7 +430,11 @@ export function QueryHistory() {
listRef.current?.focus();
eventBus.registerListener(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 ()=>{
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():
# 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'.
psycopg.adapters.register_loader(
2287, TextLoaderpgAdmin)

View File

@ -282,6 +282,7 @@ class PgadminPage:
option_set_as_required = True
break
else:
self.driver.implicitly_wait(2)
menu_option.click()
time.sleep(0.2)
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 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 Theme from '../../../pgadmin/static/js/Theme';
@ -18,7 +18,6 @@ import axios from 'axios';
import getApiInstance from '../../../pgadmin/static/js/api_instance';
import * as pgUtils from '../../../pgadmin/static/js/utils';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
const files = [
{

View File

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

View File

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

File diff suppressed because it is too large Load Diff