diff --git a/packages/app-staking/src/Query/Validator.tsx b/packages/app-staking/src/Query/Validator.tsx new file mode 100644 index 0000000000..77a1cf6dd0 --- /dev/null +++ b/packages/app-staking/src/Query/Validator.tsx @@ -0,0 +1,79 @@ +// Copyright 2017-2019 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { I18nProps } from '@polkadot/react-components/types'; +import { SessionIndex } from '@polkadot/types/interfaces'; + +import BN from 'bn.js'; +import React, { useEffect, useState } from 'react'; +import { Chart } from '@polkadot/react-components'; +import { withCalls } from '@polkadot/react-api'; +import { formatNumber } from '@polkadot/util'; + +import translate from '../translate'; + +interface Props extends I18nProps { + blockCounts?: BN[]; + className?: string; + currentIndex: SessionIndex; + validatorId: string; +} + +interface Value { + label: string; + value: BN; +} + +function getIndexRange (currentIndex: SessionIndex): BN[] { + const range: BN[] = []; + let thisIndex: BN = currentIndex; + + while (thisIndex.gtn(0) && range.length < 50) { + range.push(thisIndex); + + thisIndex = thisIndex.subn(1); + } + + return range.reverse(); +} + +function Validator ({ blockCounts, className, currentIndex, t }: Props): React.ReactElement { + const [labels, setLabels] = useState([]); + const [values, setValues] = useState([]); + + useEffect((): void => { + setLabels( + getIndexRange(currentIndex).map((index): string => formatNumber(index)) + ); + }, [currentIndex]); + + useEffect((): void => { + if (blockCounts) { + setValues( + blockCounts.map((value, index): Value => ({ + label: labels[index], + value + })) + ); + } + }, [blockCounts, labels]); + + return ( +
+

{t('blocks per session')}

+ +
+ ); +} + +export default translate( + withCalls( + ['query.imOnline.authoredBlocks', { + isMulti: true, + propName: 'blockCounts', + paramPick: ({ currentIndex, validatorId }: Props): [BN, string][] => + getIndexRange(currentIndex).map((index): [BN, string] => [index, validatorId]) + }] + )(Validator) +); diff --git a/packages/app-staking/src/Query/index.tsx b/packages/app-staking/src/Query/index.tsx new file mode 100644 index 0000000000..10436c07eb --- /dev/null +++ b/packages/app-staking/src/Query/index.tsx @@ -0,0 +1,37 @@ +// Copyright 2017-2019 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { BareProps } from '@polkadot/react-components/types'; +import { ComponentProps } from '../types'; + +import React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import Validator from './Validator'; + +interface Props extends BareProps, ComponentProps, RouteComponentProps<{}> { + match: { + isExact: boolean; + params: { + value: string; + }; + path: string; + url: string; + }; +} + +function Query ({ stakingOverview, match: { params: { value } } }: Props): React.ReactElement { + if (!stakingOverview) { + return
loading
; + } + + return ( + + ); +} + +export default withRouter(Query); diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index bc4c2f6f43..3276835a16 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -21,6 +21,7 @@ import accountObservable from '@polkadot/ui-keyring/observable/accounts'; import Accounts from './Actions/Accounts'; import basicMd from './md/basic.md'; import Overview from './Overview'; +import Query from './Query'; import translate from './translate'; interface Props extends AppProps, ApiProps, I18nProps { @@ -80,6 +81,7 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle + diff --git a/packages/app-staking/src/types.ts b/packages/app-staking/src/types.ts index a98590ab82..2cf6cf7af7 100644 --- a/packages/app-staking/src/types.ts +++ b/packages/app-staking/src/types.ts @@ -4,17 +4,9 @@ import { DerivedFees, DerivedBalances, DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import { EraPoints } from '@polkadot/types/interfaces'; -import { u32 } from '@polkadot/types'; export type Nominators = Record; -export interface DerivedStakingOverviewExt { - currentElected: string[]; - eraPointsEarned: EraPoints; - validatorCount: u32; -} - export interface ComponentProps { allAccounts?: SubjectInfo; allControllers: string[]; diff --git a/packages/react-components/src/Chart/Line.tsx b/packages/react-components/src/Chart/Line.tsx new file mode 100644 index 0000000000..88872c5b2d --- /dev/null +++ b/packages/react-components/src/Chart/Line.tsx @@ -0,0 +1,109 @@ +// Copyright 2017-2019 @polkadot/react-components authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { BareProps } from '../types'; + +import BN from 'bn.js'; +import React, { useEffect, useState } from 'react'; +import ChartJs from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import { bnToBn } from '@polkadot/util'; + +interface Value { + label: string; + value: number | BN; +} + +interface Props extends BareProps { + aspectRatio?: number; + values: Value[]; +} + +interface State { + chartData?: ChartJs.ChartData; + chartOptions?: ChartJs.ChartOptions; + jsonValues?: string; +} + +interface Config { + labels: string[]; + datasets: { + data: number[]; + fill: boolean; + backgroundColor: string; + borderColor: string; + hoverBackgroundColor: string; + }[]; +} + +const alphaColor = (hexColor: string): string => + ChartJs.helpers.color(hexColor).alpha(0.65).rgbString(); + +function calculateOptions (aspectRatio: number, values: Value[], jsonValues: string): State { + const chartData = values.reduce((data, { label, value }): Config => { + const dataset = data.datasets[0]; + + dataset.data.push(bnToBn(value).toNumber()); + data.labels.push(label); + + return data; + }, { + labels: [] as string[], + datasets: [{ + data: [] as number[], + fill: false, + backgroundColor: alphaColor('#ff8c00'), + borderColor: alphaColor('#ff8c00'), + hoverBackgroundColor: alphaColor('#ff8c00') + }] + }); + + return { + chartData, + chartOptions: { + // width/height by default this is "1", i.e. a square box + aspectRatio, + // no need for the legend, expect the labels contain everything + legend: { + display: false + }, + scales: { + xAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + }, + jsonValues + }; +} + +export default function LineChart ({ aspectRatio = 4, className, style, values }: Props): React.ReactElement | null { + const [{ chartData, chartOptions, jsonValues }, setState] = useState({}); + + useEffect((): void => { + const newJsonValues = JSON.stringify(values); + + if (newJsonValues !== jsonValues) { + setState(calculateOptions(aspectRatio, values, newJsonValues)); + } + }, [values]); + + if (!chartData) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react-components/src/Chart/index.ts b/packages/react-components/src/Chart/index.ts index 704e0ff784..882db3970b 100644 --- a/packages/react-components/src/Chart/index.ts +++ b/packages/react-components/src/Chart/index.ts @@ -4,8 +4,10 @@ import Doughnut from './Doughnut'; import HorizBar from './HorizBar'; +import Line from './Line'; export default { Doughnut, - HorizBar + HorizBar, + Line };