public inbox for [email protected]
help / color / mirror / Atom feedFrom: Sahil Harpal <[email protected]>
To: Aditya Toshniwal <[email protected]>
Cc: Ashesh Vashi <[email protected]>
Cc: Dave Page <[email protected]>
Cc: Akshay Joshi <[email protected]>
Cc: Khushboo Vashi <[email protected]>
Cc: [email protected]
Subject: Re: Pgadmin4 System Stats Extension Design
Date: Tue, 11 Jul 2023 20:45:09 +0530
Message-ID: <CAKi=nneNLFiOUPQ16ZzO1RM8K8V=qoqa=AtmewkRzLjnjJhyUA@mail.gmail.com> (raw)
In-Reply-To: <CAM9w-_=-jmhjh9Mp98pvwBxp_DDoeMexnR2Y23+=-wR+tsDOiQ@mail.gmail.com>
References: <CAKi=nneJvdbFyeDKnuQTVEnhNK0Zjdx5TWSwGUUSZO9YTC5E7Q@mail.gmail.com>
<CANxoLDfO4BE0xvGLA3EBYyAzt3hCmuRcMY_pR3Zci9nPbL7P1Q@mail.gmail.com>
<CAKi=nndnvAwgXVC=i=x3sSxNS9UCfks+EQWEjx8n5=u9CCP8og@mail.gmail.com>
<CANxoLDeNzZkxYEnLdkFMkugvk7_k+BDSt6AoH83aaMkP+VZirQ@mail.gmail.com>
<CAM9w-_mC2JKvxKKRwF7xCzoggoo0_XqBgUyqWDxXqSRQs_a0eg@mail.gmail.com>
<CAKi=nncQ+OqkJ2PfvNNs40h5PfkO57YKTMEXdYeOpxUXhTzj5A@mail.gmail.com>
<CA+OCxowriuEED8BC7DRZKwCM3dOfNaDWhf+vhJsH2PWq88yc7w@mail.gmail.com>
<CAKi=nncY7wdMyqT2tKRAVWDaFSsgUS_qnOz0aifd-BorScUNSw@mail.gmail.com>
<CA+OCxoyog3cqhZcEQ1xenmHUFMLM+VS8j91GM_RnU3VK8bGjaQ@mail.gmail.com>
<CAKi=nncGiLTK36jQWnGL-0DToo4qTqojf+iLbDvRTfk2z_48Uw@mail.gmail.com>
<CA+OCxowEmnWJHRa=QnuZpn42Cn594XZVduMPJuOg5MbdeQx0aw@mail.gmail.com>
<CAKi=nnfMHZSGP3G+gS9JSLNp2C=BHF2ztsz6W0mpBREc4y=hiQ@mail.gmail.com>
<CA+OCxowt2XSCEOxZ+Mz1CzMGQv5MCNV+GZO2b3E5PtZj1YgYOA@mail.gmail.com>
<CAKi=nndoS4g8MmOBD6wZKCtGotpmxDa17rq8-obv3B9MaSbYjg@mail.gmail.com>
<CA+OCxozjkCkVOFabHbMP+1sZ+hKftXA6Z8q00c+A2=mYHKbcuQ@mail.gmail.com>
<CAKi=nncko8TQVyGtEn_89DtZoztdhOA_e2-wSe8wROjZADgbXg@mail.gmail.com>
<CAKi=nnfj2OcTBkUoWwMZ5YifExgrxs9Pbt42-2W1+n32LEmgvQ@mail.gmail.com>
<CAG7mmow-bPFycSZeY_-=JbhmdH7qHud2GHiiZTfcNPhnsruQaA@mail.gmail.com>
<CAKi=nnfeEf6dQYcDyUExJK+1Xo5NgCHrpgfZPjS56B2rrUmd=w@mail.gmail.com>
<CAM9w-_=-jmhjh9Mp98pvwBxp_DDoeMexnR2Y23+=-wR+tsDOiQ@mail.gmail.com>
On Tue, Jul 11, 2023 at 2:29 PM Sahil Harpal <[email protected]>
wrote:
> On Tue, 11 Jul 2023 at 12:04, Ashesh Vashi <[email protected]>
> wrote:
>
>> Try preloading the extension in the PostgreSQL server.
>> e.g. Add this extension in the 'shared_preload_libraries' in
>> postgresql.conf.
>>
>> Reference: https://pgpedia.info/s/shared_preload_libraries.html
>>
> Thank you, Ashesh! I will try this and provide you with an update.
>
I tried adding 'system_stats' in the 'shared_preload_libraries', but it
doesn't work.
On Tue, 11 Jul 2023 at 14:38, Aditya Toshniwal <
[email protected]> wrote:
> Disk info doesn't need to be updated live. You can update it once on every
> tab change. I feel process info should be a bar chart. The categories are
> fixed in number and we can use StreamingChart for bar chart.
>
Okay sure.
Please ignore the previous WIP.patch file. I missed to include few
untracked changes.
I have attached the new patch file. Sorry for the inconvenience.
New patch consist:
- Single dashboard with option to toggle between General and System
Statistics
- Summary tab (except process info details).
- CPU Details
- Memory Details
Thanks,
Sahil
Attachments:
[application/octet-stream] WIP.patch (52.6K, 3-WIP.patch)
download | inline diff:
diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py
index 1dac54e74..12f491ea6 100644
--- a/web/pgadmin/dashboard/__init__.py
+++ b/web/pgadmin/dashboard/__init__.py
@@ -197,6 +197,9 @@ class DashboardModule(PgAdminModule):
'dashboard.get_prepared_by_database_id',
'dashboard.config',
'dashboard.get_config_by_server_id',
+ 'dashboard.system_statistics',
+ 'dashboard.system_statistics_sid',
+ 'dashboard.system_statistics_did',
]
@@ -536,3 +539,38 @@ def terminate_session(sid=None, did=None, pid=None):
response=gettext("Success") if res else gettext("Failed"),
status=200
)
+
+
+# System Statistics Backend
[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/CPU.jsx b/web/pgadmin/dashboard/static/js/CPU.jsx
new file mode 100644
index 000000000..9442f4754
--- /dev/null
+++ b/web/pgadmin/dashboard/static/js/CPU.jsx
@@ -0,0 +1,324 @@
+import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react';
+import PgTable from 'sources/components/PgTable';
+import gettext from 'sources/gettext';
+import PropTypes from 'prop-types';
+import { makeStyles } from '@material-ui/core/styles';
+import url_for from 'sources/url_for';
+import getApiInstance from 'sources/api_instance';
+import {getEpoch} from 'sources/utils';
+import {ChartContainer} from './Dashboard';
+import { Grid } from '@material-ui/core';
+import { DATA_POINT_SIZE } from 'sources/chartjs';
+import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
+import {useInterval} from 'sources/custom_hooks';
+import axios from 'axios';
+
+export const X_AXIS_LENGTH = 75;
+
+const useStyles = makeStyles((theme) => ({
+ autoResizer: {
+ height: '100% !important',
+ width: '100% !important',
+ background: theme.palette.grey[400],
+ padding: '7.5px',
+ overflowX: 'auto !important',
+ overflowY: 'hidden !important',
+ minHeight: '100%',
+ minWidth: '100%',
+ },
+ container: {
+ height: 'auto',
+ background: theme.palette.grey[200],
+ padding: '10px',
+ marginBottom: '30px',
+ },
+ fixedContainer: {
+ height: '577px',
+ background: theme.palette.grey[200],
+ padding: '10px',
+ marginBottom: '30px',
+ },
+ containerHeader: {
+ fontSize: '16px',
+ fontWeight: 'bold',
+ marginBottom: '5px',
+ }
+}));
+
+export function transformData(labels, refreshRate) {
+ const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39'];
+ let datasets = Object.keys(labels).map((label, i)=>{
+ return {
+ label: label,
+ data: labels[label] || [],
+ borderColor: colors[i],
+ pointHitRadius: DATA_POINT_SIZE,
+ };
+ }) || [];
+
+ return {
+ datasets: datasets,
+ refreshRate: refreshRate,
+ };
+}
+
+/* URL for fetching graphs data */
+export function getStatsUrl(sid=-1, did=-1, chart_names=[]) {
+ let base_url = url_for('dashboard.system_statistics');
+ base_url += '/' + sid;
+ base_url += (did > 0) ? ('/' + did) : '';
+ base_url += '?chart_names=' + chart_names.join(',');
+
+ return base_url;
+}
+
+/* This will process incoming charts data add it the previous charts
+ * data to get the new state.
+ */
+export function statsReducer(state, action) {
+
+ if(action.reset) {
+ return action.reset;
+ }
+
+ if(!action.incoming) {
+ return state;
+ }
+
+ if(!action.counterData) {
+ action.counterData = action.incoming;
+ }
+
+ let newState = {};
+ Object.keys(action.incoming).forEach(label => {
+ if(state[label]) {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ...state[label].slice(0, X_AXIS_LENGTH-1),
+ ];
+ } else {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ];
+ }
+ });
+ return newState;
+}
+
+const chartsDefault = {
+ 'cu_stats': {'User Normal': [], 'User Niced': [], 'Kernel': [], 'Idle': []},
+ 'la_stats': {'1 min': [], '5 mins': [], '10 mins': [], '15 mins': []},
+ 'pcu_stats': {},
+};
+
+export default function CPU({ sid, did, serverConencted, pageVisible, enablePoll=true}) {
+ const refreshOn = useRef(null);
+
+ const [cpuUsageInfo, cpuUsageInfoReduce] = useReducer(statsReducer, chartsDefault['cu_stats']);
+ const [loadAvgInfo, loadAvgInfoReduce] = useReducer(statsReducer, chartsDefault['la_stats']);
+ const [processCpuUsageStats, setProcessCpuUsageStats] = useState([]);
+
+ const [counterData, setCounterData] = useState({});
+
+ const [pollDelay, setPollDelay] = useState(5000);
+ const tableHeader = [
+ {
+ Header: 'PID',
+ accessor: 'pid',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'Name',
+ accessor: 'name',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'CPU Usage',
+ accessor: 'cpu_usage',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ ];
+ const [errorMsg, setErrorMsg] = useState(null);
+ const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
+
+ const [refreshPreferences, setRefreshPreferences] = useState({'cu_stats': 5, 'la_stats': 60, 'pcu_stats': 10});
+
+ useEffect(()=>{
+ /* Charts rendered are not visible when, the dashboard is hidden but later visible */
+ if(pageVisible && !chartDrawnOnce) {
+ setChartDrawnOnce(true);
+ }
+ }, [pageVisible]);
+
+ useInterval(()=>{
+ const currEpoch = getEpoch();
+ if(refreshOn.current === null) {
+ let tmpRef = {};
+ Object.keys(chartsDefault).forEach((name)=>{
+ tmpRef[name] = currEpoch;
+ });
+ refreshOn.current = tmpRef;
+ }
+
+ let getFor = [];
+ Object.keys(chartsDefault).forEach((name)=>{
+ if(currEpoch >= refreshOn.current[name]) {
+ getFor.push(name);
+ refreshOn.current[name] = currEpoch + refreshPreferences[name];
+ }
+ });
+
+ let path = getStatsUrl(sid, did, getFor);
+ if (!pageVisible){
+ return;
+ }
+ axios.get(path)
+ .then((resp)=>{
+ let data = resp.data;
+ console.log(data);
+ setErrorMsg(null);
+ if(data.hasOwnProperty('cu_stats')){
+ let new_cu_stats = {
+ 'User Normal': data['cu_stats']['usermode_normal_process_percent']?data['cu_stats']['usermode_normal_process_percent']:0,
+ 'User Niced': data['cu_stats']['usermode_niced_process_percent']?data['cu_stats']['usermode_niced_process_percent']:0,
+ 'Kernel': data['cu_stats']['kernelmode_process_percent']?data['cu_stats']['kernelmode_process_percent']:0,
+ 'Idle': data['cu_stats']['idle_mode_percent']?data['cu_stats']['idle_mode_percent']:0,
+ };
+ cpuUsageInfoReduce({incoming: new_cu_stats});
+ }
+
+ if(data.hasOwnProperty('la_stats')){
+ let new_la_stats = {
+ '1 min': data['la_stats']['load_avg_one_minute']?data['la_stats']['load_avg_one_minute']:0,
+ '5 mins': data['la_stats']['load_avg_five_minutes']?data['la_stats']['load_avg_five_minutes']:0,
+ '10 mins': data['la_stats']['load_avg_ten_minutes']?data['la_stats']['load_avg_ten_minutes']:0,
+ '15 mins': data['la_stats']['load_avg_fifteen_minutes']?data['la_stats']['load_avg_fifteen_minutes']:0,
+ };
+ loadAvgInfoReduce({incoming: new_la_stats});
+ }
+
+ if(data.hasOwnProperty('pcu_stats')){
+ let pcu_info_list = [];
+ const pcu_info_obj = data['pcu_stats'];
+ for (const key in pcu_info_obj) {
+ pcu_info_list.push({ icon: '', pid: pcu_info_obj[key]['pid'], name: pcu_info_obj[key]['name'], cpu_usage: pcu_info_obj[key]['cpu_usage'] });
+ }
+
+ setProcessCpuUsageStats(pcu_info_list);
+ }
+
+ setCounterData((prevCounterData)=>{
+ return {
+ ...prevCounterData,
+ ...data,
+ };
+ });
+ })
+ .catch((error)=>{
+ if(!errorMsg) {
+ cpuUsageInfoReduce({reset:chartsDefault['cu_stats']});
+ loadAvgInfoReduce({reset:chartsDefault['la_stats']});
+ setCounterData({});
+ if(error.response) {
+ if (error.response.status === 428) {
+ setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
+ } else {
+ setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
+ }
+ } else if(error.request) {
+ setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
+ return;
+ } else {
+ console.error(error);
+ }
+ }
+ });
+ }, enablePoll ? pollDelay : -1);
+
+ return (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <CPUWrapper
+ cpuUsageInfo={transformData(cpuUsageInfo, refreshPreferences['cu_stats'])}
+ loadAvgInfo={transformData(loadAvgInfo, refreshPreferences['la_stats'])}
+ processCpuUsageStats={processCpuUsageStats}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={true}
+ showDataPoints={false}
+ lineBorderWidth={1}
+ isDatabase={did > 0}
+ isTest={false}
+ />
+ }
+ </>
+ );
+}
+
+CPU.propTypes = {
+ sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ serverConnected: PropTypes.bool,
+ pageVisible: PropTypes.bool,
+ enablePoll: PropTypes.bool,
+};
+
+export function CPUWrapper(props) {
+ const classes = useStyles();
+ const options = useMemo(()=>({
+ showDataPoints: props.showDataPoints,
+ showTooltip: props.showTooltip,
+ lineBorderWidth: props.lineBorderWidth,
+ }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
+ return (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6}>
+ <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}>
+ <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/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx
index 588583eb3..5e861609e 100644
--- a/web/pgadmin/dashboard/static/js/Dashboard.jsx
+++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx
@@ -29,6 +29,9 @@ import _ from 'lodash';
import CachedOutlinedIcon from '@material-ui/icons/CachedOutlined';
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
import TabPanel from '../../../static/js/components/TabPanel';
+import Summary from './Summary';
+import CPU from './CPU';
+import Memory from './Memory';
function parseData(data) {
let res = [];
@@ -154,10 +157,14 @@ export default function Dashboard({
const [msg, setMsg] = useState('');
const [tabVal, setTabVal] = useState(0);
const [mainTabVal, setmainTabVal] = useState(0);
- const [systemStatsTabVal, setSystemStatsTabVal] = useState(0);
const [refresh, setRefresh] = useState(false);
const [activeOnly, setActiveOnly] = useState(false);
const [schemaDict, setSchemaDict] = React.useState({});
+ const [systemStatsTabVal, setSystemStatsTabVal] = useState(0);
+
+ const systemStatsTabChanged = (e, tabVal) => {
+ setSystemStatsTabVal(tabVal);
+ };
if (!did) {
tabs.push(gettext('Configuration'));
@@ -171,10 +178,6 @@ export default function Dashboard({
setmainTabVal(tabVal);
};
- const systemStatsTabChanged = (e, tabVal) => {
- setSystemStatsTabVal(tabVal);
- };
-
const serverConfigColumns = [
{
accessor: 'name',
@@ -959,8 +962,8 @@ export default function Dashboard({
</TabPanel>
{/* System Statistics */}
<TabPanel value={mainTabVal} index={1} classNameRoot={classes.tabPanel}>
- <Box height="100%" display="flex" flexDirection="column">
- <Box>
+ <Box height="100%" display="flex" flexDirection="column">
+ <Box>
<Tabs
value={systemStatsTabVal}
onChange={systemStatsTabChanged}
@@ -969,20 +972,38 @@ export default function Dashboard({
return <Tab key={tabValue} label={tabValue} />;
})}
</Tabs>
- </Box>
- <TabPanel value={systemStatsTabVal} index={0} classNameRoot={classes.tabPanel}>
- Summary
- </TabPanel>
- <TabPanel value={systemStatsTabVal} index={1} classNameRoot={classes.tabPanel}>
- CPU
- </TabPanel>
- <TabPanel value={systemStatsTabVal} index={2} classNameRoot={classes.tabPanel}>
- Memory
- </TabPanel>
- <TabPanel value={systemStatsTabVal} index={3} classNameRoot={classes.tabPanel}>
- Storage
- </TabPanel>
</Box>
+ <TabPanel value={systemStatsTabVal} index={0} classNameRoot={classes.tabPanel}>
+ <Summary
+ key={sid + did}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={1} classNameRoot={classes.tabPanel}>
+ <CPU
+ key={sid + did}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={2} classNameRoot={classes.tabPanel}>
+ <Memory
+ key={sid + did}
+ sid={sid}
+ did={did}
+ pageVisible={props.panelVisible}
+ serverConnected={props.serverConnected}
+ />
+ </TabPanel>
+ <TabPanel value={systemStatsTabVal} index={3} classNameRoot={classes.tabPanel}>
+ Storage
+ </TabPanel>
+ </Box>
</TabPanel>
</Box>
</Box>
diff --git a/web/pgadmin/dashboard/static/js/Memory.jsx b/web/pgadmin/dashboard/static/js/Memory.jsx
new file mode 100644
index 000000000..82831a976
--- /dev/null
+++ b/web/pgadmin/dashboard/static/js/Memory.jsx
@@ -0,0 +1,329 @@
+import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react';
+import PgTable from 'sources/components/PgTable';
+import gettext from 'sources/gettext';
+import PropTypes from 'prop-types';
+import { makeStyles } from '@material-ui/core/styles';
+import url_for from 'sources/url_for';
+import getApiInstance from 'sources/api_instance';
+import {getEpoch} from 'sources/utils';
+import {ChartContainer} from './Dashboard';
+import { Grid } from '@material-ui/core';
+import { DATA_POINT_SIZE } from 'sources/chartjs';
+import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
+import {useInterval} from 'sources/custom_hooks';
+import axios from 'axios';
+
+export const X_AXIS_LENGTH = 75;
+
+const useStyles = makeStyles((theme) => ({
+ autoResizer: {
+ height: '100% !important',
+ width: '100% !important',
+ background: theme.palette.grey[400],
+ padding: '7.5px',
+ overflowX: 'auto !important',
+ overflowY: 'hidden !important',
+ minHeight: '100%',
+ minWidth: '100%',
+ },
+ container: {
+ height: 'auto',
+ background: theme.palette.grey[200],
+ padding: '10px',
+ marginBottom: '30px',
+ },
+ fixedContainer: {
+ height: '577px',
+ background: theme.palette.grey[200],
+ padding: '10px',
+ marginBottom: '30px',
+ },
+ containerHeader: {
+ fontSize: '16px',
+ fontWeight: 'bold',
+ marginBottom: '5px',
+ }
+}));
+
+export function transformData(labels, refreshRate) {
+ const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39'];
+ let datasets = Object.keys(labels).map((label, i)=>{
+ return {
+ label: label,
+ data: labels[label] || [],
+ borderColor: colors[i],
+ pointHitRadius: DATA_POINT_SIZE,
+ };
+ }) || [];
+
+ return {
+ datasets: datasets,
+ refreshRate: refreshRate,
+ };
+}
+
+/* URL for fetching graphs data */
+export function getStatsUrl(sid=-1, did=-1, chart_names=[]) {
+ let base_url = url_for('dashboard.system_statistics');
+ base_url += '/' + sid;
+ base_url += (did > 0) ? ('/' + did) : '';
+ base_url += '?chart_names=' + chart_names.join(',');
+
+ return base_url;
+}
+
+/* This will process incoming charts data add it the previous charts
+ * data to get the new state.
+ */
+export function statsReducer(state, action) {
+
+ if(action.reset) {
+ return action.reset;
+ }
+
+ if(!action.incoming) {
+ return state;
+ }
+
+ if(!action.counterData) {
+ action.counterData = action.incoming;
+ }
+
+ let newState = {};
+ Object.keys(action.incoming).forEach(label => {
+ if(state[label]) {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ...state[label].slice(0, X_AXIS_LENGTH-1),
+ ];
+ } else {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ];
+ }
+ });
+ return newState;
+}
+
+const chartsDefault = {
+ 'm_stats': {'Total': [], 'Used': [], 'Free': []},
+ 'sm_stats': {'Total': [], 'Used': [], 'Free': []},
+ 'pmu_stats': {},
+};
+
+export default function Memory({ sid, did, serverConencted, pageVisible, enablePoll=true}) {
+ const refreshOn = useRef(null);
+
+ const [memoryUsageInfo, memoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['m_stats']);
+ const [swapMemoryUsageInfo, swapMemoryUsageInfoReduce] = useReducer(statsReducer, chartsDefault['sm_stats']);
+ const [processMemoryUsageStats, setProcessMemoryUsageStats] = useState([]);
+
+ const [counterData, setCounterData] = useState({});
+
+ const [pollDelay, setPollDelay] = useState(5000);
+ const tableHeader = [
+ {
+ Header: 'PID',
+ accessor: 'pid',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'Name',
+ accessor: 'name',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'Memory Usage',
+ accessor: 'memory_usage',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'Memory Bytes',
+ accessor: 'memory_bytes',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ ];
+ const [errorMsg, setErrorMsg] = useState(null);
+ const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
+
+ const [refreshPreferences, setRefreshPreferences] = useState({'m_stats': 5, 'sm_stats': 5, 'pmu_stats': 5});
+
+ useEffect(()=>{
+ /* Charts rendered are not visible when, the dashboard is hidden but later visible */
+ if(pageVisible && !chartDrawnOnce) {
+ setChartDrawnOnce(true);
+ }
+ }, [pageVisible]);
+
+ useInterval(()=>{
+ const currEpoch = getEpoch();
+ if(refreshOn.current === null) {
+ let tmpRef = {};
+ Object.keys(chartsDefault).forEach((name)=>{
+ tmpRef[name] = currEpoch;
+ });
+ refreshOn.current = tmpRef;
+ }
+
+ let getFor = [];
+ Object.keys(chartsDefault).forEach((name)=>{
+ if(currEpoch >= refreshOn.current[name]) {
+ getFor.push(name);
+ refreshOn.current[name] = currEpoch + refreshPreferences[name];
+ }
+ });
+
+ let path = getStatsUrl(sid, did, getFor);
+ if (!pageVisible){
+ return;
+ }
+ axios.get(path)
+ .then((resp)=>{
+ let data = resp.data;
+ console.log(data);
+ setErrorMsg(null);
+ if(data.hasOwnProperty('m_stats')){
+ let new_m_stats = {
+ 'Total': data['m_stats']['total_memory']?data['m_stats']['total_memory']:0,
+ 'Used': data['m_stats']['used_memory']?data['m_stats']['used_memory']:0,
+ 'Free': data['m_stats']['free_memory']?data['m_stats']['free_memory']:0,
+ };
+ memoryUsageInfoReduce({incoming: new_m_stats});
+ }
+
+ if(data.hasOwnProperty('sm_stats')){
+ let new_sm_stats = {
+ 'Total': data['sm_stats']['swap_total']?data['sm_stats']['swap_total']:0,
+ 'Used': data['sm_stats']['swap_used']?data['sm_stats']['swap_used']:0,
+ 'Free': data['sm_stats']['swap_free']?data['sm_stats']['swap_free']:0,
+ };
+ swapMemoryUsageInfoReduce({incoming: new_sm_stats});
+ }
+
+ if(data.hasOwnProperty('pmu_stats')){
+ let pmu_info_list = [];
+ const pmu_info_obj = data['pmu_stats'];
+ for (const key in pmu_info_obj) {
+ pmu_info_list.push({ icon: '', pid: pmu_info_obj[key]['pid'], name: pmu_info_obj[key]['name'], memory_usage: pmu_info_obj[key]['memory_usage'], memory_bytes: pmu_info_obj[key]['memory_bytes'] });
+ }
+
+ setProcessMemoryUsageStats(pmu_info_list);
+ }
+
+ setCounterData((prevCounterData)=>{
+ return {
+ ...prevCounterData,
+ ...data,
+ };
+ });
+ })
+ .catch((error)=>{
+ if(!errorMsg) {
+ memoryUsageInfoReduce({reset:chartsDefault['m_stats']});
+ swapMemoryUsageInfoReduce({reset:chartsDefault['sm_stats']});
+ setCounterData({});
+ if(error.response) {
+ if (error.response.status === 428) {
+ setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
+ } else {
+ setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
+ }
+ } else if(error.request) {
+ setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
+ return;
+ } else {
+ console.error(error);
+ }
+ }
+ });
+ }, enablePoll ? pollDelay : -1);
+
+ return (
+ <>
+ <div data-testid='graph-poll-delay' style={{display: 'none'}}>{pollDelay}</div>
+ {chartDrawnOnce &&
+ <MemoryWrapper
+ memoryUsageInfo={transformData(memoryUsageInfo, refreshPreferences['m_stats'])}
+ swapMemoryUsageInfo={transformData(swapMemoryUsageInfo, refreshPreferences['sm_stats'])}
+ processMemoryUsageStats={processMemoryUsageStats}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={true}
+ showDataPoints={false}
+ lineBorderWidth={1}
+ isDatabase={did > 0}
+ isTest={false}
+ />
+ }
+ </>
+ );
+}
+
+Memory.propTypes = {
+ sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ serverConnected: PropTypes.bool,
+ pageVisible: PropTypes.bool,
+ enablePoll: PropTypes.bool,
+};
+
+export function MemoryWrapper(props) {
+ const classes = useStyles();
+ const options = useMemo(()=>({
+ showDataPoints: props.showDataPoints,
+ showTooltip: props.showTooltip,
+ lineBorderWidth: props.lineBorderWidth,
+ }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
+ return (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6}>
+ <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}>
+ <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/Summary.jsx b/web/pgadmin/dashboard/static/js/Summary.jsx
new file mode 100644
index 000000000..cbfcee4db
--- /dev/null
+++ b/web/pgadmin/dashboard/static/js/Summary.jsx
@@ -0,0 +1,366 @@
+import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react';
+import gettext from 'sources/gettext';
+import PropTypes from 'prop-types';
+import { makeStyles } from '@material-ui/core/styles';
+import url_for from 'sources/url_for';
+import getApiInstance from 'sources/api_instance';
+import {getEpoch} from 'sources/utils';
+import {ChartContainer} from './Dashboard';
+import { Grid } from '@material-ui/core';
+import { DATA_POINT_SIZE } from 'sources/chartjs';
+import StreamingChart from '../../../static/js/components/PgChart/StreamingChart';
+import DonutChart from '../../../static/js/components/PgChart/DonutChart';
+import {useInterval} from 'sources/custom_hooks';
+import axios from 'axios';
+
+export const X_AXIS_LENGTH = 75;
+
+const useStyles = makeStyles((theme) => ({
+ autoResizer: {
+ height: '100% !important',
+ width: '100% !important',
+ background: theme.palette.grey[400],
+ padding: '7.5px',
+ overflowX: 'auto !important',
+ overflowY: 'hidden !important',
+ minHeight: '100%',
+ minWidth: '100%',
+ },
+ table: {
+ width: '100%',
+ backgroundColor: theme.otherVars.tableBg,
+ border: '1px solid rgb(221, 224, 230)',
+ },
+ tableVal: {
+ border: '1px solid rgb(221, 224, 230) !important',
+ padding: '10px !important',
+ },
+ container: {
+ height: 'auto',
+ background: theme.palette.grey[200],
+ padding: '10px',
+ marginBottom: '30px',
+ },
+ containerHeader: {
+ fontSize: '16px',
+ fontWeight: 'bold',
+ marginBottom: '5px',
+ },
+}));
+
+export function transformData(labels, refreshRate) {
+ const colors = ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#8D6E63','#2196F3','#FFEB3B','#9C27B0','#00BCD4','#CDDC39'];
+ let datasets = Object.keys(labels).map((label, i)=>{
+ return {
+ label: label,
+ data: labels[label] || [],
+ borderColor: colors[i],
+ pointHitRadius: DATA_POINT_SIZE,
+ };
+ }) || [];
+
+ return {
+ datasets: datasets,
+ refreshRate: refreshRate,
+ };
+}
+
+/* URL for fetching graphs data */
+export function getStatsUrl(sid=-1, did=-1, chart_names=[]) {
+ let base_url = url_for('dashboard.system_statistics');
+ base_url += '/' + sid;
+ base_url += (did > 0) ? ('/' + did) : '';
+ base_url += '?chart_names=' + chart_names.join(',');
+
+ return base_url;
+}
+
+/* This will process incoming charts data add it the previous charts
+ * data to get the new state.
+ */
+export function statsReducer(state, action) {
+
+ if(action.reset) {
+ return action.reset;
+ }
+
+ if(!action.incoming) {
+ return state;
+ }
+
+ if(!action.counterData) {
+ action.counterData = action.incoming;
+ }
+
+ let newState = {};
+ Object.keys(action.incoming).forEach(label => {
+ if(state[label]) {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ...state[label].slice(0, X_AXIS_LENGTH-1),
+ ];
+ } else {
+ newState[label] = [
+ action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label],
+ ];
+ }
+ });
+ return newState;
+}
+
+const chartsDefault = {
+ 'hpc_stats': {'Handle': [], 'Process': []},
+};
+
+const SummaryTable = ({ data }) => {
+ const classes = useStyles();
+ return (
+ <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>
+ );
+}
+
+export default function Summary({ sid, did, serverConencted, pageVisible, enablePoll=true}) {
+ const refreshOn = useRef(null);
+
+ const [processHandleCount, processHandleCountReduce] = useReducer(statsReducer, chartsDefault['hpc_stats']);
+ const [osStats, setOsStats] = useState([]);
+ const [cpuStats, setCpuStats] = useState([]);
+ const [processInfoStats, setProcessInfoStats] = useState({'Running': 4, 'Sleeping': 2, 'Stopped': 1, 'Zombie': 2});
+
+ const [counterData, setCounterData] = useState({});
+
+ const [pollDelay, setPollDelay] = useState(5000);
+ const [longPollDelay, setLongPollDelay] = useState(180000);
+ const [errorMsg, setErrorMsg] = useState(null);
+
+ const tableHeader = [
+ {
+ Header: 'Property',
+ accessor: 'name',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ {
+ Header: 'Value',
+ accessor: 'value',
+ sortable: true,
+ resizable: true,
+ disableGlobalFilter: false,
+ },
+ ];
+
+ useEffect(() => {
+ try {
+ // Fetch the latest data point from the API endpoint
+ let url;
+ url = url_for('dashboard.system_statistics');
+ url += '/' + sid;
+ url += did > 0 ? '/' + did : '';
+ url += '?chart_names=' + 'pg_sys_os_info,pg_sys_cpu_info';
+ const api = getApiInstance();
+ api({
+ url: url,
+ type: 'GET',
+ })
+ .then((res) => {
+ let data = res.data;
+
+ const os_info_obj = data['pg_sys_os_info'];
+ let os_info_list = [
+ { icon: '', name: 'Name', value: os_info_obj['name'] },
+ { icon: '', name: 'Version', value: os_info_obj['version'] },
+ { icon: '', name: 'Host name', value: os_info_obj['host_name'] },
+ { icon: '', name: 'Domain name', value: os_info_obj['domain_name'] },
+ { icon: '', name: 'Architecture', value: os_info_obj['architecture'] },
+ { icon: '', name: 'Os up since seconds', value: os_info_obj['os_up_since_seconds'] },
+ ];
+ setOsStats(os_info_list);
+
+ const cpu_info_obj = data['pg_sys_cpu_info'];
+ let cpu_info_list = [
+ { icon: '', name: 'Vendor', value: cpu_info_obj['vendor'] },
+ { icon: '', name: 'Description', value: cpu_info_obj['description'] },
+ { icon: '', name: 'Model name', value: cpu_info_obj['model_name'] },
+ { icon: '', name: 'No of cores', value: cpu_info_obj['no_of_cores'] },
+ { icon: '', name: 'Architecture', value: cpu_info_obj['architecture'] },
+ { icon: '', name: 'Clock speed Hz', value: cpu_info_obj['clock_speed_hz'] },
+ { icon: '', name: 'L1 dcache size', value: cpu_info_obj['l1dcache_size'] },
+ { icon: '', name: 'L1 icache size', value: cpu_info_obj['l1icache_size'] },
+ { icon: '', name: 'L2 cache size', value: cpu_info_obj['l2cache_size'] },
+ { icon: '', name: 'L3 cache size', value: cpu_info_obj['l3cache_size'] },
+ ];
+ setCpuStats(cpu_info_list);
+
+ setErrorMsg(null);
+ })
+ .catch((error) => {
+ console.error('Error fetching data:', error);
+ });
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ }
+ }, [sid, did, enablePoll, pageVisible]);
+
+ useInterval(()=>{
+ const currEpoch = getEpoch();
+ if(refreshOn.current === null) {
+ let tmpRef = {};
+ Object.keys(chartsDefault).forEach((name)=>{
+ tmpRef[name] = currEpoch;
+ });
+ refreshOn.current = tmpRef;
+ }
+
+ let getFor = [];
+ Object.keys(chartsDefault).forEach((name)=>{
+ if(currEpoch >= refreshOn.current[name]) {
+ getFor.push(name);
+ refreshOn.current[name] = currEpoch + 5;
+ }
+ });
+
+ let path = getStatsUrl(sid, did, getFor);
+ if (!pageVisible){
+ return;
+ }
+ axios.get(path)
+ .then((resp)=>{
+ let data = resp.data;
+ setErrorMsg(null);
+ processHandleCountReduce({incoming: data['hpc_stats']});
+
+ setCounterData((prevCounterData)=>{
+ return {
+ ...prevCounterData,
+ ...data,
+ };
+ });
+ })
+ .catch((error)=>{
+ if(!errorMsg) {
+ processHandleCountReduce({reset:chartsDefault['hpc_stats']});
+ setCounterData({});
+ if(error.response) {
+ if (error.response.status === 428) {
+ setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
+ } else {
+ setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
+ }
+ } else if(error.request) {
+ setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
+ return;
+ } else {
+ console.error(error);
+ }
+ }
+ });
+ }, enablePoll ? pollDelay : -1);
+
+ useInterval(()=>{
+ let url;
+ url = url_for('dashboard.system_statistics');
+ url += '/' + sid;
+ url += did > 0 ? '/' + did : '';
+ url += '?chart_names=' + 'pi_stats';
+ // axios.get(url)
+ // .then((resp)=>{
+ // let data = resp.data;
+ // console.log("pi data: ", data);
+ // })
+ // .catch((error)=>{
+ // if(!errorMsg) {
+ // if(error.response) {
+ // if (error.response.status === 428) {
+ // setErrorMsg(gettext('Please connect to the selected server to view the graph.'));
+ // } else {
+ // setErrorMsg(gettext('An error occurred whilst rendering the graph.'));
+ // }
+ // } else if(error.request) {
+ // setErrorMsg(gettext('Not connected to the server or the connection to the server has been closed.'));
+ // return;
+ // } else {
+ // console.error(error);
+ // }
+ // }
+ // });
+ }, enablePoll ? longPollDelay : -1);
+
+ return (
+ <>
+ <SummaryWrapper
+ processHandleCount={transformData(processHandleCount, 5)}
+ osStats={osStats}
+ cpuStats={cpuStats}
+ processInfoStats={transformData(processInfoStats, 5)}
+ tableHeader={tableHeader}
+ errorMsg={errorMsg}
+ showTooltip={true}
+ showDataPoints={false}
+ lineBorderWidth={1}
+ isDatabase={did > 0}
+ isTest={false}
+ />
+ </>
+ );
+}
+
+Summary.propTypes = {
+ sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
+ serverConnected: PropTypes.bool,
+ pageVisible: PropTypes.bool,
+ enablePoll: PropTypes.bool,
+};
+
+export function SummaryWrapper(props) {
+ const classes = useStyles();
+ const options = useMemo(()=>({
+ showDataPoints: props.showDataPoints,
+ showTooltip: props.showTooltip,
+ lineBorderWidth: props.lineBorderWidth,
+ }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]);
+ return (
+ <>
+ <Grid container spacing={1} className={classes.container}>
+ <Grid item md={6}>
+ <div className={classes.containerHeader}>{gettext('OS Information')}</div>
+ <SummaryTable data={props.osStats} />
+ </Grid>
+ <Grid item md={6}>
+ <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}>
+ <div className={classes.containerHeader}>{gettext('CPU Information')}</div>
+ <SummaryTable data={props.cpuStats} />
+ </Grid>
+ <Grid item md={6}>
+ <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>
+ </>
+ );
+}
\ No newline at end of file
diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql
new file mode 100644
index 000000000..d1fced433
--- /dev/null
+++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/system_statistics.sql
@@ -0,0 +1,87 @@
+{% set add_union = false %}
+{% if 'pg_sys_os_info' in chart_names %}
+{% set add_union = true %}
+ SELECT 'pg_sys_os_info' AS chart_name, pg_catalog.row_to_json(t) AS chart_data
+ FROM (SELECT * FROM pg_sys_os_info()) t
+{% endif %}
+{% if add_union and 'pg_sys_cpu_info' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'pg_sys_cpu_info' in chart_names %}
+{% set add_union = true %}
+ SELECT 'pg_sys_cpu_info' AS chart_name, pg_catalog.row_to_json(t) AS chart_data
+ FROM (SELECT * FROM pg_sys_cpu_info()) t
+{% endif %}
+{% if add_union and 'hpc_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'hpc_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'hpc_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data
+ FROM (SELECT
+ (SELECT handle_count FROM pg_sys_os_info()) AS "{{ _('Handle') }}",
+ (SELECT process_count FROM pg_sys_os_info()) AS "{{ _('Process') }}"
+ ) t
+{% endif %}
+{% if add_union and 'cu_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'cu_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'cu_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data
+ FROM (SELECT * FROM pg_sys_cpu_usage_info()) t
+{% endif %}
+{% if add_union and 'la_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'la_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'la_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT * FROM pg_sys_load_avg_info()) t
+{% endif %}
+{% if add_union and 'pcu_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'pcu_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'pcu_stats' AS chart_name, (
+ SELECT to_json(pg_catalog.jsonb_object_agg('process'||row_number, pg_catalog.row_to_json(t)))
+ FROM (
+ SELECT pid, name, cpu_usage, ROW_NUMBER() OVER (ORDER BY pid) AS row_number
+ FROM pg_sys_cpu_memory_by_process()
+ ) t
+ ) AS chart_data
+{% endif %}
+{% if add_union and 'm_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'm_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'm_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT total_memory, used_memory, free_memory FROM pg_sys_memory_info()) t
+{% endif %}
+{% if add_union and 'sm_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'sm_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'sm_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT swap_total, swap_used, swap_free FROM pg_sys_memory_info()) t
+{% endif %}
+{% if add_union and 'pmu_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'pmu_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'pmu_stats' AS chart_name, (
+ SELECT to_json(pg_catalog.jsonb_object_agg('process'||row_number, pg_catalog.row_to_json(t)))
+ FROM (
+ SELECT pid, name, memory_usage, memory_bytes, ROW_NUMBER() OVER (ORDER BY pid) AS row_number
+ FROM pg_sys_cpu_memory_by_process()
+ ) t
+ ) AS chart_data
+{% endif %}
+{% if add_union and 'pi_stats' in chart_names %}
+ UNION ALL
+{% endif %}
+{% if 'pi_stats' in chart_names %}
+{% set add_union = true %}
+ SELECT 'pi_stats' AS chart_name, pg_catalog.row_to_json(t) AS chart_data FROM (SELECT * FROM pg_sys_process_info()) t
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/static/js/components/PgChart/DonutChart.jsx b/web/pgadmin/static/js/components/PgChart/DonutChart.jsx
new file mode 100644
index 000000000..9c917cc20
--- /dev/null
+++ b/web/pgadmin/static/js/components/PgChart/DonutChart.jsx
@@ -0,0 +1,68 @@
+import React, { useEffect, useRef } from 'react';
+import Chart from 'chart.js/auto';
+import { useResizeDetector } from 'react-resize-detector';
+
+const DonutChart = ({ data }) => {
+ const chartRef = useRef(null);
+ const chartInstance = useRef(null);
+
+ useEffect(() => {
+ if (data && Object.keys(data).length > 0) {
+ if (chartInstance.current) {
+ // If chart instance exists, update the data
+ chartInstance.current.data.labels = data.map((item) => item.label);
+ chartInstance.current.data.datasets[0].data = data.map((item) => item.data);
+ chartInstance.current.update();
+ } else {
+ // If chart instance doesn't exist, create a new chart
+ const chartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false, // Hide the labels at the top
+ },
+ },
+ animation: {
+ duration: 0, // Disable the animation
+ },
+ tooltips: {
+ callbacks: {
+ label: function (tooltipItem, chartData) {
+ const dataset = chartData.datasets[tooltipItem.datasetIndex];
+ const total = dataset.data.reduce((previousValue, currentValue) => previousValue + currentValue);
+ const currentValue = dataset.data[tooltipItem.index];
+ const percentage = ((currentValue / total) * 100).toFixed(2) + '%';
+ return dataset.label + ': ' + currentValue + ' (' + percentage + ')';
+ },
+ },
+ },
+ };
+
+ const chartData = {
+ labels: data.map((item) => item.label),
+ datasets: [
+ {
+ data: data.map((item) => item.data),
+ backgroundColor: data.map((item) => item.borderColor),
+ hoverBackgroundColor: data.map((item) => item.borderColor),
+ },
+ ],
+ };
+
+ const ctx = chartRef.current.getContext('2d');
+ chartInstance.current = new Chart(ctx, {
+ type: 'doughnut',
+ data: chartData,
+ options: chartOptions,
+ });
+ }
+ }
+ }, [data]);
+
+ return (
+ <canvas ref={chartRef} />
+ );
+};
+
+export default DonutChart;
diff --git a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx
index bd465e3da..eba301f05 100644
--- a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx
+++ b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx
@@ -58,44 +58,89 @@ function tooltipPlugin(refreshRate) {
};
}
-export default function StreamingChart({xRange=75, data, options}) {
+export default function StreamingChart({xRange=75, data, options, showSecondAxis=false}) {
const chartRef = useRef();
const theme = useTheme();
const { width, height, ref:containerRef } = useResizeDetector();
- const defaultOptions = useMemo(()=>({
- title: '',
- width: width,
- height: height,
- padding: [10, 0, 10, 0],
- focus: {
- alpha: 0.3,
- },
- cursor: {
- y: false,
- drag: {
- setScale: false,
- }
- },
- series: [
+ const defaultOptions = useMemo(()=> {
+ const series = [
{},
- ...(data.datasets?.map((datum)=>({
+ ...(data.datasets?.map((datum, index) => ({
label: datum.label,
stroke: datum.borderColor,
width: options.lineBorderWidth ?? 1,
- points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius*2 }
- }))??{})
- ],
- scales: {
- x: {
- time: false,
- }
- },
- axes: [
+ scale: showSecondAxis && (index === 1) ? 'y1' : 'y',
+ points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius * 2 },
+ })) ?? []),
+ ];
+
+ const axes = [
{
show: false,
stroke: theme.palette.text.primary,
},
- {
+ ];
+
+ if(showSecondAxis){
+ axes.push({
+ scale: 'y',
+ grid: {
+ stroke: theme.otherVars.borderColor,
+ width: 0.5,
+ },
+ stroke: theme.palette.text.primary,
+ size: function(_obj, values) {
+ let size = 40;
+ if(values?.length > 0) {
+ size = values[values.length-1].length*12;
+ if(size < 40) size = 40;
+ }
+ return size;
+ },
+ // y-axis configuration
+ values: (self, ticks) => {
+ // Format the label
+ return ticks.map((value) => {
+ if(value < 1){
+ return value+"";
+ }
+ const suffixes = ["", "k", "M", "B", "T"];
+ const suffixNum = Math.floor(Math.log10(value) / 3);
+ const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1);
+ return shortValue + suffixes[suffixNum];
+ });
+ }
+ });
+ axes.push({
+ scale: 'y1',
+ side: 1,
+ stroke: theme.palette.text.primary,
+ grid: {show: false},
+ size: function(_obj, values) {
+ let size = 40;
+ if(values?.length > 0) {
+ size = values[values.length-1].length*12;
+ if(size < 40) size = 40;
+ }
+ return size;
+ },
+ // y-axis configuration
+ values: (self, ticks) => {
+ // Format the label
+ return ticks.map((value) => {
+ if(value < 1){
+ return value+"";
+ }
+ const suffixes = ["", "k", "M", "B", "T"];
+ const suffixNum = Math.floor(Math.log10(value) / 3);
+ const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1);
+ return shortValue + suffixes[suffixNum];
+ });
+ }
+ });
+ } else{
+ axes.push({
+ scale: 'y',
grid: {
stroke: theme.otherVars.borderColor,
width: 0.5,
@@ -108,11 +153,47 @@ export default function StreamingChart({xRange=75, data, options}) {
if(size < 40) size = 40;
}
return size;
+ },
+ // y-axis configuration
+ values: (self, ticks) => {
+ // Format the label
+ return ticks.map((value) => {
+ if(value < 1){
+ return value+"";
+ }
+ const suffixes = ["", "k", "M", "B", "T"];
+ const suffixNum = Math.floor(Math.log10(value) / 3);
+ const shortValue = (value / Math.pow(1000, suffixNum)).toFixed(1);
+ return shortValue + suffixes[suffixNum];
+ });
+ }
+ });
+ }
+
+ return {
+ title: '',
+ width: width,
+ height: height,
+ padding: [10, 0, 10, 0],
+ focus: {
+ alpha: 0.3,
+ },
+ cursor: {
+ y: false,
+ drag: {
+ setScale: false,
+ }
+ },
+ series: series,
+ scales: {
+ x: {
+ time: false,
}
- }
- ],
- plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [],
- }), [data.refreshRate, data?.datasets?.length, width, height, options]);
+ },
+ axes: axes,
+ plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [],
+ };
+ }, [data.refreshRate, data?.datasets?.length, width, height, options]);
const initialState = [
Array.from(new Array(xRange).keys()),
@@ -140,4 +221,5 @@ StreamingChart.propTypes = {
xRange: PropTypes.number.isRequired,
data: propTypeData.isRequired,
options: PropTypes.object,
+ showSecondAxis: PropTypes.bool,
};
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=nneNLFiOUPQ16ZzO1RM8K8V=qoqa=AtmewkRzLjnjJhyUA@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