From 929d8f1ed0bcce78db795fe7cab9484c4e610a13 Mon Sep 17 00:00:00 2001 From: Sahil Harpal Date: Wed, 16 Aug 2023 15:19:15 +0530 Subject: [PATCH 1/3] System stats changes except process and disk information --- web/pgadmin/dashboard/__init__.py | 131 ++++++ web/pgadmin/dashboard/static/js/Dashboard.jsx | 234 +++++++--- .../dashboard/static/js/SystemStats/CPU.jsx | 377 ++++++++++++++++ .../static/js/SystemStats/Memory.jsx | 372 +++++++++++++++ .../static/js/SystemStats/Storage.jsx | 329 ++++++++++++++ .../static/js/SystemStats/Summary.jsx | 422 ++++++++++++++++++ .../sql/default/system_statistics.sql | 100 +++++ .../js/components/PgChart/DonutChart.jsx | 70 +++ .../js/components/PgChart/StreamingChart.jsx | 146 ++++-- 9 files changed, 2090 insertions(+), 91 deletions(-) create mode 100644 web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx create mode 100644 web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx create mode 100644 web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx create mode 100644 web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx create mode 100644 web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql create mode 100644 web/pgadmin/static/js/components/PgChart/DonutChart.jsx diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index 1dac54e74..c18f5d3de 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -112,6 +112,72 @@ class DashboardModule(PgAdminModule): help_str=help_string ) + self.hpc_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'hpc_stats_refresh', + gettext("Handle & Process count statistics refresh rate"), + 'integer', 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.cu_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'cu_stats_refresh', + gettext( + "Percentage of CPU time used by different process \ + modes statistics refresh rate" + ), 'integer', 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.la_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'la_stats_refresh', + gettext("Average load statistics refresh rate"), 'integer', + 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.pcu_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'pcu_stats_refresh', + gettext("CPU usage per process statistics refresh rate"), + 'integer', 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.m_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'm_stats_refresh', + gettext("Memory usage statistics refresh rate"), 'integer', + 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.sm_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'sm_stats_refresh', + gettext("Swap memory usage statistics refresh rate"), 'integer', + 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.pmu_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'pmu_stats_refresh', + gettext("Memory usage per process statistics refresh rate"), + 'integer', 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + + self.io_stats_refresh = self.dashboard_preference.register( + 'dashboards', 'io_stats_refresh', + gettext("I/O analysis statistics refresh rate"), 'integer', + 5, min_val=1, max_val=999999, + category_label=PREF_LABEL_REFRESH_RATES, + help_str=help_string + ) + self.display_graphs = self.dashboard_preference.register( 'display', 'show_graphs', gettext("Show graphs?"), 'boolean', True, @@ -197,6 +263,12 @@ class DashboardModule(PgAdminModule): 'dashboard.get_prepared_by_database_id', 'dashboard.config', 'dashboard.get_config_by_server_id', + 'dashboard.check_system_statistics', + 'dashboard.check_system_statistics_sid', + 'dashboard.check_system_statistics_did', + 'dashboard.system_statistics', + 'dashboard.system_statistics_sid', + 'dashboard.system_statistics_did', ] @@ -536,3 +608,62 @@ def terminate_session(sid=None, did=None, pid=None): response=gettext("Success") if res else gettext("Failed"), status=200 ) + + +# To check whether system stats extesion is present or not +@blueprint.route('check_extension/system_statistics', + endpoint='check_system_statistics', methods=['GET']) +@blueprint.route('check_extension/system_statistics/', + endpoint='check_system_statistics_sid', methods=['GET']) +@blueprint.route('check_extension/system_statistics//', + endpoint='check_system_statistics_did', methods=['GET']) +@login_required +@check_precondition +def check_system_statistics(sid=None, did=None): + sql = "SELECT * FROM pg_extension WHERE extname = 'system_stats';" + status, res = g.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + data = {} + if res is not None: + data['ss_present'] = True + else: + data['ss_present'] = False + return ajax_response( + response=data, + 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/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx index 7194fcc10..e6afeff07 100644 --- a/web/pgadmin/dashboard/static/js/Dashboard.jsx +++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx @@ -29,6 +29,10 @@ 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 'SystemStats/Summary'; +import CPU from 'SystemStats/CPU'; +import Memory from 'SystemStats/Memory'; +import Storage from 'SystemStats/Storage'; function parseData(data) { let res = []; @@ -148,12 +152,21 @@ export default function Dashboard({ }) { const classes = useStyles(); let tabs = [gettext('Sessions'), gettext('Locks'), gettext('Prepared Transactions')]; + let mainTabs = [gettext('General'), gettext('System Statistics')]; + let systemStatsTabs = [gettext('Summary'), gettext('CPU'), gettext('Memory'), gettext('Storage')]; const [dashData, setdashData] = useState([]); const [msg, setMsg] = useState(''); + const [ssMsg, setSsMsg] = useState(''); const [tabVal, setTabVal] = useState(0); + const [mainTabVal, setmainTabVal] = 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')); @@ -163,6 +176,10 @@ export default function Dashboard({ setTabVal(tabVal); }; + const mainTabChanged = (e, tabVal) => { + setmainTabVal(tabVal); + }; + const serverConfigColumns = [ { accessor: 'name', @@ -745,6 +762,7 @@ export default function Dashboard({ useEffect(() => { let url, + ss_extension_check_url = url_for('dashboard.check_system_statistics'), message = gettext( 'Please connect to the selected server to view the dashboard.' ); @@ -770,6 +788,10 @@ export default function Dashboard({ if (did) url += sid + '/' + did; else url += sid; + if (did && !props.dbConnected) return; + if (did) ss_extension_check_url += '/' + sid + '/' + did; + else ss_extension_check_url += '/' + sid; + const api = getApiInstance(); if (node) { api({ @@ -787,6 +809,20 @@ export default function Dashboard({ // show failed message. setMsg(gettext('Failed to retrieve data from the server.')); }); + + api({ + url: ss_extension_check_url, + type: 'GET', + }) + .then((res) => { + const data = res.data; + if(data['ss_present'] == false){ + setSsMsg(gettext('System stats extension is not installed. You can install the extension in a database using the "CREATE EXTENSION system_stats;" SQL command. Reload the pgAdmin once you installed.')); + } + }) + .catch(() => { + setSsMsg(gettext('Failed to verify the presence of system stats extension.')); + }); } else { setMsg(message); } @@ -867,68 +903,148 @@ export default function Dashboard({ {sid && props.serverConnected ? ( - {!_.isUndefined(preferences) && preferences.show_graphs && ( - - )} - {!_.isUndefined(preferences) && preferences.show_activity && ( - - - {props.dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '} + + + + + {mainTabs.map((tabValue) => { + return ; + })} + + - - - - {tabs.map((tabValue) => { - return ; - })} - - + {/* General Statistics */} + + {!_.isUndefined(preferences) && preferences.show_graphs && ( + + )} + {!_.isUndefined(preferences) && preferences.show_activity && ( + + + {props.dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '} + + + + + {tabs.map((tabValue) => { + return ; + })} + + + + + + + + + + + + + + + + + + )} + + {/* System Statistics */} + + + {ssMsg === '' ? + <> + + + {systemStatsTabs.map((tabValue) => { + return ; + })} + + + + + + + + + + + + + + + : +
+ +
+ } - - - - - - - - - - - - - + - )} + ) : showDefaultContents() } diff --git a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx new file mode 100644 index 000000000..276034d9e --- /dev/null +++ b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx @@ -0,0 +1,377 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +// eslint-disable-next-line react/display-name +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 {getGCD, 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, usePrevious} 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 formatBytes(bytes) { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let unitIndex = 0; + + while (bytes >= 1024 && unitIndex < units.length - 1) { + bytes /= 1024; + unitIndex++; + } + + return `${bytes.toFixed(2)} ${units[unitIndex]}`; +} + +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({preferences, sid, did, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + const prevPrefernces = usePrevious(preferences); + + const [cpuUsageInfo, cpuUsageInfoReduce] = useReducer(statsReducer, chartsDefault['cu_stats']); + const [loadAvgInfo, loadAvgInfoReduce] = useReducer(statsReducer, chartsDefault['la_stats']); + const [processCpuUsageStats, setProcessCpuUsageStats] = useState([]); + + const [, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + 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, + }, + ]; + + useEffect(()=>{ + let calcPollDelay = false; + if(prevPrefernces) { + if(prevPrefernces['cu_stats_refresh'] != preferences['cu_stats_refresh']) { + cpuUsageInfoReduce({reset: chartsDefault['cu_stats']}); + calcPollDelay = true; + } + if(prevPrefernces['la_stats_refresh'] != preferences['la_stats_refresh']) { + loadAvgInfoReduce({reset: chartsDefault['la_stats']}); + calcPollDelay = true; + } + if(prevPrefernces['pcu_stats_refresh'] != preferences['pcu_stats_refresh']) { + setProcessCpuUsageStats({reset: chartsDefault['pcu_stats']}); + calcPollDelay = true; + } + } else { + calcPollDelay = true; + } + if(calcPollDelay) { + const keys = Object.keys(chartsDefault); + const length = keys.length; + if(length == 1){ + setPollDelay( + preferences[keys[0]+'_refresh']*1000 + ); + } else { + 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); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.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: formatBytes(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 = { + 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 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/SystemStats/Memory.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx new file mode 100644 index 000000000..74e8f424b --- /dev/null +++ b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx @@ -0,0 +1,372 @@ +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 {getGCD, 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, usePrevious} 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 formatBytes(bytes) { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let unitIndex = 0; + + while (bytes >= 1024 && unitIndex < units.length - 1) { + bytes /= 1024; + unitIndex++; + } + + return `${bytes.toFixed(2)} ${units[unitIndex]}`; +} + +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({preferences, sid, did, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + const prevPrefernces = usePrevious(preferences); + + const [memoryUsageInfo, memoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['m_stats']); + const [swapMemoryUsageInfo, swapMemoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['sm_stats']); + const [processMemoryUsageStats, setProcessMemoryUsageStats] = useState([]); + + const [, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + 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, + }, + ]; + + useEffect(()=>{ + let calcPollDelay = false; + if(prevPrefernces) { + if(prevPrefernces['m_stats_refresh'] != preferences['m_stats_refresh']) { + memoryUsageInfoReduce({reset: chartsDefault['m_stats']}); + calcPollDelay = true; + } + if(prevPrefernces['sm_stats_refresh'] != preferences['sm_stats_refresh']) { + swapMemoryUsageInfoReduce({reset: chartsDefault['sm_stats']}); + calcPollDelay = true; + } + if(prevPrefernces['pmu_stats_refresh'] != preferences['pmu_stats_refresh']) { + setProcessMemoryUsageStats({reset: chartsDefault['pmu_stats']}); + calcPollDelay = true; + } + } else { + calcPollDelay = true; + } + if(calcPollDelay) { + const keys = Object.keys(chartsDefault); + const length = keys.length; + if(length == 1){ + setPollDelay( + preferences[keys[0]+'_refresh']*1000 + ); + } else { + 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); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.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: formatBytes(pmu_info_obj[key]['memory_usage']), memory_bytes: formatBytes(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 = { + 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 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/SystemStats/Storage.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx new file mode 100644 index 000000000..2acb217bd --- /dev/null +++ b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx @@ -0,0 +1,329 @@ +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 {getGCD, 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, usePrevious} 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', + }, + ioDiskContainer: { + height: 'auto', + background: theme.palette.grey[200], + padding: '10px', + }, + fixedContainer: { + height: '577px', + background: theme.palette.grey[200], + padding: '10px', + marginBottom: '30px', + }, + containerHeader: { + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '5px', + }, + chartHeader: { + fontSize: '14px', + 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 ioStatsReducer(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(disk_stats => { + newState[disk_stats] = {}; + Object.keys(action.incoming[disk_stats]).forEach(label => { + if(state[disk_stats][label]) { + newState[disk_stats][label] = [ + action.counter ? action.incoming[disk_stats][label] - action.counterData[disk_stats][label] : action.incoming[disk_stats][label], + ...state[disk_stats][label].slice(0, X_AXIS_LENGTH-1), + ]; + } else { + newState[disk_stats][label] = [ + action.counter ? action.incoming[disk_stats][label] - action.counterData[disk_stats][label] : action.incoming[disk_stats][label], + ]; + } + }); + }); + return newState; +} + +const chartsDefault = { + 'io_stats': {}, +}; + +export default function Storage({preferences, sid, did, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + const prevPrefernces = usePrevious(preferences); + + const [ioInfo, ioInfoReduce] = useReducer(ioStatsReducer, chartsDefault['io_stats']); + + const [, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + useEffect(()=>{ + let calcPollDelay = false; + if(prevPrefernces) { + if(prevPrefernces['io_stats_refresh'] != preferences['io_stats_refresh']) { + ioInfoReduce({reset: chartsDefault['io_stats']}); + calcPollDelay = true; + } + } else { + calcPollDelay = true; + } + if(calcPollDelay) { + const keys = Object.keys(chartsDefault); + const length = keys.length; + if(length == 1){ + setPollDelay( + preferences[keys[0]+'_refresh']*1000 + ); + } else { + 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); + if (!pageVisible){ + return; + } + axios.get(path) + .then((resp)=>{ + let data = resp.data; + setErrorMsg(null); + if(data.hasOwnProperty('io_stats')){ + const io_info_obj = data['io_stats']; + for (const disk in io_info_obj) { + if(!chartsDefault.io_stats.hasOwnProperty(`${disk}_total_rw`)){ + chartsDefault.io_stats[`${disk}_total_rw`] = {'Read': [], 'Write': []}; + } + if(!ioInfo.hasOwnProperty(`${disk}_total_rw`)){ + ioInfo[`${disk}_total_rw`] = {'Read': [], 'Write': []}; + } + + if(!chartsDefault.io_stats.hasOwnProperty(`${disk}_bytes_rw`)){ + chartsDefault.io_stats[`${disk}_bytes_rw`] = {'Read': [], 'Write': []}; + } + if(!ioInfo.hasOwnProperty(`${disk}_bytes_rw`)){ + ioInfo[`${disk}_bytes_rw`] = {'Read': [], 'Write': []}; + } + + if(!chartsDefault.io_stats.hasOwnProperty(`${disk}_time_rw`)){ + chartsDefault.io_stats[`${disk}_time_rw`] = {'Read': [], 'Write': []}; + } + if(!ioInfo.hasOwnProperty(`${disk}_time_rw`)){ + ioInfo[`${disk}_time_rw`] = {'Read': [], 'Write': []}; + } + } + + let new_io_stats = {}; + for (const disk in io_info_obj) { + new_io_stats[`${disk}_total_rw`] = {'Read': io_info_obj[`${disk}`]['total_reads']?io_info_obj[`${disk}`]['total_reads']:0, 'Write': io_info_obj[`${disk}`]['total_writes']?io_info_obj[`${disk}`]['total_writes']:0}; + new_io_stats[`${disk}_bytes_rw`] = {'Read': io_info_obj[`${disk}`]['read_bytes']?io_info_obj[`${disk}`]['read_bytes']:0, 'Write': io_info_obj[`${disk}`]['write_bytes']?io_info_obj[`${disk}`]['write_bytes']:0}; + new_io_stats[`${disk}_time_rw`] = {'Read': io_info_obj[`${disk}`]['read_time_ms']?io_info_obj[`${disk}`]['read_time_ms']:0, 'Write': io_info_obj[`${disk}`]['write_time_ms']?io_info_obj[`${disk}`]['write_time_ms']:0}; + } + ioInfoReduce({incoming: new_io_stats}); + } + + setCounterData((prevCounterData)=>{ + return { + ...prevCounterData, + ...data, + }; + }); + }) + .catch((error)=>{ + if(!errorMsg) { + ioInfoReduce({reset:chartsDefault['io_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} + /> + } + + ); +} + +Storage.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 StorageWrapper(props) { + const classes = useStyles(); + const options = useMemo(()=>({ + showDataPoints: props.showDataPoints, + showTooltip: props.showTooltip, + lineBorderWidth: props.lineBorderWidth, + }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]); + + const keys = Object.keys(props.ioInfo); + return ( + <> + {keys.map((key, index) => ( + index % 3 === 0 && ( + + +
{gettext(`Disk ${Math.floor(index / 3) + 1}`)}
+
+ + {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => ( + +
{innerKeyIndex==0 ? gettext('I/O Operations Count'): innerKeyIndex==1? gettext('Data Transfer (Bytes)'):gettext('Time Spent in I/O Operations (Milliseconds)')}
+ + + +
+ ))} +
+
+ ) + ))} + + ); +} + +StorageWrapper.propTypes = { + ioInfo: PropTypes.objectOf( + PropTypes.shape({ + Read: PropTypes.array, + Write: PropTypes.array, + }) + ), + ioRefreshRate: PropTypes.number.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/SystemStats/Summary.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx new file mode 100644 index 000000000..9b2ee9a30 --- /dev/null +++ b/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx @@ -0,0 +1,422 @@ +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 {getGCD, 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, usePrevious} 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': new Array(X_AXIS_LENGTH).fill(null), 'Process': new Array(X_AXIS_LENGTH).fill(null)}, +}; + +const SummaryTable = (props) => { + const classes = useStyles(); + const data = props.data; + return ( + + + + + + + + + {data.map((item, index) => ( + + + + + ))} + +
PropertyValue
{item.name}{item.value}
+ ); +}; + +SummaryTable.propTypes = { + data: PropTypes.any, +}; + +export default function Summary({preferences, sid, did, pageVisible, enablePoll=true}) { + const refreshOn = useRef(null); + const prevPrefernces = usePrevious(preferences); + + const [processHandleCount, processHandleCountReduce] = useReducer(statsReducer, chartsDefault['hpc_stats']); + const [osStats, setOsStats] = useState([]); + const [cpuStats, setCpuStats] = useState([]); + const [processInfoStats] = useState({'Running': 4, 'Sleeping': 2, 'Stopped': 1, 'Zombie': 2}); + + const [, setCounterData] = useState({}); + + const [pollDelay, setPollDelay] = useState(5000); + const [longPollDelay] = useState(180000); + const [errorMsg, setErrorMsg] = useState(null); + const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + + const tableHeader = [ + { + Header: 'Property', + accessor: 'name', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + { + Header: 'Value', + accessor: 'value', + sortable: true, + resizable: true, + disableGlobalFilter: false, + }, + ]; + + useEffect(()=>{ + let calcPollDelay = false; + if(prevPrefernces) { + if(prevPrefernces['hpc_stats_refresh'] != preferences['hpc_stats_refresh']) { + processHandleCountReduce({reset: chartsDefault['hpc_stats']}); + calcPollDelay = true; + } + } else { + calcPollDelay = true; + } + if(calcPollDelay) { + const keys = Object.keys(chartsDefault); + const length = keys.length; + if(length == 1){ + setPollDelay( + preferences[keys[0]+'_refresh']*1000 + ); + } else { + 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]); + + 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 + preferences[name+'_refresh']; + } + }); + + 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 ( + <> +
{pollDelay}
+ {chartDrawnOnce && + 0} + isTest={false} + /> + } + + ); +} + +Summary.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 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')}
+ + + +
+
+ + ); +} + +SummaryWrapper.propTypes = { + processHandleCount: PropTypes.any.isRequired, + osStats: PropTypes.any.isRequired, + cpuStats: PropTypes.any.isRequired, + processInfoStats: PropTypes.any.isRequired, + tableHeader: PropTypes.any.isRequired, + errorMsg: PropTypes.any, + showTooltip: PropTypes.bool, + showDataPoints: PropTypes.bool, + lineBorderWidth: PropTypes.number, + isDatabase: PropTypes.bool, + isTest: PropTypes.bool, +}; 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..9024a2c5e --- /dev/null +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql @@ -0,0 +1,100 @@ +{% 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 'io_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'io_stats' in chart_names %} +{% set add_union = true %} + SELECT 'io_stats' AS chart_name, ( + SELECT to_json(pg_catalog.jsonb_object_agg('disk'||row_number, pg_catalog.row_to_json(t))) + FROM ( + SELECT *, ROW_NUMBER() OVER (ORDER BY device_name) AS row_number + FROM pg_sys_io_analysis_info() + ) 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..4da1d4435 --- /dev/null +++ b/web/pgadmin/static/js/components/PgChart/DonutChart.jsx @@ -0,0 +1,70 @@ +import React, { useEffect, useRef } from 'react'; +import Chart from 'chart.js/auto'; +import PropTypes from 'prop-types'; + +export default function 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 ( + + ); +} + +DonutChart.propTypes = { + data: PropTypes.array.isRequired, +}; \ No newline at end of file diff --git a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx index bd465e3da..5ccfe3464 100644 --- a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx +++ b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx @@ -32,7 +32,7 @@ function tooltipPlugin(refreshRate) { showTooltip(); let tooltipHtml=`
${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}
`; for(let i=1; i
${u.series[i].label}: ${u.legend.values[i]['_']}`; + tooltipHtml += `
${u.series[i].label}: ${u.legend.values[i]['_']}
`; } tooltip.innerHTML = tooltipHtml; @@ -58,44 +58,32 @@ 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, @@ -108,11 +96,104 @@ 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]; + }); + } + }); + 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]; + }); } - } - ], - plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [], - }), [data.refreshRate, data?.datasets?.length, width, height, options]); + }); + } else{ + 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]; + }); + } + }); + } + + 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, + } + }, + 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, }; -- 2.41.0.windows.1 From 7893b50d1b642ec9c8b66bb68fb8d19145a6b47d Mon Sep 17 00:00:00 2001 From: Sahil Harpal Date: Thu, 17 Aug 2023 15:31:59 +0530 Subject: [PATCH 2/3] Handle null values for CPU & memory usage --- web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx | 4 ++++ web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx index 276034d9e..a6341954f 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx @@ -54,6 +54,10 @@ const useStyles = makeStyles((theme) => ({ })); export function formatBytes(bytes) { + if (bytes === null) { + return 'null'; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB']; let unitIndex = 0; diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx index 74e8f424b..9d32795dd 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx @@ -45,6 +45,10 @@ const useStyles = makeStyles((theme) => ({ })); export function formatBytes(bytes) { + if (bytes === null) { + return 'null'; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB']; let unitIndex = 0; -- 2.41.0.windows.1 From 8d8717f8fbb9345ac51c3592cdb6c1f4f867aa7d Mon Sep 17 00:00:00 2001 From: Sahil Harpal Date: Fri, 25 Aug 2023 13:10:10 +0530 Subject: [PATCH 3/3] Review-2 changes --- web/pgadmin/dashboard/static/js/Dashboard.jsx | 3 + .../dashboard/static/js/SystemStats/CPU.jsx | 6 +- .../static/js/SystemStats/Memory.jsx | 4 +- .../static/js/SystemStats/Storage.jsx | 267 ++++++++++++++++-- .../static/js/SystemStats/Summary.jsx | 12 +- .../sql/default/system_statistics.sql | 13 + .../js/components/PgChart/StreamingChart.jsx | 15 +- 7 files changed, 283 insertions(+), 37 deletions(-) diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx index e6afeff07..2944a4e83 100644 --- a/web/pgadmin/dashboard/static/js/Dashboard.jsx +++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx @@ -818,6 +818,8 @@ export default function Dashboard({ const data = res.data; if(data['ss_present'] == false){ setSsMsg(gettext('System stats extension is not installed. You can install the extension in a database using the "CREATE EXTENSION system_stats;" SQL command. Reload the pgAdmin once you installed.')); + } else { + setSsMsg(gettext('')); } }) .catch(() => { @@ -1034,6 +1036,7 @@ export default function Dashboard({ did={did} pageVisible={props.panelVisible} serverConnected={props.serverConnected} + systemStatsTabVal={systemStatsTabVal} /> : diff --git a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx index a6341954f..e6b766767 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx @@ -36,13 +36,11 @@ const useStyles = makeStyles((theme) => ({ }, container: { height: 'auto', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', }, fixedContainer: { height: '577px', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', }, @@ -338,13 +336,13 @@ export function CPUWrapper(props) { <> -
{gettext('CPU Usage ()')}
+
{gettext('CPU usage')}
-
{gettext('Load Average')}
+
{gettext('Load average')}
diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx index 9d32795dd..8e8e0eb89 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx @@ -27,13 +27,11 @@ const useStyles = makeStyles((theme) => ({ }, container: { height: 'auto', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', }, fixedContainer: { height: '577px', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', }, @@ -339,7 +337,7 @@ export function MemoryWrapper(props) {
-
{gettext('Swap Memory')}
+
{gettext('Swap memory')}
diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx index 2acb217bd..9db08b093 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx @@ -10,6 +10,7 @@ import { DATA_POINT_SIZE } from 'sources/chartjs'; import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart'; import {useInterval, usePrevious} from 'sources/custom_hooks'; import axios from 'axios'; +import { BarChart } from '../../../../static/js/chartjs'; export const X_AXIS_LENGTH = 75; @@ -26,22 +27,24 @@ const useStyles = makeStyles((theme) => ({ }, container: { height: 'auto', - background: theme.palette.grey[200], padding: '10px', - marginBottom: '30px', + marginBottom: '15px', }, ioDiskContainer: { height: 'auto', - background: theme.palette.grey[200], padding: '10px', }, fixedContainer: { height: '577px', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', + overflowX: 'auto', }, containerHeader: { + height: 'auto', + padding: '10px', + }, + containerHeaderText: { fontSize: '16px', fontWeight: 'bold', marginBottom: '5px', @@ -50,9 +53,37 @@ const useStyles = makeStyles((theme) => ({ fontSize: '14px', fontWeight: 'bold', marginBottom: '5px', - } + }, + table: { + width: '100%', + backgroundColor: theme.otherVars.tableBg, + border: '1px solid rgb(221, 224, 230)', + borderCollapse: 'collapse', + borderRadius: '4px', + overflow: 'hidden', + }, + tableVal: { + border: '1px solid rgb(221, 224, 230) !important', + padding: '10px !important', + }, })); +export function formatBytes(bytes) { + if (bytes === null) { + return 'null'; + } + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let unitIndex = 0; + + while (bytes >= 1024 && unitIndex < units.length - 1) { + bytes /= 1024; + unitIndex++; + } + + return `${bytes.toFixed(2)} ${units[unitIndex]}`; +} + 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)=>{ @@ -120,10 +151,43 @@ const chartsDefault = { 'io_stats': {}, }; -export default function Storage({preferences, sid, did, pageVisible, enablePoll=true}) { + +const DiskStatsTable = (props) => { + const classes = useStyles(); + const tableHeader = props.tableHeader; + const data = props.data; + return ( + + + + {tableHeader.map((item, index) => ( + + ))} + + + + {data.map((item, index) => ( + + {tableHeader.map((header, id) => ( + + ))} + + ))} + +
{item.Header}
{item[header.accessor]}
+ ); +}; + +DiskStatsTable.propTypes = { + data: PropTypes.array.isRequired, + tableHeader: PropTypes.array.isRequired, +}; + +export default function Storage({preferences, sid, did, pageVisible, enablePoll=true, systemStatsTabVal}) { const refreshOn = useRef(null); const prevPrefernces = usePrevious(preferences); + const [diskStats, setDiskStats] = useState([]); const [ioInfo, ioInfoReduce] = useReducer(ioStatsReducer, chartsDefault['io_stats']); const [, setCounterData] = useState({}); @@ -132,6 +196,49 @@ export default function Storage({preferences, sid, did, pageVisible, enablePoll= const [errorMsg, setErrorMsg] = useState(null); const [chartDrawnOnce, setChartDrawnOnce] = useState(false); + const tableHeader = [ + { + Header: 'File system', + accessor: 'file_system', + }, + { + Header: 'File system type', + accessor: 'file_system_type', + }, + { + Header: 'Mount point', + accessor: 'mount_point', + }, + { + Header: 'Drive letter', + accessor: 'drive_letter', + }, + { + Header: 'Total space', + accessor: 'total_space', + }, + { + Header: 'Used space', + accessor: 'used_space', + }, + { + Header: 'Free space', + accessor: 'free_space', + }, + { + Header: 'Total inodes', + accessor: 'total_inodes', + }, + { + Header: 'Used inodes', + accessor: 'used_inodes', + }, + { + Header: 'Free inodes', + accessor: 'free_inodes', + }, + ]; + useEffect(()=>{ let calcPollDelay = false; if(prevPrefernces) { @@ -164,6 +271,50 @@ export default function Storage({preferences, sid, did, pageVisible, enablePoll= } }, [pageVisible]); + 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=' + 'di_stats'; + axios.get(url) + .then((res) => { + let data = res.data; + setErrorMsg(null); + if(data.hasOwnProperty('di_stats')){ + let di_info_list = []; + const di_info_obj = data['di_stats']; + for (const key in di_info_obj) { + di_info_list.push({ + icon: '', + file_system: di_info_obj[key]['file_system']?di_info_obj[key]['file_system']:'null', + file_system_type: di_info_obj[key]['file_system_type']?di_info_obj[key]['file_system_type']:'null', + mount_point: di_info_obj[key]['mount_point']?di_info_obj[key]['mount_point']:'null', + drive_letter: di_info_obj[key]['drive_letter']?di_info_obj[key]['drive_letter']:'null', + total_space: di_info_obj[key]['total_space']?formatBytes(di_info_obj[key]['total_space']):'null', + used_space: di_info_obj[key]['used_space']?formatBytes(di_info_obj[key]['used_space']):'null', + free_space: di_info_obj[key]['free_space']?formatBytes(di_info_obj[key]['free_space']):'null', + total_inodes: di_info_obj[key]['total_inodes']?di_info_obj[key]['total_inodes']:'null', + used_inodes: di_info_obj[key]['used_inodes']?di_info_obj[key]['used_inodes']:'null', + free_inodes: di_info_obj[key]['free_inodes']?di_info_obj[key]['free_inodes']:'null', + total_space_actual: di_info_obj[key]['total_space']?di_info_obj[key]['total_space']:null, + used_space_actual: di_info_obj[key]['used_space']?di_info_obj[key]['used_space']:null, + free_space_actual: di_info_obj[key]['free_space']?di_info_obj[key]['free_space']:null, + }); + } + setDiskStats(di_info_list); + } + }) + .catch((error) => { + console.error('Error fetching data:', error); + }); + } catch (error) { + console.error('Error fetching data:', error); + } + }, [systemStatsTabVal, sid, did, enablePoll, pageVisible]); + useInterval(()=>{ const currEpoch = getEpoch(); if(refreshOn.current === null) { @@ -258,6 +409,8 @@ export default function Storage({preferences, sid, did, pageVisible, enablePoll= - {keys.map((key, index) => ( - index % 3 === 0 && ( - - -
{gettext(`Disk ${Math.floor(index / 3) + 1}`)}
-
- - {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => ( - -
{innerKeyIndex==0 ? gettext('I/O Operations Count'): innerKeyIndex==1? gettext('Data Transfer (Bytes)'):gettext('Time Spent in I/O Operations (Milliseconds)')}
- - - -
- ))} -
+ + +
{gettext('Disk information')}
+
+ + + + + + + item.mount_point!='null'?item.mount_point:item.drive_letter!='null'?item.drive_letter:'disk'+index), + datasets: [ + { + label: 'Used space', + data: props.diskStats.map((item) => item.used_space_actual?item.used_space_actual:0), + backgroundColor: '#FF6384', + borderColor: '#FF6384', + borderWidth: 1, + }, + { + label: 'Available space', + data: props.diskStats.map((item) => item.free_space_actual?item.free_space_actual:0), + backgroundColor: '#36a2eb', + borderColor: '#36a2eb', + borderWidth: 1, + }, + ], + }} + options={ + { + scales: { + x: { + display: true, + ticks: { + display: true, + }, + }, + y: { + beginAtZero: true, + ticks: { + callback: function (value) { + return formatBytes(value); + }, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + } + } + /> + - ) - ))} + + + +
+ + {keys.map((key, index) => ( + index % 3 === 0 && ( + + +
{gettext(`Disk ${Math.floor(index / 3) + 1}`)}
+
+ + {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => ( + +
{innerKeyIndex==0 ? gettext('I/O operations count'): innerKeyIndex==1? gettext('Data transfer (bytes)'):gettext('Time spent in I/O operations (milliseconds)')}
+ + + +
+ ))} +
+
+ ) + ))} +
); } @@ -320,6 +537,8 @@ StorageWrapper.propTypes = { }) ), ioRefreshRate: PropTypes.number.isRequired, + diskStats: PropTypes.array.isRequired, + tableHeader: PropTypes.array.isRequired, errorMsg: PropTypes.string, showTooltip: PropTypes.bool.isRequired, showDataPoints: PropTypes.bool.isRequired, diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx index 9b2ee9a30..1cda27a01 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx @@ -30,6 +30,9 @@ const useStyles = makeStyles((theme) => ({ width: '100%', backgroundColor: theme.otherVars.tableBg, border: '1px solid rgb(221, 224, 230)', + borderCollapse: 'collapse', + borderRadius: '4px', + overflow: 'hidden', }, tableVal: { border: '1px solid rgb(221, 224, 230) !important', @@ -37,7 +40,6 @@ const useStyles = makeStyles((theme) => ({ }, container: { height: 'auto', - background: theme.palette.grey[200], padding: '10px', marginBottom: '30px', }, @@ -381,11 +383,11 @@ export function SummaryWrapper(props) { <> -
{gettext('OS Information')}
+
{gettext('OS information')}
-
{gettext('Handle & Process Count')}
+
{gettext('Handle & process count')}
@@ -393,11 +395,11 @@ export function SummaryWrapper(props) {
-
{gettext('CPU Information')}
+
{gettext('CPU information')}
-
{gettext('Process Information')}
+
{gettext('Process information')}
diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql index 9024a2c5e..35fe9042d 100644 --- a/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql @@ -91,6 +91,19 @@ ) t ) AS chart_data {% endif %} +{% if add_union and 'di_stats' in chart_names %} + UNION ALL +{% endif %} +{% if 'di_stats' in chart_names %} +{% set add_union = true %} + SELECT 'di_stats' AS chart_name, ( + SELECT to_json(pg_catalog.jsonb_object_agg('Drive'||row_number, pg_catalog.row_to_json(t))) + FROM ( + SELECT *, ROW_NUMBER() OVER (ORDER BY total_space) AS row_number + FROM pg_sys_disk_info() WHERE mount_point IS NOT NULL OR drive_letter IS NOT NULL + ) t + ) AS chart_data +{% endif %} {% if add_union and 'pi_stats' in chart_names %} UNION ALL {% endif %} diff --git a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx index 5ccfe3464..34a1a8fea 100644 --- a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx +++ b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx @@ -5,6 +5,16 @@ import gettext from 'sources/gettext'; import PropTypes from 'prop-types'; import { useTheme } from '@material-ui/styles'; +const removeExistingTooltips = () => { + // Select all elements with the class name "uplot-tooltip" + const tooltipLabels = document.querySelectorAll('.uplot-tooltip'); + + // Remove each selected element + tooltipLabels.forEach((tooltipLabel) => { + tooltipLabel.remove(); + }); +}; + function tooltipPlugin(refreshRate) { let tooltipTopOffset = -20; let tooltipLeftOffset = 10; @@ -12,13 +22,14 @@ function tooltipPlugin(refreshRate) { function showTooltip() { if(!tooltip) { + removeExistingTooltips(); tooltip = document.createElement('div'); tooltip.className = 'uplot-tooltip'; tooltip.style.display = 'block'; document.body.appendChild(tooltip); } } - + function hideTooltip() { tooltip?.remove(); tooltip = null; @@ -62,6 +73,7 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis const chartRef = useRef(); const theme = useTheme(); const { width, height, ref:containerRef } = useResizeDetector(); + const defaultOptions = useMemo(()=> { const series = [ {}, @@ -170,6 +182,7 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis }); } + return { title: '', width: width, -- 2.41.0.windows.1