mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2021, 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) {
|
|
|
|
if(action.reset) {
|
|
return action.reset;
|
|
}
|
|
|
|
if(!action.incoming) {
|
|
return state;
|
|
}
|
|
|
|
if(!action.counterData) {
|
|
action.counterData = action.incoming;
|
|
}
|
|
|
|
let newState = {};
|
|
Object.keys(action.incoming).forEach(label => {
|
|
if(state[label]) {
|
|
newState[label] = [
|
|
action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
|
|
...state[label].slice(0, X_AXIS_LENGTH-1),
|
|
];
|
|
} 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,
|
|
]),
|
|
pageVisible: PropTypes.bool,
|
|
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(()=>({
|
|
normalized: true,
|
|
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,
|
|
};
|