diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index 1dac54e74..12f491ea6 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -197,6 +197,9 @@ class DashboardModule(PgAdminModule): 'dashboard.get_prepared_by_database_id', 'dashboard.config', 'dashboard.get_config_by_server_id', + 'dashboard.system_statistics', + 'dashboard.system_statistics_sid', + 'dashboard.system_statistics_did', ] @@ -536,3 +539,38 @@ def terminate_session(sid=None, did=None, pid=None): response=gettext("Success") if res else gettext("Failed"), status=200 ) + + +# System Statistics Backend +@blueprint.route('/system_statistics', + endpoint='system_statistics', methods=['GET']) +@blueprint.route('/system_statistics/', + endpoint='system_statistics_sid', methods=['GET']) +@blueprint.route('/system_statistics//', + endpoint='system_statistics_did', methods=['GET']) + +@login_required +@check_precondition +def system_statistics(sid=None, did=None): + resp_data = {} + + if request.args['chart_names'] != '': + chart_names = request.args['chart_names'].split(',') + + if not sid: + return internal_server_error(errormsg='Server ID not specified.') + + sql = render_template( + "/".join([g.template_path, 'system_statistics.sql']), did=did, + chart_names=chart_names, + ) + status, res = g.conn.execute_dict(sql) + + for chart_row in res['rows']: + resp_data[chart_row['chart_name']] = json.loads( + chart_row['chart_data']) + + return ajax_response( + response=resp_data, + status=200 + ) diff --git a/web/pgadmin/dashboard/static/js/CPU.jsx b/web/pgadmin/dashboard/static/js/CPU.jsx new file mode 100644 index 000000000..9442f4754 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/CPU.jsx @@ -0,0 +1,324 @@ +import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react'; +import PgTable from 'sources/components/PgTable'; +import gettext from 'sources/gettext'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import url_for from 'sources/url_for'; +import getApiInstance from 'sources/api_instance'; +import {getEpoch} from 'sources/utils'; +import {ChartContainer} from './Dashboard'; +import { Grid } from '@material-ui/core'; +import { DATA_POINT_SIZE } from 'sources/chartjs'; +import StreamingChart from '../../../static/js/components/PgChart/StreamingChart'; +import {useInterval} from 'sources/custom_hooks'; +import axios from 'axios'; + +export const X_AXIS_LENGTH = 75; + +const useStyles = makeStyles((theme) => ({ + autoResizer: { + height: '100% !important', + width: '100% !important', + background: theme.palette.grey[400], + padding: '7.5px', + overflowX: 'auto !important', + overflowY: 'hidden !important', + minHeight: '100%', + minWidth: '100%', + }, + container: { + height: 'auto', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + fixedContainer: { + height: '577px', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + containerHeader: { + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '5px', + } +})); + +export function transformData(labels, refreshRate) { + const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39']; + let datasets = Object.keys(labels).map((label, i)=>{ + return { + label: label, + data: labels[label] || [], + borderColor: colors[i], + pointHitRadius: DATA_POINT_SIZE, + }; + }) || []; + + return { + datasets: datasets, + refreshRate: refreshRate, + }; +} + +/* URL for fetching graphs data */ +export function getStatsUrl(sid=-1, did=-1, chart_names=[]) { + let base_url = url_for('dashboard.system_statistics'); + 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 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 = { + 'cu_stats': {'User Normal': [], 'User Niced': [], 'Kernel': [], 'Idle': []}, + 'la_stats': {'1 min': [], '5 mins': [], '10 mins': [], '15 mins': []}, + 'pcu_stats': {}, +}; + +export default function CPU({ sid, did, serverConencted, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + + const [cpuUsageInfo, cpuUsageInfoReduce] = useReducer(statsReducer, chartsDefault['cu_stats']); + const [loadAvgInfo, loadAvgInfoReduce] = useReducer(statsReducer, chartsDefault['la_stats']); + const [processCpuUsageStats, setProcessCpuUsageStats] = useState([]); + + const [counterData, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const tableHeader = [ + { + Header: 'PID', + accessor: 'pid', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Name', + accessor: 'name', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'CPU Usage', + accessor: 'cpu_usage', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + ]; + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + const [refreshPreferences, setRefreshPreferences] = useState({'cu_stats': 5, 'la_stats': 60, 'pcu_stats': 10}); + + 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 + refreshPreferences[name]; + } + }); + + let path = getStatsUrl(sid, did, getFor); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.data; + console.log(data); + setErrorMsg(null); + if(data.hasOwnProperty('cu_stats')){ + let new_cu_stats = { + 'User Normal': data['cu_stats']['usermode_normal_process_percent']?data['cu_stats']['usermode_normal_process_percent']:0, + 'User Niced': data['cu_stats']['usermode_niced_process_percent']?data['cu_stats']['usermode_niced_process_percent']:0, + 'Kernel': data['cu_stats']['kernelmode_process_percent']?data['cu_stats']['kernelmode_process_percent']:0, + 'Idle': data['cu_stats']['idle_mode_percent']?data['cu_stats']['idle_mode_percent']:0, + }; + cpuUsageInfoReduce({incoming: new_cu_stats}); + } + + if(data.hasOwnProperty('la_stats')){ + let new_la_stats = { + '1 min': data['la_stats']['load_avg_one_minute']?data['la_stats']['load_avg_one_minute']:0, + '5 mins': data['la_stats']['load_avg_five_minutes']?data['la_stats']['load_avg_five_minutes']:0, + '10 mins': data['la_stats']['load_avg_ten_minutes']?data['la_stats']['load_avg_ten_minutes']:0, + '15 mins': data['la_stats']['load_avg_fifteen_minutes']?data['la_stats']['load_avg_fifteen_minutes']:0, + }; + loadAvgInfoReduce({incoming: new_la_stats}); + } + + if(data.hasOwnProperty('pcu_stats')){ + let pcu_info_list = []; + const pcu_info_obj = data['pcu_stats']; + for (const key in pcu_info_obj) { + pcu_info_list.push({ icon: '', pid: pcu_info_obj[key]['pid'], name: pcu_info_obj[key]['name'], cpu_usage: pcu_info_obj[key]['cpu_usage'] }); + } + + setProcessCpuUsageStats(pcu_info_list); + } + + setCounterData((prevCounterData)=>{ + return { + ...prevCounterData, + ...data, + }; + }); + }) + .catch((error)=>{ + if(!errorMsg) { + cpuUsageInfoReduce({reset:chartsDefault['cu_stats']}); + loadAvgInfoReduce({reset:chartsDefault['la_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} + isTest={false} + /> + } + + ); +} + +CPU.propTypes = { + sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + serverConnected: PropTypes.bool, + pageVisible: PropTypes.bool, + enablePoll: PropTypes.bool, +}; + +export function CPUWrapper(props) { + const classes = useStyles(); + const options = useMemo(()=>({ + showDataPoints: props.showDataPoints, + showTooltip: props.showTooltip, + lineBorderWidth: props.lineBorderWidth, + }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]); + return ( + <> + + +
{gettext('CPU Usage')}
+ + + +
+ +
{gettext('Load Average')}
+ + + +
+
+ + + + + ); +} + +const propTypeStats = PropTypes.shape({ + datasets: PropTypes.array, + refreshRate: PropTypes.number.isRequired, +}); +CPUWrapper.propTypes = { + cpuUsageInfo: propTypeStats.isRequired, + loadAvgInfo: propTypeStats.isRequired, + processCpuUsageStats: PropTypes.array.isRequired, + tableHeader: PropTypes.array.isRequired, + errorMsg: PropTypes.string, + showTooltip: PropTypes.bool.isRequired, + showDataPoints: PropTypes.bool.isRequired, + lineBorderWidth: PropTypes.number.isRequired, + isDatabase: PropTypes.bool.isRequired, + isTest: PropTypes.bool, +}; \ No newline at end of file diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx index 588583eb3..5e861609e 100644 --- a/web/pgadmin/dashboard/static/js/Dashboard.jsx +++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx @@ -29,6 +29,9 @@ import _ from 'lodash'; import CachedOutlinedIcon from '@material-ui/icons/CachedOutlined'; import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage'; import TabPanel from '../../../static/js/components/TabPanel'; +import Summary from './Summary'; +import CPU from './CPU'; +import Memory from './Memory'; function parseData(data) { let res = []; @@ -154,10 +157,14 @@ export default function Dashboard({ const [msg, setMsg] = useState(''); const [tabVal, setTabVal] = useState(0); const [mainTabVal, setmainTabVal] = useState(0); - const [systemStatsTabVal, setSystemStatsTabVal] = useState(0); const [refresh, setRefresh] = useState(false); const [activeOnly, setActiveOnly] = useState(false); const [schemaDict, setSchemaDict] = React.useState({}); + const [systemStatsTabVal, setSystemStatsTabVal] = useState(0); + + const systemStatsTabChanged = (e, tabVal) => { + setSystemStatsTabVal(tabVal); + }; if (!did) { tabs.push(gettext('Configuration')); @@ -171,10 +178,6 @@ export default function Dashboard({ setmainTabVal(tabVal); }; - const systemStatsTabChanged = (e, tabVal) => { - setSystemStatsTabVal(tabVal); - }; - const serverConfigColumns = [ { accessor: 'name', @@ -959,8 +962,8 @@ export default function Dashboard({ {/* System Statistics */} - - + + ; })} - - - Summary - - - CPU - - - Memory - - - Storage - + + + + + + + + + + + Storage + + diff --git a/web/pgadmin/dashboard/static/js/Memory.jsx b/web/pgadmin/dashboard/static/js/Memory.jsx new file mode 100644 index 000000000..82831a976 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Memory.jsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react'; +import PgTable from 'sources/components/PgTable'; +import gettext from 'sources/gettext'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import url_for from 'sources/url_for'; +import getApiInstance from 'sources/api_instance'; +import {getEpoch} from 'sources/utils'; +import {ChartContainer} from './Dashboard'; +import { Grid } from '@material-ui/core'; +import { DATA_POINT_SIZE } from 'sources/chartjs'; +import StreamingChart from '../../../static/js/components/PgChart/StreamingChart'; +import {useInterval} from 'sources/custom_hooks'; +import axios from 'axios'; + +export const X_AXIS_LENGTH = 75; + +const useStyles = makeStyles((theme) => ({ + autoResizer: { + height: '100% !important', + width: '100% !important', + background: theme.palette.grey[400], + padding: '7.5px', + overflowX: 'auto !important', + overflowY: 'hidden !important', + minHeight: '100%', + minWidth: '100%', + }, + container: { + height: 'auto', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + fixedContainer: { + height: '577px', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + containerHeader: { + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '5px', + } +})); + +export function transformData(labels, refreshRate) { + const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39']; + let datasets = Object.keys(labels).map((label, i)=>{ + return { + label: label, + data: labels[label] || [], + borderColor: colors[i], + pointHitRadius: DATA_POINT_SIZE, + }; + }) || []; + + return { + datasets: datasets, + refreshRate: refreshRate, + }; +} + +/* URL for fetching graphs data */ +export function getStatsUrl(sid=-1, did=-1, chart_names=[]) { + let base_url = url_for('dashboard.system_statistics'); + 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 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 = { + 'm_stats': {'Total': [], 'Used': [], 'Free': []}, + 'sm_stats': {'Total': [], 'Used': [], 'Free': []}, + 'pmu_stats': {}, +}; + +export default function Memory({ sid, did, serverConencted, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + + const [memoryUsageInfo, memoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['m_stats']); + const [swapMemoryUsageInfo, swapMemoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['sm_stats']); + const [processMemoryUsageStats, setProcessMemoryUsageStats] = useState([]); + + const [counterData, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const tableHeader = [ + { + Header: 'PID', + accessor: 'pid', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Name', + accessor: 'name', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Memory Usage', + accessor: 'memory_usage', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Memory Bytes', + accessor: 'memory_bytes', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + ]; + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + const [refreshPreferences, setRefreshPreferences] = useState({'m_stats': 5, 'sm_stats': 5, 'pmu_stats': 5}); + + 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 + refreshPreferences[name]; + } + }); + + let path = getStatsUrl(sid, did, getFor); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.data; + console.log(data); + setErrorMsg(null); + if(data.hasOwnProperty('m_stats')){ + let new_m_stats = { + 'Total': data['m_stats']['total_memory']?data['m_stats']['total_memory']:0, + 'Used': data['m_stats']['used_memory']?data['m_stats']['used_memory']:0, + 'Free': data['m_stats']['free_memory']?data['m_stats']['free_memory']:0, + }; + memoryUsageInfoReduce({incoming: new_m_stats}); + } + + if(data.hasOwnProperty('sm_stats')){ + let new_sm_stats = { + 'Total': data['sm_stats']['swap_total']?data['sm_stats']['swap_total']:0, + 'Used': data['sm_stats']['swap_used']?data['sm_stats']['swap_used']:0, + 'Free': data['sm_stats']['swap_free']?data['sm_stats']['swap_free']:0, + }; + swapMemoryUsageInfoReduce({incoming: new_sm_stats}); + } + + if(data.hasOwnProperty('pmu_stats')){ + let pmu_info_list = []; + const pmu_info_obj = data['pmu_stats']; + for (const key in pmu_info_obj) { + pmu_info_list.push({ icon: '', pid: pmu_info_obj[key]['pid'], name: pmu_info_obj[key]['name'], memory_usage: pmu_info_obj[key]['memory_usage'], memory_bytes: pmu_info_obj[key]['memory_bytes'] }); + } + + setProcessMemoryUsageStats(pmu_info_list); + } + + setCounterData((prevCounterData)=>{ + return { + ...prevCounterData, + ...data, + }; + }); + }) + .catch((error)=>{ + if(!errorMsg) { + memoryUsageInfoReduce({reset:chartsDefault['m_stats']}); + swapMemoryUsageInfoReduce({reset:chartsDefault['sm_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} + isTest={false} + /> + } + + ); +} + +Memory.propTypes = { + sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + serverConnected: PropTypes.bool, + pageVisible: PropTypes.bool, + enablePoll: PropTypes.bool, +}; + +export function MemoryWrapper(props) { + const classes = useStyles(); + const options = useMemo(()=>({ + showDataPoints: props.showDataPoints, + showTooltip: props.showTooltip, + lineBorderWidth: props.lineBorderWidth, + }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]); + return ( + <> + + +
{gettext('Memory')}
+ + + +
+ +
{gettext('Swap Memory')}
+ + + +
+
+ + + + + ); +} + +const propTypeStats = PropTypes.shape({ + datasets: PropTypes.array, + refreshRate: PropTypes.number.isRequired, +}); +MemoryWrapper.propTypes = { + memoryUsageInfo: propTypeStats.isRequired, + swapMemoryUsageInfo: propTypeStats.isRequired, + processMemoryUsageStats: PropTypes.array.isRequired, + tableHeader: PropTypes.array.isRequired, + errorMsg: PropTypes.string, + showTooltip: PropTypes.bool.isRequired, + showDataPoints: PropTypes.bool.isRequired, + lineBorderWidth: PropTypes.number.isRequired, + isDatabase: PropTypes.bool.isRequired, + isTest: PropTypes.bool, +}; \ No newline at end of file diff --git a/web/pgadmin/dashboard/static/js/Summary.jsx b/web/pgadmin/dashboard/static/js/Summary.jsx new file mode 100644 index 000000000..cbfcee4db --- /dev/null +++ b/web/pgadmin/dashboard/static/js/Summary.jsx @@ -0,0 +1,366 @@ +import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react'; +import gettext from 'sources/gettext'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import url_for from 'sources/url_for'; +import getApiInstance from 'sources/api_instance'; +import {getEpoch} from 'sources/utils'; +import {ChartContainer} from './Dashboard'; +import { Grid } from '@material-ui/core'; +import { DATA_POINT_SIZE } from 'sources/chartjs'; +import StreamingChart from '../../../static/js/components/PgChart/StreamingChart'; +import DonutChart from '../../../static/js/components/PgChart/DonutChart'; +import {useInterval} from 'sources/custom_hooks'; +import axios from 'axios'; + +export const X_AXIS_LENGTH = 75; + +const useStyles = makeStyles((theme) => ({ + autoResizer: { + height: '100% !important', + width: '100% !important', + background: theme.palette.grey[400], + padding: '7.5px', + overflowX: 'auto !important', + overflowY: 'hidden !important', + minHeight: '100%', + minWidth: '100%', + }, + table: { + width: '100%', + backgroundColor: theme.otherVars.tableBg, + border: '1px solid rgb(221, 224, 230)', + }, + tableVal: { + border: '1px solid rgb(221, 224, 230) !important', + padding: '10px !important', + }, + container: { + height: 'auto', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + containerHeader: { + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '5px', + }, +})); + +export function transformData(labels, refreshRate) { + const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39']; + let datasets = Object.keys(labels).map((label, i)=>{ + return { + label: label, + data: labels[label] || [], + borderColor: colors[i], + pointHitRadius: DATA_POINT_SIZE, + }; + }) || []; + + return { + datasets: datasets, + refreshRate: refreshRate, + }; +} + +/* URL for fetching graphs data */ +export function getStatsUrl(sid=-1, did=-1, chart_names=[]) { + let base_url = url_for('dashboard.system_statistics'); + 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 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 = { + 'hpc_stats': {'Handle': [], 'Process': []}, +}; + +const SummaryTable = ({ data }) => { + const classes = useStyles(); + return ( + + + + + + + + + {data.map((item, index) => ( + + + + + ))} + +
PropertyValue
{item.name}{item.value}
+ ); +} + +export default function Summary({ sid, did, serverConencted, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + + const [processHandleCount, processHandleCountReduce] = useReducer(statsReducer, chartsDefault['hpc_stats']); + const [osStats, setOsStats] = useState([]); + const [cpuStats, setCpuStats] = useState([]); + const [processInfoStats, setProcessInfoStats] = useState({'Running': 4, 'Sleeping': 2, 'Stopped': 1, 'Zombie': 2}); + + const [counterData, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const [longPollDelay, setLongPollDelay] = useState(180000); + const [errorMsg, setErrorMsg] = useState(null); + + const tableHeader = [ + { + Header: 'Property', + accessor: 'name', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Value', + accessor: 'value', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + ]; + + useEffect(() => { + try { + // Fetch the latest data point from the API endpoint + let url; + url = url_for('dashboard.system_statistics'); + url += '/' + sid; + url += did > 0 ? '/' + did : ''; + url += '?chart_names=' + 'pg_sys_os_info,pg_sys_cpu_info'; + const api = getApiInstance(); + api({ + url: url, + type: 'GET', + }) + .then((res) => { + let data = res.data; + + const os_info_obj = data['pg_sys_os_info']; + let os_info_list = [ + { icon: '', name: 'Name', value: os_info_obj['name'] }, + { icon: '', name: 'Version', value: os_info_obj['version'] }, + { icon: '', name: 'Host name', value: os_info_obj['host_name'] }, + { icon: '', name: 'Domain name', value: os_info_obj['domain_name'] }, + { icon: '', name: 'Architecture', value: os_info_obj['architecture'] }, + { icon: '', name: 'Os up since seconds', value: os_info_obj['os_up_since_seconds'] }, + ]; + setOsStats(os_info_list); + + const cpu_info_obj = data['pg_sys_cpu_info']; + let cpu_info_list = [ + { icon: '', name: 'Vendor', value: cpu_info_obj['vendor'] }, + { icon: '', name: 'Description', value: cpu_info_obj['description'] }, + { icon: '', name: 'Model name', value: cpu_info_obj['model_name'] }, + { icon: '', name: 'No of cores', value: cpu_info_obj['no_of_cores'] }, + { icon: '', name: 'Architecture', value: cpu_info_obj['architecture'] }, + { icon: '', name: 'Clock speed Hz', value: cpu_info_obj['clock_speed_hz'] }, + { icon: '', name: 'L1 dcache size', value: cpu_info_obj['l1dcache_size'] }, + { icon: '', name: 'L1 icache size', value: cpu_info_obj['l1icache_size'] }, + { icon: '', name: 'L2 cache size', value: cpu_info_obj['l2cache_size'] }, + { icon: '', name: 'L3 cache size', value: cpu_info_obj['l3cache_size'] }, + ]; + setCpuStats(cpu_info_list); + + setErrorMsg(null); + }) + .catch((error) => { + console.error('Error fetching data:', error); + }); + } catch (error) { + console.error('Error fetching data:', error); + } + }, [sid, did, enablePoll, 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 + 5; + } + }); + + let path = getStatsUrl(sid, did, getFor); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.data; + setErrorMsg(null); + processHandleCountReduce({incoming: data['hpc_stats']}); + + setCounterData((prevCounterData)=>{ + return { + ...prevCounterData, + ...data, + }; + }); + }) + .catch((error)=>{ + if(!errorMsg) { + processHandleCountReduce({reset:chartsDefault['hpc_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); + + useInterval(()=>{ + let url; + url = url_for('dashboard.system_statistics'); + url += '/' + sid; + url += did > 0 ? '/' + did : ''; + url += '?chart_names=' + 'pi_stats'; + // axios.get(url) + // .then((resp)=>{ + // let data = resp.data; + // console.log("pi data: ", data); + // }) + // .catch((error)=>{ + // if(!errorMsg) { + // 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 ? longPollDelay : -1); + + return ( + <> + 0} + isTest={false} + /> + + ); +} + +Summary.propTypes = { + sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]), + serverConnected: PropTypes.bool, + pageVisible: PropTypes.bool, + enablePoll: PropTypes.bool, +}; + +export function SummaryWrapper(props) { + const classes = useStyles(); + const options = useMemo(()=>({ + showDataPoints: props.showDataPoints, + showTooltip: props.showTooltip, + lineBorderWidth: props.lineBorderWidth, + }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]); + return ( + <> + + +
{gettext('OS Information')}
+ +
+ +
{gettext('Handle & Process Count')}
+ + + +
+
+ + +
{gettext('CPU Information')}
+ +
+ +
{gettext('Process Information')}
+ + + +
+
+ + ); +} \ No newline at end of file diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql new file mode 100644 index 000000000..d1fced433 --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql @@ -0,0 +1,87 @@ +{% set add_union = false %} +{% if 'pg_sys_os_info' in chart_names %} +{% set add_union = true %} + SELECT 'pg_sys_os_info' AS chart_name, pg_catalog.row_to_json(t) AS chart_data + FROM (SELECT * FROM pg_sys_os_info()) t +{% endif %} +{% if add_union and 'pg_sys_cpu_info' in chart_names %} + UNION ALL +{% endif %} +{% if 'pg_sys_cpu_info' in chart_names %} +{% set add_union = true %} + SELECT 'pg_sys_cpu_info' AS chart_name, pg_catalog.row_to_json(t) AS chart_data + FROM (SELECT * FROM pg_sys_cpu_info()) t +{% endif %} +{% if add_union and 'hpc_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'hpc_stats' in chart_names %} +{% set add_union = true %} + SELECT 'hpc_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data + FROM (SELECT + (SELECT handle_count FROM pg_sys_os_info()) AS "{{ _('Handle') }}", + (SELECT process_count FROM pg_sys_os_info()) AS "{{ _('Process') }}" + ) t +{% endif %} +{% if add_union and 'cu_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'cu_stats' in chart_names %} +{% set add_union = true %} + SELECT 'cu_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data + FROM (SELECT * FROM pg_sys_cpu_usage_info()) t +{% endif %} +{% if add_union and 'la_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'la_stats' in chart_names %} +{% set add_union = true %} + SELECT 'la_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT * FROM pg_sys_load_avg_info()) t +{% endif %} +{% if add_union and 'pcu_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'pcu_stats' in chart_names %} +{% set add_union = true %} + SELECT 'pcu_stats' AS chart_name, ( + SELECT to_json(pg_catalog.jsonb_object_agg('process'||row_number, pg_catalog.row_to_json(t))) + FROM ( + SELECT pid, name, cpu_usage, ROW_NUMBER() OVER (ORDER BY pid) AS row_number + FROM pg_sys_cpu_memory_by_process() + ) t + ) AS chart_data +{% endif %} +{% if add_union and 'm_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'm_stats' in chart_names %} +{% set add_union = true %} + SELECT 'm_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT total_memory, used_memory, free_memory FROM pg_sys_memory_info()) t +{% endif %} +{% if add_union and 'sm_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'sm_stats' in chart_names %} +{% set add_union = true %} + SELECT 'sm_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT swap_total, swap_used, swap_free FROM pg_sys_memory_info()) t +{% endif %} +{% if add_union and 'pmu_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'pmu_stats' in chart_names %} +{% set add_union = true %} + SELECT 'pmu_stats' AS chart_name, ( + SELECT to_json(pg_catalog.jsonb_object_agg('process'||row_number, pg_catalog.row_to_json(t))) + FROM ( + SELECT pid, name, memory_usage, memory_bytes, ROW_NUMBER() OVER (ORDER BY pid) AS row_number + FROM pg_sys_cpu_memory_by_process() + ) t + ) AS chart_data +{% endif %} +{% if add_union and 'pi_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'pi_stats' in chart_names %} +{% set add_union = true %} + SELECT 'pi_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT * FROM pg_sys_process_info()) t +{% endif %} \ No newline at end of file diff --git a/web/pgadmin/static/js/components/PgChart/DonutChart.jsx b/web/pgadmin/static/js/components/PgChart/DonutChart.jsx new file mode 100644 index 000000000..9c917cc20 --- /dev/null +++ b/web/pgadmin/static/js/components/PgChart/DonutChart.jsx @@ -0,0 +1,68 @@ +import React, { useEffect, useRef } from 'react'; +import Chart from 'chart.js/auto'; +import { useResizeDetector } from 'react-resize-detector'; + +const DonutChart = ({ data }) => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + + useEffect(() => { + if (data && Object.keys(data).length > 0) { + if (chartInstance.current) { + // If chart instance exists, update the data + chartInstance.current.data.labels = data.map((item) => item.label); + chartInstance.current.data.datasets[0].data = data.map((item) => item.data); + chartInstance.current.update(); + } else { + // If chart instance doesn't exist, create a new chart + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, // Hide the labels at the top + }, + }, + animation: { + duration: 0, // Disable the animation + }, + tooltips: { + callbacks: { + label: function (tooltipItem, chartData) { + const dataset = chartData.datasets[tooltipItem.datasetIndex]; + const total = dataset.data.reduce((previousValue, currentValue) => previousValue + currentValue); + const currentValue = dataset.data[tooltipItem.index]; + const percentage = ((currentValue / total) * 100).toFixed(2) + '%'; + return dataset.label + ': ' + currentValue + ' (' + percentage + ')'; + }, + }, + }, + }; + + const chartData = { + labels: data.map((item) => item.label), + datasets: [ + { + data: data.map((item) => item.data), + backgroundColor: data.map((item) => item.borderColor), + hoverBackgroundColor: data.map((item) => item.borderColor), + }, + ], + }; + + const ctx = chartRef.current.getContext('2d'); + chartInstance.current = new Chart(ctx, { + type: 'doughnut', + data: chartData, + options: chartOptions, + }); + } + } + }, [data]); + + return ( + + ); +}; + +export default DonutChart; diff --git a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx index bd465e3da..eba301f05 100644 --- a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx +++ b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx @@ -58,44 +58,89 @@ function tooltipPlugin(refreshRate) { }; } -export default function StreamingChart({xRange=75, data, options}) { +export default function StreamingChart({xRange=75, data, options, showSecondAxis=false}) { const chartRef = useRef(); const theme = useTheme(); const { width, height, ref:containerRef } = useResizeDetector(); - const defaultOptions = useMemo(()=>({ - title: '', - width: width, - height: height, - padding: [10, 0, 10, 0], - focus: { - alpha: 0.3, - }, - cursor: { - y: false, - drag: { - setScale: false, - } - }, - series: [ + const defaultOptions = useMemo(()=> { + const series = [ {}, - ...(data.datasets?.map((datum)=>({ + ...(data.datasets?.map((datum, index) => ({ label: datum.label, stroke: datum.borderColor, width: options.lineBorderWidth ?? 1, - points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius*2 } - }))??{}) - ], - scales: { - x: { - time: false, - } - }, - axes: [ + scale: showSecondAxis && (index === 1) ? 'y1' : 'y', + points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius * 2 }, + })) ?? []), + ]; + + const axes = [ { show: false, stroke: theme.palette.text.primary, }, - { + ]; + + if(showSecondAxis){ + axes.push({ + scale: 'y', + grid: { + stroke: theme.otherVars.borderColor, + width: 0.5, + }, + stroke: theme.palette.text.primary, + size: function(_obj, values) { + let size = 40; + if(values?.length > 0) { + size = values[values.length-1].length*12; + if(size < 40) size = 40; + } + return size; + }, + // y-axis configuration + values: (self, ticks) => { + // Format the label + return ticks.map((value) => { + if(value < 1){ + return value+""; + } + const suffixes = ["", "k", "M", "B", "T"]; + const suffixNum = Math.floor(Math.log10(value) / 3); + const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1); + return shortValue + suffixes[suffixNum]; + }); + } + }); + axes.push({ + scale: 'y1', + side: 1, + stroke: theme.palette.text.primary, + grid: {show: false}, + size: function(_obj, values) { + let size = 40; + if(values?.length > 0) { + size = values[values.length-1].length*12; + if(size < 40) size = 40; + } + return size; + }, + // y-axis configuration + values: (self, ticks) => { + // Format the label + return ticks.map((value) => { + if(value < 1){ + return value+""; + } + const suffixes = ["", "k", "M", "B", "T"]; + const suffixNum = Math.floor(Math.log10(value) / 3); + const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1); + return shortValue + suffixes[suffixNum]; + }); + } + }); + } else{ + axes.push({ + scale: 'y', grid: { stroke: theme.otherVars.borderColor, width: 0.5, @@ -108,11 +153,47 @@ export default function StreamingChart({xRange=75, data, options}) { if(size < 40) size = 40; } return size; + }, + // y-axis configuration + values: (self, ticks) => { + // Format the label + return ticks.map((value) => { + if(value < 1){ + return value+""; + } + const suffixes = ["", "k", "M", "B", "T"]; + const suffixNum = Math.floor(Math.log10(value) / 3); + const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1); + return shortValue + suffixes[suffixNum]; + }); + } + }); + } + + return { + title: '', + width: width, + height: height, + padding: [10, 0, 10, 0], + focus: { + alpha: 0.3, + }, + cursor: { + y: false, + drag: { + setScale: false, + } + }, + series: series, + scales: { + x: { + time: false, } - } - ], - plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [], - }), [data.refreshRate, data?.datasets?.length, width, height, options]); + }, + axes: axes, + plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [], + }; + }, [data.refreshRate, data?.datasets?.length, width, height, options]); const initialState = [ Array.from(new Array(xRange).keys()), @@ -140,4 +221,5 @@ StreamingChart.propTypes = { xRange: PropTypes.number.isRequired, data: propTypeData.isRequired, options: PropTypes.object, + showSecondAxis: PropTypes.bool, };