diff --git a/web/package.json b/web/package.json
index 19c21768860..f43473b081c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -150,6 +150,7 @@
"react-draggable": "^4.4.4",
"react-dropzone": "^14.2.1",
"react-leaflet": "^3.2.2",
+ "react-resize-detector": "^8.0.3",
"react-rnd": "^10.3.5",
"react-router-dom": "^6.2.2",
"react-select": "^4.2.1",
@@ -164,6 +165,8 @@
"styled-components": "^5.2.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.19.3",
+ "uplot": "^1.6.24",
+ "uplot-react": "^1.1.4",
"valid-filename": "^2.0.1",
"webcabin-docker": "git+https://github.com/pgadmin-org/wcdocker/#3df8aac825ee2892f4d824de273b779cc6dbcad8",
"wkx": "^0.5.0",
diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx
index 5b230bf33da..d2d7c8c4d75 100644
--- a/web/pgadmin/dashboard/static/js/Dashboard.jsx
+++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx
@@ -911,13 +911,20 @@ export function ChartContainer(props) {
{props.title}
-
+
+
+ {props.datasets?.map((datum, i)=>(
+
+
+ {datum.label}
+
+ ))}
+
+
-
- {props.children}
-
+ {!props.errorMsg && !props.isTest && props.children}
@@ -927,12 +934,10 @@ export function ChartContainer(props) {
ChartContainer.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
- legendRef: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.shape({ current: PropTypes.any }),
- ]).isRequired,
+ datasets: PropTypes.array.isRequired,
children: PropTypes.node.isRequired,
errorMsg: PropTypes.string,
+ isTest: PropTypes.bool
};
export function ChartError(props) {
diff --git a/web/pgadmin/dashboard/static/js/Graphs.jsx b/web/pgadmin/dashboard/static/js/Graphs.jsx
index e3d0c4c8394..ec0feecce76 100644
--- a/web/pgadmin/dashboard/static/js/Graphs.jsx
+++ b/web/pgadmin/dashboard/static/js/Graphs.jsx
@@ -6,8 +6,8 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import React, { useEffect, useRef, useState, useReducer, useCallback, useMemo } from 'react';
-import { LineChart, DATA_POINT_STYLE, DATA_POINT_SIZE } from 'sources/chartjs';
+import React, { useEffect, useRef, useState, useReducer, useMemo } from 'react';
+import { DATA_POINT_SIZE } from 'sources/chartjs';
import {ChartContainer, DashboardRowCol, DashboardRow} from './Dashboard';
import url_for from 'sources/url_for';
import axios from 'axios';
@@ -15,45 +15,28 @@ import gettext from 'sources/gettext';
import {getGCD, getEpoch} from 'sources/utils';
import {useInterval, usePrevious} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
+import TimeLineChart from '../../../static/js/components/PgChart/TimeLineChart';
export const X_AXIS_LENGTH = 75;
/* Transform the labels data to suit ChartJS */
-export function transformData(labels, refreshRate, use_diff_point_style) {
+export function transformData(labels, refreshRate) {
const colors = ['#00BCD4', '#9CCC65', '#E64A19'];
let datasets = Object.keys(labels).map((label, i)=>{
return {
label: label,
data: labels[label] || [],
borderColor: colors[i],
- backgroundColor: colors[i],
pointHitRadius: DATA_POINT_SIZE,
- pointStyle: use_diff_point_style ? DATA_POINT_STYLE[i] : 'circle'
};
}) || [];
return {
- labels: [...Array(X_AXIS_LENGTH).keys()],
datasets: datasets,
refreshRate: refreshRate,
};
}
-/* Custom ChartJS legend callback */
-export function generateLegend(chart) {
- let text = [];
- text.push('');
- for (let chart_val of chart.data.datasets) {
- text.push('
');
- if (chart_val.label) {
- text.push('' + chart_val.label + '');
- }
- text.push('
');
- }
- text.push('
');
- return text.join('');
-}
-
/* URL for fetching graphs data */
export function getStatsUrl(sid=-1, did=-1, chart_names=[]) {
let base_url = url_for('dashboard.dashboard_stats');
@@ -104,7 +87,7 @@ const chartsDefault = {
'bio_stats': {'Reads': [], 'Hits': []},
};
-export default function Graphs({preferences, sid, did, pageVisible, enablePoll=true}) {
+export default function Graphs({preferences, sid, did, pageVisible, enablePoll=true, isTest}) {
const refreshOn = useRef(null);
const prevPrefernces = usePrevious(preferences);
@@ -238,6 +221,7 @@ export default function Graphs({preferences, sid, did, pageVisible, enablePoll=t
showDataPoints={preferences['graph_data_points']}
lineBorderWidth={preferences['graph_line_border_width']}
isDatabase={did > 0}
+ isTest={isTest}
/>
}
>
@@ -256,92 +240,45 @@ Graphs.propTypes = {
]),
pageVisible: PropTypes.bool,
enablePoll: PropTypes.bool,
+ isTest: PropTypes.bool,
};
export function GraphsWrapper(props) {
- const sessionStatsLegendRef = useRef();
- const tpsStatsLegendRef = useRef();
- const tiStatsLegendRef = useRef();
- const toStatsLegendRef = useRef();
- const bioStatsLegendRef = useRef();
const options = useMemo(()=>({
- elements: {
- point: {
- radius: props.showDataPoints ? DATA_POINT_SIZE : 0,
- },
- line: {
- borderWidth: props.lineBorderWidth,
- },
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: props.showTooltip,
- callbacks: {
- title: function(tooltipItem) {
- let title = '';
- try {
- title = parseInt(tooltipItem[0].label) * tooltipItem[0].chart?.data.refreshRate + gettext(' seconds ago');
- } catch (error) {
- title = '';
- }
- return title;
- },
- },
- }
- },
- scales: {
- x: {
- reverse: true,
- },
- y: {
- min: 0,
- }
- },
+ showDataPoints: props.showDataPoints,
+ showTooltip: props.showTooltip,
+ lineBorderWidth: props.lineBorderWidth,
}), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
- const updateOptions = useMemo(()=>({duration: 0}), []);
-
- const onInitCallback = useCallback(
- (legendRef)=>(chart)=>{
- legendRef.current.innerHTML = generateLegend(chart);
- }
- );
return (
<>
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
@@ -350,7 +287,6 @@ export function GraphsWrapper(props) {
}
const propTypeStats = PropTypes.shape({
- labels: PropTypes.array.isRequired,
datasets: PropTypes.array,
refreshRate: PropTypes.number.isRequired,
});
@@ -365,4 +301,5 @@ GraphsWrapper.propTypes = {
showDataPoints: PropTypes.bool.isRequired,
lineBorderWidth: PropTypes.number.isRequired,
isDatabase: PropTypes.bool.isRequired,
+ isTest: PropTypes.bool,
};
diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css
index 9928a52ac88..d66ce13f146 100644
--- a/web/pgadmin/static/css/style.css
+++ b/web/pgadmin/static/css/style.css
@@ -17,3 +17,6 @@
@import 'node_modules/react-checkbox-tree/lib/react-checkbox-tree.css';
@import 'node_modules/@simonwep/pickr/dist/themes/monolith.min.css';
+
+@import 'node_modules/uplot/dist/uPlot.min.css';
+
diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx
index 578d260a1a5..055775a33c3 100644
--- a/web/pgadmin/static/js/Theme/index.jsx
+++ b/web/pgadmin/static/js/Theme/index.jsx
@@ -21,6 +21,7 @@ import getDarkTheme from './dark';
import getHightContrastTheme from './high_contrast';
import { CssBaseline } from '@material-ui/core';
import pickrOverride from './overrides/pickr.override';
+import uplotOverride from './overrides/uplot.override';
/* Common settings across all themes */
let basicSettings = createMuiTheme();
@@ -313,6 +314,7 @@ function getFinalTheme(baseTheme) {
padding: 0,
},
...pickrOverride(baseTheme),
+ ...uplotOverride(baseTheme),
},
},
MuiOutlinedInput: {
diff --git a/web/pgadmin/static/js/Theme/overrides/uplot.override.js b/web/pgadmin/static/js/Theme/overrides/uplot.override.js
new file mode 100644
index 00000000000..0780da9f4b8
--- /dev/null
+++ b/web/pgadmin/static/js/Theme/overrides/uplot.override.js
@@ -0,0 +1,32 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2023, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export default function uplotOverride(theme) {
+ return {
+ '.uplot': {
+ '& .u-legend': {
+ display: 'none',
+ }
+ },
+ '.uplot-tooltip': {
+ position: 'absolute',
+ fontSize: '0.9em',
+ padding: '4px 8px',
+ borderRadius: theme.shape.borderRadius,
+ color: theme.palette.background.default,
+ backgroundColor: theme.palette.text.primary,
+
+ '& .uplot-tooltip-label': {
+ display: 'flex',
+ gap: '4px',
+ alignItems: 'center',
+ }
+ }
+ };
+}
diff --git a/web/pgadmin/static/js/components/PgChart/TimeLineChart.jsx b/web/pgadmin/static/js/components/PgChart/TimeLineChart.jsx
new file mode 100644
index 00000000000..801744254ee
--- /dev/null
+++ b/web/pgadmin/static/js/components/PgChart/TimeLineChart.jsx
@@ -0,0 +1,116 @@
+import React, { useRef, useMemo } from 'react';
+import UplotReact from 'uplot-react';
+import { useResizeDetector } from 'react-resize-detector';
+import gettext from 'sources/gettext';
+import PropTypes from 'prop-types';
+
+function tooltipPlugin(refreshRate) {
+ let tooltipTopOffset = 10;
+ let tooltipLeftOffset = 10;
+ let tooltip;
+
+ function showTooltip() {
+ if(!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.className = 'uplot-tooltip';
+ tooltip.style.display = 'block';
+ document.body.appendChild(tooltip);
+ }
+ }
+
+ function hideTooltip() {
+ tooltip?.remove();
+ tooltip = null;
+ }
+
+ function setTooltip(u) {
+ if(u.cursor.top <= 0) {
+ hideTooltip();
+ return;
+ }
+ showTooltip();
+ tooltip.style.top = (tooltipTopOffset + u.cursor.top + u.over.getBoundingClientRect().top) + 'px';
+ tooltip.style.left = (tooltipLeftOffset + u.cursor.left + u.over.getBoundingClientRect().left) + 'px';
+ let tooltipHtml=`${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}
`;
+ for(let i=1; i ${u.series[i].label}: ${u.legend.values[i]['_']}`;
+ }
+ tooltip.innerHTML = tooltipHtml;
+ }
+
+ return {
+ hooks: {
+ setCursor: [
+ u => {
+ setTooltip(u);
+ }
+ ],
+ }
+ };
+}
+
+export default function TimeLineChart({xRange=75, data, options}) {
+ const chartRef = useRef();
+ const { width, height, ref:containerRef } = useResizeDetector();
+ const defaultOptions = useMemo(()=>({
+ title: '',
+ width: width,
+ height: height,
+ padding: [10, 0, 10, 0],
+ focus: {
+ alpha: 0.3,
+ },
+ cursor: {
+ drag: {
+ setScale: false,
+ }
+ },
+ series: [
+ {},
+ ...data.datasets?.map((datum)=>({
+ label: datum.label,
+ stroke: datum.borderColor,
+ width: options.lineBorderWidth ?? 1,
+ points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius*2 }
+ }))
+ ],
+ scales: {
+ x: {
+ time: false,
+ }
+ },
+ axes: [
+ {
+ show: false,
+ },
+ ],
+ plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [],
+ }), [data.refreshRate, data?.datasets?.length, width, height, options]);
+
+ const initialState = [
+ Array.from(new Array(xRange).keys()),
+ ...data.datasets?.map((d)=>{
+ let ret = [...d.data];
+ ret.reverse();
+ return ret;
+ }),
+ ];
+
+ chartRef.current?.setScale('x', {min: data.datasets[0]?.data?.length-xRange, max: data.datasets[0]?.data?.length});
+ return (
+
+ chartRef.current=obj} />
+
+ );
+}
+
+const propTypeData = PropTypes.shape({
+ datasets: PropTypes.array,
+ refreshRate: PropTypes.number.isRequired,
+});
+
+TimeLineChart.propTypes = {
+ xRange: PropTypes.number.isRequired,
+ data: propTypeData.isRequired,
+ options: PropTypes.object,
+};
diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py
index 8c8c6448515..e1753171b73 100644
--- a/web/pgadmin/utils/driver/psycopg2/connection.py
+++ b/web/pgadmin/utils/driver/psycopg2/connection.py
@@ -310,8 +310,8 @@ def connect(self, **kwargs):
conn_id = self.conn_id
import os
- os.environ['PGAPPNAME'] = '{0} - {1}'.format(
- config.APP_NAME, conn_id)
+ # os.environ['PGAPPNAME'] = '{0} - {1}'.format(
+ # config.APP_NAME, conn_id)
ssl_key = get_complete_file_path(
manager.get_connection_param_value('sslkey'))
diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
index 145ff6b518b..04022171db9 100644
--- a/web/pgadmin/utils/driver/psycopg2/server_manager.py
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -634,7 +634,7 @@ def create_connection_string(self, database, user, password=None):
parameters.
"""
full_connection_string = \
- 'host=\'{0}\' port=\'{1}\' dbname=\'{2}\' user=\'{3}\''.format(
+ 'host=\'{0}\' port=\'{1}\' dbname=\'{2}\' user=\'{3}\' application_name=Ψ'.format(
self.local_bind_host if self.use_ssh_tunnel else self.host,
self.local_bind_port if self.use_ssh_tunnel else self.port,
database, user)
diff --git a/web/regression/javascript/dashboard/graphs_spec.js b/web/regression/javascript/dashboard/graphs_spec.js
index 7e7e485b34f..fac94e78e8c 100644
--- a/web/regression/javascript/dashboard/graphs_spec.js
+++ b/web/regression/javascript/dashboard/graphs_spec.js
@@ -4,27 +4,22 @@ import {mount} from 'enzyme';
import '../helper/enzyme.helper';
import { DATA_POINT_SIZE } from 'sources/chartjs';
-import Graphs, {GraphsWrapper, X_AXIS_LENGTH, transformData,
+import Graphs, {GraphsWrapper, transformData,
getStatsUrl, statsReducer} from '../../../pgadmin/dashboard/static/js/Graphs';
describe('Graphs.js', ()=>{
it('transformData', ()=>{
expect(transformData({'Label1': [], 'Label2': []}, 1, false)).toEqual({
- labels: [...Array(X_AXIS_LENGTH).keys()],
datasets: [{
label: 'Label1',
data: [],
borderColor: '#00BCD4',
- backgroundColor: '#00BCD4',
pointHitRadius: DATA_POINT_SIZE,
- pointStyle: 'circle',
},{
label: 'Label2',
data: [],
borderColor: '#9CCC65',
- backgroundColor: '#9CCC65',
pointHitRadius: DATA_POINT_SIZE,
- pointStyle: 'circle',
}],
refreshRate: 1,
});
@@ -112,7 +107,7 @@ describe('Graphs.js', ()=>{
graph_line_border_width: 2
};
- graphComp = mount();
+ graphComp = mount();
});
it('GraphsWrapper is rendered', (done)=>{
diff --git a/web/regression/javascript/dashboard/graphs_wrapper_spec.js b/web/regression/javascript/dashboard/graphs_wrapper_spec.js
index 42d66eb73b7..fd901d56a37 100644
--- a/web/regression/javascript/dashboard/graphs_wrapper_spec.js
+++ b/web/regression/javascript/dashboard/graphs_wrapper_spec.js
@@ -35,7 +35,8 @@ describe(' component', ()=>{
showTooltip={true}
showDataPoints={true}
lineBorderWidth={2}
- isDatabase={false} />);
+ isDatabase={false}
+ isTest={true} />);
});
it('graph containers are rendered', (done)=>{
@@ -61,24 +62,9 @@ describe(' component', ()=>{
done();
});
- it('graph body has the canvas', (done)=>{
- let found = graphComp.find('.card.dashboard-graph .dashboard-graph-body canvas');
- expect(found.at(0).length).toBe(1);
- expect(found.at(1).length).toBe(1);
- expect(found.at(2).length).toBe(1);
- expect(found.at(3).length).toBe(1);
- expect(found.at(4).length).toBe(1);
- done();
- });
-
it('graph body shows the error', (done)=>{
graphComp.setProps({errorMsg: 'Some error occurred'});
- let found = graphComp.find('.card.dashboard-graph .dashboard-graph-body .chart-wrapper');
- expect(found.at(0)).toHaveClassName('d-none');
- expect(found.at(1)).toHaveClassName('d-none');
- expect(found.at(2)).toHaveClassName('d-none');
- expect(found.at(3)).toHaveClassName('d-none');
- expect(found.at(4)).toHaveClassName('d-none');
+ let found = graphComp.find('.card.dashboard-graph .dashboard-graph-body');
found = graphComp.find('.card.dashboard-graph .dashboard-graph-body .pg-panel-error.pg-panel-message');
expect(found.at(0)).toIncludeText('Some error occurred');
diff --git a/web/regression/javascript/erd/erd_core_spec.js b/web/regression/javascript/erd/erd_core_spec.js
index 9c5d7bd1032..7a0d6f2ab4d 100644
--- a/web/regression/javascript/erd/erd_core_spec.js
+++ b/web/regression/javascript/erd/erd_core_spec.js
@@ -259,7 +259,7 @@ describe('ERDCore', ()=>{
return new FakeNode({}, `id-${data.name}`);
});
spyOn(erdCoreObj, 'addLink');
- spyOn(erdCoreObj, 'dagreDistributeNodes');
+ spyOn(erdCoreObj, 'dagreDistributeNodes').and.callFake(()=>{/* intentionally empty */});
erdCoreObj.deserializeData(TEST_TABLES_DATA);
expect(erdCoreObj.addNode).toHaveBeenCalledTimes(TEST_TABLES_DATA.length);
diff --git a/web/regression/javascript/erd/ui_components/ERDTool.spec.js b/web/regression/javascript/erd/ui_components/ERDTool.spec.js
index 16dee160c02..94be2ce635c 100644
--- a/web/regression/javascript/erd/ui_components/ERDTool.spec.js
+++ b/web/regression/javascript/erd/ui_components/ERDTool.spec.js
@@ -125,7 +125,7 @@ describe('ERDTool', ()=>{
erd = mount(
-
+
);
@@ -319,7 +319,7 @@ describe('ERDTool', ()=>{
});
it('onAutoDistribute', ()=>{
- spyOn(bodyInstance.diagram, 'dagreDistributeNodes');
+ spyOn(bodyInstance.diagram, 'dagreDistributeNodes').and.callFake(()=>{/* intentionally empty */});
bodyInstance.onAutoDistribute();
expect(bodyInstance.diagram.dagreDistributeNodes).toHaveBeenCalled();
});
diff --git a/web/yarn.lock b/web/yarn.lock
index 1f89e73134e..f506196c204 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -8609,6 +8609,13 @@ react-property@2.0.0:
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
integrity sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==
+react-resize-detector@^8.0.3:
+ version "8.0.3"
+ resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-8.0.3.tgz#dab4470aae23bb07deb857230ccf945d000ef99b"
+ integrity sha512-c3eqm5BVcluVhxHsBQnhyPO/5uYB3XHIHz6D1ZOHzU2WcnZF0Cr3KLl5OIozRC2RSsdQlu5vn1PHEqrvKRnIYA==
+ dependencies:
+ lodash "^4.17.21"
+
react-rnd@^10.3.5:
version "10.3.7"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.3.7.tgz#037ce277e6c5e682989b51278e44a6ba299990af"
@@ -10192,6 +10199,16 @@ update-browserslist-db@^1.0.9:
escalade "^3.1.1"
picocolors "^1.0.0"
+uplot-react@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/uplot-react/-/uplot-react-1.1.4.tgz#02b9918a199da9983fc0d375fb44e443749e2ac0"
+ integrity sha512-qO1UkQwjVKdj5vTm3O3yldvu1T6hwY4++rH4KznLhjqpnLdncq1zsRxq/zQz/HUHPVD0j7WBcEISbNM61JsuAQ==
+
+uplot@^1.6.24:
+ version "1.6.24"
+ resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12"
+ integrity sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg==
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"