Skip to content

Commit

Permalink
feat: new distributor home page
Browse files Browse the repository at this point in the history
Since we now plan to support more than 1 feeds manager/job distributor, we want to have a home page where it shows a list of registered feeds managers

JIRA: https://smartcontract-it.atlassian.net/browse/OPCORE-858
  • Loading branch information
graham-chainlink committed Sep 3, 2024
1 parent cfd4ce8 commit d74af46
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 64 deletions.
27 changes: 15 additions & 12 deletions src/hooks/queries/useFeedsManagersQuery.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { gql, useQuery } from '@apollo/client'
import { gql, QueryHookOptions, useQuery } from '@apollo/client'

export const FEEDS_MANAGERS_QUERY = gql`
fragment FetchFeedsManagersPayload_ResultsFields on FeedsManager {
__typename
id
name
uri
publicKey
isConnectionActive
createdAt
}
query FetchFeedsManagers {
feedsManagers {
results {
__typename
id
name
uri
publicKey
isConnectionActive
createdAt
...FetchFeedsManagersPayload_ResultsFields
}
}
}
`

export const useFeedsManagersQuery = () => {
return useQuery<FetchFeedsManagers, FetchFeedsManagersVariables>(
FEEDS_MANAGERS_QUERY,
)
export const useFeedsManagersQuery = (
options?: QueryHookOptions<FetchFeedsManagers>,
) => {
return useQuery<FetchFeedsManagers>(FEEDS_MANAGERS_QUERY, options)
}
47 changes: 47 additions & 0 deletions src/screens/FeedsManager/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react'
import green from '@material-ui/core/colors/green'
import red from '@material-ui/core/colors/red'
import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'
import Typography from '@material-ui/core/Typography'
import CancelIcon from '@material-ui/icons/Cancel'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'

const connectionStatusStyles = () => {
return createStyles({
root: {
display: 'flex',
},
connectedIcon: {
color: green[500],
},
disconnectedIcon: {
color: red[500],
},
text: {
marginLeft: 4,
},
})
}

interface ConnectionStatusProps
extends WithStyles<typeof connectionStatusStyles> {
isConnected: boolean
}

export const ConnectionStatus = withStyles(connectionStatusStyles)(
({ isConnected, classes }: ConnectionStatusProps) => {
return (
<div className={classes.root}>
{isConnected ? (
<CheckCircleIcon fontSize="small" className={classes.connectedIcon} />
) : (
<CancelIcon fontSize="small" className={classes.disconnectedIcon} />
)}

<Typography variant="body1" inline className={classes.text}>
{isConnected ? 'Connected' : 'Disconnected'}
</Typography>
</div>
)
},
)
55 changes: 5 additions & 50 deletions src/screens/FeedsManager/FeedsManagerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,22 @@
import React from 'react'

import CancelIcon from '@material-ui/icons/Cancel'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
import EditIcon from '@material-ui/icons/Edit'
import IconButton from '@material-ui/core/IconButton'
import Grid from '@material-ui/core/Grid'
import IconButton from '@material-ui/core/IconButton'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Menu from '@material-ui/core/Menu'
import EditIcon from '@material-ui/icons/Edit'
import MoreVertIcon from '@material-ui/icons/MoreVert'
import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'
import Typography from '@material-ui/core/Typography'
import green from '@material-ui/core/colors/green'
import red from '@material-ui/core/colors/red'

import { CopyIconButton } from 'src/components/Copy/CopyIconButton'
import {
DetailsCard,
DetailsCardItemTitle,
DetailsCardItemValue,
} from 'src/components/Cards/DetailsCard'
import { shortenHex } from 'src/utils/shortenHex'
import { CopyIconButton } from 'src/components/Copy/CopyIconButton'
import { MenuItemLink } from 'src/components/MenuItemLink'

const connectionStatusStyles = () => {
return createStyles({
root: {
display: 'flex',
},
connectedIcon: {
color: green[500],
},
disconnectedIcon: {
color: red[500],
},
text: {
marginLeft: 4,
},
})
}

interface ConnectionStatusProps
extends WithStyles<typeof connectionStatusStyles> {
isConnected: boolean
}

const ConnectionStatus = withStyles(connectionStatusStyles)(
({ isConnected, classes }: ConnectionStatusProps) => {
return (
<div className={classes.root}>
{isConnected ? (
<CheckCircleIcon fontSize="small" className={classes.connectedIcon} />
) : (
<CancelIcon fontSize="small" className={classes.disconnectedIcon} />
)}

<Typography variant="body1" inline className={classes.text}>
{isConnected ? 'Connected' : 'Disconnected'}
</Typography>
</div>
)
},
)
import { shortenHex } from 'src/utils/shortenHex'
import { ConnectionStatus } from './ConnectionStatus'

interface Props {
manager: FeedsManagerFields
Expand Down
40 changes: 40 additions & 0 deletions src/screens/JobDistributors/JobDistributorsRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'

import { withStyles, WithStyles } from '@material-ui/core/styles'
import TableCell from '@material-ui/core/TableCell'
import TableRow from '@material-ui/core/TableRow'

import Link from 'components/Link'
import { tableStyles } from 'components/Table'
import { CopyIconButton } from 'src/components/Copy/CopyIconButton'
import { shortenHex } from 'src/utils/shortenHex'
import { ConnectionStatus } from '../FeedsManager/ConnectionStatus'

interface Props extends WithStyles<typeof tableStyles> {
jobDistributor: FetchFeedsManagersPayload_ResultsFields
}

export const JobDistributorsRow = withStyles(tableStyles)(
({ jobDistributor, classes }: Props) => {
return (
<TableRow className={classes.row} hover>
<TableCell className={classes.cell} component="th" scope="row">
<Link
className={classes.link}
href={`/job_distributors/${jobDistributor.id}`}
>
{jobDistributor.name}
</Link>
</TableCell>
<TableCell>
<ConnectionStatus isConnected={jobDistributor.isConnectionActive} />
</TableCell>
<TableCell>
{shortenHex(jobDistributor.publicKey, { start: 6, end: 6 })}
<CopyIconButton data={jobDistributor.publicKey} />
</TableCell>
<TableCell>{jobDistributor.uri}</TableCell>
</TableRow>
)
},
)
74 changes: 74 additions & 0 deletions src/screens/JobDistributors/JobDistributorsScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react'

import { MockedProvider, MockedResponse } from '@apollo/client/testing'
import { GraphQLError } from 'graphql'
import { Route } from 'react-router-dom'
import { renderWithRouter, screen, within } from 'test-utils'

import { FEEDS_MANAGERS_QUERY } from 'src/hooks/queries/useFeedsManagersQuery'
import { buildFeedsManager } from 'support/factories/gql/fetchFeedsManagers'
import { waitForLoading } from 'support/test-helpers/wait'
import { JobDistributorsScreen } from './JobDistributorsScreen'

const { findAllByRole, findByText } = screen

function renderComponent(mocks: MockedResponse[]) {
renderWithRouter(
<>
<Route exact path="/job_distributors">
<MockedProvider mocks={mocks} addTypename={false}>
<JobDistributorsScreen />
</MockedProvider>
</Route>
</>,
{ initialEntries: ['/job_distributors'] },
)
}

describe('JobDistributorsScreen', () => {
it('should render the list of job distributors', async () => {
const mocks: MockedResponse[] = [
{
request: {
query: FEEDS_MANAGERS_QUERY,
},
result: {
data: {
feedsManagers: {
results: [buildFeedsManager()],
},
},
},
},
]

renderComponent(mocks)

await waitForLoading()

const rows = await findAllByRole('row')

// header counts as a row
expect(rows).toHaveLength(2)
expect(
within(rows[1]).getByText('Chainlink Feeds Manager'),
).toBeInTheDocument()
})

it('should renders GQL errors', async () => {
const mocks: MockedResponse[] = [
{
request: {
query: FEEDS_MANAGERS_QUERY,
},
result: {
errors: [new GraphQLError('Error!')],
},
},
]

renderComponent(mocks)

expect(await findByText('Error: Error!')).toBeInTheDocument()
})
})
24 changes: 24 additions & 0 deletions src/screens/JobDistributors/JobDistributorsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler'
import { Loading } from 'src/components/Feedback/Loading'
import { useFeedsManagersQuery } from 'src/hooks/queries/useFeedsManagersQuery'
import { JobDistributorsView } from './JobDistributorsView'

export const JobDistributorsScreen = () => {
const { data, loading, error } = useFeedsManagersQuery({
fetchPolicy: 'cache-and-network',
})

if (loading) {
return <Loading />
}

if (error) {
return <GraphqlErrorHandler error={error} />
}

return (
<JobDistributorsView jobDistributors={data?.feedsManagers.results ?? []} />
)
}
87 changes: 87 additions & 0 deletions src/screens/JobDistributors/JobDistributorsView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'

import userEvent from '@testing-library/user-event'
import { Route, Switch } from 'react-router-dom'

import { buildFeedsManager } from 'support/factories/gql/fetchFeedsManagers'
import { renderWithRouter, screen, within } from 'support/test-utils'
import { JobDistributorsView } from './JobDistributorsView'

const { getAllByRole, getByRole, getByText, findByText } = screen

function renderComponent(
mockData: ReadonlyArray<FetchFeedsManagersPayload_ResultsFields>,
) {
renderWithRouter(
<Switch>
<Route exact path="/job_distributors">
<JobDistributorsView jobDistributors={mockData} />,
</Route>
<Route exact path="/job_distributors/new">
New Job Distributor Page
</Route>
<Route exact path="/job_distributors/1">
Edit Job Distributor Page
</Route>
</Switch>,
{ initialEntries: ['/job_distributors'] },
)
}

describe('JobDistributorsView', () => {
test('should render the list of job distributors', () => {
renderComponent([
buildFeedsManager(),
buildFeedsManager({
name: 'Job Distributor 2',
id: '2',
isConnectionActive: true,
}),
])

expect(getByRole('heading')).toHaveTextContent('Job Distributors')

// header row counts as 1 row too
const rows = getAllByRole('row')
expect(rows).toHaveLength(3)

expect(getByText('Name')).toBeInTheDocument()
expect(getByText('Status')).toBeInTheDocument()
expect(getByText('CSA Public Key')).toBeInTheDocument()
expect(getByText('RPC URL')).toBeInTheDocument()

expect(
within(rows[1]).getByText('Chainlink Feeds Manager'),
).toBeInTheDocument()
expect(within(rows[1]).getByText('Disconnected')).toBeInTheDocument()
expect(within(rows[1]).getByText('localhost:8080')).toBeInTheDocument()

expect(within(rows[2]).getByText('Job Distributor 2')).toBeInTheDocument()
expect(within(rows[2]).getByText('Connected')).toBeInTheDocument()
expect(within(rows[2]).getByText('localhost:8080')).toBeInTheDocument()
})

test('should navigate to create new job distributor page when new button is clicked', async () => {
renderComponent([buildFeedsManager()])

userEvent.click(getByText(/New Job Distributor/i))

expect(await findByText('New Job Distributor Page')).toBeInTheDocument()
})

test('should show placeholder message when there are no job distributors', async () => {
renderComponent([])

expect(
await findByText('Job Distributors have not been registered'),
).toBeInTheDocument()
})

test('should navigate to detail job distributor page when row is clicked', async () => {
renderComponent([buildFeedsManager()])

userEvent.click(getByText(/chainlink feeds manager/i))

expect(await findByText('Edit Job Distributor Page')).toBeInTheDocument()
})
})
Loading

0 comments on commit d74af46

Please sign in to comment.