Skip to content

Commit

Permalink
Refactor listing groups to read feeds from redux store
Browse files Browse the repository at this point in the history
- Simplifies feed + answer matching
- Decouples listing + grid item components
  • Loading branch information
rupurt committed Apr 3, 2020
1 parent 092c400 commit e2ec8e9
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 241 deletions.
1 change: 1 addition & 0 deletions feeds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@chainlink/ts-helpers": "0.0.1",
"antd": "^3.23.3",
"classnames": "^2.2.6",
"core-js": "^3.6.4",
"d3": "^5.11.0",
"eslint": "^6.6.0",
"ethers": "^4.0.45",
Expand Down
48 changes: 48 additions & 0 deletions feeds/src/components/listing/GridItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/react'
import { Provider as ReduxProvider } from 'react-redux'
import { partialAsFull } from '@chainlink/ts-helpers'
import { FeedConfig } from 'feeds'
import { ListingAnswer } from '../../state/ducks/listing/operations'
import createStore from '../../state/createStore'
import { GridItem } from './GridItem'

const AllTheProviders: React.FC = ({ children }) => {
const { store } = createStore()

return (
<ReduxProvider store={store}>
<MemoryRouter>{children}</MemoryRouter>
</ReduxProvider>
)
}

const feed = partialAsFull<FeedConfig>({
name: 'pair name',
path: '/link',
valuePrefix: 'prefix',
sponsored: ['sponsor 1', 'sponsor 2'],
})
const listingAnswer: ListingAnswer = {
answer: '10.1',
config: feed,
}

describe('components/listing/GridItem', () => {
it('renders answer value with prefix', () => {
const { container } = render(
<AllTheProviders>
<GridItem
feed={feed}
listingAnswer={listingAnswer}
enableHealth={false}
/>
</AllTheProviders>,
)

expect(container).toHaveTextContent('10.1')
expect(container).toHaveTextContent('prefix')
})
})
75 changes: 47 additions & 28 deletions feeds/src/components/listing/GridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,34 @@ import { connect, MapStateToProps } from 'react-redux'
import { Col, Popover, Tooltip } from 'antd'
import classNames from 'classnames'
import { AppState } from 'state'
import { FeedConfig } from 'feeds'
import { listingSelectors } from '../../state/ducks/listing'
import { ListingAnswer } from 'state/ducks/listing/operations'
import { HealthCheck } from 'state/ducks/listing/reducers'

interface StateProps {
healthCheck: any
healthCheck?: HealthCheck
listingAnswer?: ListingAnswer
}

interface OwnProps {
item: any
feed: FeedConfig
compareOffchain?: boolean
enableHealth: boolean
}

interface Props extends StateProps, OwnProps {}

interface Status {
result: string
errors: string[]
}

const GRID = { xs: 24, sm: 12, md: 8 }

const GridItem: React.FC<Props> = ({
item,
export const GridItem: React.FC<Props> = ({
feed,
listingAnswer,
compareOffchain,
enableHealth,
healthCheck,
}) => {
const status = normalizeStatus(item, healthCheck)
const status = normalizeStatus(feed, listingAnswer, healthCheck)
const tooltipErrors = status.errors.join(', ')
const title = `${status.result}${tooltipErrors}`
const classes = classNames(
Expand All @@ -39,27 +40,27 @@ const GridItem: React.FC<Props> = ({
)
const gridItem = (
<div className={classes}>
{compareOffchain && <CompareOffchain item={item} />}
{compareOffchain && <CompareOffchain feed={feed} />}
<Link
to={item.config.path}
to={feed.path}
onClick={scrollToTop}
className="listing-grid__item--link"
>
<div className="listing-grid__item--name">{item.config.name}</div>
<div className="listing-grid__item--name">{feed.name}</div>
<div className="listing-grid__item--answer">
{item.answer && (
{listingAnswer && (
<>
{item.config.valuePrefix} {item.answer}
{feed.valuePrefix} {listingAnswer.answer}
</>
)}
</div>
{item.config.sponsored.length > 0 && (
{feed.sponsored && feed.sponsored.length > 0 && (
<>
<div className="listing-grid__item--sponsored-title">
Sponsored by
</div>
<div className="listing-grid__item--sponsored">
<Sponsored data={item.config.sponsored} />
<Sponsored data={feed.sponsored} />
</div>
</>
)}
Expand All @@ -74,12 +75,16 @@ const GridItem: React.FC<Props> = ({
)
}

function CompareOffchain({ item }: any) {
interface CompareOffchainProps {
feed: FeedConfig
}

function CompareOffchain({ feed }: CompareOffchainProps) {
let content: any = 'No offchain comparison'

if (item.config.compare_offchain) {
if (feed.compareOffchain) {
content = (
<a href={item.config.compare_offchain} rel="noopener noreferrer">
<a href={feed.compareOffchain} rel="noopener noreferrer">
Compare Offchain
</a>
)
Expand Down Expand Up @@ -122,6 +127,11 @@ function scrollToTop() {
window.scrollTo(0, 0)
}

interface Status {
result: string
errors: string[]
}

function healthClasses(status: Status, enableHeath: boolean) {
if (!enableHeath) {
return
Expand All @@ -136,20 +146,24 @@ function healthClasses(status: Status, enableHeath: boolean) {
return 'listing-grid__item--health-ok'
}

function normalizeStatus(item: any, healthCheck: any): Status {
function normalizeStatus(
feed: FeedConfig,
listingAnswer?: ListingAnswer,
healthCheck?: HealthCheck,
): Status {
const errors: string[] = []

if (item.answer === undefined || healthCheck === undefined) {
if (listingAnswer === undefined || healthCheck === undefined) {
return { result: 'unknown', errors }
}

const thresholdDiff = healthCheck.currentPrice * (item.config.threshold / 100)
const answer = parseFloat(listingAnswer.answer)
const thresholdDiff = healthCheck.currentPrice * (feed.threshold / 100)
const thresholdMin = Math.max(healthCheck.currentPrice - thresholdDiff, 0)
const thresholdMax = healthCheck.currentPrice + thresholdDiff
const withinThreshold =
item.answer >= thresholdMin && item.answer < thresholdMax
const withinThreshold = answer >= thresholdMin && answer < thresholdMax

if (item.answer === 0) {
if (answer === 0) {
errors.push('answer price is 0')
}
if (!withinThreshold) {
Expand All @@ -169,9 +183,14 @@ const mapStateToProps: MapStateToProps<StateProps, OwnProps, AppState> = (
state,
ownProps,
) => {
const contractAddress = ownProps.item.config.contractAddress
const contractAddress = ownProps.feed.contractAddress
const listingAnswer = listingSelectors.answer(state, contractAddress)
const healthCheck = state.listing.healthChecks[contractAddress]
return { healthCheck }

return {
listingAnswer,
healthCheck,
}
}

export default connect(mapStateToProps)(GridItem)
115 changes: 43 additions & 72 deletions feeds/src/components/listing/Listing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { render } from '@testing-library/react'
import { Provider as ReduxProvider } from 'react-redux'
import createStore from '../../state/createStore'
import { Listing } from './Listing'
import { ListingGroup } from 'state/ducks/listing/selectors'
import { FeedConfig } from 'feeds'

const AllTheProviders: React.FC = ({ children }) => {
const { store } = createStore()
Expand All @@ -16,61 +18,48 @@ const AllTheProviders: React.FC = ({ children }) => {
)
}

const groupMock = [
{
name: 'List 1',
list: [
{
answer: 'answer',
config: {
name: 'pair name 1',
path: '/link',
valuePrefix: 'prefix ',
sponsored: ['sponsor 1', 'sponsor 2'],
},
},
{
answer: 'answer2',
config: {
name: 'pair name 2',
path: '/link2',
valuePrefix: 'prefix2',
sponsored: ['sponsor 1', 'sponsor 2'],
},
},
],
},
{
name: 'List 2',
list: [
{
answer: 'answer',
config: {
name: 'pair name 3',
path: '/link',
valuePrefix: 'prefix',
sponsored: ['sponsor 1', 'sponsor 2'],
},
},
{
answer: 'answer2',
config: {
name: 'pair name 4',
path: '/link2',
valuePrefix: 'prefix2',
sponsored: ['sponsor 1', 'sponsor 2'],
},
},
],
},
]
const listingGroup1: ListingGroup = {
name: 'List 1',
feeds: [
{
name: 'pair name 1',
path: '/link',
valuePrefix: 'prefix ',
sponsored: ['sponsor 1', 'sponsor 2'],
} as FeedConfig,
{
name: 'pair name 2',
path: '/link2',
valuePrefix: 'prefix2',
sponsored: ['sponsor 1', 'sponsor 2'],
} as FeedConfig,
],
}
const listingGroup2 = {
name: 'List 2',
feeds: [
{
name: 'pair name 3',
path: '/link',
valuePrefix: 'prefix',
sponsored: ['sponsor 1', 'sponsor 2'],
} as FeedConfig,
{
name: 'pair name 4',
path: '/link2',
valuePrefix: 'prefix2',
sponsored: ['sponsor 1', 'sponsor 2'],
} as FeedConfig,
],
}
const listingGroups: ListingGroup[] = [listingGroup1, listingGroup2]

describe('components/listing/Listing.component', () => {
describe('components/listing/Listing', () => {
it('renders the name from a list of groups', () => {
const { container } = render(
<AllTheProviders>
<Listing
groups={groupMock}
groups={listingGroups}
fetchAnswers={() => {}}
fetchHealthStatus={() => {}}
enableHealth={false}
Expand All @@ -82,11 +71,11 @@ describe('components/listing/Listing.component', () => {
expect(container).toHaveTextContent('List 2 Pairs')
})

it('should renders pair name value', () => {
it('renders pair name value', () => {
const { container } = render(
<AllTheProviders>
<Listing
groups={groupMock}
groups={listingGroups}
fetchAnswers={() => {}}
fetchHealthStatus={() => {}}
enableHealth={false}
Expand All @@ -100,29 +89,11 @@ describe('components/listing/Listing.component', () => {
expect(container).toHaveTextContent('pair name 4')
})

it('should renders answer value with prefix', () => {
const { container } = render(
<AllTheProviders>
<Listing
groups={groupMock}
fetchAnswers={() => {}}
fetchHealthStatus={() => {}}
enableHealth={false}
/>
</AllTheProviders>,
)

expect(container).toHaveTextContent('prefix')
expect(container).toHaveTextContent('answer')
expect(container).toHaveTextContent('prefix answer')
expect(container).toHaveTextContent('prefix2 answer2')
})

it('should renders sponsored names', () => {
it('renders sponsored names', () => {
const { container } = render(
<AllTheProviders>
<Listing
groups={groupMock}
groups={listingGroups}
fetchAnswers={() => {}}
fetchHealthStatus={() => {}}
enableHealth={false}
Expand Down
Loading

0 comments on commit e2ec8e9

Please sign in to comment.