2024-05-20 16:54:49 +05:30
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
2024-06-19 11:54:18 +05:30
import { Box , Grid , useTheme } from '@mui/material' ;
2024-05-20 16:54:49 +05:30
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' ;
2024-12-16 14:52:56 +05:30
import { usePgAdmin } from '../../../../static/js/PgAdminProvider' ;
2024-05-20 16:54:49 +05:30
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 ) ;
2024-06-19 11:54:18 +05:30
const theme = useTheme ( ) ;
2024-05-20 16:54:49 +05:30
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 ) ;
2024-06-19 11:54:18 +05:30
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 ] ) ;
2024-05-20 16:54:49 +05:30
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 ,
} ;