Oleksandr Korniienko 3 лет назад
Родитель
Сommit
74ab183de2

+ 44 - 0
src/components/Navbar/index.tsx

@@ -0,0 +1,44 @@
+import { AppBar, Button, createStyles, makeStyles, Toolbar } from '@material-ui/core'
+import { Link } from 'react-router-dom'
+import { MemoryRouter } from 'react-router'
+import React from 'react'
+import joystream from '../../joystream.svg'
+
+const useStyles = makeStyles(() =>
+  createStyles({
+    root: {
+      flexGrow: 1,
+    },
+  }),
+)
+
+const Navbar = () => {
+
+  const classes = useStyles()
+
+  return (
+    <div className={classes.root}>
+      <MemoryRouter>
+        <AppBar position='static' style={{ flexDirection: 'row', backgroundColor: '#4138ff', color: '#fff' }}>
+          <Toolbar style={{ paddingLeft: '12px', backgroundColor: '#4138ff' }}>
+            <Button color='inherit' component={Link} to='/'>
+              <img style={{ width: 50, height: 50 }} src={joystream} className='App-logo' alt='Joystream logo' />
+            </Button>
+            <Button color='secondary' component={Link} to='/validator-report'>Validator Report</Button>
+            <Button color='inherit' component={Link} to='/calendar'>Calendar</Button>
+            <Button color='inherit' component={Link} to='/curation'>Curation</Button>
+            <Button color='inherit' component={Link} to='/timeline'>Timeline</Button>
+            <Button color='inherit' component={Link} to='/tokenomics'>Reports</Button>
+            <Button color='inherit' component={Link} to='/validators'>Validators</Button>
+            <Button color='inherit' component={Link} to='/storage'>Storage</Button>
+            <Button color='inherit' component={Link} to='/spending'>Spending</Button>
+            <Button color='inherit' component={Link} to='/transactions'>Transfers</Button>
+            <Button color='inherit' component={Link} to='/burners'>Top Burners</Button>
+            <Button color='inherit' component={Link} to='/mint'>Toolbox</Button>
+          </Toolbar>
+        </AppBar>
+      </MemoryRouter>
+    </div>
+  )
+
+}

+ 2 - 2
src/components/ValidatorReport/BootstrapButton.tsx

@@ -1,4 +1,4 @@
-import { Button, withStyles } from '@material-ui/core';
+import { Button, withStyles } from '@material-ui/core'
 
 export const BootstrapButton = withStyles({
   root: {
@@ -37,4 +37,4 @@ export const BootstrapButton = withStyles({
       boxShadow: '0 0 0 0.2rem rgba(0,123,255,.5)',
     },
   },
-})(Button);
+})(Button)

+ 3 - 3
src/components/ValidatorReport/Types.tsx

@@ -11,7 +11,7 @@ export interface ActiveEra {
   points: number
 }
 
-export interface Reports { 
+export interface Reports {
   pageSize: number,
   totalCount: number,
   totalBlocks: number,
@@ -22,9 +22,9 @@ export interface Reports {
   startTime: number,
   endTime: number,
   report: Array<Report>
-};
+}
 
-export interface Report { 
+export interface Report {
   id: number,
   stakeTotal: number,
   stakeOwn: number,

+ 264 - 231
src/components/ValidatorReport/ValidatorReport.tsx

@@ -1,255 +1,288 @@
-import { getChainState } from './get-status';
+import { getChainState } from './get-status'
 import moment from 'moment'
-import { Card, CardActions, CardContent, CircularProgress, Container, createStyles, Grid, makeStyles, TextField, Typography } from '@material-ui/core';
-import Button from '@material-ui/core/Button';
-import { BootstrapButton } from './BootstrapButton';
-import Autocomplete from '@material-ui/lab/Autocomplete';
-import { useEffect, useState } from 'react';
+import {
+  Card,
+  CardActions,
+  CardContent,
+  CircularProgress,
+  Container,
+  createStyles,
+  Grid,
+  makeStyles,
+  TextField,
+  Typography,
+} from '@material-ui/core'
+import Button from '@material-ui/core/Button'
+import { BootstrapButton } from './BootstrapButton'
+import Autocomplete from '@material-ui/lab/Autocomplete'
+import { useEffect, useState } from 'react'
 import axios from 'axios'
-import { config } from "dotenv";
-import { Report, Reports } from './Types';
-import { ColDef, DataGrid, PageChangeParams, ValueFormatterParams } from '@material-ui/data-grid';
-import Alert from '@material-ui/lab/Alert';
+import { config } from 'dotenv'
+import { Report, Reports } from './Types'
+import { ColDef, DataGrid, PageChangeParams, ValueFormatterParams } from '@material-ui/data-grid'
+import Alert from '@material-ui/lab/Alert'
 import './index.css'
 
-config();
+config()
 
 const useStyles = makeStyles(() =>
-    createStyles({
-        root: {
-            flexGrow: 1,
-            backgroundColor: '#ffffff'
-        },
-    }),
-);
+  createStyles({
+    root: {
+      flexGrow: 1,
+      backgroundColor: '#ffffff',
+    },
+  }),
+)
 
 
 const ValidatorReport = () => {
-    const dateFormat = 'yyyy-MM-DD';
-    const [activeValidators, setActiveValidators] = useState([]);
-    const [lastBlock, setLastBlock] = useState(0);
-    const [stash, setStash] = useState('5EhDdcWm4TdqKp1ew1PqtSpoAELmjbZZLm5E34aFoVYkXdRW');
-    const [dateFrom, setDateFrom] = useState(moment().subtract(14, 'd').format(dateFormat));
-    const [dateTo, setDateTo] = useState(moment().format(dateFormat));
-    const [startBlock, setStartBlock] = useState('' as unknown as number);
-    const [endBlock, setEndBlock] = useState('' as unknown as number);
-    const [isLoading, setIsLoading] = useState(false);
-    const [error, setError] = useState(undefined);
-    const [backendUrl] = useState("https://validators.joystreamstats.live");
-    const [columns] = useState(
-        [
-            { field: 'id', headerName: 'Era', width: 150, sortable: true },
-            { field: 'stakeTotal', headerName: 'Total Stake', width: 150, sortable: true },
-            { field: 'stakeOwn', headerName: 'Own Stake', width: 150, sortable: true },
-            { field: 'points', headerName: 'Points', width: 150, sortable: true },
-            { field: 'rewards', headerName: 'Rewards', width: 150, sortable: true },
-            { field: 'commission', headerName: 'Commission', width: 150, sortable: true, valueFormatter: (params: ValueFormatterParams) => {
-                if (isNaN(params.value as unknown as number)) {
-                    return `${params.value}%`
-                }
-                return `${Number(params.value).toFixed(0)}%`
-            }},
-            { field: 'blocksCount', headerName: 'Blocks Produced', width: 150, sortable: true },
-        ]
-    );
-    const [report, setReport] = useState({
-        pageSize: 0,
-        totalCount: 0,
-        totalBlocks: 0,
-        startEra: -1,
-        endEra: -1,
-        startBlock: -1,
-        endBlock: -1,
-        startTime: -1,
-        endTime: -1,
-        report: [] as unknown as Report[]
-    } as unknown as Reports );
-
-    useEffect(() => {
-        updateChainState()
-        const interval = setInterval(() => { updateChainState() }, 10000);
-        return () => clearInterval(interval);
-    }, []);
-
-    const updateChainState = () => {
-        getChainState().then((chainState) => {
-            setLastBlock(chainState.finalizedBlockHeight)
-            setActiveValidators(chainState.validators.validators)
-        })
-    }
+  const dateFormat = 'yyyy-MM-DD'
+  const [activeValidators, setActiveValidators] = useState([])
+  const [lastBlock, setLastBlock] = useState(0)
+  const [stash, setStash] = useState('5EhDdcWm4TdqKp1ew1PqtSpoAELmjbZZLm5E34aFoVYkXdRW')
+  const [dateFrom, setDateFrom] = useState(moment().subtract(14, 'd').format(dateFormat))
+  const [dateTo, setDateTo] = useState(moment().format(dateFormat))
+  const [startBlock, setStartBlock] = useState('' as unknown as number)
+  const [endBlock, setEndBlock] = useState('' as unknown as number)
+  const [isLoading, setIsLoading] = useState(false)
+  const [error, setError] = useState(undefined)
+  const [backendUrl] = useState('https://validators.joystreamstats.live')
+  const [columns] = useState(
+    [
+      { field: 'id', headerName: 'Era', width: 150, sortable: true },
+      { field: 'stakeTotal', headerName: 'Total Stake', width: 150, sortable: true },
+      { field: 'stakeOwn', headerName: 'Own Stake', width: 150, sortable: true },
+      { field: 'points', headerName: 'Points', width: 150, sortable: true },
+      { field: 'rewards', headerName: 'Rewards', width: 150, sortable: true },
+      {
+        field: 'commission',
+        headerName: 'Commission',
+        width: 150,
+        sortable: true,
+        valueFormatter: (params: ValueFormatterParams) => {
+          if (isNaN(params.value as unknown as number)) {
+            return `${params.value}%`
+          }
+          return `${Number(params.value).toFixed(0)}%`
+        },
+      },
+      { field: 'blocksCount', headerName: 'Blocks Produced', width: 150, sortable: true },
+    ],
+  )
+  const [report, setReport] = useState({
+    pageSize: 0,
+    totalCount: 0,
+    totalBlocks: 0,
+    startEra: -1,
+    endEra: -1,
+    startBlock: -1,
+    endBlock: -1,
+    startTime: -1,
+    endTime: -1,
+    report: [] as unknown as Report[],
+  } as unknown as Reports)
 
-    const handlePageChange = (params: PageChangeParams) => {
-        if (report.totalCount > 0) {
-            loadReport(params.page)
-        }
-    }
+  useEffect(() => {
+    updateChainState()
+    const interval = setInterval(() => {
+      updateChainState()
+    }, 10000)
+    return () => clearInterval(interval)
+  }, [])
 
-    const loadReport = (page: number) => {
-        setIsLoading(true)
-        const blockParam = startBlock && endBlock ? `&start_block=${startBlock}&end_block=${endBlock}` : ''
-        const dateParam = !(startBlock && endBlock) && dateFrom && dateTo ? `&start_time=${moment(dateFrom, dateFormat).format(dateFormat)}&end_time=${moment(dateTo, dateFormat).format(dateFormat)}` : ''
-        const apiUrl = `${backendUrl}/validator-report?addr=${stash}&page=${page}${blockParam}${dateParam}`
-        axios.get(apiUrl).then((response) => {
-            if (response.data.report !== undefined) {
-                setReport(response.data);
-            }
-            setIsLoading(false)
-            setError(undefined)
-        }).catch((err) => {
-            setIsLoading(false)
-            setError(err)
-        })
-    }
+  const updateChainState = () => {
+    getChainState().then((chainState) => {
+      setLastBlock(chainState.finalizedBlockHeight)
+      setActiveValidators(chainState.validators.validators)
+    })
+  }
 
-    const stopLoadingReport = () => {
-        setIsLoading(false)
+  const handlePageChange = (params: PageChangeParams) => {
+    if (report.totalCount > 0) {
+      loadReport(params.page)
     }
+  }
 
-    const canLoadReport = () => stash && ((startBlock && endBlock) || (dateFrom && dateTo))
-    const startOrStopLoading = () => isLoading ? stopLoadingReport() : loadReport(1);
-    const updateStartBlock = (e: { target: { value: unknown; }; }) => setStartBlock((e.target.value as unknown as number));
-    const updateEndblock = (e: { target: { value: unknown; }; }) => setEndBlock((e.target.value as unknown as number));
-    const updateDateFrom = (e: { target: { value: unknown; }; }) => setDateFrom((e.target.value as unknown as string))
-    const updateDateTo = (e: { target: { value: unknown; }; }) => setDateTo((e.target.value as unknown as string));
-
-    const getButtonTitle = (isLoading: boolean) => {
-        if (isLoading) {
-            return (<div style={{ display: 'flex', alignItems: 'center' }}>Stop loading <CircularProgress style={ { color: '#fff', height: 20, width: 20, marginLeft: 12 } } /></div>)
-        }
-        if (startBlock && endBlock) {
-            return `Load data between blocks ${startBlock} - ${endBlock}`
-        }
-        if (dateFrom && dateTo) {
-            return `Load data between dates ${dateFrom} - ${dateTo}`;
-        }
-        return 'Choose dates or blocks range'
+  const loadReport = (page: number) => {
+    setIsLoading(true)
+    const blockParam = startBlock && endBlock ? `&start_block=${startBlock}&end_block=${endBlock}` : ''
+    const dateParam = !(startBlock && endBlock) && dateFrom && dateTo ? `&start_time=${moment(dateFrom, dateFormat).format(dateFormat)}&end_time=${moment(dateTo, dateFormat).format(dateFormat)}` : ''
+    const apiUrl = `${backendUrl}/validator-report?addr=${stash}&page=${page}${blockParam}${dateParam}`
+    axios.get(apiUrl).then((response) => {
+      if (response.data.report !== undefined) {
+        setReport(response.data)
+      }
+      setIsLoading(false)
+      setError(undefined)
+    }).catch((err) => {
+      setIsLoading(false)
+      setError(err)
+    })
+  }
+
+  const stopLoadingReport = () => {
+    setIsLoading(false)
+  }
+
+  const canLoadReport = () => stash && ((startBlock && endBlock) || (dateFrom && dateTo))
+  const startOrStopLoading = () => isLoading ? stopLoadingReport() : loadReport(1)
+  const updateStartBlock = (e: { target: { value: unknown; }; }) => setStartBlock((e.target.value as unknown as number))
+  const updateEndblock = (e: { target: { value: unknown; }; }) => setEndBlock((e.target.value as unknown as number))
+  const updateDateFrom = (e: { target: { value: unknown; }; }) => setDateFrom((e.target.value as unknown as string))
+  const updateDateTo = (e: { target: { value: unknown; }; }) => setDateTo((e.target.value as unknown as string))
+
+  const getButtonTitle = (isLoading: boolean) => {
+    if (isLoading) {
+      return (<div style={{ display: 'flex', alignItems: 'center' }}>Stop loading <CircularProgress
+        style={{ color: '#fff', height: 20, width: 20, marginLeft: 12 }} /></div>)
+    }
+    if (startBlock && endBlock) {
+      return `Load data between blocks ${startBlock} - ${endBlock}`
     }
-    const classes = useStyles();
-    return (
-        <div className={classes.root}>
-            <Container maxWidth="lg">
-                <Grid container spacing={2}>
-                    <Grid item lg={12}>
-                        <div style={{ display: 'flex', justifyContent: 'flex-start' }}>
-                            <h1>Validator Report</h1>
-                        </div>
-                    </Grid>
-                    <Grid item xs={12} lg={12}>
-                        <Autocomplete
-                            freeSolo
-                            style={{ width: '100%' }}
-                            options={activeValidators}
-                            onChange={(e, value) => setStash(value || '')}
-                            value={stash}
-                            renderInput={(params) => <TextField {...params} label="Validator stash address" variant="filled" />} />
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <TextField fullWidth type="date" onChange={updateDateFrom} id="block-start" InputLabelProps={{ shrink: true }} label="Date From" value={dateFrom} variant="filled" />
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth onClick={() => setDateFrom(moment().subtract(2, 'w').format('yyyy-MM-DD'))}>2 weeks from today</BootstrapButton>
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <TextField fullWidth type="date" onChange={updateDateTo} id="block-end" InputLabelProps={{ shrink: true }} label="Date To" value={dateTo} variant="filled" />
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth onClick={() => setDateTo(moment().format('yyyy-MM-DD'))}>Today</BootstrapButton>
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <TextField fullWidth type="number" onChange={updateStartBlock} id="block-start" label="Start Block" value={startBlock} variant="filled" />
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock} onClick={() => setStartBlock(lastBlock - (600 * 24 * 14))}>{lastBlock ? `2 weeks before latest (${lastBlock - (600 * 24 * 14)})` : '2 weeks from latest'}</BootstrapButton>
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <TextField fullWidth type="number" onChange={updateEndblock} id="block-end" label="End Block" value={endBlock} variant="filled" />
-                    </Grid>
-                    <Grid item xs={6} lg={3}>
-                        <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock} onClick={() => setEndBlock(lastBlock)}>{lastBlock ? `Pick latest block (${lastBlock})` : 'Use latest block'}</BootstrapButton>
-                    </Grid>
-                    <Grid item xs={12} lg={12}>
-                        <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!canLoadReport()} onClick={startOrStopLoading}>{getButtonTitle(isLoading)}</BootstrapButton>
-                        <Alert style={ error !== undefined ? { marginTop: 12 } : { display: 'none'} } onClose={() => setError(undefined)} severity="error">Error loading validator report, please try again.</Alert>
-                    </Grid>
-                    <Grid item xs={12} lg={12}>
-                        <ValidatorReportCard stash={stash} report={report} />
-                    </Grid>
-                    <Grid item xs={12} lg={12}>
-                        <div style={{ height: 600 }}>
-                            <DataGrid 
-                                rows={report.report} 
-                                columns={columns as unknown as ColDef[]}
-                                rowCount={report.totalCount}
-                                paginationMode="server"
-                                onPageChange={handlePageChange} 
-                                pageSize={report.pageSize}
-                                rowsPerPageOptions={[]}
-                                disableSelectionOnClick
-                                autoHeight
-                                />
-                        </div>
-                    </Grid>
-                </Grid>
-            </Container>
-        </div>
-    )
+    if (dateFrom && dateTo) {
+      return `Load data between dates ${dateFrom} - ${dateTo}`
+    }
+    return 'Choose dates or blocks range'
+  }
+  const classes = useStyles()
+  return (
+    <div className={classes.root}>
+      <Container maxWidth='lg'>
+        <Grid container spacing={2}>
+          <Grid item lg={12}>
+            <div style={{ display: 'flex', justifyContent: 'flex-start' }}>
+              <h1>Validator Report</h1>
+            </div>
+          </Grid>
+          <Grid item xs={12} lg={12}>
+            <Autocomplete
+              freeSolo
+              style={{ width: '100%' }}
+              options={activeValidators}
+              onChange={(e, value) => setStash(value || '')}
+              value={stash}
+              renderInput={(params) => <TextField {...params} label='Validator stash address' variant='filled' />} />
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <TextField fullWidth type='date' onChange={updateDateFrom} id='block-start'
+                       InputLabelProps={{ shrink: true }} label='Date From' value={dateFrom} variant='filled' />
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth
+                             onClick={() => setDateFrom(moment().subtract(2, 'w').format('yyyy-MM-DD'))}>2 weeks from
+              today</BootstrapButton>
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <TextField fullWidth type='date' onChange={updateDateTo} id='block-end' InputLabelProps={{ shrink: true }}
+                       label='Date To' value={dateTo} variant='filled' />
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth
+                             onClick={() => setDateTo(moment().format('yyyy-MM-DD'))}>Today</BootstrapButton>
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <TextField fullWidth type='number' onChange={updateStartBlock} id='block-start' label='Start Block'
+                       value={startBlock} variant='filled' />
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock}
+                             onClick={() => setStartBlock(lastBlock - (600 * 24 * 14))}>{lastBlock ? `2 weeks before latest (${lastBlock - (600 * 24 * 14)})` : '2 weeks from latest'}</BootstrapButton>
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <TextField fullWidth type='number' onChange={updateEndblock} id='block-end' label='End Block'
+                       value={endBlock} variant='filled' />
+          </Grid>
+          <Grid item xs={6} lg={3}>
+            <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!lastBlock}
+                             onClick={() => setEndBlock(lastBlock)}>{lastBlock ? `Pick latest block (${lastBlock})` : 'Use latest block'}</BootstrapButton>
+          </Grid>
+          <Grid item xs={12} lg={12}>
+            <BootstrapButton size='large' style={{ minHeight: 56 }} fullWidth disabled={!canLoadReport()}
+                             onClick={startOrStopLoading}>{getButtonTitle(isLoading)}</BootstrapButton>
+            <Alert style={error !== undefined ? { marginTop: 12 } : { display: 'none' }}
+                   onClose={() => setError(undefined)} severity='error'>Error loading validator report, please try
+              again.</Alert>
+          </Grid>
+          <Grid item xs={12} lg={12}>
+            <ValidatorReportCard stash={stash} report={report} />
+          </Grid>
+          <Grid item xs={12} lg={12}>
+            <div style={{ height: 600 }}>
+              <DataGrid
+                rows={report.report}
+                columns={columns as unknown as ColDef[]}
+                rowCount={report.totalCount}
+                paginationMode='server'
+                onPageChange={handlePageChange}
+                pageSize={report.pageSize}
+                rowsPerPageOptions={[]}
+                disableSelectionOnClick
+                autoHeight
+              />
+            </div>
+          </Grid>
+        </Grid>
+      </Container>
+    </div>
+  )
 }
 
 const ValidatorReportCard = (props: { stash: string, report: Reports }) => {
-    const copyValidatorStatistics = () => navigator.clipboard.writeText(scoringPeriodText)
-    const [scoringPeriodText, setScoringPeriodText] = useState('')
-    const useStyles = makeStyles({
-        root: {
-            minWidth: '100%',
-            textAlign: 'left',
-            color: '#343a40',
-        },
-        title: {
-            fontSize: 18,
-        },
-        pos: {
-            marginTop: 12,
-        },
-    });
-
-    const classes = useStyles();
-
-    useEffect(() => {
-        updateScoringPeriodText()
-    });
-
-    const updateScoringPeriodText = () => {
-        if (props.report.report.length > 0) {
-            const scoringDateFormat = 'DD-MM-yyyy';
-            const report = `Validator Date: ${moment(props.report.startTime).format(scoringDateFormat)} - ${moment(props.report.startTime).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)`
-            setScoringPeriodText(report)
-        } else {
-            setScoringPeriodText('')
-        }
-    }
+  const copyValidatorStatistics = () => navigator.clipboard.writeText(scoringPeriodText)
+  const [scoringPeriodText, setScoringPeriodText] = useState('')
+  const useStyles = makeStyles({
+    root: {
+      minWidth: '100%',
+      textAlign: 'left',
+      color: '#343a40',
+    },
+    title: {
+      fontSize: 18,
+    },
+    pos: {
+      marginTop: 12,
+    },
+  })
+
+  const classes = useStyles()
 
+  useEffect(() => {
+    updateScoringPeriodText()
+  })
+
+  const updateScoringPeriodText = () => {
     if (props.report.report.length > 0) {
-        return (<Card className={classes.root}>
-            <CardContent>
-                <Typography className={classes.title} color="textSecondary" gutterBottom>
-                    Validator Report:
-                </Typography>
-                { scoringPeriodText.split('\n').map((i, key) => <Typography key={key} className={classes.pos} color="textSecondary">{i}</Typography>) }
-            </CardContent>
-            <CardActions>
-                <Button onClick={copyValidatorStatistics} size="small">Copy to clipboard</Button>
-            </CardActions>
-        </Card>)
+      const scoringDateFormat = 'DD-MM-yyyy'
+      const report = `Validator Date: ${moment(props.report.startTime).format(scoringDateFormat)} - ${moment(props.report.startTime).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)`
+      setScoringPeriodText(report)
+    } else {
+      setScoringPeriodText('')
     }
-    return (
-        <Card className={classes.root}>
-            <CardContent>
-                <Typography className={classes.pos} color="textSecondary">
-                    No Data Available
-                </Typography>
-            </CardContent>
-        </Card>
-    )
+  }
+
+  if (props.report.report.length > 0) {
+    return (<Card className={classes.root}>
+      <CardContent>
+        <Typography className={classes.title} color='textSecondary' gutterBottom>
+          Validator Report:
+        </Typography>
+        {scoringPeriodText.split('\n').map((i, key) => <Typography key={key} className={classes.pos}
+                                                                   color='textSecondary'>{i}</Typography>)}
+      </CardContent>
+      <CardActions>
+        <Button onClick={copyValidatorStatistics} size='small'>Copy to clipboard</Button>
+      </CardActions>
+    </Card>)
+  }
+  return (
+    <Card className={classes.root}>
+      <CardContent>
+        <Typography className={classes.pos} color='textSecondary'>
+          No Data Available
+        </Typography>
+      </CardContent>
+    </Card>
+  )
 }
 
 export default ValidatorReport

+ 4 - 4
src/components/ValidatorReport/debug.ts

@@ -1,11 +1,11 @@
-import moment from 'moment';
+import moment from 'moment'
 
-moment.defaultFormat = 'YYYY-MM-DD HH:mm:ss';
+moment.defaultFormat = 'YYYY-MM-DD HH:mm:ss'
 
 export const log = (...values: any[]) => {
-    // console.log(`[${moment().format()}]:`, ...values);
+  // console.log(`[${moment().format()}]:`, ...values);
 }
 
 export const error = (...values: any[]) => {
-    console.error(`[${moment().format()}]:`, ...values);
+  console.error(`[${moment().format()}]:`, ...values)
 }

+ 10 - 10
src/components/ValidatorReport/get-status.ts

@@ -1,28 +1,28 @@
-import { log } from "./debug";
-import { JoyApi } from "./joyApi";
-import { EraStatus } from "./Types";
-import { PromiseAllObj } from "./utils";
+import { log } from './debug'
+import { JoyApi } from './joyApi'
+import { EraStatus } from './Types'
+import { PromiseAllObj } from './utils'
 
-const api = new JoyApi();
+const api = new JoyApi()
 
 export async function getChainState() {
-  await api.init;
+  await api.init
 
   const status = await PromiseAllObj({
     totalIssuance: await api.totalIssuance(),
     finalizedBlockHeight: await api.finalizedBlockHeight(),
     validators: await api.validatorsData(),
     system: await api.systemData(),
-  });
+  })
 
   log(status)
-  return status;
+  return status
 }
 
 export async function getValidatorStatistics(address: string, blockStart: number): Promise<EraStatus> {
-  await api.init;
+  await api.init
   const status = await PromiseAllObj({
-    status: await api.getActiveErasForBlock(address, blockStart)
+    status: await api.getActiveErasForBlock(address, blockStart),
   })
   return status as unknown as EraStatus
 }

+ 53 - 52
src/components/ValidatorReport/joyApi.ts

@@ -1,42 +1,43 @@
-import { WsProvider, ApiPromise } from "@polkadot/api";
-import { types } from "@joystream/types";
-import { AccountId, Hash } from "@polkadot/types/interfaces";
-import { config } from "dotenv";
-import BN from "bn.js";
-import { Option, Vec } from "@polkadot/types";
-import { log } from "./debug"
-import { ActiveEra } from "./Types";
+import { WsProvider, ApiPromise } from '@polkadot/api'
+import { types } from '@joystream/types'
+import { AccountId, Hash } from '@polkadot/types/interfaces'
+import { config } from 'dotenv'
+import BN from 'bn.js'
+import { Option, Vec } from '@polkadot/types'
+import { log } from './debug'
+import { ActiveEra } from './Types'
 
-config();
+config()
 
 export class JoyApi {
-  endpoint: string;
-  isReady: Promise<ApiPromise>;
-  api!: ApiPromise;
+  endpoint: string
+  isReady: Promise<ApiPromise>
+  api!: ApiPromise
 
   constructor(endpoint?: string) {
-    const wsEndpoint = endpoint || process.env.REACT_APP_WS_PROVIDER || "ws://127.0.0.1:9944";
-    this.endpoint = wsEndpoint;
+    const wsEndpoint = endpoint || process.env.REACT_APP_WS_PROVIDER || 'ws://127.0.0.1:9944'
+    this.endpoint = wsEndpoint
     this.isReady = (async () => {
       const api = await new ApiPromise({ provider: new WsProvider(wsEndpoint), types })
-        .isReadyOrError;
-      return api;
-    })();
+        .isReadyOrError
+      return api
+    })()
   }
+
   get init(): Promise<JoyApi> {
     return this.isReady.then((instance) => {
-      this.api = instance;
-      return this;
-    });
+      this.api = instance
+      return this
+    })
   }
 
   async totalIssuance(blockHash?: Hash) {
     const issuance =
       blockHash === undefined
         ? await this.api.query.balances.totalIssuance()
-        : await this.api.query.balances.totalIssuance.at(blockHash);
+        : await this.api.query.balances.totalIssuance.at(blockHash)
 
-    return issuance.toNumber();
+    return issuance.toNumber()
   }
 
   async systemData() {
@@ -44,35 +45,35 @@ export class JoyApi {
       this.api.rpc.system.chain(),
       this.api.rpc.system.name(),
       this.api.rpc.system.version(),
-    ]);
+    ])
 
     return {
       chain: chain.toString(),
       nodeName: nodeName.toString(),
       nodeVersion: nodeVersion.toString(),
-    };
+    }
   }
 
   async finalizedHash() {
-    return this.api.rpc.chain.getFinalizedHead();
+    return this.api.rpc.chain.getFinalizedHead()
   }
 
   async finalizedBlockHeight() {
-    const finalizedHash = await this.finalizedHash();
-    const { number } = await this.api.rpc.chain.getHeader(`${finalizedHash}`);
-    return number.toNumber();
+    const finalizedHash = await this.finalizedHash()
+    const { number } = await this.api.rpc.chain.getHeader(`${finalizedHash}`)
+    return number.toNumber()
   }
 
   async getActiveErasForBlock(address: string, blockStart: number): Promise<ActiveEra[] | undefined> {
-    const stash = address;
-    const startHash = (await this.api.rpc.chain.getBlockHash(blockStart));
-    const startEra = (await this.api.query.staking.activeEra.at(startHash)).unwrap().index.toNumber();
-    const startTimestamp = new Date((await this.api.query.timestamp.now.at(startHash)).toNumber()).toISOString();
+    const stash = address
+    const startHash = (await this.api.rpc.chain.getBlockHash(blockStart))
+    const startEra = (await this.api.query.staking.activeEra.at(startHash)).unwrap().index.toNumber()
+    const startTimestamp = new Date((await this.api.query.timestamp.now.at(startHash)).toNumber()).toISOString()
     const eraPoints = await this.api.query.staking.erasRewardPoints.at(startHash, startEra)
     let data = undefined
     eraPoints.individual.forEach((points, author) => {
-      log(`Author Points [${author}]`);
-      log(`Individual Points [${points}]`);
+      log(`Author Points [${author}]`)
+      log(`Individual Points [${points}]`)
       if (author.toString() === stash) {
         const pn = Number(points.toBigInt())
         const activeEra: ActiveEra = {
@@ -81,49 +82,49 @@ export class JoyApi {
           hash: startHash.toString(),
           block: blockStart,
           date: startTimestamp,
-          points: pn
+          points: pn,
         }
-        log(`Era [${activeEra.era}], Block [${activeEra.block}], Date [${activeEra.date}], Points [${activeEra.points}], Hash [${activeEra.hash}]`);
+        log(`Era [${activeEra.era}], Block [${activeEra.block}], Date [${activeEra.date}], Points [${activeEra.points}], Hash [${activeEra.hash}]`)
         data = activeEra
       }
-    });
+    })
     return data
   }
 
   async findActiveValidators(hash: Hash, searchPreviousBlocks: boolean): Promise<AccountId[]> {
-    const block = await this.api.rpc.chain.getBlock(hash);
+    const block = await this.api.rpc.chain.getBlock(hash)
 
-    let currentBlockNr = block.block.header.number.toNumber();
-    let activeValidators;
+    let currentBlockNr = block.block.header.number.toNumber()
+    let activeValidators
     do {
-      let currentHash = (await this.api.rpc.chain.getBlockHash(currentBlockNr)) as Hash;
-      let allValidators = await this.api.query.staking.snapshotValidators.at(currentHash) as Option<Vec<AccountId>>;
+      let currentHash = (await this.api.rpc.chain.getBlockHash(currentBlockNr)) as Hash
+      let allValidators = await this.api.query.staking.snapshotValidators.at(currentHash) as Option<Vec<AccountId>>
       if (!allValidators.isEmpty) {
-        let max = (await this.api.query.staking.validatorCount.at(currentHash)).toNumber();
-        activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
+        let max = (await this.api.query.staking.validatorCount.at(currentHash)).toNumber()
+        activeValidators = Array.from(allValidators.unwrap()).slice(0, max)
       }
 
       if (searchPreviousBlocks) {
-        --currentBlockNr;
+        --currentBlockNr
       } else {
-        ++currentBlockNr;
+        ++currentBlockNr
       }
 
-    } while (activeValidators === undefined);
-    return activeValidators;
+    } while (activeValidators === undefined)
+    return activeValidators
   }
 
   async validatorsData() {
-    const validators = await this.api.query.session.validators();
-    const era = await this.api.query.staking.currentEra();
+    const validators = await this.api.query.session.validators()
+    const era = await this.api.query.staking.currentEra()
     const totalStake = era.isSome ?
       await this.api.query.staking.erasTotalStake(era.unwrap())
-      : new BN(0);
+      : new BN(0)
 
     return {
       count: validators.length,
       validators: validators.toJSON(),
       total_stake: totalStake.toNumber(),
-    };
+    }
   }
 }

+ 4 - 4
src/components/ValidatorReport/utils.ts

@@ -1,5 +1,5 @@
 const fromEntries = (xs: [string | number | symbol, any][]) =>
-  xs.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
+  xs.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
 
 export function PromiseAllObj(obj: {
   [k: string]: any;
@@ -8,7 +8,7 @@ export function PromiseAllObj(obj: {
     Object.entries(obj).map(([key, val]) =>
       val instanceof Promise
         ? val.then((res) => [key, res])
-        : new Promise((res) => res([key, val]))
-    )
-  ).then((res: any[]) => fromEntries(res));
+        : new Promise((res) => res([key, val])),
+    ),
+  ).then((res: any[]) => fromEntries(res))
 }