2023-09-27 05:34:48 -05:00
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' ,
2023-10-11 00:57:21 -05:00
marginBottom : '6px' ,
} ,
driveContainer : {
width : '100%' ,
2023-09-27 05:34:48 -05:00
} ,
diskInfoContainer : {
height : 'auto' ,
2023-10-11 00:57:21 -05:00
padding : '8px 8px 0px 8px' ,
marginBottom : '0px' ,
2023-09-27 05:34:48 -05:00
} ,
diskInfoSummary : {
height : 'auto' ,
2023-10-11 00:57:21 -05:00
padding : '0px 0px 4px 0px' ,
marginBottom : '0px' ,
} ,
diskInfoCharts : {
height : 'auto' ,
padding : '0px 0px 2px 0px' ,
marginBottom : '0px' ,
2023-09-27 05:34:48 -05:00
} ,
containerHeaderText : {
fontWeight : 'bold' ,
padding : '4px 8px' ,
} ,
tableContainer : {
background : theme . otherVars . tableBg ,
padding : '0px' ,
border : '1px solid ' + theme . otherVars . borderColor ,
borderCollapse : 'collapse' ,
borderRadius : '4px' ,
2023-10-11 00:57:21 -05:00
overflow : 'auto' ,
2023-09-27 05:34:48 -05:00
width : '100%' ,
2023-09-28 04:53:15 -05:00
margin : '4px 4px 4px 4px' ,
2023-09-27 05:34:48 -05:00
} ,
tableWhiteSpace : {
'& td, & th' : {
whiteSpace : 'break-spaces !important' ,
} ,
} ,
driveContainerHeader : {
height : 'auto' ,
padding : '5px 0px 0px 0px' ,
background : theme . otherVars . tableBg ,
2023-10-11 00:57:21 -05:00
marginBottom : '5px' ,
2023-09-27 05:34:48 -05:00
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'
] ;
/ * T h i s w i l l p r o c e s s i n c o m i n g c h a r t s d a t a a d d i t t h e p r e v i o u s c h a r t s
* 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 ) {
2023-09-28 04:53:15 -05:00
di _info _list . push ( {
icon : '' ,
2023-09-27 05:34:48 -05:00
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 >
2023-10-11 00:57:21 -05:00
< Grid container spacing = { 1 } className = { classes . diskInfoCharts } >
2023-09-27 05:34:48 -05:00
< Grid item md = { 6 } sm = { 12 } >
2023-09-28 04:53:15 -05:00
< ChartContainer
id = 't-space-graph'
title = { gettext ( '' ) }
2023-09-27 05:34:48 -05:00
datasets = { props . diskStats . map ( ( item , index ) => ( {
borderColor : colors [ ( index + 2 ) % colors . length ] ,
2023-10-11 00:57:21 -05:00
label : item . mount _point !== '' ? item . mount _point : item . drive _letter !== '' ? item . drive _letter : 'disk' + index ,
2023-09-27 05:34:48 -05:00
} ) ) }
2023-09-28 04:53:15 -05:00
errorMsg = { props . errorMsg }
2023-09-27 05:34:48 -05:00
isTest = { props . isTest } >
< PieChart data = { {
2023-10-11 00:57:21 -05:00
labels : props . diskStats . map ( ( item , index ) => item . mount _point != '' ? item . mount _point : item . drive _letter != '' ? item . drive _letter : 'disk' + index ) ,
2023-09-27 05:34:48 -05:00
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 = { {
2023-10-11 00:57:21 -05:00
labels : props . diskStats . map ( ( item , index ) => item . mount _point != '' ? item . mount _point : item . drive _letter != '' ? item . drive _letter : 'disk' + index ) ,
2023-09-27 05:34:48 -05:00
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 = {
2023-09-28 04:53:15 -05:00
{
2023-09-27 05:34:48 -05:00
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 ,
2023-10-11 00:57:21 -05:00
} ;