mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
484 lines
14 KiB
JavaScript
484 lines
14 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
import { Box, Grid, useTheme } from '@mui/material';
|
|
import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
|
|
import gettext from 'sources/gettext';
|
|
import PgTable from 'sources/components/PgTable';
|
|
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
|
import SectionContainer from '../components/SectionContainer';
|
|
import RefreshButton from '../components/RefreshButtons';
|
|
import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
|
|
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
|
|
import url_for from 'sources/url_for';
|
|
import PropTypes from 'prop-types';
|
|
import PGDOutgoingSchema from './schema_ui/pgd_outgoing.ui';
|
|
import PGDIncomingSchema from './schema_ui/pgd_incoming.ui';
|
|
import ChartContainer from '../components/ChartContainer';
|
|
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
|
import { DATA_POINT_SIZE } from '../../../../static/js/chartjs';
|
|
import { X_AXIS_LENGTH, statsReducer, transformData } from '../Graphs';
|
|
import { getEpoch, getGCD, toPrettySize } from '../../../../static/js/utils';
|
|
import { useInterval, usePrevious } from '../../../../static/js/custom_hooks';
|
|
|
|
|
|
const outgoingReplicationColumns = [{
|
|
accessorKey: 'view_details',
|
|
header: () => null,
|
|
enableSorting: false,
|
|
enableResizing: false,
|
|
size: 35,
|
|
maxSize: 35,
|
|
minSize: 35,
|
|
id: 'btn-edit',
|
|
cell: getExpandCell({
|
|
title: gettext('View details')
|
|
}),
|
|
},
|
|
{
|
|
accessorKey: 'active_pid',
|
|
header: gettext('Active PID'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 40,
|
|
},
|
|
{
|
|
accessorKey: 'state',
|
|
header: gettext('State'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60
|
|
},
|
|
{
|
|
accessorKey: 'slot_name',
|
|
header: gettext('Slot'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60
|
|
},
|
|
{
|
|
accessorKey: 'write_lag',
|
|
header: gettext('Write lag'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60
|
|
},
|
|
{
|
|
accessorKey: 'flush_lag',
|
|
header: gettext('Flush lag'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60
|
|
},
|
|
{
|
|
accessorKey: 'replay_lag',
|
|
header: gettext('Replay lag'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60
|
|
},
|
|
];
|
|
|
|
const incomingReplicationColumns = [{
|
|
accessorKey: 'view_details',
|
|
header: () => null,
|
|
enableSorting: false,
|
|
enableResizing: false,
|
|
size: 35,
|
|
maxSize: 35,
|
|
minSize: 35,
|
|
id: 'btn-details',
|
|
cell: getExpandCell({
|
|
title: gettext('View details')
|
|
}),
|
|
},
|
|
{
|
|
accessorKey: 'sub_name',
|
|
header: gettext('Subscription'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 50,
|
|
},
|
|
{
|
|
accessorKey: 'sub_slot_name',
|
|
header: gettext('Slot name'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 200,
|
|
},
|
|
{
|
|
accessorKey: 'subscription_status',
|
|
header: gettext('Status'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'last_xact_replay_timestamp',
|
|
header: gettext('Replay timestamp'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
}
|
|
];
|
|
|
|
|
|
const clusterNodeColumns = [{
|
|
accessorKey: 'node_name',
|
|
header: gettext('Node'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'node_group_name',
|
|
header: gettext('Group'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'peer_state_name',
|
|
header: gettext('Peer state'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 40,
|
|
},
|
|
{
|
|
accessorKey: 'node_kind_name',
|
|
header: gettext('Kind'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 40,
|
|
},
|
|
{
|
|
accessorKey: 'pg_version',
|
|
header: gettext('PostgreSQL version'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 80,
|
|
},
|
|
{
|
|
accessorKey: 'bdr_version',
|
|
header: gettext('BDR version'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 30,
|
|
},
|
|
{
|
|
accessorKey: 'catchup_state_name',
|
|
header: gettext('Catchup state'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 90,
|
|
}
|
|
];
|
|
|
|
const raftStatusColumns = [{
|
|
accessorKey: 'node_name',
|
|
header: gettext('Node'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'node_group_name',
|
|
header: gettext('Group'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'state',
|
|
header: gettext('State'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'leader_type',
|
|
header: gettext('Leader type'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'leader_name',
|
|
header: gettext('Leader'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'voting',
|
|
header: gettext('Voting?'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 40,
|
|
cell: getSwitchCell(),
|
|
},
|
|
{
|
|
accessorKey: 'voting_for',
|
|
header: gettext('Voting for'),
|
|
enableSorting: true,
|
|
enableResizing: true,
|
|
minSize: 26,
|
|
size: 60,
|
|
}
|
|
];
|
|
|
|
const outgoingSchemaObj = new PGDOutgoingSchema();
|
|
const incomingSchemaObj = new PGDIncomingSchema();
|
|
|
|
function getChartsUrl(sid=-1, chart_names=[]) {
|
|
let base_url = url_for('dashboard.pgd.charts', {sid: sid});
|
|
base_url += '?chart_names=' + chart_names.join(',');
|
|
return base_url;
|
|
}
|
|
|
|
const chartsDefault = {
|
|
'pgd_replication_lag': {},
|
|
};
|
|
|
|
export default function PGDReplication({preferences, treeNodeInfo, pageVisible, enablePoll=true, ...props}) {
|
|
const api = getApiInstance();
|
|
const refreshOn = useRef(null);
|
|
const theme = useTheme();
|
|
const prevPreferences = usePrevious(preferences);
|
|
const [pollDelay, setPollDelay] = useState(5000);
|
|
|
|
const [replicationLagTime, replicationLagTimeReduce] = useReducer(statsReducer, chartsDefault['pgd_replication_lag']);
|
|
const [replicationLagBytes, replicationLagBytesReduce] = useReducer(statsReducer, chartsDefault['pgd_replication_lag']);
|
|
const [clusterNodes, setClusterNodes] = useState([]);
|
|
const [raftStatus, setRaftStatus] = useState([]);
|
|
const [outgoingReplication, setOutgoingReplication] = useState([]);
|
|
const [incomingReplication, setIncomingReplication] = useState([]);
|
|
const [errorMsg, setErrorMsg] = useState(null);
|
|
|
|
const pgAdmin = usePgAdmin();
|
|
|
|
const sid = treeNodeInfo.server._id;
|
|
|
|
const options = useMemo(()=>({
|
|
showDataPoints: preferences['graph_data_points'],
|
|
showTooltip: preferences['graph_mouse_track'],
|
|
lineBorderSize: preferences['graph_line_border_width'],
|
|
}), [preferences]);
|
|
|
|
const getReplicationData = (endpoint, setter)=>{
|
|
const url = url_for(`dashboard.pgd.${endpoint}`, {sid: sid});
|
|
api.get(url)
|
|
.then((res)=>{
|
|
setter(res.data);
|
|
})
|
|
.catch((error)=>{
|
|
console.error(error);
|
|
pgAdmin.Browser.notifier.error(parseApiError(error));
|
|
});
|
|
};
|
|
|
|
useEffect(()=>{
|
|
let calcPollDelay = false;
|
|
if(prevPreferences) {
|
|
if(prevPreferences['pgd_replication_lag_refresh'] != preferences['pgd_replication_lag_refresh']) {
|
|
replicationLagTimeReduce({reset: chartsDefault['pgd_replication_lag']});
|
|
replicationLagBytesReduce({reset: chartsDefault['pgd_replication_lag']});
|
|
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(()=>{
|
|
if(pageVisible) {
|
|
getReplicationData('cluster_nodes', setClusterNodes);
|
|
getReplicationData('raft_status', setRaftStatus);
|
|
getReplicationData('outgoing', setOutgoingReplication);
|
|
getReplicationData('incoming', setIncomingReplication);
|
|
}
|
|
}, [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 = getChartsUrl(sid, getFor);
|
|
if (!pageVisible){
|
|
return;
|
|
}
|
|
|
|
api.get(path)
|
|
.then((resp)=>{
|
|
let data = resp.data;
|
|
setErrorMsg(null);
|
|
if(data.hasOwnProperty('pgd_replication_lag')){
|
|
let newTime = {};
|
|
let newBytes = {};
|
|
for(const row of data['pgd_replication_lag']) {
|
|
newTime[row['name']] = row['replay_lag'] ?? 0;
|
|
newBytes[row['name']] = row['replay_lag_bytes'] ?? 0;
|
|
}
|
|
replicationLagTimeReduce({incoming: newTime});
|
|
replicationLagBytesReduce({incoming: newBytes});
|
|
}
|
|
})
|
|
.catch((error)=>{
|
|
if(!errorMsg) {
|
|
replicationLagTimeReduce({reset: chartsDefault['pgd_replication_lag']});
|
|
replicationLagBytesReduce({reset: chartsDefault['pgd_replication_lag']});
|
|
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);
|
|
|
|
const replicationLagTimeData = useMemo(()=>transformData(replicationLagTime, preferences['pgd_replication_lag_refresh'], theme.name), [replicationLagTime, theme.name]);
|
|
const replicationLagBytesData = useMemo(()=>transformData(replicationLagBytes, preferences['pgd_replication_lag_refresh'], theme.name), [replicationLagBytes, theme.name]);
|
|
|
|
return (
|
|
<Box height="100%" display="flex" flexDirection="column">
|
|
<Grid container spacing={0.5}>
|
|
<Grid item md={6}>
|
|
<ChartContainer id='sessions-graph' title={gettext('Replication lag (Time)')}
|
|
datasets={replicationLagTimeData.datasets} errorMsg={errorMsg} isTest={props.isTest}>
|
|
<StreamingChart data={replicationLagTimeData} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
|
|
valueFormatter={(v)=>toPrettySize(v, 's')} />
|
|
</ChartContainer>
|
|
</Grid>
|
|
<Grid item md={6}>
|
|
<ChartContainer id='tps-graph' title={gettext('Replication lag (Size)')} datasets={replicationLagBytesData.datasets} errorMsg={errorMsg} isTest={props.isTest}>
|
|
<StreamingChart data={replicationLagBytesData} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
|
|
valueFormatter={toPrettySize} />
|
|
</ChartContainer>
|
|
</Grid>
|
|
</Grid>
|
|
<SectionContainer
|
|
titleExtras={<RefreshButton onClick={()=>{
|
|
getReplicationData('cluster_nodes', setClusterNodes);
|
|
}}/>}
|
|
title={gettext('Cluster nodes')} style={{minHeight: '300px', marginTop: '4px'}}>
|
|
<PgTable
|
|
caveTable={false}
|
|
tableNoBorder={false}
|
|
columns={clusterNodeColumns}
|
|
data={clusterNodes}
|
|
></PgTable>
|
|
</SectionContainer>
|
|
<SectionContainer
|
|
titleExtras={<RefreshButton onClick={()=>{
|
|
getReplicationData('raft_status', setRaftStatus);
|
|
}}/>}
|
|
title={gettext('Raft status')} style={{minHeight: '300px', marginTop: '4px'}}>
|
|
<PgTable
|
|
caveTable={false}
|
|
tableNoBorder={false}
|
|
columns={raftStatusColumns}
|
|
data={raftStatus}
|
|
></PgTable>
|
|
</SectionContainer>
|
|
<SectionContainer
|
|
titleExtras={<RefreshButton onClick={()=>{
|
|
getReplicationData('outgoing', setOutgoingReplication);
|
|
}}/>}
|
|
title={gettext('Outgoing Replication')} style={{minHeight: '300px', marginTop: '4px'}}>
|
|
<PgTable
|
|
caveTable={false}
|
|
tableNoBorder={false}
|
|
columns={outgoingReplicationColumns}
|
|
data={outgoingReplication}
|
|
schema={outgoingSchemaObj}
|
|
></PgTable>
|
|
</SectionContainer>
|
|
<SectionContainer
|
|
titleExtras={<RefreshButton onClick={()=>{
|
|
getReplicationData('incoming', setIncomingReplication);
|
|
}}/>}
|
|
title={gettext('Incoming Replication')} style={{minHeight: '300px', marginTop: '4px'}}>
|
|
<PgTable
|
|
caveTable={false}
|
|
tableNoBorder={false}
|
|
columns={incomingReplicationColumns}
|
|
data={incomingReplication}
|
|
schema={incomingSchemaObj}
|
|
></PgTable>
|
|
</SectionContainer>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
PGDReplication.propTypes = {
|
|
preferences: PropTypes.object,
|
|
treeNodeInfo: PropTypes.object.isRequired,
|
|
pageVisible: PropTypes.bool,
|
|
enablePoll: PropTypes.bool,
|
|
isTest: PropTypes.bool,
|
|
};
|