From aab4795cebb4c021583a749bb473de3f9a95383d Mon Sep 17 00:00:00 2001 From: Manuel Calavera Date: Fri, 26 Jul 2019 08:00:24 -0700 Subject: [PATCH] feat: validators search and details page update --- src/v2/@types/validator.js | 36 ++++++++ .../NetworkOverview/StatCards/index.jsx | 20 ++--- .../NetworkOverview/StatCards/styles.js | 13 +-- src/v2/components/Header/index.jsx | 3 +- src/v2/components/Search/index.jsx | 85 ++++++++++++++----- src/v2/components/Search/result.jsx | 41 +++++++++ src/v2/components/Search/styles.js | 26 ++++++ src/v2/components/Validators/Detail/index.jsx | 46 +++++++--- src/v2/components/Validators/Detail/styles.js | 12 +++ src/v2/stores/nodes.js | 6 +- 10 files changed, 233 insertions(+), 55 deletions(-) create mode 100644 src/v2/@types/validator.js create mode 100644 src/v2/components/Search/result.jsx diff --git a/src/v2/@types/validator.js b/src/v2/@types/validator.js new file mode 100644 index 00000000..a2ab1a22 --- /dev/null +++ b/src/v2/@types/validator.js @@ -0,0 +1,36 @@ +// @flow + +export type Identity = { + avatarUrl: string, + details: string, + keybaseUsername: string, + name: string, + pubkey: string, + verified: string, + verifyUrl: string, + website: string, +}; + +export type UptimeItem = { + creditsEarned: number, + epoch: number, + percentage: string, + slotsInEpoch: number, +}; + +export type Uptime = { + lat: number, + ts: number, + votePubkey: string, + uptime: UptimeItem[], +}; + +export type Validator = { + commission: number, + coordinated: [number, number], + gossip: string, + identity: Identity, + nodePubkey: string, + stake: number, + uptime: Uptime, +}; diff --git a/src/v2/components/Dashboard/NetworkOverview/StatCards/index.jsx b/src/v2/components/Dashboard/NetworkOverview/StatCards/index.jsx index 52545bcf..bf7a4514 100644 --- a/src/v2/components/Dashboard/NetworkOverview/StatCards/index.jsx +++ b/src/v2/components/Dashboard/NetworkOverview/StatCards/index.jsx @@ -1,6 +1,7 @@ // @flow import React from 'react'; +import {Link} from 'react-router-dom'; import {observer} from 'mobx-react-lite'; import {map} from 'lodash/fp'; import {Grid, Typography} from '@material-ui/core'; @@ -11,38 +12,35 @@ import NodesStore from 'v2/stores/nodes'; import useStyles from './styles'; const StatCards = () => { - const {globalStats, statsChanges} = OverviewStore; - const {cluster, clusterChanges} = NodesStore; + const {globalStats} = OverviewStore; + const {cluster} = NodesStore; const classes = useStyles(); const cards = [ { title: 'Node Count', value: cluster.nodes.length, - changes: clusterChanges.nodes, }, { title: 'Block Height', value: globalStats['!blkLastSlot'], - changes: statsChanges['!blkLastSlot'], }, { title: 'Transactions Count', value: globalStats['!txnCount'], - changes: statsChanges['!txnCount'], }, { title: 'Current Leader', value() { return ( - - {globalStats['!entLastLeader']} - + + {globalStats['!entLastLeader']} + + ); }, }, diff --git a/src/v2/components/Dashboard/NetworkOverview/StatCards/styles.js b/src/v2/components/Dashboard/NetworkOverview/StatCards/styles.js index 8d71892f..7498a0fc 100644 --- a/src/v2/components/Dashboard/NetworkOverview/StatCards/styles.js +++ b/src/v2/components/Dashboard/NetworkOverview/StatCards/styles.js @@ -16,11 +16,14 @@ export default makeStyles(theme => ({ margin: '20px 0', }, leader: { - fontSize: 20, - fontWeight: 'bold', - color: getColor('main')(theme), - marginTop: 40, - letterSpacing: 3.4, + textDecoration: 'none', + '& h2': { + fontSize: 20, + fontWeight: 'bold', + color: getColor('main')(theme), + marginTop: 40, + letterSpacing: 3.4, + } }, changes: { fontSize: 18, diff --git a/src/v2/components/Header/index.jsx b/src/v2/components/Header/index.jsx index 066f7afc..2895850d 100644 --- a/src/v2/components/Header/index.jsx +++ b/src/v2/components/Header/index.jsx @@ -18,7 +18,6 @@ const Header = () => { const [isDrawerOpen, setDrawerOpen] = useState(false); const {endpointName, updateEndpointName} = socketActions; const classes = useStyles(); - const onSearch = () => {}; const handleEndpointChange = (event: SyntheticEvent) => { const endpointName = event.currentTarget.value; EndpointConfig.setEndpointName(endpointName); @@ -50,7 +49,7 @@ const Header = () => {
- +

Real-time updated:

diff --git a/src/v2/components/Search/index.jsx b/src/v2/components/Search/index.jsx index 8cef8183..7b4365a3 100644 --- a/src/v2/components/Search/index.jsx +++ b/src/v2/components/Search/index.jsx @@ -1,40 +1,79 @@ // @flow import React, {useState} from 'react'; +import {observer} from 'mobx-react-lite'; +import {compose, get, filter, lowerCase, contains} from 'lodash/fp'; import {InputBase, IconButton} from '@material-ui/core'; import {Search as SearchIcon} from '@material-ui/icons'; +import NodesStore from 'v2/stores/nodes'; +import SearchResult from './result'; import useStyles from './styles'; -type SearchProps = { - onSubmit: (val: string) => void, -}; - -const Search = ({onSubmit}: SearchProps) => { +const Search = () => { const classes = useStyles(); + const {validators} = NodesStore; const [value, setValue] = useState(''); + const [isDirty, setDirty] = useState(false); + const [isFocus, setFocus] = useState(false); + const [searchResult, setSearchResult] = useState([]); + + const onFocus = () => setFocus(true); + const onBlur = () => { + setTimeout(() => { + setFocus(false); + }, 250); + }; + + const handleClear = () => { + setDirty(false); + setValue(''); + setSearchResult([]); + }; + const handleSearch = ({currentTarget}: SyntheticEvent) => { + if (!currentTarget.value) { + handleClear(); + return; + } setValue(currentTarget.value); + const filteredValidators = filter(v => { + const lowerVal = lowerCase(value); + return ( + compose(contains(lowerVal), lowerCase, get('nodePubkey'))(v) || + compose(contains(lowerVal), lowerCase, get('identity.keybaseUsername'))(v) + ); + })(validators); + setDirty(true); + setSearchResult(filteredValidators); }; - const handleSubmit = () => { - onSubmit(value); - }; + return ( -
- +
+ + + + +
+ - - - - +
); }; -export default Search; +export default observer(Search); diff --git a/src/v2/components/Search/result.jsx b/src/v2/components/Search/result.jsx new file mode 100644 index 00000000..ca2149bb --- /dev/null +++ b/src/v2/components/Search/result.jsx @@ -0,0 +1,41 @@ +// @flow +import React, {memo} from 'react'; +import {map} from 'lodash/fp'; +import {Link} from 'react-router-dom'; +import type {Validator} from 'v2/@types/validator'; + +import useStyles from './styles'; + +type SearchResultProps = { + items: Validator[], + onClear: () => void, + isDirty: boolean, + isFocus: boolean, +}; + +const SearchResult = ({isDirty, isFocus, items, onClear}: SearchResultProps) => { + const classes = useStyles(); + const renderItem = ({nodePubkey}: {nodePubkey: string}) => ( +
  • + + {nodePubkey} + +
  • + ); + if ((!isDirty && !items.length ) || !isFocus) { + return null; + } + + return ( +
    +
    + {!items.length + ? 'There were no results for this search term.' + : 'Validators'} +
    +
      {map(renderItem)(items)}
    +
    + ); +}; + +export default memo(SearchResult); diff --git a/src/v2/components/Search/styles.js b/src/v2/components/Search/styles.js index dce091dc..3ccd03be 100644 --- a/src/v2/components/Search/styles.js +++ b/src/v2/components/Search/styles.js @@ -4,6 +4,9 @@ import getColor from 'v2/utils/getColor'; export default makeStyles(theme => { return { root: { + position: 'relative', + }, + form: { border: `1px solid ${getColor('grey')(theme)}`, padding: 5, display: 'flex', @@ -20,5 +23,28 @@ export default makeStyles(theme => { borderRadius: 0, width: 40, }, + list: { + position: 'absolute', + background: getColor('white')(theme), + width: '100%', + padding: '12px 20px', + '& ul': { + padding: 0, + margin: 0, + '& a': { + color: getColor('grey3')(theme), + fontSize: 12, + textDecoration: 'none', + padding: '5px 0', + display: 'block', + '&:hover': { + color: getColor('dark')(theme) + } + } + }, + }, + title: { + color: getColor('dark')(theme) + } }; }); diff --git a/src/v2/components/Validators/Detail/index.jsx b/src/v2/components/Validators/Detail/index.jsx index 5e2c248b..70bf6a51 100644 --- a/src/v2/components/Validators/Detail/index.jsx +++ b/src/v2/components/Validators/Detail/index.jsx @@ -75,9 +75,20 @@ const ValidatorsDetail = ({match}: {match: Match}) => { const specs = [ { - label: 'Website', + label: 'Address', hint: '', - value: identity.website || '', + value() { + return ( +
    + {nodePubkey} + +
    + +
    +
    +
    + ); + }, }, { label: 'Voting power', @@ -85,9 +96,15 @@ const ValidatorsDetail = ({match}: {match: Match}) => { value: stake, }, { - label: 'Address', + label: 'Website', hint: '', - value: nodePubkey, + value() { + return identity.website ? ( + {identity.website} + ) : ( + '' + ); + }, }, { label: 'Missed blocks', @@ -97,7 +114,15 @@ const ValidatorsDetail = ({match}: {match: Match}) => { { label: 'keybase', hint: '', - value: identity.keybaseUsername || '', + value() { + return identity.keybaseUsername ? ( + + {identity.keybaseUsername} + + ) : ( + '' + ); + }, }, { label: 'commission', @@ -134,7 +159,9 @@ const ValidatorsDetail = ({match}: {match: Match}) => { {label} -
    {value}
    +
    + {typeof value === 'function' ? value() : value} +
    ); @@ -146,11 +173,6 @@ const ValidatorsDetail = ({match}: {match: Match}) => {
    {identity.name || nodePubkey} - -
    - -
    -
    )}
    @@ -159,7 +181,7 @@ const ValidatorsDetail = ({match}: {match: Match}) => { size="large" fullWidth color="primary" - href="#" + href="https://github.com/solana-labs/tour-de-sol#validator-public-key-registration" > Connect To Keybase diff --git a/src/v2/components/Validators/Detail/styles.js b/src/v2/components/Validators/Detail/styles.js index 10e53ad2..dfc4c9b1 100644 --- a/src/v2/components/Validators/Detail/styles.js +++ b/src/v2/components/Validators/Detail/styles.js @@ -87,5 +87,17 @@ export default makeStyles(theme => ({ lineHeight: '29px', overflow: 'hidden', textOverflow: 'ellipsis', + '& a': { + color: getColor('main')(theme), + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline' + } + } }, + address: { + display: 'flex', + alignItem: 'center', + color: getColor('main')(theme) + } })); diff --git a/src/v2/stores/nodes.js b/src/v2/stores/nodes.js index 2b430f14..a06ee3ac 100644 --- a/src/v2/stores/nodes.js +++ b/src/v2/stores/nodes.js @@ -58,9 +58,10 @@ class Store { get validators() { return map(vote => { - const {lng, lat, gossip} = find({pubkey: vote.nodePubkey})( + const cluster = find({pubkey: vote.nodePubkey})( this.cluster.cluster, - ); + ) || {}; + const {lng = 0, lat = 0, gossip} = cluster; return { ...vote, coordinates: [lng, lat], @@ -83,6 +84,7 @@ decorate(Store, { cluster: observable, updateClusterInfo: action.bound, mapMarkers: computed, + validators: computed, fetchClusterInfo: action.bound, });