mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Replace charting library Flotr2 with ChartJS using React. Fixes #3904
This commit is contained in:
committed by
Akshay Joshi
parent
5820b9521e
commit
f715373218
@@ -16,12 +16,17 @@ module.exports = {
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
],
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
'ecmaFeatures': {
|
||||
'jsx': true
|
||||
},
|
||||
'sourceType': 'module',
|
||||
},
|
||||
'plugins': [
|
||||
'react'
|
||||
],
|
||||
'globals': {
|
||||
'_': true,
|
||||
@@ -49,4 +54,9 @@ module.exports = {
|
||||
// We need to exclude below for RegEx case
|
||||
"no-useless-escape": 0,
|
||||
},
|
||||
'settings': {
|
||||
'react': {
|
||||
'version': 'detect',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
"copy-webpack-plugin": "^5.1.0",
|
||||
"core-js": "^3.2.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "5.15.1",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"file-loader": "^3.0.1",
|
||||
"iconfont-webpack-plugin": "^4.2.1",
|
||||
"image-webpack-loader": "^4.6.0",
|
||||
"is-docker": "^1.1.0",
|
||||
"jasmine-core": "~3.3.0",
|
||||
"jasmine-enzyme": "^7.1.2",
|
||||
"karma": "^4.0.1",
|
||||
"karma-babel-preprocessor": "^8.0.0",
|
||||
"karma-browserify": "~6.0.0",
|
||||
@@ -36,6 +40,7 @@
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"popper.js": "^1.14.7",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"raw-loader": "^1.0.0",
|
||||
"sass": "^1.24.4",
|
||||
"sass-loader": "^7.1.0",
|
||||
@@ -50,6 +55,8 @@
|
||||
"yarn-audit-html": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@simonwep/pickr": "^1.5.1",
|
||||
"acitree": "git+https://github.com/imsurinder90/jquery-aciTree.git#rc.7",
|
||||
"alertifyjs": "git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263",
|
||||
@@ -67,12 +74,12 @@
|
||||
"bootstrap4-toggle": "3.4.0",
|
||||
"bowser": "2.1.2",
|
||||
"browserify": "~16.2.3",
|
||||
"chart.js": "^2.9.3",
|
||||
"codemirror": "^5.54.0",
|
||||
"css-loader": "2.1.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dropzone": "^5.5.1",
|
||||
"exports-loader": "~0.7.0",
|
||||
"flotr2": "git+https://github.com/EnterpriseDB/Flotr2.git",
|
||||
"font-awesome": "^4.7.0",
|
||||
"immutability-helper": "^3.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
@@ -85,6 +92,8 @@
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.23",
|
||||
"mousetrap": "^1.6.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"requirejs": "~2.3.6",
|
||||
"select2": "^4.0.6-rc.1",
|
||||
"shim-loader": "^1.0.1",
|
||||
|
||||
@@ -24,11 +24,6 @@
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
margin-top: 10px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.icon-postgres:before {
|
||||
height: 43px;
|
||||
margin-top: 13px;
|
||||
|
||||
51
web/pgadmin/dashboard/static/js/ChartsDOM.jsx
Normal file
51
web/pgadmin/dashboard/static/js/ChartsDOM.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Graphs from './Graphs';
|
||||
|
||||
export default class ChartsDOM {
|
||||
constructor(container, preferences, sid, did, pageVisible=true) {
|
||||
this.container = container;
|
||||
this.preferences = preferences;
|
||||
this.sid = sid;
|
||||
this.did = did;
|
||||
this.pageVisible = pageVisible;
|
||||
}
|
||||
|
||||
mount() {
|
||||
if(this.container && this.preferences.show_graphs) {
|
||||
ReactDOM.render(<Graphs sid={this.sid} did={this.did} preferences={this.preferences} pageVisible={this.pageVisible}/>, this.container);
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.container && ReactDOM.unmountComponentAtNode(this.container);
|
||||
}
|
||||
|
||||
setSidDid(sid, did) {
|
||||
this.sid = sid;
|
||||
this.did = did;
|
||||
this.mount();
|
||||
}
|
||||
|
||||
reflectPreferences(preferences) {
|
||||
this.preferences = preferences;
|
||||
if(preferences.show_graphs) {
|
||||
this.mount();
|
||||
} else {
|
||||
this.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
setPageVisible(visible) {
|
||||
this.pageVisible = visible;
|
||||
this.mount();
|
||||
}
|
||||
}
|
||||
388
web/pgadmin/dashboard/static/js/Graphs.jsx
Normal file
388
web/pgadmin/dashboard/static/js/Graphs.jsx
Normal file
@@ -0,0 +1,388 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import React, { useEffect, useRef, useState, useReducer, useCallback, useMemo } from 'react';
|
||||
import {LineChart} from 'sources/chartjs';
|
||||
import {ChartContainer, DashboardRowCol, DashboardRow} from './dashboard_components';
|
||||
import url_for from 'sources/url_for';
|
||||
import axios from 'axios';
|
||||
import gettext from 'sources/gettext';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import {useInterval, usePrevious} from 'sources/custom_hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const X_AXIS_LENGTH = 75;
|
||||
export const POINT_SIZE = 2;
|
||||
|
||||
/* Transform the labels data to suit ChartJS */
|
||||
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: POINT_SIZE,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
labels: [...Array(X_AXIS_LENGTH).keys()],
|
||||
datasets: datasets,
|
||||
refreshRate: refreshRate,
|
||||
};
|
||||
}
|
||||
|
||||
/* Custom ChartJS legend callback */
|
||||
export function legendCallback(chart) {
|
||||
var text = [];
|
||||
text.push('<div class="' + chart.id + '-legend d-flex">');
|
||||
for (var i = 0; i < chart.data.datasets.length; i++) {
|
||||
text.push('<div class="legend-value"><span style="background-color:' + chart.data.datasets[i].backgroundColor + '"> </span>');
|
||||
if (chart.data.datasets[i].label) {
|
||||
text.push('<span class="legend-label">' + chart.data.datasets[i].label + '</span>');
|
||||
}
|
||||
text.push('</div>');
|
||||
}
|
||||
text.push('</div>');
|
||||
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');
|
||||
base_url += '/' + sid;
|
||||
base_url += (did > 0) ? ('/' + did) : '';
|
||||
base_url += '?chart_names=' + chart_names.join(',');
|
||||
return base_url;
|
||||
}
|
||||
|
||||
/* This will process incoming charts data add it the the previous charts
|
||||
* data to get the new state.
|
||||
*/
|
||||
export function statsReducer(state, action) {
|
||||
let newState = {};
|
||||
|
||||
if(action.reset) {
|
||||
return action.reset;
|
||||
}
|
||||
|
||||
if(!action.incoming) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if(!action.counterData) {
|
||||
action.counterData = action.incoming;
|
||||
}
|
||||
|
||||
Object.keys(action.incoming).forEach(label => {
|
||||
if(state[label]) {
|
||||
if(state[label].length >= X_AXIS_LENGTH) {
|
||||
state[label].unshift();
|
||||
}
|
||||
newState[label] = [
|
||||
action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
|
||||
...state[label],
|
||||
];
|
||||
} else {
|
||||
newState[label] = [
|
||||
action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
|
||||
];
|
||||
}
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
const chartsDefault = {
|
||||
'session_stats': {'Total': [], 'Active': [], 'Idle': []},
|
||||
'tps_stats': {'Transactions': [], 'Commits': [], 'Rollbacks': []},
|
||||
'ti_stats': {'Inserts': [], 'Updates': [], 'Delete': []},
|
||||
'to_stats': {'Fetched': [], 'Returned': []},
|
||||
'bio_stats': {'Reads': [], 'Hits': []},
|
||||
};
|
||||
|
||||
export default function Graphs({preferences, sid, did, pageVisible, enablePoll=true}) {
|
||||
const refreshOn = useRef(null);
|
||||
const prevPrefernces = usePrevious(preferences);
|
||||
|
||||
const [sessionStats, sessionStatsReduce] = useReducer(statsReducer, chartsDefault['session_stats']);
|
||||
const [tpsStats, tpsStatsReduce] = useReducer(statsReducer, chartsDefault['tps_stats']);
|
||||
const [tiStats, tiStatsReduce] = useReducer(statsReducer, chartsDefault['ti_stats']);
|
||||
const [toStats, toStatsReduce] = useReducer(statsReducer, chartsDefault['to_stats']);
|
||||
const [bioStats, bioStatsReduce] = useReducer(statsReducer, chartsDefault['bio_stats']);
|
||||
|
||||
const [counterData, setCounterData] = useState({});
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState(null);
|
||||
const [pollDelay, setPollDelay] = useState(1000);
|
||||
const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
let calcPollDelay = false;
|
||||
if(prevPrefernces) {
|
||||
if(prevPrefernces['session_stats_refresh'] != preferences['session_stats_refresh']) {
|
||||
sessionStatsReduce({reset: chartsDefault['session_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['tps_stats_refresh'] != preferences['tps_stats_refresh']) {
|
||||
tpsStatsReduce({reset:chartsDefault['tps_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['ti_stats_refresh'] != preferences['ti_stats_refresh']) {
|
||||
tiStatsReduce({reset:chartsDefault['ti_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['to_stats_refresh'] != preferences['to_stats_refresh']) {
|
||||
toStatsReduce({reset:chartsDefault['to_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['bio_stats_refresh'] != preferences['bio_stats_refresh']) {
|
||||
bioStatsReduce({reset:chartsDefault['bio_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
} else {
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(calcPollDelay) {
|
||||
setPollDelay(
|
||||
getGCD(Object.keys(chartsDefault).map((name)=>preferences[name+'_refresh']))*1000
|
||||
);
|
||||
}
|
||||
}, [preferences]);
|
||||
|
||||
useEffect(()=>{
|
||||
/* Charts rendered are not visible when, the dashboard is hidden but later visible */
|
||||
if(pageVisible && !chartDrawnOnce) {
|
||||
setChartDrawnOnce(true);
|
||||
}
|
||||
}, [pageVisible]);
|
||||
|
||||
useInterval(()=>{
|
||||
const currEpoch = getEpoch();
|
||||
if(refreshOn.current === null) {
|
||||
let tmpRef = {};
|
||||
Object.keys(chartsDefault).forEach((name)=>{
|
||||
tmpRef[name] = currEpoch;
|
||||
});
|
||||
refreshOn.current = tmpRef;
|
||||
}
|
||||
|
||||
let getFor = [];
|
||||
Object.keys(chartsDefault).forEach((name)=>{
|
||||
if(currEpoch >= refreshOn.current[name]) {
|
||||
getFor.push(name);
|
||||
refreshOn.current[name] = currEpoch + preferences[name+'_refresh'];
|
||||
}
|
||||
});
|
||||
|
||||
let path = getStatsUrl(sid, did, getFor);
|
||||
axios.get(path)
|
||||
.then((resp)=>{
|
||||
let data = resp.data;
|
||||
setErrorMsg(null);
|
||||
sessionStatsReduce({incoming: data['session_stats']});
|
||||
tpsStatsReduce({incoming: data['tps_stats'], counter: true, counterData: counterData['tps_stats']});
|
||||
tiStatsReduce({incoming: data['ti_stats'], counter: true, counterData: counterData['ti_stats']});
|
||||
toStatsReduce({incoming: data['to_stats'], counter: true, counterData: counterData['to_stats']});
|
||||
bioStatsReduce({incoming: data['bio_stats'], counter: true, counterData: counterData['bio_stats']});
|
||||
|
||||
setCounterData((prevCounterData)=>{
|
||||
return {
|
||||
...prevCounterData,
|
||||
...data,
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error)=>{
|
||||
if(!errorMsg) {
|
||||
sessionStatsReduce({reset: chartsDefault['session_stats']});
|
||||
tpsStatsReduce({reset:chartsDefault['tps_stats']});
|
||||
tiStatsReduce({reset:chartsDefault['ti_stats']});
|
||||
toStatsReduce({reset:chartsDefault['to_stats']});
|
||||
bioStatsReduce({reset:chartsDefault['bio_stats']});
|
||||
setCounterData({});
|
||||
if(error.response) {
|
||||
if (error.response.status === 428) {
|
||||
setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
|
||||
} else {
|
||||
setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
|
||||
}
|
||||
} else if(error.request) {
|
||||
setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
|
||||
return;
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, enablePoll ? pollDelay : -1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid='graph-poll-delay' className='d-none'>{pollDelay}</div>
|
||||
{chartDrawnOnce &&
|
||||
<GraphsWrapper
|
||||
sessionStats={transformData(sessionStats, preferences['session_stats_refresh'])}
|
||||
tpsStats={transformData(tpsStats, preferences['tps_stats_refresh'])}
|
||||
tiStats={transformData(tiStats, preferences['ti_stats_refresh'])}
|
||||
toStats={transformData(toStats, preferences['to_stats_refresh'])}
|
||||
bioStats={transformData(bioStats, preferences['bio_stats_refresh'])}
|
||||
errorMsg={errorMsg}
|
||||
showTooltip={preferences['graph_mouse_track']}
|
||||
showDataPoints={preferences['graph_data_points']}
|
||||
isDatabase={did > 0}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Graphs.propTypes = {
|
||||
preferences: PropTypes.object.isRequired,
|
||||
sid: PropTypes.oneOfType([
|
||||
PropTypes.string.isRequired,
|
||||
PropTypes.number.isRequired,
|
||||
]),
|
||||
did: PropTypes.oneOfType([
|
||||
PropTypes.string.isRequired,
|
||||
PropTypes.number.isRequired,
|
||||
]),
|
||||
enablePoll: PropTypes.bool,
|
||||
};
|
||||
|
||||
export function GraphsWrapper(props) {
|
||||
const sessionStatsLegendRef = useRef();
|
||||
const tpsStatsLegendRef = useRef();
|
||||
const tiStatsLegendRef = useRef();
|
||||
const toStatsLegendRef = useRef();
|
||||
const bioStatsLegendRef = useRef();
|
||||
const options = useMemo(()=>({
|
||||
legendCallback: legendCallback,
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 0,
|
||||
},
|
||||
responsiveAnimationDuration: 0,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: props.showDataPoints ? POINT_SIZE : 0,
|
||||
},
|
||||
},
|
||||
tooltips: {
|
||||
enabled: props.showTooltip,
|
||||
callbacks: {
|
||||
title: function(tooltipItem, data) {
|
||||
let title = '';
|
||||
try {
|
||||
title = parseInt(tooltipItem[0].xLabel)*data.refreshRate + gettext(' seconds ago');
|
||||
} catch (error) {
|
||||
title = '';
|
||||
}
|
||||
return title;
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
min: 0,
|
||||
userCallback: function(label) {
|
||||
if (Math.floor(label) === label) {
|
||||
return label;
|
||||
}
|
||||
},
|
||||
fontColor: getComputedStyle(document.documentElement).getPropertyValue('--color-fg'),
|
||||
},
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
zeroLineColor: getComputedStyle(document.documentElement).getPropertyValue('--border-color'),
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--border-color'),
|
||||
},
|
||||
}],
|
||||
xAxes: [{
|
||||
display: false,
|
||||
gridLines: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
reverse: true,
|
||||
},
|
||||
}],
|
||||
},
|
||||
}), [props.showTooltip, props.showDataPoints]);
|
||||
const updateOptions = useMemo(()=>({duration: 0}), []);
|
||||
|
||||
const onInitCallback = useCallback(
|
||||
(legendRef)=>(chart)=>{
|
||||
legendRef.current.innerHTML = chart.generateLegend();
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardRow>
|
||||
<DashboardRowCol breakpoint='md' parts={6}>
|
||||
<ChartContainer id='sessions-graph' title={props.isDatabase ? gettext('Database sessions') : gettext('Server sessions')} legendRef={sessionStatsLegendRef} errorMsg={props.errorMsg}>
|
||||
<LineChart options={options} data={props.sessionStats} updateOptions={updateOptions}
|
||||
onInit={onInitCallback(sessionStatsLegendRef)}/>
|
||||
</ChartContainer>
|
||||
</DashboardRowCol>
|
||||
<DashboardRowCol breakpoint='md' parts={6}>
|
||||
<ChartContainer id='tps-graph' title={gettext('Transactions per second')} legendRef={tpsStatsLegendRef} errorMsg={props.errorMsg}>
|
||||
<LineChart options={options} data={props.tpsStats} updateOptions={updateOptions}
|
||||
onInit={onInitCallback(tpsStatsLegendRef)}/>
|
||||
</ChartContainer>
|
||||
</DashboardRowCol>
|
||||
</DashboardRow>
|
||||
<DashboardRow>
|
||||
<DashboardRowCol breakpoint='md' parts={4}>
|
||||
<ChartContainer id='ti-graph' title={gettext('Tuples in')} legendRef={tiStatsLegendRef} errorMsg={props.errorMsg}>
|
||||
<LineChart options={options} data={props.tiStats} updateOptions={updateOptions}
|
||||
onInit={onInitCallback(tiStatsLegendRef)}/>
|
||||
</ChartContainer>
|
||||
</DashboardRowCol>
|
||||
<DashboardRowCol breakpoint='md' parts={4}>
|
||||
<ChartContainer id='to-graph' title={gettext('Tuples out')} legendRef={toStatsLegendRef} errorMsg={props.errorMsg}>
|
||||
<LineChart options={options} data={props.toStats} updateOptions={updateOptions}
|
||||
onInit={onInitCallback(toStatsLegendRef)}/>
|
||||
</ChartContainer>
|
||||
</DashboardRowCol>
|
||||
<DashboardRowCol breakpoint='md' parts={4}>
|
||||
<ChartContainer id='bio-graph' title={gettext('Block I/O')} legendRef={bioStatsLegendRef} errorMsg={props.errorMsg}>
|
||||
<LineChart options={options} data={props.bioStats} updateOptions={updateOptions}
|
||||
onInit={onInitCallback(bioStatsLegendRef)}/>
|
||||
</ChartContainer>
|
||||
</DashboardRowCol>
|
||||
</DashboardRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const propTypeStats = PropTypes.shape({
|
||||
labels: PropTypes.array.isRequired,
|
||||
datasets: PropTypes.array,
|
||||
refreshRate: PropTypes.number.isRequired,
|
||||
});
|
||||
GraphsWrapper.propTypes = {
|
||||
sessionStats: propTypeStats.isRequired,
|
||||
tpsStats: propTypeStats.isRequired,
|
||||
tiStats: propTypeStats.isRequired,
|
||||
toStats: propTypeStats.isRequired,
|
||||
bioStats: propTypeStats.isRequired,
|
||||
errorMsg: PropTypes.string,
|
||||
showTooltip: PropTypes.bool.isRequired,
|
||||
showDataPoints: PropTypes.bool.isRequired,
|
||||
isDatabase: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
export class Chart {
|
||||
constructor(container, options) {
|
||||
let self = this;
|
||||
|
||||
require.ensure(['flotr2'], function(require) {
|
||||
self.chartApi = require('flotr2');
|
||||
}, function(error){
|
||||
throw(error);
|
||||
}, 'chart');
|
||||
|
||||
/* Html Node where the graph goes */
|
||||
this._container = container;
|
||||
/* Graph library options */
|
||||
this._options = {};
|
||||
this._defaultOptions = {
|
||||
legend: {
|
||||
position: 'nw',
|
||||
backgroundColor: '#D2E8FF',
|
||||
},
|
||||
lines: {
|
||||
show: true,
|
||||
lineWidth: 2,
|
||||
},
|
||||
shadowSize: 0,
|
||||
resolution : 3,
|
||||
grid: {
|
||||
color: 'transparent',
|
||||
tickColor: '#8f8f8f',
|
||||
},
|
||||
};
|
||||
|
||||
this._dataset = null;
|
||||
this._tooltipFormatter = null;
|
||||
/* Just to store other data related to charts. Used nowhere here in the module */
|
||||
this._otherData = {};
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
getContainer() {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
getContainerDimensions() {
|
||||
return {
|
||||
height: this._container.clientHeight,
|
||||
width: this._container.clientWidth,
|
||||
};
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
/* This should be changed if library changed */
|
||||
setOptions(options, mergeOptions=true) {
|
||||
/* If mergeOptions then merge the options, else replace existing options */
|
||||
if(mergeOptions) {
|
||||
this._options = {...this._defaultOptions, ...this._options, ...options};
|
||||
} else {
|
||||
this._options = {...this._defaultOptions, ...options};
|
||||
}
|
||||
}
|
||||
|
||||
removeOptions(optionKey) {
|
||||
if(this._options[optionKey]) {
|
||||
delete this._options[optionKey];
|
||||
}
|
||||
}
|
||||
|
||||
getOtherData(key) {
|
||||
if(this._otherData[key]) {
|
||||
return this._otherData[key];
|
||||
}
|
||||
}
|
||||
|
||||
setOtherData(key, value) {
|
||||
this._otherData[key] = value;
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
let dim = this.getContainerDimensions();
|
||||
return (dim.height > 0 && dim.width > 0);
|
||||
}
|
||||
|
||||
isInPage() {
|
||||
return (this._container === document.body) ? false : document.body.contains(this._container);
|
||||
}
|
||||
|
||||
setTooltipFormatter(tooltipFormatter) {
|
||||
let opt = this.getOptions();
|
||||
|
||||
this._tooltipFormatter = tooltipFormatter;
|
||||
|
||||
if(this._tooltipFormatter) {
|
||||
this.setOptions({
|
||||
mouse: {
|
||||
...opt.mouse,
|
||||
trackFormatter: this._tooltipFormatter,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
draw(dataset) {
|
||||
this._dataset = dataset;
|
||||
if(this._container) {
|
||||
if(this.chartApi) {
|
||||
this.chartApi.draw(this._container, this._dataset, this._options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@
|
||||
|
||||
define('pgadmin.dashboard', [
|
||||
'sources/url_for', 'sources/gettext', 'require', 'jquery', 'underscore',
|
||||
'sources/pgadmin', 'backbone', 'backgrid', './charting',
|
||||
'sources/pgadmin', 'backbone', 'backgrid',
|
||||
'pgadmin.alertifyjs', 'pgadmin.backform', 'sources/nodes/dashboard',
|
||||
'sources/utils', 'sources/window', 'pgadmin.browser', 'bootstrap', 'wcdocker',
|
||||
'sources/window', './ChartsDOM', 'pgadmin.browser', 'bootstrap', 'wcdocker',
|
||||
], function(
|
||||
url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, charting,
|
||||
Alertify, Backform, NodesDashboard, commonUtils, pgWindow
|
||||
url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid,
|
||||
Alertify, Backform, NodesDashboard, pgWindow, ChartsDOM
|
||||
) {
|
||||
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
@@ -203,6 +203,7 @@ define('pgadmin.dashboard', [
|
||||
return;
|
||||
|
||||
this.initialized = true;
|
||||
this.chartsDomObj = null;
|
||||
|
||||
this.sid = this.did = -1;
|
||||
this.version = -1;
|
||||
@@ -221,10 +222,6 @@ define('pgadmin.dashboard', [
|
||||
// Load the default welcome dashboard
|
||||
var url = url_for('dashboard.index');
|
||||
|
||||
/* Store the chart objects, refresh freq and next refresh time */
|
||||
this.chart_store = {};
|
||||
this.charts_poller_int_id = -1;
|
||||
|
||||
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
|
||||
if (dashboardPanel) {
|
||||
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
|
||||
@@ -283,18 +280,7 @@ define('pgadmin.dashboard', [
|
||||
object_selected: function(item, itemData, node) {
|
||||
let self = this;
|
||||
|
||||
if (dashboardVisible === false) {
|
||||
/*
|
||||
* Clear all the interval functions, even when dashboard is not
|
||||
* visible (in case of connection of the object got disconnected).
|
||||
*/
|
||||
if (
|
||||
!_.isUndefined(itemData.connected) &&
|
||||
itemData.connected !== true
|
||||
) {
|
||||
self.clearChartFromStore();
|
||||
}
|
||||
} else if (itemData && itemData._type) {
|
||||
if (dashboardVisible && itemData && itemData._type) {
|
||||
var treeHierarchy = node.getTreeNodeHierarchy(item),
|
||||
url = NodesDashboard.url(itemData, item, treeHierarchy);
|
||||
|
||||
@@ -355,11 +341,9 @@ define('pgadmin.dashboard', [
|
||||
$(dashboardPanel).data('server_status') == false
|
||||
)
|
||||
) {
|
||||
this.chartsDomObj && this.chartsDomObj.unmount();
|
||||
$(div).empty();
|
||||
|
||||
/* Clear all the charts previous dashboards */
|
||||
self.clearChartFromStore();
|
||||
|
||||
let ajaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
@@ -393,14 +377,8 @@ define('pgadmin.dashboard', [
|
||||
$(dashboardPanel).data('server_status', true);
|
||||
}
|
||||
} else {
|
||||
this.chartsDomObj && this.chartsDomObj.unmount();
|
||||
$(div).empty();
|
||||
if (
|
||||
!_.isUndefined(itemData.connected) &&
|
||||
itemData.connected !== true
|
||||
) {
|
||||
/* Clear all the charts previous dashboards */
|
||||
self.clearChartFromStore();
|
||||
}
|
||||
$(div).html(
|
||||
'<div class="pg-panel-message" role="alert">' + gettext('Please connect to the selected server to view the dashboard.') + '</div>'
|
||||
);
|
||||
@@ -413,218 +391,6 @@ define('pgadmin.dashboard', [
|
||||
}
|
||||
},
|
||||
|
||||
// Render the charts
|
||||
renderCharts: function(charts_config) {
|
||||
|
||||
let self = this,
|
||||
tooltipFormatter = function(refresh, currVal) {
|
||||
return(`Seconds ago: ${parseInt(currVal.x * refresh)}</br>
|
||||
Value: ${currVal.y}`);
|
||||
},
|
||||
curr_epoch=commonUtils.getEpoch();
|
||||
|
||||
self.stopChartsPoller();
|
||||
|
||||
charts_config.map((chart_config) => {
|
||||
if(self.chart_store[chart_config.chart_name]
|
||||
&& self.old_preferences[chart_config.refresh_pref_name] !=
|
||||
self.preferences[chart_config.refresh_pref_name]) {
|
||||
self.clearChartFromStore(chart_config.chart_name);
|
||||
}
|
||||
|
||||
if(self.chart_store[chart_config.chart_name]) {
|
||||
let chart_obj = self.chart_store[chart_config.chart_name].chart_obj;
|
||||
chart_obj.setOptions(chart_config.options, false);
|
||||
chart_obj.setTooltipFormatter(
|
||||
tooltipFormatter.bind(null, self.preferences[chart_config.refresh_pref_name])
|
||||
);
|
||||
}
|
||||
|
||||
if(!self.chart_store[chart_config.chart_name]) {
|
||||
let chart_obj = new charting.Chart(chart_config.container, chart_config.options);
|
||||
|
||||
chart_obj.setTooltipFormatter(
|
||||
tooltipFormatter.bind(null, self.preferences[chart_config.refresh_pref_name])
|
||||
);
|
||||
|
||||
chart_obj.setOtherData('counter', chart_config.counter);
|
||||
|
||||
self.chart_store[chart_config.chart_name] = {
|
||||
'chart_obj' : chart_obj,
|
||||
'refresh_on': curr_epoch,
|
||||
'refresh_rate': self.preferences[chart_config.refresh_pref_name],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
self.startChartsPoller(self.chart_store, self.sid, self.did);
|
||||
},
|
||||
|
||||
getStatsUrl: function(sid=-1, did=-1, chart_names=[]) {
|
||||
let base_url = url_for('dashboard.dashboard_stats');
|
||||
base_url += '/' + sid;
|
||||
base_url += (did > 0) ? ('/' + did) : '';
|
||||
base_url += '?chart_names=' + chart_names.join(',');
|
||||
return base_url;
|
||||
},
|
||||
|
||||
updateChart: function(chart_obj, new_data){
|
||||
// Dataset format:
|
||||
// [
|
||||
// { data: [[0, y0], [1, y1]...], label: 'Label 1', [options] },
|
||||
// { data: [[0, y0], [1, y1]...], label: 'Label 2', [options] },
|
||||
// { data: [[0, y0], [1, y1]...], label: 'Label 3', [options] }
|
||||
// ]
|
||||
let dataset = chart_obj.getOtherData('dataset') || [],
|
||||
counter_prev_data = chart_obj.getOtherData('counter_prev_data') || new_data,
|
||||
counter = chart_obj.getOtherData('counter') || false;
|
||||
|
||||
if (dataset.length == 0) {
|
||||
// Create the initial data structure
|
||||
for (let label in new_data) {
|
||||
dataset.push({
|
||||
'data': [
|
||||
[0, counter ? (new_data[label] - counter_prev_data[label]) : new_data[label]],
|
||||
],
|
||||
'label': label,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Object.keys(new_data).forEach((label, label_ind) => {
|
||||
// Push new values onto the existing data structure
|
||||
// If this is a counter stat, we need to subtract the previous value
|
||||
if (!counter) {
|
||||
dataset[label_ind]['data'].unshift([0, new_data[label]]);
|
||||
} else {
|
||||
// Store the current value, minus the previous one we stashed.
|
||||
// It's possible the tab has been reloaded, in which case out previous values are gone
|
||||
if (_.isUndefined(counter_prev_data))
|
||||
return;
|
||||
|
||||
dataset[label_ind]['data'].unshift([0, new_data[label] - counter_prev_data[label]]);
|
||||
}
|
||||
|
||||
// Reset the time index to get a proper scrolling display
|
||||
for (var time_ind = 0; time_ind < dataset[label_ind]['data'].length; time_ind++) {
|
||||
dataset[label_ind]['data'][time_ind][0] = time_ind;
|
||||
}
|
||||
});
|
||||
counter_prev_data = new_data;
|
||||
}
|
||||
|
||||
// Remove old data points
|
||||
for (let label_ind = 0; label_ind < dataset.length; label_ind++) {
|
||||
if (dataset[label_ind]['data'].length > 101) {
|
||||
dataset[label_ind]['data'].pop();
|
||||
}
|
||||
}
|
||||
|
||||
chart_obj.setOtherData('dataset', dataset);
|
||||
chart_obj.setOtherData('counter_prev_data', counter_prev_data);
|
||||
|
||||
if (chart_obj.isInPage()) {
|
||||
if (chart_obj.isVisible()) {
|
||||
chart_obj.draw(dataset);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
stopChartsPoller: function() {
|
||||
clearInterval(this.charts_poller_int_id);
|
||||
},
|
||||
|
||||
startChartsPoller: function(chart_store, sid, did) {
|
||||
let self = this;
|
||||
/* polling will the greatest common divisor of the refresh rates*/
|
||||
let poll_interval = commonUtils.getGCD(
|
||||
Object.values(chart_store).map(item => item.refresh_rate)
|
||||
);
|
||||
const WAIT_COUNTER = 3;
|
||||
let last_poll_wait_counter = 0;
|
||||
let resp_not_received_counter = 0;
|
||||
|
||||
/* Stop if running, only one poller lives */
|
||||
self.stopChartsPoller();
|
||||
|
||||
var thePollingFunc = function() {
|
||||
let curr_epoch = commonUtils.getEpoch();
|
||||
let chart_names_to_get = [];
|
||||
|
||||
for(let chart_name in chart_store) {
|
||||
/* when its time to get the data */
|
||||
if(chart_store[chart_name].refresh_on <= curr_epoch) {
|
||||
/* set the next trigger point */
|
||||
chart_store[chart_name].refresh_on = curr_epoch + chart_store[chart_name].refresh_rate;
|
||||
chart_names_to_get.push(chart_name);
|
||||
}
|
||||
}
|
||||
|
||||
/* If none of the chart wants data, don't trouble
|
||||
* If response not received from prev poll, don't trouble !!
|
||||
*/
|
||||
if(chart_names_to_get.length == 0 || last_poll_wait_counter > 0 || resp_not_received_counter >= WAIT_COUNTER) {
|
||||
/* reduce the number of tries, request should be sent if last_poll_wait_counter
|
||||
* completes WAIT_COUNTER times.*/
|
||||
last_poll_wait_counter--;
|
||||
return;
|
||||
}
|
||||
|
||||
var path = self.getStatsUrl(sid, did, chart_names_to_get);
|
||||
resp_not_received_counter++;
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'GET',
|
||||
})
|
||||
.done(function(resp) {
|
||||
for(let chart_name in resp) {
|
||||
let chart_obj = chart_store[chart_name].chart_obj;
|
||||
$(chart_obj.getContainer()).removeClass('graph-error');
|
||||
self.updateChart(chart_obj, resp[chart_name]);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
let err = '';
|
||||
let msg = '';
|
||||
let cls = 'info';
|
||||
|
||||
if (xhr.readyState === 0) {
|
||||
msg = gettext('Not connected to the server or the connection to the server has been closed.');
|
||||
} else {
|
||||
err = JSON.parse(xhr.responseText);
|
||||
msg = err.errormsg;
|
||||
|
||||
// If we get a 428, it means the server isn't connected
|
||||
if (xhr.status === 428) {
|
||||
if (_.isUndefined(msg) || _.isNull(msg)) {
|
||||
msg = gettext('Please connect to the selected server to view the graph.');
|
||||
}
|
||||
} else {
|
||||
msg = gettext('An error occurred whilst rendering the graph.');
|
||||
cls = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
for(let chart_name in chart_store) {
|
||||
let chart_obj = chart_store[chart_name].chart_obj;
|
||||
$(chart_obj.getContainer()).addClass('graph-error');
|
||||
$(chart_obj.getContainer()).html(
|
||||
'<div class="pg-panel-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>'
|
||||
);
|
||||
}
|
||||
})
|
||||
.always(function() {
|
||||
last_poll_wait_counter = 0;
|
||||
resp_not_received_counter--;
|
||||
});
|
||||
last_poll_wait_counter = WAIT_COUNTER;
|
||||
};
|
||||
/* Execute once for the first time as setInterval will not do */
|
||||
thePollingFunc();
|
||||
self.charts_poller_int_id = setInterval(thePollingFunc, poll_interval * 1000);
|
||||
},
|
||||
|
||||
// Handler function to support the "Add Server" link
|
||||
add_new_server: function() {
|
||||
if (pgBrowser && pgBrowser.tree) {
|
||||
@@ -759,23 +525,18 @@ define('pgadmin.dashboard', [
|
||||
});
|
||||
},
|
||||
|
||||
clearChartFromStore: function(chartName) {
|
||||
var self = this;
|
||||
if(!chartName){
|
||||
self.stopChartsPoller();
|
||||
_.each(self.chart_store, function(chart, key) {
|
||||
delete self.chart_store[key];
|
||||
});
|
||||
}
|
||||
else {
|
||||
delete self.chart_store[chartName];
|
||||
}
|
||||
},
|
||||
|
||||
// Rock n' roll on the dashboard
|
||||
init_dashboard: function() {
|
||||
let self = this;
|
||||
|
||||
this.chartsDomObj = new ChartsDOM.default(
|
||||
document.getElementById('dashboard-graphs'),
|
||||
self.preferences,
|
||||
self.sid,
|
||||
self.did,
|
||||
$('.dashboard-container')[0].clientHeight <=0 ? false : true
|
||||
);
|
||||
|
||||
/* Cache may take time to load for the first time
|
||||
* Keep trying till available
|
||||
*/
|
||||
@@ -796,81 +557,14 @@ define('pgadmin.dashboard', [
|
||||
pgBrowser.onPreferencesChange('dashboards', function() {
|
||||
self.reflectPreferences();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
reflectPreferences: function() {
|
||||
var self = this;
|
||||
var tickColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color');
|
||||
|
||||
/* We will use old preferences for selective graph updates on preference change */
|
||||
if(self.preferences) {
|
||||
self.old_preferences = self.preferences;
|
||||
self.preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('dashboards');
|
||||
}
|
||||
else {
|
||||
self.preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('dashboards');
|
||||
self.old_preferences = self.preferences;
|
||||
}
|
||||
self.preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('dashboards');
|
||||
this.chartsDomObj.reflectPreferences(self.preferences);
|
||||
|
||||
if(is_server_dashboard || is_database_dashboard) {
|
||||
/* Common things can come here */
|
||||
var div_sessions = $('.dashboard-container').find('#graph-sessions')[0];
|
||||
var div_tps = $('.dashboard-container').find('#graph-tps')[0];
|
||||
var div_ti = $('.dashboard-container').find('#graph-ti')[0];
|
||||
var div_to = $('.dashboard-container').find('#graph-to')[0];
|
||||
var div_bio = $('.dashboard-container').find('#graph-bio')[0];
|
||||
var options_line = {
|
||||
parseFloat: false,
|
||||
xaxis: {
|
||||
min: 100,
|
||||
max: 0,
|
||||
autoscale: 0,
|
||||
},
|
||||
yaxis: {
|
||||
autoscale: 1,
|
||||
},
|
||||
grid: {
|
||||
color: 'transparent',
|
||||
tickColor: tickColor,
|
||||
},
|
||||
};
|
||||
|
||||
if(self.preferences.graph_data_points) {
|
||||
/* Merge data points related options */
|
||||
options_line = {
|
||||
...options_line,
|
||||
...{
|
||||
points: {
|
||||
show:true,
|
||||
radius: 1,
|
||||
hitRadius: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if(self.preferences.graph_mouse_track) {
|
||||
/* Merge mouse track related options */
|
||||
options_line = {
|
||||
...options_line,
|
||||
...{
|
||||
mouse: {
|
||||
track:true,
|
||||
position: 'sw',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if(self.preferences.show_graphs && $('#dashboard-graphs').hasClass('dashboard-hidden')) {
|
||||
$('#dashboard-graphs').removeClass('dashboard-hidden');
|
||||
}
|
||||
else if(!self.preferences.show_graphs) {
|
||||
$('#dashboard-graphs').addClass('dashboard-hidden');
|
||||
self.clearChartFromStore();
|
||||
}
|
||||
|
||||
if (self.preferences.show_activity && $('#dashboard-activity').hasClass('dashboard-hidden')) {
|
||||
$('#dashboard-activity').removeClass('dashboard-hidden');
|
||||
}
|
||||
@@ -878,46 +572,11 @@ define('pgadmin.dashboard', [
|
||||
$('#dashboard-activity').addClass('dashboard-hidden');
|
||||
}
|
||||
|
||||
if(self.preferences.show_graphs) {
|
||||
// Render the graphs
|
||||
pgAdmin.Dashboard.renderCharts([{
|
||||
chart_name: 'session_stats',
|
||||
container: div_sessions,
|
||||
options: options_line,
|
||||
counter: false,
|
||||
refresh_pref_name: 'session_stats_refresh',
|
||||
}, {
|
||||
chart_name: 'tps_stats',
|
||||
container: div_tps,
|
||||
options: options_line,
|
||||
counter: true,
|
||||
refresh_pref_name: 'tps_stats_refresh',
|
||||
}, {
|
||||
chart_name: 'ti_stats',
|
||||
container: div_ti,
|
||||
options: options_line,
|
||||
counter: true,
|
||||
refresh_pref_name: 'ti_stats_refresh',
|
||||
}, {
|
||||
chart_name: 'to_stats',
|
||||
container: div_to,
|
||||
options: options_line,
|
||||
counter: true,
|
||||
refresh_pref_name: 'to_stats_refresh',
|
||||
}, {
|
||||
chart_name: 'bio_stats',
|
||||
container: div_bio,
|
||||
options: options_line,
|
||||
counter: true,
|
||||
refresh_pref_name: 'bio_stats_refresh',
|
||||
}]);
|
||||
if (self.preferences.show_activity && $('#dashboard-activity').hasClass('dashboard-hidden')) {
|
||||
$('#dashboard-activity').removeClass('dashboard-hidden');
|
||||
}
|
||||
|
||||
if(!self.preferences.show_graphs && !self.preferences.show_activity) {
|
||||
$('#dashboard-none-show').removeClass('dashboard-hidden');
|
||||
}
|
||||
else {
|
||||
$('#dashboard-none-show').addClass('dashboard-hidden');
|
||||
else if(!self.preferences.show_activity) {
|
||||
$('#dashboard-activity').addClass('dashboard-hidden');
|
||||
}
|
||||
|
||||
/* Dashboard specific preferences can be updated in the
|
||||
@@ -1440,7 +1099,9 @@ define('pgadmin.dashboard', [
|
||||
toggleVisibility: function(visible, closed=false) {
|
||||
dashboardVisible = visible;
|
||||
if(closed) {
|
||||
this.clearChartFromStore();
|
||||
this.chartsDomObj && this.chartsDomObj.unmount();
|
||||
} else {
|
||||
this.chartsDomObj && this.chartsDomObj.setPageVisible(dashboardVisible);
|
||||
}
|
||||
},
|
||||
can_take_action: function(m) {
|
||||
|
||||
74
web/pgadmin/dashboard/static/js/dashboard_components.jsx
Normal file
74
web/pgadmin/dashboard/static/js/dashboard_components.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export function ChartContainer(props) {
|
||||
return (
|
||||
<div className="card dashboard-graph" role="object-document" tabIndex="0" aria-labelledby={props.id}>
|
||||
<div className="card-header">
|
||||
<div className="d-flex">
|
||||
<div id={props.id}>{props.title}</div>
|
||||
<div className="ml-auto my-auto legend" ref={props.legendRef}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body dashboard-graph-body">
|
||||
<div className={'chart-wrapper ' + (props.errorMsg ? 'd-none': '')}>
|
||||
{props.children}
|
||||
</div>
|
||||
<ChartError message={props.errorMsg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChartContainer.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
legendRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.any }),
|
||||
]).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
errorMsg: PropTypes.string,
|
||||
};
|
||||
|
||||
export function ChartError(props) {
|
||||
if(props.message === null) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<div className="pg-panel-error pg-panel-message" role="alert">{props.message}</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChartError.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
export function DashboardRow({children}) {
|
||||
return (
|
||||
<div className="row dashboard-row">{children}</div>
|
||||
);
|
||||
}
|
||||
DashboardRow.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export function DashboardRowCol({breakpoint, parts, children}) {
|
||||
return (
|
||||
<div className={`col-${breakpoint}-${parts}`}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardRowCol.propTypes = {
|
||||
breakpoint: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']).isRequired,
|
||||
parts: PropTypes.number.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -35,8 +35,22 @@
|
||||
color: $color-fg;
|
||||
}
|
||||
|
||||
.dashboard-graph {
|
||||
& .legend {
|
||||
font-size: $tree-font-size;
|
||||
& .legend-value {
|
||||
font-weight: normal;
|
||||
margin-left: 0.25rem;
|
||||
& .legend-label {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-graph-body {
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: 175px;
|
||||
|
||||
& .flotr-labels {
|
||||
color: $color-fg !important;
|
||||
|
||||
@@ -1,60 +1,5 @@
|
||||
<div class="container-fluid dashboard-container negative-space">
|
||||
<div id="dashboard-graphs" class="dashboard-hidden">
|
||||
<div class="row dashboard-row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="database-session-graph">
|
||||
<div class="card-header" id="database-session-graph">
|
||||
{{ _('Database sessions') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-sessions" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="transactions-per-second-graph">
|
||||
<div class="card-header" id="transactions-per-second-graph">
|
||||
{{ _('Transactions per second') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-tps" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row dashboard-row">
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="tuples-in-graph">
|
||||
<div class="card-header" id="tuples-in-graph">
|
||||
{{ _('Tuples in') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-ti" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="tuples-out-graph">
|
||||
<div class="card-header" id="tuples-out-graph">
|
||||
{{ _('Tuples out') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-to" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="block-io-graph">
|
||||
<div class="card-header" id="block-io-graph">
|
||||
{{ _('Block I/O') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-bio" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashboard-graphs"></div>
|
||||
<div id="dashboard-activity" class="card dashboard-row dashboard-hidden">
|
||||
<div class="card-header">
|
||||
<span id="dashboard-activity-header">{{ _('Server activity') }}</span>
|
||||
|
||||
@@ -1,59 +1,5 @@
|
||||
<div class="container-fluid dashboard-container negative-space">
|
||||
<div id="dashboard-graphs" class="dashboard-hidden">
|
||||
<div class="row dashboard-row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="server-sessions-graph">
|
||||
<div class="card-header" id="server-sessions-graph">
|
||||
{{ _('Server sessions') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-sessions" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="transactions-per-second-graph">
|
||||
<div class="card-header" id="transactions-per-second-graph">
|
||||
{{ _('Transactions per second') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-tps" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row dashboard-row">
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="tuples-in-graph">
|
||||
<div class="card-header" id="tuples-in-graph">
|
||||
{{ _('Tuples in') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-ti" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="tuples-out-graph">
|
||||
<div class="card-header" id="tuples-out-graph">
|
||||
{{ _('Tuples out') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-to" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card" role="object-document" tabindex="0" aria-labelledby="block-o-graph">
|
||||
<div class="card-header" id="block-o-graph">
|
||||
{{ _('Block I/O') }}
|
||||
</div>
|
||||
<div class="card-body dashboard-graph-body">
|
||||
<div id="graph-bio" class="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashboard-graphs">
|
||||
</div>
|
||||
<div id="dashboard-activity" class="card dashboard-row dashboard-hidden">
|
||||
<div class="card-header">
|
||||
|
||||
95
web/pgadmin/static/js/chartjs/index.jsx
Normal file
95
web/pgadmin/static/js/chartjs/index.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import React, { useEffect } from 'react';
|
||||
import Chart from 'chart.js';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const defaultOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
padding: 8,
|
||||
},
|
||||
hover: {
|
||||
animationDuration:0,
|
||||
},
|
||||
};
|
||||
|
||||
export default function BaseChart({type='line', id, options, data, redraw=false, ...props}) {
|
||||
const chartRef = React.useRef();
|
||||
const chartObj = React.useRef();
|
||||
let optionsMerged = Chart.helpers.configMerge(defaultOptions, options);
|
||||
|
||||
const initChart = function() {
|
||||
let chartContext = chartRef.current.getContext('2d');
|
||||
chartObj.current = new Chart(chartContext, {
|
||||
type: type,
|
||||
data: data,
|
||||
options: optionsMerged,
|
||||
});
|
||||
props.onInit && props.onInit(chartObj.current);
|
||||
}
|
||||
|
||||
const destroyChart = function() {
|
||||
chartObj.current && chartObj.current.destroy();
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
initChart();
|
||||
return destroyChart;
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
if(typeof(chartObj.current) != 'undefined') {
|
||||
chartObj.current.data = data;
|
||||
for(let i=0; i<chartObj.current.data.datasets.length; i++) {
|
||||
chartObj.current.data.datasets[i] = {
|
||||
...chartObj.current.data.datasets[i],
|
||||
...data.datasets[i],
|
||||
};
|
||||
}
|
||||
chartObj.current.options = optionsMerged;
|
||||
chartObj.current.update(props.updateOptions || {});
|
||||
props.onUpdate && props.onUpdate(chartObj.current);
|
||||
}
|
||||
}, [data, options]);
|
||||
|
||||
useEffect(()=>{
|
||||
if(redraw) {
|
||||
destroyChart();
|
||||
initChart();
|
||||
}
|
||||
}, [redraw])
|
||||
|
||||
return (
|
||||
<canvas id={id} ref={chartRef}></canvas>
|
||||
);
|
||||
}
|
||||
|
||||
BaseChart.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
options: PropTypes.object,
|
||||
updateOptions: PropTypes.object,
|
||||
onInit: PropTypes.func,
|
||||
onUpdate: PropTypes.func,
|
||||
};
|
||||
|
||||
export function LineChart(props) {
|
||||
return (
|
||||
<BaseChart {...props} type='line'/>
|
||||
);
|
||||
}
|
||||
29
web/pgadmin/static/js/custom_hooks.js
Normal file
29
web/pgadmin/static/js/custom_hooks.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/* React hook for setInterval */
|
||||
export function useInterval(callback, delay) {
|
||||
const savedCallback = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
|
||||
if(delay > -1) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
export function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ $theme-colors: (
|
||||
|
||||
/* Certain variables are required in JS directly */
|
||||
:root {
|
||||
--color-fg: #{$color-fg};
|
||||
--border-color: #{$border-color};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import $ from 'jquery';
|
||||
import {Chart} from 'top/dashboard/static/js/charting';
|
||||
|
||||
describe('In charting related testcases', ()=> {
|
||||
let chartObj = undefined,
|
||||
chartDiv = undefined,
|
||||
options = {};
|
||||
|
||||
beforeEach(()=> {
|
||||
$('body').append(
|
||||
'<div id="charting-test-container"></div>'
|
||||
);
|
||||
chartDiv = $('#charting-test-container')[0];
|
||||
chartObj = new Chart(chartDiv, options);
|
||||
});
|
||||
|
||||
it('Return the correct container', ()=>{
|
||||
expect(chartObj.getContainer()).toEqual(chartDiv);
|
||||
});
|
||||
|
||||
it('Returns the container dimensions', ()=>{
|
||||
let dim = chartObj.getContainerDimensions();
|
||||
expect(dim.height).toBeDefined();
|
||||
expect(dim.width).toBeDefined();
|
||||
});
|
||||
|
||||
it('Check if options are set', ()=>{
|
||||
chartObj.setOptions({
|
||||
mouse: {
|
||||
track:true,
|
||||
},
|
||||
});
|
||||
|
||||
let opt = chartObj.getOptions();
|
||||
|
||||
expect(opt.mouse).toBeDefined();
|
||||
});
|
||||
|
||||
it('Check if options are set with mergeOptions false', ()=>{
|
||||
let overOpt = {
|
||||
mouse: {
|
||||
track:true,
|
||||
},
|
||||
};
|
||||
chartObj.setOptions(overOpt, false);
|
||||
|
||||
let newOptShouldBe = {...chartObj._defaultOptions, ...overOpt};
|
||||
|
||||
let opt = chartObj.getOptions();
|
||||
expect(JSON.stringify(opt)).toEqual(JSON.stringify(newOptShouldBe));
|
||||
});
|
||||
|
||||
it('Check if other data is set', ()=>{
|
||||
chartObj.setOtherData('some_val', 1);
|
||||
expect(chartObj._otherData['some_val']).toEqual(1);
|
||||
});
|
||||
|
||||
it('Check if other data is get', ()=>{
|
||||
chartObj.setOtherData('some_val', 1);
|
||||
expect(chartObj.getOtherData('some_val')).toEqual(1);
|
||||
});
|
||||
|
||||
it('Check if other data returns undefined for not set', ()=>{
|
||||
expect(chartObj.getOtherData('some_val_not_set')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('Check if isVisible returns correct', ()=>{
|
||||
let dimSpy = spyOn(chartObj, 'getContainerDimensions');
|
||||
|
||||
dimSpy.and.returnValue({
|
||||
height: 1, width: 1,
|
||||
});
|
||||
expect(chartObj.isVisible()).toEqual(true);
|
||||
dimSpy.and.stub();
|
||||
|
||||
dimSpy.and.returnValue({
|
||||
height: 0, width: 0,
|
||||
});
|
||||
expect(chartObj.isVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
it('Check if isInPage returns correct', ()=>{
|
||||
expect(chartObj.isInPage()).toEqual(true);
|
||||
$('body').find('#charting-test-container').remove();
|
||||
expect(chartObj.isInPage()).toEqual(false);
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
$('body').find('#charting-test-container').remove();
|
||||
});
|
||||
});
|
||||
169
web/regression/javascript/dashboard/graphs_spec.js
Normal file
169
web/regression/javascript/dashboard/graphs_spec.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../helper/enzyme.helper';
|
||||
|
||||
import Graphs, {GraphsWrapper} from '../../../pgadmin/dashboard/static/js/Graphs';
|
||||
import {X_AXIS_LENGTH, POINT_SIZE, transformData, legendCallback,
|
||||
getStatsUrl, statsReducer} from '../../../pgadmin/dashboard/static/js/Graphs';
|
||||
|
||||
describe('Graphs.js', ()=>{
|
||||
it('transformData', ()=>{
|
||||
expect(transformData({'Label1': [], 'Label2': []}, 1)).toEqual({
|
||||
labels: [...Array(X_AXIS_LENGTH).keys()],
|
||||
datasets: [{
|
||||
label: 'Label1',
|
||||
data: [],
|
||||
borderColor: '#00BCD4',
|
||||
backgroundColor: '#00BCD4',
|
||||
pointHitRadius: POINT_SIZE,
|
||||
},{
|
||||
label: 'Label2',
|
||||
data: [],
|
||||
borderColor: '#9CCC65',
|
||||
backgroundColor: '#9CCC65',
|
||||
pointHitRadius: POINT_SIZE,
|
||||
}],
|
||||
refreshRate: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('legendCallback', ()=>{
|
||||
expect(legendCallback({
|
||||
id: 1,
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Label1',
|
||||
backgroundColor: '#00BCD4',
|
||||
},{
|
||||
label: 'Label2',
|
||||
backgroundColor: '#9CCC65',
|
||||
}],
|
||||
},
|
||||
})).toEqual([
|
||||
'<div class="1-legend d-flex">',
|
||||
'<div class="legend-value"><span style="background-color:#00BCD4"> </span>',
|
||||
'<span class="legend-label">Label1</span>',
|
||||
'</div>',
|
||||
'<div class="legend-value"><span style="background-color:#9CCC65"> </span>',
|
||||
'<span class="legend-label">Label2</span>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
].join(''));
|
||||
});
|
||||
|
||||
describe('getStatsUrl', ()=>{
|
||||
it('for server', ()=>{
|
||||
expect(getStatsUrl(432, -1, ['chart1'])).toEqual('/dashboard/dashboard_stats/432?chart_names=chart1');
|
||||
});
|
||||
it('for database', ()=>{
|
||||
expect(getStatsUrl(432, 123, ['chart1'])).toEqual('/dashboard/dashboard_stats/432/123?chart_names=chart1');
|
||||
});
|
||||
it('for multiple graphs', ()=>{
|
||||
expect(getStatsUrl(432, 123, ['chart1', 'chart2'])).toEqual('/dashboard/dashboard_stats/432/123?chart_names=chart1,chart2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('statsReducer', ()=>{
|
||||
it('with incoming no counter', ()=>{
|
||||
let state = {
|
||||
'Label1': [], 'Label2': [],
|
||||
};
|
||||
let action = {
|
||||
incoming: {
|
||||
'Label1': 1, 'Label2': 2,
|
||||
},
|
||||
};
|
||||
let newState = {
|
||||
'Label1': [1], 'Label2': [2],
|
||||
};
|
||||
state = statsReducer(state, action);
|
||||
expect(state).toEqual(newState);
|
||||
});
|
||||
|
||||
it('with incoming with counter', ()=>{
|
||||
let state = {
|
||||
'Label1': [1], 'Label2': [2],
|
||||
};
|
||||
let action = {
|
||||
incoming: {
|
||||
'Label1': 1, 'Label2': 3,
|
||||
},
|
||||
counter: true,
|
||||
counterData: {'Label1': 1, 'Label2': 2},
|
||||
};
|
||||
let newState = {
|
||||
'Label1': [0, 1], 'Label2': [1, 2],
|
||||
};
|
||||
state = statsReducer(state, action);
|
||||
expect(state).toEqual(newState);
|
||||
});
|
||||
|
||||
it('with reset', ()=>{
|
||||
let state = {
|
||||
'Label1': [0, 1], 'Label2': [1, 2],
|
||||
};
|
||||
let action = {
|
||||
reset: {
|
||||
'Label1': [2], 'Label2': [2],
|
||||
},
|
||||
};
|
||||
let newState = {
|
||||
'Label1': [2], 'Label2': [2],
|
||||
};
|
||||
state = statsReducer(state, action);
|
||||
expect(state).toEqual(newState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<Graphs /> component', ()=>{
|
||||
let graphComp = null;
|
||||
let sid = 1;
|
||||
let did = 1;
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
let dashboardPref = {
|
||||
session_stats_refresh: 1,
|
||||
tps_stats_refresh: 1,
|
||||
ti_stats_refresh: 1,
|
||||
to_stats_refresh: 1,
|
||||
bio_stats_refresh: 1,
|
||||
show_graphs: true,
|
||||
graph_data_points: true,
|
||||
graph_mouse_track: true,
|
||||
};
|
||||
|
||||
graphComp = mount(<Graphs preferences={dashboardPref} sid={sid} did={did} enablePoll={false} pageVisible={true} />);
|
||||
});
|
||||
|
||||
it('GraphsWrapper is rendered', (done)=>{
|
||||
let found = graphComp.find(GraphsWrapper);
|
||||
expect(found.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('pollDelay is set', (done)=>{
|
||||
let found = graphComp.find('[data-testid="graph-poll-delay"]');
|
||||
expect(found).toHaveClassName('d-none');
|
||||
expect(found).toHaveText('1000');
|
||||
done();
|
||||
});
|
||||
|
||||
it('pollDelay on preference update', (done)=>{
|
||||
let dashboardPref = {
|
||||
session_stats_refresh: 5,
|
||||
tps_stats_refresh: 10,
|
||||
ti_stats_refresh: 5,
|
||||
to_stats_refresh: 10,
|
||||
bio_stats_refresh: 10,
|
||||
show_graphs: true,
|
||||
graph_data_points: true,
|
||||
graph_mouse_track: true,
|
||||
};
|
||||
graphComp.setProps({preferences: dashboardPref});
|
||||
let found = graphComp.find('[data-testid="graph-poll-delay"]');
|
||||
expect(found).toHaveText('5000');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
90
web/regression/javascript/dashboard/graphs_wrapper_spec.js
Normal file
90
web/regression/javascript/dashboard/graphs_wrapper_spec.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import '../helper/enzyme.helper';
|
||||
|
||||
import {GraphsWrapper, X_AXIS_LENGTH, POINT_SIZE} from '../../../pgadmin/dashboard/static/js/Graphs';
|
||||
|
||||
describe('<GraphsWrapper /> component', ()=>{
|
||||
let graphComp = null;
|
||||
let defaultStats = {
|
||||
labels: [...Array(X_AXIS_LENGTH).keys()],
|
||||
datasets: [{
|
||||
label: 'Label1',
|
||||
data: [],
|
||||
borderColor: '#00BCD4',
|
||||
backgroundColor: '#00BCD4',
|
||||
pointHitRadius: POINT_SIZE,
|
||||
},{
|
||||
label: 'Label2',
|
||||
data: [],
|
||||
borderColor: '#9CCC65',
|
||||
backgroundColor: '#9CCC65',
|
||||
pointHitRadius: POINT_SIZE,
|
||||
}],
|
||||
refreshRate: 1,
|
||||
};
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
graphComp = mount(<GraphsWrapper sessionStats={defaultStats}
|
||||
tpsStats={defaultStats}
|
||||
tiStats={defaultStats}
|
||||
toStats={defaultStats}
|
||||
bioStats={defaultStats}
|
||||
errorMsg={null}
|
||||
showTooltip={true}
|
||||
showDataPoints={true}
|
||||
isDatabase={false} />);
|
||||
});
|
||||
|
||||
it('graph containers are rendered', (done)=>{
|
||||
let found = graphComp.find('.card.dashboard-graph');
|
||||
expect(found.length).toBe(5);
|
||||
done();
|
||||
});
|
||||
|
||||
it('graph headers are correct', (done)=>{
|
||||
let found = graphComp.find('.card.dashboard-graph');
|
||||
expect(found.at(0)).toIncludeText('Server sessions');
|
||||
expect(found.at(1)).toIncludeText('Transactions per second');
|
||||
expect(found.at(2)).toIncludeText('Tuples in');
|
||||
expect(found.at(3)).toIncludeText('Tuples out');
|
||||
expect(found.at(4)).toIncludeText('Block I/O');
|
||||
done();
|
||||
});
|
||||
|
||||
it('graph headers when database', (done)=>{
|
||||
let found = graphComp.find('.card.dashboard-graph');
|
||||
graphComp.setProps({isDatabase: true});
|
||||
expect(found.at(0)).toIncludeText('Database sessions');
|
||||
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');
|
||||
|
||||
found = graphComp.find('.card.dashboard-graph .dashboard-graph-body .pg-panel-error.pg-panel-message');
|
||||
expect(found.at(0)).toIncludeText('Some error occurred');
|
||||
expect(found.at(1)).toIncludeText('Some error occurred');
|
||||
expect(found.at(2)).toIncludeText('Some error occurred');
|
||||
expect(found.at(3)).toIncludeText('Some error occurred');
|
||||
expect(found.at(4)).toIncludeText('Some error occurred');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -21,5 +21,6 @@ define(function () {
|
||||
'datagrid.panel': '/panel/<int:trans_id>',
|
||||
'search_objects.types': '/search_objects/types/<int:sid>/<int:did>',
|
||||
'search_objects.search': '/search_objects/search/<int:sid>/<int:did>',
|
||||
'dashboard.dashboard_stats': '/dashboard/dashboard_stats',
|
||||
};
|
||||
});
|
||||
|
||||
4
web/regression/javascript/helper/enzyme.helper.js
Normal file
4
web/regression/javascript/helper/enzyme.helper.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({adapter: new Adapter()});
|
||||
@@ -309,11 +309,6 @@ var getThemeWebpackConfig = function(theme_name) {
|
||||
// Module and Rules: https://webpack.js.org/configuration/module/
|
||||
// Loaders: https://webpack.js.org/loaders/
|
||||
//
|
||||
// imports-loader: it adds dependent modules(use:imports-loader?module1)
|
||||
// at the beginning of module it is dependency of like:
|
||||
// var jQuery = require('jquery'); var browser = require('pgadmin.browser')
|
||||
// It solves number of problems
|
||||
// Ref: http:/github.com/webpack-contrib/imports-loader/
|
||||
rules: themeCssRules(theme_name),
|
||||
},
|
||||
resolve: {
|
||||
@@ -393,12 +388,13 @@ module.exports = [{
|
||||
// It solves number of problems
|
||||
// Ref: http:/github.com/webpack-contrib/imports-loader/
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
test: /\.jsx?$/,
|
||||
exclude: [/node_modules/, /vendor/],
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [['@babel/preset-env', {'modules': 'commonjs', 'useBuiltIns': 'usage', 'corejs': 3}]],
|
||||
presets: [['@babel/preset-env', {'modules': 'commonjs', 'useBuiltIns': 'usage', 'corejs': 3}], '@babel/preset-react'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
@@ -420,6 +416,11 @@ module.exports = [{
|
||||
query: webpackShimConfig,
|
||||
include: path.join(__dirname, '/pgadmin/browser'),
|
||||
}, {
|
||||
// imports-loader: it adds dependent modules(use:imports-loader?module1)
|
||||
// at the beginning of module it is dependency of like:
|
||||
// var jQuery = require('jquery'); var browser = require('pgadmin.browser')
|
||||
// It solves number of problems
|
||||
// Ref: http:/github.com/webpack-contrib/imports-loader/
|
||||
test: require.resolve('./pgadmin/tools/datagrid/static/js/datagrid'),
|
||||
use: {
|
||||
loader: 'imports-loader?' +
|
||||
@@ -517,7 +518,7 @@ module.exports = [{
|
||||
resolve: {
|
||||
alias: webpackShimConfig.resolveAlias,
|
||||
modules: ['node_modules', '.'],
|
||||
extensions: ['.js'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
unsafeCache: true,
|
||||
},
|
||||
// Watch mode Configuration: After initial build, webpack will watch for
|
||||
|
||||
@@ -82,9 +82,6 @@ var webpackShimConfig = {
|
||||
'deps': ['jquery', 'jquery.ui', 'jquery.event.drag'],
|
||||
'exports': 'Slick',
|
||||
},
|
||||
'flotr2': {
|
||||
deps: ['bean'],
|
||||
},
|
||||
'alertify': {
|
||||
'exports': 'alertify',
|
||||
},
|
||||
@@ -141,8 +138,6 @@ var webpackShimConfig = {
|
||||
'moment': path.join(__dirname, './node_modules/moment/moment'),
|
||||
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
|
||||
'jquery.ui': path.join(__dirname, './node_modules/slickgrid/lib/jquery-ui-1.11.3'),
|
||||
'flotr2': path.join(__dirname, './node_modules/flotr2/flotr2.amd'),
|
||||
'bean': path.join(__dirname, './node_modules/flotr2/lib/bean'),
|
||||
'jqueryui.position': path.join(__dirname, './node_modules/jquery-contextmenu/dist/jquery.ui.position'),
|
||||
'jquery.contextmenu': path.join(__dirname, './node_modules/jquery-contextmenu/dist/jquery.contextMenu'),
|
||||
'dropzone': path.join(__dirname, './node_modules/dropzone/dist/dropzone'),
|
||||
|
||||
@@ -31,12 +31,13 @@ module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
test: /\.jsx?$/,
|
||||
exclude: [/node_modules/, /vendor/],
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [['@babel/preset-env', {'modules': 'commonjs', 'useBuiltIns': 'usage', 'corejs': 3}]],
|
||||
presets: [['@babel/preset-env', {'modules': 'commonjs', 'useBuiltIns': 'usage', 'corejs': 3}], '@babel/preset-react'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
sourceMap: 'inline',
|
||||
},
|
||||
},
|
||||
@@ -68,7 +69,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.js'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
alias: {
|
||||
'top': path.join(__dirname, './pgadmin'),
|
||||
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
|
||||
@@ -90,8 +91,6 @@ module.exports = {
|
||||
'slickgrid': nodeModulesDir + '/slickgrid/',
|
||||
'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/',
|
||||
'slickgrid.grid': nodeModulesDir + '/slickgrid/slick.grid',
|
||||
'bean': path.join(__dirname, './node_modules/flotr2/lib/bean'),
|
||||
'flotr2': path.join(__dirname, './node_modules/flotr2/flotr2.amd'),
|
||||
'moment': path.join(__dirname, './node_modules/moment/moment'),
|
||||
'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'),
|
||||
'pgadmin': sourcesDir + '/js/pgadmin',
|
||||
|
||||
854
web/yarn.lock
854
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user