public inbox for [email protected]
help / color / mirror / Atom feedFrom: Sahil Harpal <[email protected]>
To: Aditya Toshniwal <[email protected]>
Cc: Khushboo Vashi <[email protected]>
Cc: [email protected]
Cc: Dave Page <[email protected]>
Cc: Akshay Joshi <[email protected]>
Cc: Ashesh Vashi <[email protected]>
Subject: Re: Pgadmin4 System Stats Extension Design
Date: Fri, 25 Aug 2023 13:34:57 +0530
Message-ID: <CAKi=nndiSd72JfW+2npo6r4Q_7csoK-UT37EErVAY0vi2BziJQ@mail.gmail.com> (raw)
In-Reply-To: <CAM9w-_ki8J4F5p3ioKFaa4Qq6OKykbFnJ=0xnSispVaRyec1yA@mail.gmail.com>
References: <CAKi=nnc6Ze8QWBmSHe9YBYciWeDzMf=-rw9BgyNocFSokrpi6w@mail.gmail.com>
<CAKi=nneuTPLMppZaAm-Y1X=4mY3UnbkbJAMCHF=Fu-2+kateRg@mail.gmail.com>
<CAFOhELdhcYr9NBNrkEAWt_cwP77U2ANRK4xonqKA1p6WpMBdMA@mail.gmail.com>
<CAKi=nnfSoagHGwa4To7ND39Sb0qcUVBsL2HZB0X4d_4qAus2Nw@mail.gmail.com>
<CAKi=nndD92uV-RifeDJqs_3YV+ABWiN1_iMLUi7Zf1AaHzwM0w@mail.gmail.com>
<CAFOhELeVGiCarA9-AK_Wi9EpNxKrF4Ev7VNNQCD=LZy9OM0aqg@mail.gmail.com>
<CAKi=nncs=LaytGKL8muxfP--nXjdW4P+cdZW=RRprd4UZ3mMZQ@mail.gmail.com>
<CAKi=nncpnEObc29MmGBAq58QKuFAdXrV7EDtf4Sp0QBuHV0Z4Q@mail.gmail.com>
<CAFOhELeBxSfF8PUk556R-cBoyvNrLkU3NDWhzbFbD1OQfp+xwA@mail.gmail.com>
<CAKi=nnfY22PmFOfGsyN1-piwicLDDrs8v8ACkzzQVXdywzNVQQ@mail.gmail.com>
<CAM9w-_m_zj456pHLQ0vkthgoxfbkd_C8ywguYdTFdG4W2+Znnw@mail.gmail.com>
<CAKi=nnfrnbsiGaSVNPq-+wUcLiihhBEvjGjY5JMaFiw6KmGOgw@mail.gmail.com>
<CAM9w-_=Mz0gT0SZyfLa+5Gjq9WHKtiDDvKfJcQhkDE1dC7HcyA@mail.gmail.com>
<CAKi=nnesjQ_z58hrzjw7pjCTp95N67O5Jyq4uqAcYhTJwCUHGw@mail.gmail.com>
<CAM9w-_ki8J4F5p3ioKFaa4Qq6OKykbFnJ=0xnSispVaRyec1yA@mail.gmail.com>
Hi Aditya,
On Fri, 25 Aug 2023 at 12:32, Aditya Toshniwal <
[email protected]> wrote:
>
> On Fri, Aug 25, 2023 at 12:15 PM Sahil Harpal <[email protected]>
> wrote:
>
>> Hi Aditya,
>>
>> On Fri, 25 Aug 2023 at 12:06, Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Sahil,
>>>
>>> On Fri, Aug 25, 2023 at 12:02 PM Sahil Harpal <[email protected]>
>>> wrote:
>>>
>>>> Hi Aditya,
>>>> On Fri, 25 Aug 2023 at 11:29, Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>> [image: image.gif]
>>>>
>>>>> Hi Sahil,
>>>>> On Thu, Aug 24, 2023 at 8:07 PM Sahil Harpal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi,
>>>>>> Can we include total space stats (left pie chart) in the same bar
>>>>>> chart? or would it be better if we keep it separate?
>>>>>> [image: image.png]
>>>>>>
>>>>> How do you propose to merge both? I would also suggest using a stacked
>>>>> bar chart on the right. And format from bytes to MBs.
>>>>>
>>>>>> [image: image.gif]
>>>>>>
>>>>>
>>>> Currently, I have implemented like this:
>>>> [image: image.png]
>>>> So, I was thinking of adding one more column for total space for each
>>>> disk.
>>>> Regarding the stacked bar chart, we will need to increase the height of
>>>> the default chart container; otherwise, the proportion of different
>>>> categories won't be clearly visible in some cases.
>>>> And I believe that if we use a stacked bar chart, there won't be a need
>>>> to provide total space details, as the height of that stacked bar will be
>>>> nothing but the total space, right?
>>>>
>>> I don't think we'll need to increase the height though. Even if you use
>>> stacks, they will show total for each drive and not total available space.
>>>
>>
>> Ahh yes, users won't be able to see the absolute total value. So should I
>> change it to a stacked bar with 3 categories then?
>>
> I would suggest keeping the pie chart and making the stacks of used and
> unused. If possible please keep the pie chart colors different from bar
> chart.
>
Ok, sure I'll do it.
On Fri, 25 Aug 2023 at 11:56, Aditya Toshniwal <
[email protected]> wrote:
> Hi Sahil,
> Can you please share the rebased patch?
>
Please find the attached patch with recent changes. It doesn't include this
stacked bar, but it contains things suggested in the second review, and I
also tried to resolve the tooltip issue. Let me know if it's working
correctly.
I have also pushed these changes here:
https://github.com/Sahil1479/pgadmin4/tree/system_stats [Branch:
system_stats]
I will share the final patch with a detailed summary of all the things that
have been changed once I complete this stacked/pie chart and a few more
refinements.
Thank you,
Sahil
Attachments:
[image/png] image.png (44.9K, 3-image.png)
download | view image
[image/png] image.png (13.6K, 4-image.png)
download | view image
[image/gif] image.gif (42B, 5-image.gif)
download | view image
[image/gif] image.gif (42B, 6-image.gif)
download | view image
[application/octet-stream] system_stats.patch (108.9K, 7-system_stats.patch)
download | inline diff:
From 929d8f1ed0bcce78db795fe7cab9484c4e610a13 Mon Sep 17 00:00:00 2001
From: Sahil Harpal <[email protected]>
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
[email protected]('check_extension/system_statistics',
+ endpoint='check_system_statistics', methods=['GET'])
[email protected]('check_extension/system_statistics/<int:sid>',
+ endpoint='check_system_statistics_sid', methods=['GET'])
[email protected]('check_extension/system_statistics/<int:sid>/<int:did>',
+ 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
[email protected]('/system_statistics',
+ endpoint='system_statistics', methods=['GET'])
[email protected]('/system_statistics/<int:sid>',
+ endpoint='system_statistics_sid', methods=['GET'])
[email protected]('/system_statistics/<int:sid>/<int:did>',
+ 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 ? (
<Box className={classes.dashboardPanel}>
<Box className={classes.emptyPanel}>
- {!_.isUndefined(preferences) && preferences.show_graphs && (
- <Graphs
- key={sid + did}
- preferences={preferences}
- sid={sid}
- did={did}
- pageVisible={props.panelVisible}
- ></Graphs>
- )}
- {!_.isUndefined(preferences) && preferences.show_activity && (
- <Box className={classes.panelContent}>
- <Box
- className={classes.cardHeader}
- title={props.dbConnected ? gettext('Database activity') : gettext('Server activity')}
- >
- {props.dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '}
+ <Box className={classes.panelContent}>
+ <Box height="100%" display="flex" flexDirection="column">
+ <Box>
+ <Tabs
+ value={mainTabVal}
+ onChange={mainTabChanged}
+ >
+ {mainTabs.map((tabValue) => {
+ return <Tab key={tabValue} label={tabValue} />;
+ })}
+ <RefreshButton/>
+ </Tabs>
</Box>
- <Box height="100%" display="flex" flexDirection="column">
- <Box>
- <Tabs
- value={tabVal}
- onChange={tabChanged}
- >
- {tabs.map((tabValue) => {
- return <Tab key={tabValue} label={tabValue} />;
- })}
- <RefreshButton/>
- </Tabs>
+ {/* General Statistics */}
+ <TabPanel value={mainTabVal} index={0} classNameRoot={classes.tabPanel}>
+ {!_.isUndefined(preferences) && preferences.show_graphs && (
+ <Graphs
+ key={sid + did}
+ preferences={preferences}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ ></Graphs>
+ )}
+ {!_.isUndefined(preferences) && preferences.show_activity && (
+ <Box className={classes.panelContent}>
+ <Box
+ className={classes.cardHeader}
+ title={props.dbConnected ? gettext('Database activity') : gettext('Server activity')}
+ >
+ {props.dbConnected ? gettext('Database activity') : gettext('Server activity')}{' '}
+ </Box>
+ <Box height="100%" display="flex" flexDirection="column">
+ <Box>
+ <Tabs
+ value={tabVal}
+ onChange={tabChanged}
+ >
+ {tabs.map((tabValue) => {
+ return <Tab key={tabValue} label={tabValue} />;
+ })}
+ <RefreshButton/>
+ </Tabs>
+ </Box>
+ <TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
+ <PgTable
+ caveTable={false}
+ CustomHeader={CustomActiveOnlyHeader}
+ columns={activityColumns}
+ data={filteredDashData}
+ schema={schemaDict}
+ ></PgTable>
+ </TabPanel>
+ <TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
+ <PgTable
+ caveTable={false}
+ columns={databaseLocksColumns}
+ data={dashData}
+ ></PgTable>
+ </TabPanel>
+ <TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
+ <PgTable
+ caveTable={false}
+ columns={databasePreparedColumns}
+ data={dashData}
+ ></PgTable>
+ </TabPanel>
+ <TabPanel value={tabVal} index={3} classNameRoot={classes.tabPanel}>
+ <PgTable
+ caveTable={false}
+ columns={serverConfigColumns}
+ data={dashData}
+ ></PgTable>
+ </TabPanel>
+ </Box>
+ </Box>
+ )}
+ </TabPanel>
+ {/* System Statistics */}
+ <TabPanel value={mainTabVal} index={1} classNameRoot={classes.tabPanel}>
+ <Box height="100%" display="flex" flexDirection="column">
+ {ssMsg === '' ?
+ <>
+ <Box>
+ <Tabs
+ value={systemStatsTabVal}
+ onChange={systemStatsTabChanged}
+ >
+ {systemStatsTabs.map((tabValue) => {
+ return <Tab key={tabValue} label={tabValue} />;
+ })}
+ </Tabs>
+ </Box>
+ <TabPanel value={systemStatsTabVal} index={0} classNameRoot={classes.tabPanel}>
+ <Summary
+ key={sid + did}
+ preferences={preferences}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={1} classNameRoot={classes.tabPanel}>
+ <CPU
+ key={sid + did}
+ preferences={preferences}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={2} classNameRoot={classes.tabPanel}>
+ <Memory
+ key={sid + did}
+ preferences={preferences}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={3} classNameRoot={classes.tabPanel}>
+ <Storage
+ key={sid + did}
+ preferences={preferences}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ </> :
+ <div className={classes.emptyPanel}>
+ <EmptyPanelMessage text={ssMsg}/>
+ </div>
+ }
</Box>
- <TabPanel value={tabVal} index={0} classNameRoot={classes.tabPanel}>
- <PgTable
- caveTable={false}
- CustomHeader={CustomActiveOnlyHeader}
- columns={activityColumns}
- data={filteredDashData}
- schema={schemaDict}
- ></PgTable>
- </TabPanel>
- <TabPanel value={tabVal} index={1} classNameRoot={classes.tabPanel}>
- <PgTable
- caveTable={false}
- columns={databaseLocksColumns}
- data={dashData}
- ></PgTable>
- </TabPanel>
- <TabPanel value={tabVal} index={2} classNameRoot={classes.tabPanel}>
- <PgTable
- caveTable={false}
- columns={databasePreparedColumns}
- data={dashData}
- ></PgTable>
- </TabPanel>
- <TabPanel value={tabVal} index={3} classNameRoot={classes.tabPanel}>
- <PgTable
- caveTable={false}
- columns={serverConfigColumns}
- data={dashData}
- ></PgTable>
- </TabPanel>
- </Box>
+ </TabPanel>
</Box>
- )}
+ </Box>
</Box>
</Box>
) : 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 (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <CPUWrapper
+ cpuUsageInfo={transformData(cpuUsageInfo, preferences['cu_stats_refresh'])}
+ loadAvgInfo={transformData(loadAvgInfo, preferences['la_stats_refresh'])}
+ processCpuUsageStats={processCpuUsageStats}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={preferences['graph_mouse_track']}
+ showDataPoints={preferences['graph_data_points']}
+ lineBorderWidth={preferences['graph_line_border_width']}
+ isDatabase={did > 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 (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('CPU Usage ()')}</div>
+ <ChartContainer id='cu-graph' title={gettext('')} datasets={props.cpuUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={props.cpuUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('Load Average')}</div>
+ <ChartContainer id='la-graph' title={gettext('')} datasets={props.loadAvgInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={props.loadAvgInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ </Grid>
+ <Grid container spacing={1} className={classes.fixedContainer}>
+ <PgTable
+ className={classes.autoResizer}
+ columns={props.tableHeader}
+ data={props.processCpuUsageStats}
+ msg={props.errorMsg}
+ type={'panel'}
+ ></PgTable>
+ </Grid>
+ </>
+ );
+}
+
+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 (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <MemoryWrapper
+ memoryUsageInfo={transformData(memoryUsageInfo, preferences['m_stats_refresh'])}
+ swapMemoryUsageInfo={transformData(swapMemoryUsageInfo, preferences['sm_stats_refresh'])}
+ processMemoryUsageStats={processMemoryUsageStats}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={preferences['graph_mouse_track']}
+ showDataPoints={preferences['graph_data_points']}
+ lineBorderWidth={preferences['graph_line_border_width']}
+ isDatabase={did > 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 (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('Memory')}</div>
+ <ChartContainer id='m-graph' title={gettext('')} datasets={props.memoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={props.memoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('Swap Memory')}</div>
+ <ChartContainer id='sm-graph' title={gettext('')} datasets={props.swapMemoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={props.swapMemoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ </Grid>
+ <Grid container spacing={1} className={classes.fixedContainer}>
+ <PgTable
+ className={classes.autoResizer}
+ columns={props.tableHeader}
+ data={props.processMemoryUsageStats}
+ msg={props.errorMsg}
+ type={'panel'}
+ ></PgTable>
+ </Grid>
+ </>
+ );
+}
+
+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 (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <StorageWrapper
+ ioInfo={ioInfo}
+ ioRefreshRate={preferences['io_stats_refresh']}
+ errorMsg={errorMsg}
+ showTooltip={preferences['graph_mouse_track']}
+ showDataPoints={preferences['graph_data_points']}
+ lineBorderWidth={preferences['graph_line_border_width']}
+ isDatabase={did > 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 && (
+ <Grid key={`disk-${index}`} container spacing={1} className={classes.container}>
+ <Grid container spacing={1} className={classes.ioDiskContainer}>
+ <div className={classes.containerHeader}>{gettext(`Disk ${Math.floor(index / 3) + 1}`)}</div>
+ </Grid>
+ <Grid container spacing={1} className={classes.ioDiskContainer}>
+ {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => (
+ <Grid key={`${innerKey}`} item md={4} sm={6}>
+ <div className={classes.chartHeader}>{innerKeyIndex==0 ? gettext('I/O Operations Count'): innerKeyIndex==1? gettext('Data Transfer (Bytes)'):gettext('Time Spent in I/O Operations (Milliseconds)')}</div>
+ <ChartContainer id={`io-graph-${innerKey}`} title={gettext('')} datasets={transformData(props.ioInfo[innerKey], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={transformData(props.ioInfo[innerKey], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ ))}
+ </Grid>
+ </Grid>
+ )
+ ))}
+ </>
+ );
+}
+
+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 (
+ <table className={classes.table}>
+ <thead>
+ <tr>
+ <th className={classes.tableVal}>Property</th>
+ <th className={classes.tableVal}>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {data.map((item, index) => (
+ <tr className={classes.tableVal} key={index}>
+ <td className={classes.tableVal}>{item.name}</td>
+ <td className={classes.tableVal}>{item.value}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ );
+};
+
+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 (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <SummaryWrapper
+ processHandleCount={transformData(processHandleCount, preferences['hpc_stats_refresh'])}
+ osStats={osStats}
+ cpuStats={cpuStats}
+ processInfoStats={transformData(processInfoStats, 5)}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={preferences['graph_mouse_track']}
+ showDataPoints={preferences['graph_data_points']}
+ lineBorderWidth={preferences['graph_line_border_width']}
+ isDatabase={did > 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 (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('OS Information')}</div>
+ <SummaryTable data={props.osStats} />
+ </Grid>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('Handle & Process Count')}</div>
+ <ChartContainer id='hpc-graph' title={gettext('')} datasets={props.processHandleCount.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={props.processHandleCount} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} showSecondAxis={true} />
+ </ChartContainer>
+ </Grid>
+ </Grid>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('CPU Information')}</div>
+ <SummaryTable data={props.cpuStats} />
+ </Grid>
+ <Grid item md={6} sm={12}>
+ <div className={classes.containerHeader}>{gettext('Process Information')}</div>
+ <ChartContainer id='pi-graph' title={gettext('')} datasets={props.processInfoStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <DonutChart data={props.processInfoStats.datasets} />
+ </ChartContainer>
+ </Grid>
+ </Grid>
+ </>
+ );
+}
+
+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 (
+ <canvas ref={chartRef} />
+ );
+}
+
+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=`<div>${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}</div>`;
for(let i=1; i<u.series.length; i++) {
- tooltipHtml += `<div class="uplot-tooltip-label"><div style="height:12px; width:12px; background-color:${u.series[i].stroke()}"></div> ${u.series[i].label}: ${u.legend.values[i]['_']}</div>`;
+ tooltipHtml += `<div class='uplot-tooltip-label'><div style='height:12px; width:12px; background-color:${u.series[i].stroke()}'></div> ${u.series[i].label}: ${u.legend.values[i]['_']}</div>`;
}
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 <[email protected]>
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 <[email protected]>
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}
/>
</TabPanel>
</> :
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) {
<>
<Grid container spacing={1} className={classes.container}>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('CPU Usage ()')}</div>
+ <div className={classes.containerHeader}>{gettext('CPU usage')}</div>
<ChartContainer id='cu-graph' title={gettext('')} datasets={props.cpuUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.cpuUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
</Grid>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('Load Average')}</div>
+ <div className={classes.containerHeader}>{gettext('Load average')}</div>
<ChartContainer id='la-graph' title={gettext('')} datasets={props.loadAvgInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.loadAvgInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
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) {
</ChartContainer>
</Grid>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('Swap Memory')}</div>
+ <div className={classes.containerHeader}>{gettext('Swap memory')}</div>
<ChartContainer id='sm-graph' title={gettext('')} datasets={props.swapMemoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.swapMemoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
</ChartContainer>
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 (
+ <table className={classes.table}>
+ <thead>
+ <tr>
+ {tableHeader.map((item, index) => (
+ <th className={classes.tableVal} key={index}>{item.Header}</th>
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {data.map((item, index) => (
+ <tr className={classes.tableVal} key={index}>
+ {tableHeader.map((header, id) => (
+ <td className={classes.tableVal} key={header.accessor+'-'+id}>{item[header.accessor]}</td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ );
+};
+
+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=
<StorageWrapper
ioInfo={ioInfo}
ioRefreshRate={preferences['io_stats_refresh']}
+ diskStats={diskStats}
+ tableHeader={tableHeader}
errorMsg={errorMsg}
showTooltip={preferences['graph_mouse_track']}
showDataPoints={preferences['graph_data_points']}
@@ -276,6 +429,7 @@ Storage.propTypes = {
did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
pageVisible: PropTypes.bool,
enablePoll: PropTypes.bool,
+ systemStatsTabVal: PropTypes.number,
};
export function StorageWrapper(props) {
@@ -289,25 +443,88 @@ export function StorageWrapper(props) {
const keys = Object.keys(props.ioInfo);
return (
<>
- {keys.map((key, index) => (
- index % 3 === 0 && (
- <Grid key={`disk-${index}`} container spacing={1} className={classes.container}>
- <Grid container spacing={1} className={classes.ioDiskContainer}>
- <div className={classes.containerHeader}>{gettext(`Disk ${Math.floor(index / 3) + 1}`)}</div>
- </Grid>
- <Grid container spacing={1} className={classes.ioDiskContainer}>
- {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => (
- <Grid key={`${innerKey}`} item md={4} sm={6}>
- <div className={classes.chartHeader}>{innerKeyIndex==0 ? gettext('I/O Operations Count'): innerKeyIndex==1? gettext('Data Transfer (Bytes)'):gettext('Time Spent in I/O Operations (Milliseconds)')}</div>
- <ChartContainer id={`io-graph-${innerKey}`} title={gettext('')} datasets={transformData(props.ioInfo[innerKey], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
- <StreamingChart data={transformData(props.ioInfo[innerKey], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
- </ChartContainer>
- </Grid>
- ))}
- </Grid>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid container spacing={1} className={classes.containerHeader}>
+ <div className={classes.containerHeaderText}>{gettext('Disk information')}</div>
+ </Grid>
+ <Grid container spacing={1} className={classes.container}>
+ <DiskStatsTable tableHeader={props.tableHeader} data={props.diskStats} />
+ </Grid>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6} sm={12}>
+ <ChartContainer id='ua-space-graph' title={gettext('')} datasets={[{borderColor: '#FF6384', label: 'Used space'}, {borderColor: '#36a2eb', label: 'Available space'}]} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <BarChart data={{
+ labels: props.diskStats.map((item, index) => 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,
+ },
+ },
+ }
+ }
+ />
+ </ChartContainer>
</Grid>
- )
- ))}
+ <Grid item md={6} sm={12}>
+ </Grid>
+ </Grid>
+ </Grid>
+ <Grid container spacing={1} className={classes.container}>
+ {keys.map((key, index) => (
+ index % 3 === 0 && (
+ <Grid key={`disk-${index}`} container spacing={1} className={classes.container}>
+ <Grid container spacing={1} className={classes.containerHeader}>
+ <div className={classes.containerHeaderText}>{gettext(`Disk ${Math.floor(index / 3) + 1}`)}</div>
+ </Grid>
+ <Grid container spacing={1} className={classes.ioDiskContainer}>
+ {keys.slice(index, index + 3).map((innerKey, innerKeyIndex) => (
+ <Grid key={`${innerKey}`} item md={4} sm={6}>
+ <div className={classes.chartHeader}>{innerKeyIndex==0 ? gettext('I/O operations count'): innerKeyIndex==1? gettext('Data transfer (bytes)'):gettext('Time spent in I/O operations (milliseconds)')}</div>
+ <ChartContainer id={`io-graph-${innerKey}`} title={gettext('')} datasets={transformData(props.ioInfo[innerKey], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
+ <StreamingChart data={transformData(props.ioInfo[innerKey], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
+ </ChartContainer>
+ </Grid>
+ ))}
+ </Grid>
+ </Grid>
+ )
+ ))}
+ </Grid>
</>
);
}
@@ -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) {
<>
<Grid container spacing={1} className={classes.container}>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('OS Information')}</div>
+ <div className={classes.containerHeader}>{gettext('OS information')}</div>
<SummaryTable data={props.osStats} />
</Grid>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('Handle & Process Count')}</div>
+ <div className={classes.containerHeader}>{gettext('Handle & process count')}</div>
<ChartContainer id='hpc-graph' title={gettext('')} datasets={props.processHandleCount.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.processHandleCount} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} showSecondAxis={true} />
</ChartContainer>
@@ -393,11 +395,11 @@ export function SummaryWrapper(props) {
</Grid>
<Grid container spacing={1} className={classes.container}>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('CPU Information')}</div>
+ <div className={classes.containerHeader}>{gettext('CPU information')}</div>
<SummaryTable data={props.cpuStats} />
</Grid>
<Grid item md={6} sm={12}>
- <div className={classes.containerHeader}>{gettext('Process Information')}</div>
+ <div className={classes.containerHeader}>{gettext('Process information')}</div>
<ChartContainer id='pi-graph' title={gettext('')} datasets={props.processInfoStats.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<DonutChart data={props.processInfoStats.datasets} />
</ChartContainer>
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
view thread (106+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Pgadmin4 System Stats Extension Design
In-Reply-To: <CAKi=nndiSd72JfW+2npo6r4Q_7csoK-UT37EErVAY0vi2BziJQ@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox