ValidatorReport.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { getChainState } from './get-status'
  2. import moment from 'moment'
  3. import {
  4. Card,
  5. CardActions,
  6. CardContent,
  7. CircularProgress,
  8. Container,
  9. createStyles,
  10. Grid,
  11. makeStyles,
  12. TextField,
  13. Typography,
  14. } from '@material-ui/core'
  15. import Button from '@material-ui/core/Button'
  16. import { BootstrapButton } from './BootstrapButton'
  17. import Autocomplete from '@material-ui/lab/Autocomplete'
  18. import { useEffect, useState } from 'react'
  19. import axios from 'axios'
  20. import { config } from 'dotenv'
  21. import { Report, Reports } from './Types'
  22. import { ColDef, DataGrid, PageChangeParams, ValueFormatterParams } from '@material-ui/data-grid'
  23. import Alert from '@material-ui/lab/Alert'
  24. import './index.css'
  25. config()
  26. const useStyles = makeStyles(() =>
  27. createStyles({
  28. root: {
  29. flexGrow: 1,
  30. backgroundColor: '#ffffff',
  31. },
  32. }),
  33. )
  34. const ValidatorReport = () => {
  35. const dateFormat = 'yyyy-MM-DD'
  36. const [activeValidators, setActiveValidators] = useState([])
  37. const [lastBlock, setLastBlock] = useState(0)
  38. const [stash, setStash] = useState('5EhDdcWm4TdqKp1ew1PqtSpoAELmjbZZLm5E34aFoVYkXdRW')
  39. const [dateFrom, setDateFrom] = useState(moment().subtract(14, 'd').format(dateFormat))
  40. const [dateTo, setDateTo] = useState(moment().format(dateFormat))
  41. const [startBlock, setStartBlock] = useState('' as unknown as number)
  42. const [endBlock, setEndBlock] = useState('' as unknown as number)
  43. const [isLoading, setIsLoading] = useState(false)
  44. const [error, setError] = useState(undefined)
  45. const [backendUrl] = useState('https://validators.joystreamstats.live')
  46. const [columns] = useState(
  47. [
  48. { field: 'id', headerName: 'Era', width: 150, sortable: true },
  49. { field: 'stakeTotal', headerName: 'Total Stake', width: 150, sortable: true },
  50. { field: 'stakeOwn', headerName: 'Own Stake', width: 150, sortable: true },
  51. { field: 'points', headerName: 'Points', width: 150, sortable: true },
  52. { field: 'rewards', headerName: 'Rewards', width: 150, sortable: true },
  53. {
  54. field: 'commission',
  55. headerName: 'Commission',
  56. width: 150,
  57. sortable: true,
  58. valueFormatter: (params: ValueFormatterParams) => {
  59. if (isNaN(params.value as unknown as number)) {
  60. return `${params.value}%`
  61. }
  62. return `${Number(params.value).toFixed(0)}%`
  63. },
  64. },
  65. { field: 'blocksCount', headerName: 'Blocks Produced', width: 150, sortable: true },
  66. ],
  67. )
  68. const [report, setReport] = useState({
  69. pageSize: 0,
  70. totalCount: 0,
  71. totalBlocks: 0,
  72. startEra: -1,
  73. endEra: -1,
  74. startBlock: -1,
  75. endBlock: -1,
  76. startTime: -1,
  77. endTime: -1,
  78. report: [] as unknown as Report[],
  79. } as unknown as Reports)
  80. useEffect(() => {
  81. updateChainState()
  82. const interval = setInterval(() => {
  83. updateChainState()
  84. }, 10000)
  85. return () => clearInterval(interval)
  86. }, [])
  87. const updateChainState = () => {
  88. getChainState().then((chainState) => {
  89. setLastBlock(chainState.finalizedBlockHeight)
  90. setActiveValidators(chainState.validators.validators)
  91. })
  92. }
  93. const handlePageChange = (params: PageChangeParams) => {
  94. if (report.totalCount > 0) {
  95. loadReport(params.page)
  96. }
  97. }
  98. const loadReport = (page: number) => {
  99. setIsLoading(true)
  100. const blockParam = startBlock && endBlock ? `&start_block=${startBlock}&end_block=${endBlock}` : ''
  101. const dateParam = !(startBlock && endBlock) && dateFrom && dateTo ? `&start_time=${moment(dateFrom, dateFormat).format(dateFormat)}&end_time=${moment(dateTo, dateFormat).format(dateFormat)}` : ''
  102. const apiUrl = `${backendUrl}/validator-report?addr=${stash}&page=${page}${blockParam}${dateParam}`
  103. axios.get(apiUrl).then((response) => {
  104. if (response.data.report !== undefined) {
  105. setReport(response.data)
  106. }
  107. setIsLoading(false)
  108. setError(undefined)
  109. }).catch((err) => {
  110. setIsLoading(false)
  111. setError(err)
  112. })
  113. }
  114. const stopLoadingReport = () => {
  115. setIsLoading(false)
  116. }
  117. const canLoadReport = () => stash && ((startBlock && endBlock) || (dateFrom && dateTo))
  118. const startOrStopLoading = () => isLoading ? stopLoadingReport() : loadReport(1)
  119. const updateStartBlock = (e: { target: { value: unknown; }; }) => setStartBlock((e.target.value as unknown as number))
  120. const updateEndblock = (e: { target: { value: unknown; }; }) => setEndBlock((e.target.value as unknown as number))
  121. const updateDateFrom = (e: { target: { value: unknown; }; }) => setDateFrom((e.target.value as unknown as string))
  122. const updateDateTo = (e: { target: { value: unknown; }; }) => setDateTo((e.target.value as unknown as string))
  123. const getButtonTitle = (isLoading: boolean) => {
  124. if (isLoading) {
  125. return (<div style={{ display: 'flex', alignItems: 'center' }}>Stop loading <CircularProgress
  126. style={{ color: '#fff', height: 20, width: 20, marginLeft: 12 }} /></div>)
  127. }
  128. if (startBlock && endBlock) {
  129. return `Load data between blocks ${startBlock} - ${endBlock}`
  130. }
  131. if (dateFrom && dateTo) {
  132. return `Load data between dates ${dateFrom} - ${dateTo}`
  133. }
  134. return 'Choose dates or blocks range'
  135. }
  136. const classes = useStyles()
  137. return (
  138. <div className={classes.root}>
  139. <Container maxWidth='lg'>
  140. <Grid container spacing={2}>
  141. <Grid item lg={12}>
  142. <div style={{ display: 'flex', justifyContent: 'flex-start' }}>
  143. <h1>Validator Report</h1>
  144. </div>
  145. </Grid>
  146. <Grid item xs={12} lg={12}>
  147. <Autocomplete
  148. freeSolo
  149. style={{ width: '100%' }}
  150. options={activeValidators}
  151. onChange={(e, value) => setStash(value || '')}
  152. value={stash}
  153. renderInput={(params) => <TextField {...params} label='Validator stash address' variant='filled' />} />
  154. </Grid>
  155. <Grid item xs={6} lg={3}>
  156. <TextField fullWidth type='date' onChange={updateDateFrom} id='block-start'
  157. InputLabelProps={{ shrink: true }} label='Date From' value={dateFrom} variant='filled' />
  158. </Grid>
  159. <Grid item xs={6} lg={3}>
  160. <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth
  161. onClick={() => setDateFrom(moment().subtract(2, 'w').format('yyyy-MM-DD'))}>2 weeks from
  162. today</BootstrapButton>
  163. </Grid>
  164. <Grid item xs={6} lg={3}>
  165. <TextField fullWidth type='date' onChange={updateDateTo} id='block-end' InputLabelProps={{ shrink: true }}
  166. label='Date To' value={dateTo} variant='filled' />
  167. </Grid>
  168. <Grid item xs={6} lg={3}>
  169. <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth
  170. onClick={() => setDateTo(moment().format('yyyy-MM-DD'))}>Today</BootstrapButton>
  171. </Grid>
  172. <Grid item xs={6} lg={3}>
  173. <TextField fullWidth type='number' onChange={updateStartBlock} id='block-start' label='Start Block'
  174. value={startBlock} variant='filled' />
  175. </Grid>
  176. <Grid item xs={6} lg={3}>
  177. <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock}
  178. onClick={() => setStartBlock(lastBlock - (600 * 24 * 14))}>{lastBlock ? `2 weeks before latest (${lastBlock - (600 * 24 * 14)})` : '2 weeks from latest'}</BootstrapButton>
  179. </Grid>
  180. <Grid item xs={6} lg={3}>
  181. <TextField fullWidth type='number' onChange={updateEndblock} id='block-end' label='End Block'
  182. value={endBlock} variant='filled' />
  183. </Grid>
  184. <Grid item xs={6} lg={3}>
  185. <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock}
  186. onClick={() => setEndBlock(lastBlock)}>{lastBlock ? `Pick latest block (${lastBlock})` : 'Use latest block'}</BootstrapButton>
  187. </Grid>
  188. <Grid item xs={12} lg={12}>
  189. <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!canLoadReport()}
  190. onClick={startOrStopLoading}>{getButtonTitle(isLoading)}</BootstrapButton>
  191. <Alert style={error !== undefined ? { marginTop: 12 } : { display: 'none' }}
  192. onClose={() => setError(undefined)} severity='error'>Error loading validator report, please try
  193. again.</Alert>
  194. </Grid>
  195. <Grid item xs={12} lg={12}>
  196. <ValidatorReportCard stash={stash} report={report} />
  197. </Grid>
  198. <Grid item xs={12} lg={12}>
  199. <div style={{ height: 600 }}>
  200. <DataGrid
  201. rows={report.report}
  202. columns={columns as unknown as ColDef[]}
  203. rowCount={report.totalCount}
  204. paginationMode='server'
  205. onPageChange={handlePageChange}
  206. pageSize={report.pageSize}
  207. rowsPerPageOptions={[]}
  208. disableSelectionOnClick
  209. autoHeight
  210. />
  211. </div>
  212. </Grid>
  213. </Grid>
  214. </Container>
  215. </div>
  216. )
  217. }
  218. const ValidatorReportCard = (props: { stash: string, report: Reports }) => {
  219. const copyValidatorStatistics = () => navigator.clipboard.writeText(scoringPeriodText)
  220. const [scoringPeriodText, setScoringPeriodText] = useState('')
  221. const useStyles = makeStyles({
  222. root: {
  223. minWidth: '100%',
  224. textAlign: 'left',
  225. color: '#343a40',
  226. },
  227. title: {
  228. fontSize: 18,
  229. },
  230. pos: {
  231. marginTop: 12,
  232. },
  233. })
  234. const classes = useStyles()
  235. useEffect(() => {
  236. updateScoringPeriodText()
  237. })
  238. const updateScoringPeriodText = () => {
  239. if (props.report.report.length > 0) {
  240. const scoringDateFormat = 'DD-MM-yyyy'
  241. const report = `Validator Date: ${moment(props.report.startTime).format(scoringDateFormat)} - ${moment(props.report.endTime).format(scoringDateFormat)}\nDescription: I was an active validator from era/block ${props.report.startEra}/${props.report.startBlock} to era/block ${props.report.endEra}/${props.report.endBlock}\nwith stash account ${props.stash}. (I was active in all the eras in this range and found a total of ${props.report.totalBlocks} blocks)`
  242. setScoringPeriodText(report)
  243. } else {
  244. setScoringPeriodText('')
  245. }
  246. }
  247. if (props.report.report.length > 0) {
  248. return (<Card className={classes.root}>
  249. <CardContent>
  250. <Typography className={classes.title} color='textSecondary' gutterBottom>
  251. Validator Report:
  252. </Typography>
  253. {scoringPeriodText.split('\n').map((i, key) => <Typography key={key} className={classes.pos}
  254. color='textSecondary'>{i}</Typography>)}
  255. </CardContent>
  256. <CardActions>
  257. <Button onClick={copyValidatorStatistics} size='small'>Copy to clipboard</Button>
  258. </CardActions>
  259. </Card>)
  260. }
  261. return (
  262. <Card className={classes.root}>
  263. <CardContent>
  264. <Typography className={classes.pos} color='textSecondary'>
  265. No Data Available
  266. </Typography>
  267. </CardContent>
  268. </Card>
  269. )
  270. }
  271. export default ValidatorReport