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"