mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-22 16:56:30 -06:00
GUI representation of the system's activity using the system_stats extension. #6797
This commit is contained in:
parent
bae912fa40
commit
16c95d21a7
@ -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.cpu_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'cpu_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.pcpu_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'pcpu_stats_refresh',
|
||||
gettext("CPU usage per process statistics refresh rate"),
|
||||
'integer', 5, min_val=1, max_val=999999,
|
||||
category_label=PREF_LABEL_REFRESH_RATES,
|
||||
help_str=help_string
|
||||
)
|
||||
|
||||
self.m_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'm_stats_refresh',
|
||||
gettext("Memory usage statistics refresh rate"), 'integer',
|
||||
5, min_val=1, max_val=999999,
|
||||
category_label=PREF_LABEL_REFRESH_RATES,
|
||||
help_str=help_string
|
||||
)
|
||||
|
||||
self.sm_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'sm_stats_refresh',
|
||||
gettext("Swap memory usage statistics refresh rate"), 'integer',
|
||||
5, min_val=1, max_val=999999,
|
||||
category_label=PREF_LABEL_REFRESH_RATES,
|
||||
help_str=help_string
|
||||
)
|
||||
|
||||
self.pmu_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'pmu_stats_refresh',
|
||||
gettext("Memory usage per process statistics refresh rate"),
|
||||
'integer', 5, min_val=1, max_val=999999,
|
||||
category_label=PREF_LABEL_REFRESH_RATES,
|
||||
help_str=help_string
|
||||
)
|
||||
|
||||
self.io_stats_refresh = self.dashboard_preference.register(
|
||||
'dashboards', 'io_stats_refresh',
|
||||
gettext("I/O analysis statistics refresh rate"), 'integer',
|
||||
5, min_val=1, max_val=999999,
|
||||
category_label=PREF_LABEL_REFRESH_RATES,
|
||||
help_str=help_string
|
||||
)
|
||||
|
||||
self.display_graphs = self.dashboard_preference.register(
|
||||
'display', 'show_graphs',
|
||||
gettext("Show graphs?"), 'boolean', True,
|
||||
@ -197,6 +263,12 @@ class DashboardModule(PgAdminModule):
|
||||
'dashboard.get_prepared_by_database_id',
|
||||
'dashboard.config',
|
||||
'dashboard.get_config_by_server_id',
|
||||
'dashboard.check_system_statistics',
|
||||
'dashboard.check_system_statistics_sid',
|
||||
'dashboard.check_system_statistics_did',
|
||||
'dashboard.system_statistics',
|
||||
'dashboard.system_statistics_sid',
|
||||
'dashboard.system_statistics_did',
|
||||
]
|
||||
|
||||
|
||||
@ -536,3 +608,62 @@ def terminate_session(sid=None, did=None, pid=None):
|
||||
response=gettext("Success") if res else gettext("Failed"),
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
# To check whether system stats extesion is present or not
|
||||
@blueprint.route('check_extension/system_statistics',
|
||||
endpoint='check_system_statistics', methods=['GET'])
|
||||
@blueprint.route('check_extension/system_statistics/<int:sid>',
|
||||
endpoint='check_system_statistics_sid', methods=['GET'])
|
||||
@blueprint.route('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
|
||||
@blueprint.route('/system_statistics',
|
||||
endpoint='system_statistics', methods=['GET'])
|
||||
@blueprint.route('/system_statistics/<int:sid>',
|
||||
endpoint='system_statistics_sid', methods=['GET'])
|
||||
@blueprint.route('/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
|
||||
)
|
||||
|
@ -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,
|
||||
ssExtensionCheckUrl = 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) ssExtensionCheckUrl += '/' + sid + '/' + did;
|
||||
else ssExtensionCheckUrl += '/' + sid;
|
||||
|
||||
const api = getApiInstance();
|
||||
if (node) {
|
||||
api({
|
||||
@ -787,6 +809,22 @@ export default function Dashboard({
|
||||
// show failed message.
|
||||
setMsg(gettext('Failed to retrieve data from the server.'));
|
||||
});
|
||||
|
||||
api({
|
||||
url: ssExtensionCheckUrl,
|
||||
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.'));
|
||||
} else {
|
||||
setSsMsg(gettext(''));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSsMsg(gettext('Failed to verify the presence of system stats extension.'));
|
||||
});
|
||||
} else {
|
||||
setMsg(message);
|
||||
}
|
||||
@ -867,68 +905,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} />;
|
||||
})}
|
||||
</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}
|
||||
systemStatsTabVal={systemStatsTabVal}
|
||||
/>
|
||||
</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() }
|
||||
|
308
web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx
Normal file
308
web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx
Normal file
@ -0,0 +1,308 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 {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';
|
||||
import { getStatsUrl, transformData, statsReducer, X_AXIS_LENGTH } from './utility.js';
|
||||
import { toPrettySize } from '../../../../static/js/utils';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
autoResizer: {
|
||||
height: '100% !important',
|
||||
width: '100% !important',
|
||||
background: theme.palette.grey[400],
|
||||
padding: '8px',
|
||||
overflowX: 'auto !important',
|
||||
overflowY: 'hidden !important',
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
},
|
||||
container: {
|
||||
height: 'auto',
|
||||
padding: '0px !important',
|
||||
marginBottom: '30px',
|
||||
},
|
||||
fixedContainer: {
|
||||
height: '577px',
|
||||
padding: '0px !important',
|
||||
marginBottom: '30px',
|
||||
},
|
||||
tableContainer: {
|
||||
padding: '6px',
|
||||
width: '100%',
|
||||
},
|
||||
containerHeader: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '5px',
|
||||
},
|
||||
}));
|
||||
|
||||
const chartsDefault = {
|
||||
'cpu_stats': {'User Normal': [], 'User Niced': [], 'Kernel': [], 'Idle': []},
|
||||
'la_stats': {'1 min': [], '5 mins': [], '10 mins': [], '15 mins': []},
|
||||
'pcpu_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['cpu_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: gettext('PID'),
|
||||
accessor: 'pid',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Name'),
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('CPU Usage'),
|
||||
accessor: 'cpu_usage',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(()=>{
|
||||
let calcPollDelay = false;
|
||||
if(prevPrefernces) {
|
||||
if(prevPrefernces['cpu_stats_refresh'] != preferences['cpu_stats_refresh']) {
|
||||
cpuUsageInfoReduce({reset: chartsDefault['cpu_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['la_stats_refresh'] != preferences['la_stats_refresh']) {
|
||||
loadAvgInfoReduce({reset: chartsDefault['la_stats']});
|
||||
calcPollDelay = true;
|
||||
}
|
||||
if(prevPrefernces['pcpu_stats_refresh'] != preferences['pcpu_stats_refresh']) {
|
||||
setProcessCpuUsageStats([]);
|
||||
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('cpu_stats')){
|
||||
let new_cu_stats = {
|
||||
'User Normal': data['cpu_stats']['usermode_normal_process_percent']?data['cpu_stats']['usermode_normal_process_percent']:0,
|
||||
'User Niced': data['cpu_stats']['usermode_niced_process_percent']?data['cpu_stats']['usermode_niced_process_percent']:0,
|
||||
'Kernel': data['cpu_stats']['kernelmode_process_percent']?data['cpu_stats']['kernelmode_process_percent']:0,
|
||||
'Idle': data['cpu_stats']['idle_mode_percent']?data['cpu_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('pcpu_stats')){
|
||||
let pcu_info_list = [];
|
||||
const pcu_info_obj = data['pcpu_stats'];
|
||||
for (const key in pcu_info_obj) {
|
||||
pcu_info_list.push({ icon: '', pid: pcu_info_obj[key]['pid'], name: gettext(pcu_info_obj[key]['name']), cpu_usage: gettext(toPrettySize(pcu_info_obj[key]['cpu_usage'])) });
|
||||
}
|
||||
|
||||
setProcessCpuUsageStats(pcu_info_list);
|
||||
}
|
||||
|
||||
setCounterData((prevCounterData)=>{
|
||||
return {
|
||||
...prevCounterData,
|
||||
...data,
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error)=>{
|
||||
if(!errorMsg) {
|
||||
cpuUsageInfoReduce({reset:chartsDefault['cpu_stats']});
|
||||
loadAvgInfoReduce({reset:chartsDefault['la_stats']});
|
||||
setProcessCpuUsageStats([]);
|
||||
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['cpu_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}>
|
||||
<ChartContainer id='cu-graph' title={gettext('CPU usage')} 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}>
|
||||
<ChartContainer id='la-graph' title={gettext('Load average')} 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}>
|
||||
<div className={classes.tableContainer}>
|
||||
<PgTable
|
||||
className={classes.autoResizer}
|
||||
columns={props.tableHeader}
|
||||
data={props.processCpuUsageStats}
|
||||
msg={props.errorMsg}
|
||||
type={'panel'}
|
||||
caveTable={false}
|
||||
></PgTable>
|
||||
</div>
|
||||
</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,
|
||||
};
|
303
web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx
Normal file
303
web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx
Normal file
@ -0,0 +1,303 @@
|
||||
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 {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';
|
||||
import { getStatsUrl, transformData, statsReducer, X_AXIS_LENGTH } from './utility.js';
|
||||
import { toPrettySize } from '../../../../static/js/utils';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
autoResizer: {
|
||||
height: '100% !important',
|
||||
width: '100% !important',
|
||||
background: theme.palette.grey[400],
|
||||
padding: '8px',
|
||||
overflowX: 'auto !important',
|
||||
overflowY: 'hidden !important',
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
},
|
||||
container: {
|
||||
height: 'auto',
|
||||
padding: '0px !important',
|
||||
marginBottom: '30px',
|
||||
},
|
||||
fixedContainer: {
|
||||
height: '577px',
|
||||
padding: '0px !important',
|
||||
marginBottom: '30px',
|
||||
},
|
||||
tableContainer: {
|
||||
padding: '6px',
|
||||
width: '100%',
|
||||
},
|
||||
containerHeader: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '5px',
|
||||
}
|
||||
}));
|
||||
|
||||
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: gettext('PID'),
|
||||
accessor: 'pid',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Name'),
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Memory Usage'),
|
||||
accessor: 'memory_usage',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('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([]);
|
||||
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: gettext(pmu_info_obj[key]['name']), memory_usage: gettext(toPrettySize(pmu_info_obj[key]['memory_usage'])), memory_bytes: gettext(toPrettySize(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']});
|
||||
setProcessMemoryUsageStats([]);
|
||||
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}>
|
||||
<ChartContainer id='m-graph' title={gettext('Memory')} 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}>
|
||||
<ChartContainer id='sm-graph' title={gettext('Swap memory')} 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}>
|
||||
<div className={classes.tableContainer}>
|
||||
<PgTable
|
||||
className={classes.autoResizer}
|
||||
columns={props.tableHeader}
|
||||
data={props.processMemoryUsageStats}
|
||||
msg={props.errorMsg}
|
||||
type={'panel'}
|
||||
caveTable={false}
|
||||
></PgTable>
|
||||
</div>
|
||||
</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,
|
||||
};
|
551
web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx
Normal file
551
web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx
Normal file
@ -0,0 +1,551 @@
|
||||
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';
|
||||
import { BarChart, PieChart } from '../../../../static/js/chartjs';
|
||||
import { getStatsUrl, transformData, X_AXIS_LENGTH } from './utility.js';
|
||||
import { toPrettySize } from '../../../../static/js/utils';
|
||||
import clsx from 'clsx';
|
||||
import { commonTableStyles } from '../../../../static/js/Theme';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
height: 'auto',
|
||||
padding: '8px',
|
||||
marginBottom: '15px',
|
||||
},
|
||||
diskInfoContainer: {
|
||||
height: 'auto',
|
||||
padding: '8px',
|
||||
marginBottom: '15px',
|
||||
},
|
||||
diskInfoSummary: {
|
||||
height: 'auto',
|
||||
padding: '0px',
|
||||
marginBottom: '5px',
|
||||
},
|
||||
containerHeaderText: {
|
||||
fontWeight: 'bold',
|
||||
padding: '4px 8px',
|
||||
},
|
||||
tableContainer: {
|
||||
background: theme.otherVars.tableBg,
|
||||
padding: '0px',
|
||||
border: '1px solid '+theme.otherVars.borderColor,
|
||||
borderCollapse: 'collapse',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
margin: '4px 4px 15px 4px',
|
||||
},
|
||||
tableWhiteSpace: {
|
||||
'& td, & th': {
|
||||
whiteSpace: 'break-spaces !important',
|
||||
},
|
||||
},
|
||||
driveContainerHeader: {
|
||||
height: 'auto',
|
||||
padding: '5px 0px 0px 0px',
|
||||
background: theme.otherVars.tableBg,
|
||||
marginBottom: '5px',
|
||||
borderRadius: '4px 4px 0px 0px',
|
||||
},
|
||||
driveContainerBody: {
|
||||
height: 'auto',
|
||||
padding: '0px',
|
||||
background: theme.otherVars.tableBg,
|
||||
borderRadius: '0px 0px 4px 4px',
|
||||
},
|
||||
}));
|
||||
|
||||
const colors = [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#8D6E63', '#2196F3', '#FFEB3B', '#9C27B0',
|
||||
'#00BCD4', '#CDDC39', '#FF5722', '#3F51B5', '#FFC107',
|
||||
'#607D8B', '#E91E63', '#009688', '#795548', '#FF9800'
|
||||
];
|
||||
|
||||
/* 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(type => {
|
||||
newState[disk_stats][type] = {};
|
||||
Object.keys(action.incoming[disk_stats][type]).forEach(label => {
|
||||
if(state[disk_stats][type][label]) {
|
||||
newState[disk_stats][type][label] = [
|
||||
action.counter ? action.incoming[disk_stats][type][label] - action.counterData[disk_stats][type][label] : action.incoming[disk_stats][type][label],
|
||||
...state[disk_stats][type][label].slice(0, X_AXIS_LENGTH-1),
|
||||
];
|
||||
} else {
|
||||
newState[disk_stats][type][label] = [
|
||||
action.counter ? action.incoming[disk_stats][type][label] - action.counterData[disk_stats][type][label] : action.incoming[disk_stats][type][label],
|
||||
];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
const chartsDefault = {
|
||||
'io_stats': {},
|
||||
};
|
||||
|
||||
|
||||
const DiskStatsTable = (props) => {
|
||||
const tableClasses = commonTableStyles();
|
||||
const classes = useStyles();
|
||||
const tableHeader = props.tableHeader;
|
||||
const data = props.data;
|
||||
return (
|
||||
<table className={clsx(tableClasses.table, classes.tableWhiteSpace)}>
|
||||
<thead>
|
||||
<tr>
|
||||
{tableHeader.map((item, index) => (
|
||||
<th key={index}>{item.Header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index}>
|
||||
{tableHeader.map((header, id) => (
|
||||
<td 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({});
|
||||
|
||||
const [pollDelay, setPollDelay] = useState(5000);
|
||||
const [errorMsg, setErrorMsg] = useState(null);
|
||||
const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
|
||||
|
||||
const tableHeader = [
|
||||
{
|
||||
Header: gettext('File system'),
|
||||
accessor: 'file_system',
|
||||
},
|
||||
{
|
||||
Header: gettext('File system type'),
|
||||
accessor: 'file_system_type',
|
||||
},
|
||||
{
|
||||
Header: gettext('Mount point'),
|
||||
accessor: 'mount_point',
|
||||
},
|
||||
{
|
||||
Header: gettext('Drive letter'),
|
||||
accessor: 'drive_letter',
|
||||
},
|
||||
{
|
||||
Header: gettext('Total space'),
|
||||
accessor: 'total_space',
|
||||
},
|
||||
{
|
||||
Header: gettext('Used space'),
|
||||
accessor: 'used_space',
|
||||
},
|
||||
{
|
||||
Header: gettext('Free space'),
|
||||
accessor: 'free_space',
|
||||
},
|
||||
{
|
||||
Header: gettext('Total inodes'),
|
||||
accessor: 'total_inodes',
|
||||
},
|
||||
{
|
||||
Header: gettext('Used inodes'),
|
||||
accessor: 'used_inodes',
|
||||
},
|
||||
{
|
||||
Header: gettext('Free inodes'),
|
||||
accessor: 'free_inodes',
|
||||
},
|
||||
];
|
||||
|
||||
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]);
|
||||
|
||||
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']?gettext(di_info_obj[key]['file_system']):'',
|
||||
file_system_type: di_info_obj[key]['file_system_type']?gettext(di_info_obj[key]['file_system_type']):'',
|
||||
mount_point: di_info_obj[key]['mount_point']?gettext(di_info_obj[key]['mount_point']):'',
|
||||
drive_letter: di_info_obj[key]['drive_letter']?gettext(di_info_obj[key]['drive_letter']):'',
|
||||
total_space: di_info_obj[key]['total_space']?toPrettySize(di_info_obj[key]['total_space']):'',
|
||||
used_space: di_info_obj[key]['used_space']?toPrettySize(di_info_obj[key]['used_space']):'',
|
||||
free_space: di_info_obj[key]['free_space']?toPrettySize(di_info_obj[key]['free_space']):'',
|
||||
total_inodes: di_info_obj[key]['total_inodes']?di_info_obj[key]['total_inodes']:'',
|
||||
used_inodes: di_info_obj[key]['used_inodes']?di_info_obj[key]['used_inodes']:'',
|
||||
free_inodes: di_info_obj[key]['free_inodes']?di_info_obj[key]['free_inodes']:'',
|
||||
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) {
|
||||
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) {
|
||||
const device_name = (io_info_obj[disk]['device_name'] != null && io_info_obj[disk]['device_name'] != '')?io_info_obj[disk]['device_name']:`${disk}`;
|
||||
if(!chartsDefault.io_stats.hasOwnProperty(device_name)){
|
||||
chartsDefault.io_stats[device_name] = {};
|
||||
chartsDefault.io_stats[device_name][`${device_name}_total_rw`] = {'Read': [], 'Write': []};
|
||||
chartsDefault.io_stats[device_name][`${device_name}_bytes_rw`] = {'Read': [], 'Write': []};
|
||||
chartsDefault.io_stats[device_name][`${device_name}_time_rw`] = {'Read': [], 'Write': []};
|
||||
}
|
||||
if(!ioInfo.hasOwnProperty(device_name)){
|
||||
ioInfo[device_name] = {};
|
||||
ioInfo[device_name][`${device_name}_total_rw`] = {'Read': [], 'Write': []};
|
||||
ioInfo[device_name][`${device_name}_bytes_rw`] = {'Read': [], 'Write': []};
|
||||
ioInfo[device_name][`${device_name}_time_rw`] = {'Read': [], 'Write': []};
|
||||
}
|
||||
}
|
||||
|
||||
let new_io_stats = {};
|
||||
for (const disk in io_info_obj) {
|
||||
const device_name = (io_info_obj[disk]['device_name'] != null && io_info_obj[disk]['device_name'] != '')?io_info_obj[disk]['device_name']:`${disk}`;
|
||||
new_io_stats[device_name] = {};
|
||||
new_io_stats[device_name][`${device_name}_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[device_name][`${device_name}_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[device_name][`${device_name}_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']}
|
||||
diskStats={diskStats}
|
||||
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}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
systemStatsTabVal: PropTypes.number,
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={1} className={classes.diskInfoContainer}>
|
||||
<Grid container spacing={1} className={classes.diskInfoSummary}>
|
||||
<div className={classes.tableContainer}>
|
||||
<div className={classes.containerHeaderText}>{gettext('Disk information')}</div>
|
||||
<DiskStatsTable tableHeader={props.tableHeader} data={props.diskStats} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid container spacing={1} className={classes.diskInfoSummary}>
|
||||
<Grid item md={6} sm={12}>
|
||||
<ChartContainer
|
||||
id='t-space-graph'
|
||||
title={gettext('')}
|
||||
datasets={props.diskStats.map((item, index) => ({
|
||||
borderColor: colors[(index + 2) % colors.length],
|
||||
label: item.mount_point !== 'null' ? item.mount_point : item.drive_letter !== 'null' ? item.drive_letter : 'disk' + index,
|
||||
}))}
|
||||
errorMsg={props.errorMsg}
|
||||
isTest={props.isTest}>
|
||||
<PieChart data={{
|
||||
labels: props.diskStats.map((item, index) => item.mount_point!='null'?item.mount_point:item.drive_letter!='null'?item.drive_letter:'disk'+index),
|
||||
datasets: [
|
||||
{
|
||||
data: props.diskStats.map((item) => item.total_space_actual?item.total_space_actual:0),
|
||||
backgroundColor: props.diskStats.map((item, index) => colors[(index + 2) % colors.length]),
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: function (context) {
|
||||
const label = context[0].label || '';
|
||||
return label;
|
||||
},
|
||||
label: function (context) {
|
||||
const value = context.formattedValue || 0;
|
||||
return 'Total space: ' + value;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ChartContainer>
|
||||
</Grid>
|
||||
<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,
|
||||
stacked: true,
|
||||
ticks: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: true,
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return toPrettySize(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ChartContainer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} className={classes.container}>
|
||||
{Object.keys(props.ioInfo).map((drive, index) => (
|
||||
<Grid key={`disk-${index}`} container spacing={1} className={classes.container}>
|
||||
<div className={classes.driveContainer}>
|
||||
<Grid container spacing={1} className={classes.driveContainerHeader}>
|
||||
<div className={classes.containerHeaderText}>{gettext(drive)}</div>
|
||||
</Grid>
|
||||
<Grid container spacing={1} className={classes.driveContainerBody}>
|
||||
{Object.keys(props.ioInfo[drive]).map((type, innerKeyIndex) => (
|
||||
<Grid key={`${type}-${innerKeyIndex}`} item md={4} sm={6}>
|
||||
<ChartContainer id={`io-graph-${type}`} title={type.endsWith('_bytes_rw') ? gettext('Data transfer (bytes)'): type.endsWith('_total_rw') ? gettext('I/O operations count'): type.endsWith('_time_rw') ? gettext('Time spent in I/O operations (milliseconds)'):''} datasets={transformData(props.ioInfo[drive][type], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
|
||||
<StreamingChart data={transformData(props.ioInfo[drive][type], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
|
||||
</ChartContainer>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
StorageWrapper.propTypes = {
|
||||
ioInfo: PropTypes.objectOf(
|
||||
PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
Read: PropTypes.array,
|
||||
Write: PropTypes.array,
|
||||
})
|
||||
)
|
||||
),
|
||||
ioRefreshRate: PropTypes.number.isRequired,
|
||||
diskStats: 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,
|
||||
};
|
319
web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx
Normal file
319
web/pgadmin/dashboard/static/js/SystemStats/Summary.jsx
Normal file
@ -0,0 +1,319 @@
|
||||
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 {useInterval, usePrevious} from 'sources/custom_hooks';
|
||||
import axios from 'axios';
|
||||
import { getStatsUrl, transformData,statsReducer, X_AXIS_LENGTH } from './utility.js';
|
||||
import clsx from 'clsx';
|
||||
import { commonTableStyles } from '../../../../static/js/Theme';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
height: 'auto',
|
||||
padding: '0px !important',
|
||||
marginBottom: '30px',
|
||||
},
|
||||
tableContainer: {
|
||||
background: theme.otherVars.tableBg,
|
||||
padding: '0px',
|
||||
border: '1px solid '+theme.otherVars.borderColor,
|
||||
borderCollapse: 'collapse',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
chartContainer: {
|
||||
padding: '4px',
|
||||
},
|
||||
containerHeader: {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0px',
|
||||
borderBottom: '1px solid '+theme.otherVars.borderColor,
|
||||
padding: '4px 8px',
|
||||
},
|
||||
}));
|
||||
|
||||
const chartsDefault = {
|
||||
'hpc_stats': {'Handle': [], 'Process': []},
|
||||
};
|
||||
|
||||
const SummaryTable = (props) => {
|
||||
const tableClasses = commonTableStyles();
|
||||
const data = props.data;
|
||||
return (
|
||||
<table className={clsx(tableClasses.table)}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>{item.name}</td>
|
||||
<td>{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 [, setCounterData] = useState({});
|
||||
const [pollDelay, setPollDelay] = useState(5000);
|
||||
const [errorMsg, setErrorMsg] = useState(null);
|
||||
const [chartDrawnOnce, setChartDrawnOnce] = useState(false);
|
||||
|
||||
const tableHeader = [
|
||||
{
|
||||
Header: gettext('Property'),
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('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: gettext('Name'), value: gettext(os_info_obj['name']) },
|
||||
{ icon: '', name: gettext('Version'), value: gettext(os_info_obj['version']) },
|
||||
{ icon: '', name: gettext('Host name'), value: gettext(os_info_obj['host_name']) },
|
||||
{ icon: '', name: gettext('Domain name'), value: gettext(os_info_obj['domain_name']) },
|
||||
{ icon: '', name: gettext('Architecture'), value: gettext(os_info_obj['architecture']) },
|
||||
{ icon: '', name: gettext('Os up since seconds'), value: gettext(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: gettext('Vendor'), value: gettext(cpu_info_obj['vendor']) },
|
||||
{ icon: '', name: gettext('Description'), value: gettext(cpu_info_obj['description']) },
|
||||
{ icon: '', name: gettext('Model name'), value: gettext(cpu_info_obj['model_name']) },
|
||||
{ icon: '', name: gettext('No of cores'), value: gettext(cpu_info_obj['no_of_cores']) },
|
||||
{ icon: '', name: gettext('Architecture'), value: gettext(cpu_info_obj['architecture']) },
|
||||
{ icon: '', name: gettext('Clock speed Hz'), value: gettext(cpu_info_obj['clock_speed_hz']) },
|
||||
{ icon: '', name: gettext('L1 dcache size'), value: gettext(cpu_info_obj['l1dcache_size']) },
|
||||
{ icon: '', name: gettext('L1 icache size'), value: gettext(cpu_info_obj['l1icache_size']) },
|
||||
{ icon: '', name: gettext('L2 cache size'), value: gettext(cpu_info_obj['l2cache_size']) },
|
||||
{ icon: '', name: gettext('L3 cache size'), value: gettext(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);
|
||||
|
||||
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}
|
||||
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.tableContainer}>
|
||||
<div className={classes.containerHeader}>{gettext('OS information')}</div>
|
||||
<SummaryTable data={props.osStats} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item md={6} sm={12} className={classes.chartContainer}>
|
||||
<ChartContainer id='hpc-graph' title={gettext('Handle & process count')} 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.tableContainer}>
|
||||
<div className={classes.containerHeader}>{gettext('CPU information')}</div>
|
||||
<SummaryTable data={props.cpuStats} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item md={6} sm={12}>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SummaryWrapper.propTypes = {
|
||||
processHandleCount: PropTypes.any.isRequired,
|
||||
osStats: PropTypes.any.isRequired,
|
||||
cpuStats: 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,
|
||||
};
|
69
web/pgadmin/dashboard/static/js/SystemStats/utility.js
Normal file
69
web/pgadmin/dashboard/static/js/SystemStats/utility.js
Normal file
@ -0,0 +1,69 @@
|
||||
import url_for from 'sources/url_for';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
|
||||
export const X_AXIS_LENGTH = 75;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
export function transformData(labels, refreshRate) {
|
||||
const colors = [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#8D6E63', '#2196F3', '#FFEB3B', '#9C27B0',
|
||||
'#00BCD4', '#CDDC39', '#FF5722', '#3F51B5', '#FFC107',
|
||||
'#607D8B', '#E91E63', '#009688', '#795548', '#FF9800'
|
||||
];
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
{% 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 'cpu_stats' in chart_names %}
|
||||
UNION ALL
|
||||
{% endif %}
|
||||
{% if 'cpu_stats' in chart_names %}
|
||||
{% set add_union = true %}
|
||||
SELECT 'cpu_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 'pcpu_stats' in chart_names %}
|
||||
UNION ALL
|
||||
{% endif %}
|
||||
{% if 'pcpu_stats' in chart_names %}
|
||||
{% set add_union = true %}
|
||||
SELECT 'pcpu_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 '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 %}
|
||||
{% 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 %}
|
@ -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;
|
||||
@ -32,7 +43,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 +69,33 @@ 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 +108,105 @@ 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];
|
||||
});
|
||||
}
|
||||
}
|
||||
],
|
||||
plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [],
|
||||
}), [data.refreshRate, data?.datasets?.length, width, height, options]);
|
||||
});
|
||||
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,
|
||||
},
|
||||
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 +234,5 @@ StreamingChart.propTypes = {
|
||||
xRange: PropTypes.number.isRequired,
|
||||
data: propTypeData.isRequired,
|
||||
options: PropTypes.object,
|
||||
showSecondAxis: PropTypes.bool,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user