///////////////////////////////////////////////////////////// // // 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('
'); for (var i = 0; i < chart.data.datasets.length; i++) { text.push('
    '); if (chart.data.datasets[i].label) { text.push('' + chart.data.datasets[i].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'); 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 ( <>
{pollDelay}
{chartDrawnOnce && 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 ( <> ); } 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, };