From d74af465787854fe7fa404d79b29aae60b834821 Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Sat, 31 Aug 2024 12:08:32 +1000 Subject: [PATCH] feat: new distributor home page 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 --- src/hooks/queries/useFeedsManagersQuery.ts | 27 +++--- src/screens/FeedsManager/ConnectionStatus.tsx | 47 ++++++++++ src/screens/FeedsManager/FeedsManagerCard.tsx | 55 ++---------- .../JobDistributors/JobDistributorsRow.tsx | 40 +++++++++ .../JobDistributorsScreen.test.tsx | 74 ++++++++++++++++ .../JobDistributors/JobDistributorsScreen.tsx | 24 +++++ .../JobDistributorsView.test.tsx | 87 +++++++++++++++++++ .../JobDistributors/JobDistributorsView.tsx | 73 ++++++++++++++++ support/factories/gql/fetchFeedsManagers.ts | 4 +- 9 files changed, 367 insertions(+), 64 deletions(-) create mode 100644 src/screens/FeedsManager/ConnectionStatus.tsx create mode 100644 src/screens/JobDistributors/JobDistributorsRow.tsx create mode 100644 src/screens/JobDistributors/JobDistributorsScreen.test.tsx create mode 100644 src/screens/JobDistributors/JobDistributorsScreen.tsx create mode 100644 src/screens/JobDistributors/JobDistributorsView.test.tsx create mode 100644 src/screens/JobDistributors/JobDistributorsView.tsx diff --git a/src/hooks/queries/useFeedsManagersQuery.ts b/src/hooks/queries/useFeedsManagersQuery.ts index eb39bcb5..3737171b 100644 --- a/src/hooks/queries/useFeedsManagersQuery.ts +++ b/src/hooks/queries/useFeedsManagersQuery.ts @@ -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( - FEEDS_MANAGERS_QUERY, - ) +export const useFeedsManagersQuery = ( + options?: QueryHookOptions, +) => { + return useQuery(FEEDS_MANAGERS_QUERY, options) } diff --git a/src/screens/FeedsManager/ConnectionStatus.tsx b/src/screens/FeedsManager/ConnectionStatus.tsx new file mode 100644 index 00000000..0474b1a8 --- /dev/null +++ b/src/screens/FeedsManager/ConnectionStatus.tsx @@ -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 { + isConnected: boolean +} + +export const ConnectionStatus = withStyles(connectionStatusStyles)( + ({ isConnected, classes }: ConnectionStatusProps) => { + return ( +
+ {isConnected ? ( + + ) : ( + + )} + + + {isConnected ? 'Connected' : 'Disconnected'} + +
+ ) + }, +) diff --git a/src/screens/FeedsManager/FeedsManagerCard.tsx b/src/screens/FeedsManager/FeedsManagerCard.tsx index 9317e2cd..372ba135 100644 --- a/src/screens/FeedsManager/FeedsManagerCard.tsx +++ b/src/screens/FeedsManager/FeedsManagerCard.tsx @@ -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 { - isConnected: boolean -} - -const ConnectionStatus = withStyles(connectionStatusStyles)( - ({ isConnected, classes }: ConnectionStatusProps) => { - return ( -
- {isConnected ? ( - - ) : ( - - )} - - - {isConnected ? 'Connected' : 'Disconnected'} - -
- ) - }, -) +import { shortenHex } from 'src/utils/shortenHex' +import { ConnectionStatus } from './ConnectionStatus' interface Props { manager: FeedsManagerFields diff --git a/src/screens/JobDistributors/JobDistributorsRow.tsx b/src/screens/JobDistributors/JobDistributorsRow.tsx new file mode 100644 index 00000000..2e2078f3 --- /dev/null +++ b/src/screens/JobDistributors/JobDistributorsRow.tsx @@ -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 { + jobDistributor: FetchFeedsManagersPayload_ResultsFields +} + +export const JobDistributorsRow = withStyles(tableStyles)( + ({ jobDistributor, classes }: Props) => { + return ( + + + + {jobDistributor.name} + + + + + + + {shortenHex(jobDistributor.publicKey, { start: 6, end: 6 })} + + + {jobDistributor.uri} + + ) + }, +) diff --git a/src/screens/JobDistributors/JobDistributorsScreen.test.tsx b/src/screens/JobDistributors/JobDistributorsScreen.test.tsx new file mode 100644 index 00000000..b958e3fb --- /dev/null +++ b/src/screens/JobDistributors/JobDistributorsScreen.test.tsx @@ -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( + <> + + + + + + , + { 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() + }) +}) diff --git a/src/screens/JobDistributors/JobDistributorsScreen.tsx b/src/screens/JobDistributors/JobDistributorsScreen.tsx new file mode 100644 index 00000000..a1c95171 --- /dev/null +++ b/src/screens/JobDistributors/JobDistributorsScreen.tsx @@ -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 + } + + if (error) { + return + } + + return ( + + ) +} diff --git a/src/screens/JobDistributors/JobDistributorsView.test.tsx b/src/screens/JobDistributors/JobDistributorsView.test.tsx new file mode 100644 index 00000000..6d630d39 --- /dev/null +++ b/src/screens/JobDistributors/JobDistributorsView.test.tsx @@ -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, +) { + renderWithRouter( + + + , + + + New Job Distributor Page + + + Edit Job Distributor Page + + , + { 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() + }) +}) diff --git a/src/screens/JobDistributors/JobDistributorsView.tsx b/src/screens/JobDistributors/JobDistributorsView.tsx new file mode 100644 index 00000000..2500dfaa --- /dev/null +++ b/src/screens/JobDistributors/JobDistributorsView.tsx @@ -0,0 +1,73 @@ +import React from 'react' + +import Card from '@material-ui/core/Card' +import Grid from '@material-ui/core/Grid' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' + +import BaseLink from 'components/BaseLink' +import Button from 'components/Button' +import Content from 'components/Content' +import { Heading1 } from 'src/components/Heading/Heading1' +import { JobDistributorsRow } from './JobDistributorsRow' + +interface Props { + jobDistributors: ReadonlyArray +} + +export const JobDistributorsView: React.FC = ({ jobDistributors }) => { + return ( + + + + Job Distributors + + + + + + + + + + + + + + + + Name + Status + CSA Public Key + RPC URL + + + + {jobDistributors.length === 0 && ( + + + Job Distributors have not been registered + + + )} + + {jobDistributors.map((j) => ( + + ))} + +
+
+
+
+
+ ) +} diff --git a/support/factories/gql/fetchFeedsManagers.ts b/support/factories/gql/fetchFeedsManagers.ts index 76026c5d..4e8473d2 100644 --- a/support/factories/gql/fetchFeedsManagers.ts +++ b/support/factories/gql/fetchFeedsManagers.ts @@ -1,7 +1,7 @@ // buildFeedsManager builds a feeds manager for the FetchFeedsManagers query. export function buildFeedsManager( - overrides?: Partial, -): FetchFeedsManagers['feedsManagers']['results'][number] { + overrides?: Partial, +): FetchFeedsManagersPayload_ResultsFields { return { __typename: 'FeedsManager', id: '1',