Added support for viewing PostgreSQL Server Logs in Text, CSV and JSON formats. #3981
parent
93b5bc6bf8
commit
4f415f9768
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 |
|
@ -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
|
||||
************
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#############################################################################
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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']
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'),
|
||||
}
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
SELECT
|
||||
setting
|
||||
FROM
|
||||
pg_show_all_settings()
|
||||
WHERE
|
||||
name='log_destination'
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -627,7 +627,6 @@ function SchemaDialogView({
|
|||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: schema.origData,
|
||||
});
|
||||
return true;
|
||||
}, [props.resetKey]);
|
||||
|
||||
const onResetClick = ()=>{
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...'));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
600
web/yarn.lock
600
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue