ValidatorReport.tsx 16 KB

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