├── .changeset ├── README.md ├── angry-bobcats-share.md ├── brave-monkeys-tie.md ├── brown-horses-give.md ├── brown-rivers-accept.md ├── chilly-garlics-remember.md ├── config.json ├── cyan-kids-boil.md ├── dirty-eels-drive.md ├── dirty-planes-promise.md ├── early-chefs-stare.md ├── eight-adults-cry.md ├── eight-parents-leave.md ├── eighty-apricots-rest.md ├── eighty-moose-float.md ├── fast-kangaroos-tap.md ├── few-pens-brake.md ├── flat-impalas-peel.md ├── forty-chairs-play.md ├── forty-doors-whisper.md ├── friendly-cooks-exercise.md ├── gold-poems-relax.md ├── gold-starfishes-sing.md ├── green-otters-pull.md ├── green-roses-itch.md ├── grumpy-maps-watch.md ├── grumpy-olives-yell.md ├── heavy-lobsters-walk.md ├── hot-mails-rhyme.md ├── immutable-config.md ├── khaki-bananas-camp.md ├── kind-chairs-brake.md ├── late-pugs-jog.md ├── many-seahorses-protect.md ├── mean-wasps-cover.md ├── modern-balloons-sneeze.md ├── nervous-cups-invite.md ├── purple-feet-nail.md ├── purple-humans-explain.md ├── quick-days-cross.md ├── real-zoos-jump.md ├── serious-turtles-fold.md ├── sharp-houses-applaud.md ├── sharp-rice-melt.md ├── shiny-lobsters-check.md ├── short-tables-drop.md ├── shy-ducks-reply.md ├── shy-moose-walk.md ├── silly-jokes-lay.md ├── smart-rabbits-yell.md ├── stale-mails-bake.md ├── strong-ads-jump.md ├── sweet-queens-camp.md ├── sweet-turkeys-mate.md ├── tame-penguins-shake.md ├── ten-crabs-dream.md ├── tender-trees-cover.md ├── thin-beers-peel.md ├── twelve-chefs-juggle.md ├── twenty-radios-tease.md └── wise-bugs-smile.md ├── .eslintignore ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── cd.yml │ ├── chainlink-ci.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .swcrc ├── .tool-versions ├── @types ├── chainlink.d.ts ├── core │ └── store │ │ └── models.d.ts ├── globals.d.ts ├── graphlib-dot.d.ts ├── json-api-normalizer.d.ts ├── json-pretty-html.d.ts ├── react-copy-to-clipboard.d.ts ├── react-time-ago.d.ts └── redux-object.d.ts ├── CHANGELOG.md ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── bin └── codecov ├── codegen.ts ├── jest.config.js ├── jest.globalSetup.js ├── jest.setup.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── robots.txt ├── schema └── type │ └── configv2.graphql ├── scripts ├── package.json ├── release.mts ├── tsconfig.json └── yarn.lock ├── serve.json ├── sonar-project.properties ├── src ├── App.tsx ├── Layout.tsx ├── Private.tsx ├── PrivateRoute.test.js ├── PrivateRoute.tsx ├── actionCreators.ts ├── api │ ├── index.ts │ ├── sessions.ts │ └── v2 │ │ ├── buildInfo.ts │ │ ├── bulkDeleteRuns.ts │ │ ├── chains.ts │ │ ├── evmKeys.ts │ │ ├── index.ts │ │ ├── jobs.ts │ │ ├── logConfig.ts │ │ ├── nodes.ts │ │ └── webauthn.ts ├── apollo.ts ├── components │ ├── AccountMenu.tsx │ ├── BaseLink.test.tsx │ ├── BaseLink.tsx │ ├── Button.tsx │ ├── CardTitle.tsx │ ├── Cards │ │ ├── DetailsCard.tsx │ │ ├── KeyValueListCard.test.tsx │ │ ├── KeyValueListCard.tsx │ │ ├── TaskListCard.test.tsx │ │ └── TaskListCard.tsx │ ├── Content.js │ ├── Copy │ │ ├── CopyButton.tsx │ │ └── CopyIconButton.tsx │ ├── D3Chart │ │ ├── D3Graph.test.tsx │ │ ├── D3Graph.tsx │ │ ├── D3GraphTooltip.test.tsx │ │ └── D3GraphTooltip.tsx │ ├── Dialogs │ │ └── ConfirmationDialog.tsx │ ├── ElapsedDuration.tsx │ ├── ErrorHandler │ │ ├── GraphqlErrorHandler.test.tsx │ │ └── GraphqlErrorHandler.tsx │ ├── Feedback │ │ ├── Loading.test.tsx │ │ └── Loading.tsx │ ├── Flash.js │ ├── Form │ │ ├── BridgeForm.test.tsx │ │ ├── BridgeForm.tsx │ │ ├── ChainConfigurationForm.test.tsx │ │ ├── ChainConfigurationForm.tsx │ │ ├── ChainTypes.ts │ │ ├── FeedsManagerForm.test.tsx │ │ ├── FeedsManagerForm.tsx │ │ ├── JobForm.test.tsx │ │ └── JobForm.tsx │ ├── Heading │ │ ├── Heading1.tsx │ │ └── Heading2.tsx │ ├── Icons │ │ ├── Close.tsx │ │ ├── Error.tsx │ │ ├── JobRunStatusIcon.test.tsx │ │ ├── JobRunStatusIcon.tsx │ │ ├── ListIcon.tsx │ │ ├── Pending.tsx │ │ ├── Success.tsx │ │ ├── TaskRunStatusIcon.test.tsx │ │ └── TaskRunStatusIcon.tsx │ ├── Link.tsx │ ├── Loading.js │ ├── LoadingBar.js │ ├── Logo.tsx │ ├── Logos │ │ ├── Hexagon.js │ │ ├── Main.js │ │ └── NoContent.js │ ├── MenuItemLink.tsx │ ├── Notifications │ │ ├── DefaultError.tsx │ │ └── UnhandledError.tsx │ ├── Search │ │ ├── SearchTextField.test.tsx │ │ └── SearchTextField.tsx │ ├── SettingsMenu.tsx │ ├── Syntax │ │ ├── PrettyJson.css │ │ └── PrettyJson.tsx │ ├── Tab │ │ └── TabLink.tsx │ ├── Table.ts │ ├── Table │ │ ├── JobRunsTable.test.tsx │ │ └── JobRunsTable.tsx │ ├── TableButtons.js │ ├── TableRow │ │ ├── ErrorRow.test.tsx │ │ ├── ErrorRow.tsx │ │ ├── LoadingRow.test.tsx │ │ ├── LoadingRow.tsx │ │ ├── NoContentRow.test.tsx │ │ └── NoContentRow.tsx │ ├── TimeAgo.tsx │ └── Tooltip.tsx ├── core │ └── store │ │ └── models.ts ├── createStore.ts ├── hooks │ ├── queries │ │ ├── useChainsQuery.ts │ │ ├── useEVMAccountsQuery.ts │ │ ├── useFeedsManagerWithProposalsQuery.ts │ │ ├── useFeedsManagersQuery.ts │ │ ├── useNonEvmAccountsQuery.test.tsx │ │ ├── useNonEvmAccountsQuery.ts │ │ ├── useOCR2KeysQuery.ts │ │ ├── useOCRKeysQuery.ts │ │ └── useP2PKeysQuery.ts │ ├── useErrorHandler.test.tsx │ ├── useErrorHandler.tsx │ ├── useFeatureFlag.test.tsx │ ├── useFeatureFlag.tsx │ ├── useLoadingPlaceholder.test.tsx │ ├── useLoadingPlaceholder.tsx │ ├── useMutationErrorHandler.test.tsx │ ├── useMutationErrorHandler.tsx │ ├── useQueryErrorHandler.test.tsx │ ├── useQueryErrorHandler.tsx │ ├── useQueryParams.test.tsx │ └── useQueryParams.tsx ├── images │ ├── chainlink-operator-logo.svg │ ├── four-oh-four.js │ ├── icon-logo-blue.svg │ └── no-activity-icon.svg ├── index.css ├── index.html ├── index.js ├── middleware │ ├── explorerConnection.test.ts │ ├── explorerConnection.ts │ └── index.ts ├── pages │ ├── Chains │ │ ├── ChainNodes.test.tsx │ │ ├── ChainNodes.tsx │ │ ├── NodeRow.tsx │ │ ├── NodesList.tsx │ │ ├── RegionalNav.tsx │ │ └── Show.tsx │ ├── Configuration │ │ ├── JobRuns.tsx │ │ ├── LoggingCard.test.tsx │ │ └── LoggingCard.tsx │ ├── Header.tsx │ ├── JobsIndex │ │ └── index.tsx │ ├── NotFound.js │ ├── Notifications.test.js │ ├── Notifications.tsx │ ├── SignIn.js │ ├── SignIn.test.js │ ├── SignOut.js │ ├── Transactions │ │ └── index.tsx │ ├── bridges │ │ └── index.tsx │ ├── config │ │ └── index.tsx │ ├── dashboard │ │ └── index.tsx │ ├── feeds_manager │ │ └── index.tsx │ ├── job_proposals │ │ └── index.tsx │ ├── job_runs │ │ └── index.tsx │ ├── keys │ │ └── index.tsx │ └── nodes │ │ └── index.tsx ├── reducers.ts ├── reducers │ ├── actions.ts │ ├── authentication.test.ts │ ├── authentication.ts │ ├── buildInfo.test.ts │ ├── buildInfo.ts │ ├── fetching.test.ts │ ├── fetching.ts │ ├── notifications.test.tsx │ ├── notifications.ts │ ├── redirect.test.ts │ └── redirect.ts ├── screens │ ├── Bridge │ │ ├── BridgeCard.test.tsx │ │ ├── BridgeCard.tsx │ │ ├── BridgeScreen.test.tsx │ │ ├── BridgeScreen.tsx │ │ ├── BridgeView.test.tsx │ │ └── BridgeView.tsx │ ├── Bridges │ │ ├── BridgeRow.test.tsx │ │ ├── BridgeRow.tsx │ │ ├── BridgesScreen.test.tsx │ │ ├── BridgesScreen.tsx │ │ ├── BridgesView.test.tsx │ │ └── BridgesView.tsx │ ├── Chains │ │ ├── BetaAlert.tsx │ │ ├── ChainRow.test.tsx │ │ ├── ChainRow.tsx │ │ ├── ChainsScreen.test.tsx │ │ ├── ChainsScreen.tsx │ │ ├── ChainsView.test.tsx │ │ └── ChainsView.tsx │ ├── Configuration │ │ ├── ConfigurationScreen.tsx │ │ ├── ConfigurationV2Card │ │ │ └── ConfigurationV2Card.tsx │ │ ├── ConfigurationView.test.tsx │ │ ├── ConfigurationView.tsx │ │ └── NodeInfoCard │ │ │ ├── NodeInfoCard.test.tsx │ │ │ └── NodeInfoCard.tsx │ ├── Dashboard │ │ ├── AccountBalance.test.tsx │ │ ├── AccountBalance.tsx │ │ ├── AccountBalanceCard.test.tsx │ │ ├── AccountBalanceCard.tsx │ │ ├── Activity.test.tsx │ │ ├── Activity.tsx │ │ ├── ActivityCard.test.tsx │ │ ├── ActivityCard.tsx │ │ ├── ActivityRow.test.tsx │ │ ├── ActivityRow.tsx │ │ ├── BuildInfoFooter.test.tsx │ │ ├── BuildInfoFooter.tsx │ │ ├── ChainAccountBalanceCard.test.tsx │ │ ├── ChainAccountBalanceCard.tsx │ │ ├── DashboardScreen.tsx │ │ ├── DashboardView.test.tsx │ │ ├── DashboardView.tsx │ │ ├── RecentJobRow.test.tsx │ │ ├── RecentJobRow.tsx │ │ ├── RecentJobs.test.tsx │ │ ├── RecentJobs.tsx │ │ ├── RecentJobsCard.test.tsx │ │ └── RecentJobsCard.tsx │ ├── EditBridge │ │ ├── EditBridgeScreen.test.tsx │ │ ├── EditBridgeScreen.tsx │ │ ├── EditBridgeView.test.tsx │ │ └── EditBridgeView.tsx │ ├── EditFeedsManager │ │ ├── EditFeedsManagerScreen.test.tsx │ │ ├── EditFeedsManagerScreen.tsx │ │ ├── EditFeedsManagerView.test.tsx │ │ └── EditFeedsManagerView.tsx │ ├── FeedsManager │ │ ├── ApprovedTable.test.tsx │ │ ├── ApprovedTable.tsx │ │ ├── EditSupportedChainDialog.tsx │ │ ├── FeedsManagerCard.test.tsx │ │ ├── FeedsManagerCard.tsx │ │ ├── FeedsManagerScreen.test.tsx │ │ ├── FeedsManagerScreen.tsx │ │ ├── FeedsManagerView.test.tsx │ │ ├── FeedsManagerView.tsx │ │ ├── InactiveTable.test.tsx │ │ ├── InactiveTable.tsx │ │ ├── JobProposalsCard.test.tsx │ │ ├── JobProposalsCard.tsx │ │ ├── NewSupportedChainDialog.tsx │ │ ├── PendingTable.test.tsx │ │ ├── PendingTable.tsx │ │ ├── StatusIndicator.tsx │ │ ├── SupportedChainsCard.tsx │ │ ├── UpdatesTable.test.tsx │ │ └── UpdatesTable.tsx │ ├── Job │ │ ├── JobCard.test.tsx │ │ ├── JobCard.tsx │ │ ├── JobScreen.test.tsx │ │ ├── JobScreen.tsx │ │ ├── JobTabs.test.tsx │ │ ├── JobTabs.tsx │ │ ├── JobView.test.tsx │ │ ├── JobView.tsx │ │ ├── RunJobDialog.test.tsx │ │ ├── RunJobDialog.tsx │ │ ├── TabDefinition.test.tsx │ │ ├── TabDefinition.tsx │ │ ├── TabErrors.test.tsx │ │ ├── TabErrors.tsx │ │ ├── TabOverview.test.tsx │ │ ├── TabOverview.tsx │ │ ├── TabRuns.test.tsx │ │ ├── TabRuns.tsx │ │ ├── __snapshots__ │ │ │ └── TabDefinition.test.tsx.snap │ │ ├── generateJobDefinition.test.ts │ │ └── generateJobDefinition.ts │ ├── JobDistributors │ │ ├── JobDistributorsRow.tsx │ │ ├── JobDistributorsScreen.test.tsx │ │ ├── JobDistributorsScreen.tsx │ │ ├── JobDistributorsView.test.tsx │ │ └── JobDistributorsView.tsx │ ├── JobProposal │ │ ├── EditJobSpecDialog.test.tsx │ │ ├── EditJobSpecDialog.tsx │ │ ├── JobProposalCard.test.tsx │ │ ├── JobProposalCard.tsx │ │ ├── JobProposalScreen.test.tsx │ │ ├── JobProposalScreen.tsx │ │ ├── JobProposalView.test.tsx │ │ ├── JobProposalView.tsx │ │ ├── SpecsView.test.tsx │ │ └── SpecsView.tsx │ ├── JobRun │ │ ├── ErrorsCard.test.tsx │ │ ├── ErrorsCard.tsx │ │ ├── JSONCard.test.tsx │ │ ├── JSONCard.tsx │ │ ├── JobRunCard.test.tsx │ │ ├── JobRunCard.tsx │ │ ├── JobRunScreen.test.tsx │ │ ├── JobRunScreen.tsx │ │ ├── JobRunTabs.test.tsx │ │ ├── JobRunTabs.tsx │ │ ├── JobRunView.test.tsx │ │ ├── JobRunView.tsx │ │ ├── StatusCard.test.tsx │ │ ├── StatusCard.tsx │ │ ├── TaskRunItem.test.tsx │ │ ├── TaskRunItem.tsx │ │ ├── TaskRunsCard.test.tsx │ │ ├── TaskRunsCard.tsx │ │ └── __snapshots__ │ │ │ └── JSONCard.test.tsx.snap │ ├── JobRuns │ │ ├── JobRunsScreen.test.tsx │ │ ├── JobRunsScreen.tsx │ │ ├── JobRunsView.test.tsx │ │ └── JobRunsView.tsx │ ├── Jobs │ │ ├── JobRow.test.tsx │ │ ├── JobRow.tsx │ │ ├── JobsScreen.test.tsx │ │ ├── JobsScreen.tsx │ │ ├── JobsView.test.tsx │ │ └── JobsView.tsx │ ├── KeyManagement │ │ ├── CSAKeyRow.test.tsx │ │ ├── CSAKeyRow.tsx │ │ ├── CSAKeys.test.tsx │ │ ├── CSAKeys.tsx │ │ ├── CSAKeysCard.test.tsx │ │ ├── CSAKeysCard.tsx │ │ ├── EVMAccountRow.test.tsx │ │ ├── EVMAccountRow.tsx │ │ ├── EVMAccounts.test.tsx │ │ ├── EVMAccounts.tsx │ │ ├── EVMAccountsCard.test.tsx │ │ ├── EVMAccountsCard.tsx │ │ ├── KeyBundle.test.tsx │ │ ├── KeyBundle.tsx │ │ ├── KeyManagementScreen.tsx │ │ ├── KeyManagementView.tsx │ │ ├── NonEVMKeyRow.tsx │ │ ├── NonEVMKeys.test.tsx │ │ ├── NonEVMKeys.tsx │ │ ├── NonEVMKeysCard.tsx │ │ ├── OCR2KeyBundleRow.test.tsx │ │ ├── OCR2KeyBundleRow.tsx │ │ ├── OCR2KeyCreate.test.tsx │ │ ├── OCR2Keys.test.tsx │ │ ├── OCR2Keys.tsx │ │ ├── OCR2KeysCard.test.tsx │ │ ├── OCR2KeysCard.tsx │ │ ├── OCR2KeysCreate.tsx │ │ ├── OCRKeyBundleRow.test.tsx │ │ ├── OCRKeyBundleRow.tsx │ │ ├── OCRKeys.test.tsx │ │ ├── OCRKeys.tsx │ │ ├── OCRKeysCard.test.tsx │ │ ├── OCRKeysCard.tsx │ │ ├── P2PKeyRow.test.tsx │ │ ├── P2PKeyRow.tsx │ │ ├── P2PKeys.test.tsx │ │ ├── P2PKeys.tsx │ │ ├── P2PKeysCard.test.tsx │ │ ├── P2PKeysCard.tsx │ │ └── notifications.tsx │ ├── NewBridge │ │ ├── NewBridgeScreen.test.tsx │ │ ├── NewBridgeScreen.tsx │ │ ├── NewBridgeView.test.tsx │ │ └── NewBridgeView.tsx │ ├── NewFeedsManager │ │ ├── NewFeedsManagerScreen.test.tsx │ │ ├── NewFeedsManagerScreen.tsx │ │ ├── NewFeedsManagerView.test.tsx │ │ └── NewFeedsManagerView.tsx │ ├── NewJob │ │ ├── NewJobFormCard │ │ │ ├── NewJobFormCard.test.tsx │ │ │ └── NewJobFormCard.tsx │ │ ├── NewJobScreen.test.tsx │ │ ├── NewJobScreen.tsx │ │ ├── NewJobView.test.tsx │ │ ├── NewJobView.tsx │ │ └── TaskListPreviewCard │ │ │ ├── TaskListPreviewCard.test.tsx │ │ │ └── TaskListPreviewCard.tsx │ ├── Node │ │ ├── NodeCard.test.tsx │ │ ├── NodeCard.tsx │ │ ├── NodeScreen.test.tsx │ │ ├── NodeScreen.tsx │ │ ├── NodeView.test.tsx │ │ └── NodeView.tsx │ ├── Nodes │ │ ├── NodeRow.test.tsx │ │ ├── NodeRow.tsx │ │ ├── NodesScreen.test.tsx │ │ ├── NodesScreen.tsx │ │ ├── NodesView.test.tsx │ │ └── NodesView.tsx │ ├── Transaction │ │ ├── TransactionCard.test.tsx │ │ ├── TransactionCard.tsx │ │ ├── TransactionScreen.test.tsx │ │ ├── TransactionScreen.tsx │ │ ├── TransactionView.test.tsx │ │ └── TransactionView.tsx │ └── Transactions │ │ ├── TransactionRow.test.tsx │ │ ├── TransactionRow.tsx │ │ ├── TransactionsScreen.test.tsx │ │ ├── TransactionsScreen.tsx │ │ ├── TransactionsView.test.tsx │ │ └── TransactionsView.tsx ├── selectors │ ├── buildInfo.ts │ ├── fetchCount.test.ts │ └── fetchCount.ts ├── theme.ts └── utils │ ├── constants.ts │ ├── formatJobSpecType.test.ts │ ├── formatJobSpecType.ts │ ├── inputErrors.ts │ ├── json-api-client │ ├── errors.ts │ ├── fetchWithTimeout.test.ts │ ├── fetchWithTimeout.ts │ ├── index.ts │ └── transport │ │ ├── http.test.ts │ │ ├── http.ts │ │ └── json.ts │ ├── local-storage.ts │ ├── matchRouteAndMapDispatchToProps.js │ ├── parseDot.test.ts │ ├── parseDot.ts │ ├── shortenHex.test.ts │ ├── shortenHex.ts │ ├── storage.test.ts │ ├── storage.ts │ ├── taskRunStatus.ts │ ├── titleize.js │ ├── titleize.test.js │ └── tokens │ ├── link.test.ts │ └── link.ts ├── support ├── factories │ ├── gql │ │ ├── fetchAccountBalances.ts │ │ ├── fetchBridge.ts │ │ ├── fetchBridges.ts │ │ ├── fetchCSAKeys.ts │ │ ├── fetchChains.ts │ │ ├── fetchETHKeys.ts │ │ ├── fetchEthTransaction.ts │ │ ├── fetchEthTransactions.ts │ │ ├── fetchFeedsManagers.ts │ │ ├── fetchFeedsManagersWithProposals.ts │ │ ├── fetchJob.ts │ │ ├── fetchJobProposal.ts │ │ ├── fetchJobRun.ts │ │ ├── fetchJobRuns.ts │ │ ├── fetchJobs.ts │ │ ├── fetchNode.ts │ │ ├── fetchNodes.ts │ │ ├── fetchNonEVMKeys.ts │ │ ├── fetchOCR2KeyBundles.ts │ │ ├── fetchOCRKeyBundles.ts │ │ ├── fetchP2PKeys.ts │ │ ├── fetchRecentJobRuns.ts │ │ └── fetchRecentJobs.ts │ └── jsonApiLogConfig.ts ├── test-helpers │ ├── globPath.js │ ├── isoDate.js │ ├── partialAsFull.ts │ └── wait.ts └── test-utils.tsx ├── tsconfig.cjs.json ├── tsconfig.json ├── webpack.config.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/angry-bobcats-share.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | - Replaced rendering library to fix a bug that crashed the UI when task list contained two nodes in a loop 6 | - Change Pending, Success and Error svg borders 7 | - Task list tooltips are not positioned so that they don't go off-screen 8 | - Added zoom and pan functionality to Task List 9 | -------------------------------------------------------------------------------- /.changeset/brave-monkeys-tie.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | rename fromAddress to fromAddresses in blockhashStoreSpec 6 | -------------------------------------------------------------------------------- /.changeset/brown-horses-give.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | feat: create tron chain config 6 | -------------------------------------------------------------------------------- /.changeset/brown-rivers-accept.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Add support for Workflow Spec job types 6 | -------------------------------------------------------------------------------- /.changeset/chilly-garlics-remember.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | stop redirect to /job_distributors/ when there are registered job distributors 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "snapshot": { 10 | "useCalculatedVersion": true, 11 | "prereleaseTemplate": "{commit}" 12 | }, 13 | "updateInternalDependencies": "patch", 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /.changeset/cyan-kids-boil.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Peer ID field is introduced when Node is running as bootstrap peer 6 | -------------------------------------------------------------------------------- /.changeset/dirty-eels-drive.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Remove the `maxGasPriceGWei` field from VRF job details page. 6 | -------------------------------------------------------------------------------- /.changeset/dirty-planes-promise.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | remove filter for chaintype for bundleids in feeds manager 6 | -------------------------------------------------------------------------------- /.changeset/early-chefs-stare.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add more options to the OCR2 plugin selection for FMS 6 | -------------------------------------------------------------------------------- /.changeset/eight-adults-cry.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Show node type 6 | -------------------------------------------------------------------------------- /.changeset/eight-parents-leave.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | #added Enable and Disable Feeds Manager mutations 6 | -------------------------------------------------------------------------------- /.changeset/eighty-apricots-rest.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | fix: override unique handling logic of apollo client for chains 6 | -------------------------------------------------------------------------------- /.changeset/eighty-moose-float.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Displays feed ID on OCR2 JobSpec 6 | -------------------------------------------------------------------------------- /.changeset/fast-kangaroos-tap.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add deprecation warning for TelemetryIngress.URL and TelemetryIngress.ServerPubKey 6 | -------------------------------------------------------------------------------- /.changeset/few-pens-brake.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add account address public key field if the chain selected is for starknet 6 | -------------------------------------------------------------------------------- /.changeset/flat-impalas-peel.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Support creating solana chain config 6 | -------------------------------------------------------------------------------- /.changeset/forty-chairs-play.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Add ability to show TOML config 6 | 7 | On the configuration screen, the user is now able to view their node's TOML config 8 | -------------------------------------------------------------------------------- /.changeset/forty-doors-whisper.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Enable job distributor new home page and route changes 6 | -------------------------------------------------------------------------------- /.changeset/friendly-cooks-exercise.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | remove Next Nonce Manual Override UI setting as it is no longer connected to any backend functionality. This reduces UI complexity and avoids confussions. 6 | -------------------------------------------------------------------------------- /.changeset/gold-poems-relax.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | remove p2p v1 field p2pBootstrapPeers 6 | -------------------------------------------------------------------------------- /.changeset/gold-starfishes-sing.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Update the VRF job spec UI to include vrfOwnerAddress; Update the BHS job spec UI to include coordinatorV2PlusAddress, trustedBlockhashStoreAddress and trustedBlockhashStoreBatchSize 6 | -------------------------------------------------------------------------------- /.changeset/green-otters-pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixes task run status display for unfinished tasks 6 | -------------------------------------------------------------------------------- /.changeset/green-roses-itch.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add rebalancer option to OCR2 plugin selection in FMS 6 | -------------------------------------------------------------------------------- /.changeset/grumpy-maps-watch.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Display the name of job proposals in Feeds Manager 6 | -------------------------------------------------------------------------------- /.changeset/grumpy-olives-yell.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Change the Account Balance section to accommodate multiple accounts on different chains. 6 | -------------------------------------------------------------------------------- /.changeset/heavy-lobsters-walk.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | feat: add TON operator UI support 6 | -------------------------------------------------------------------------------- /.changeset/hot-mails-rhyme.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Increase limit for number of chains in FMS 6 | -------------------------------------------------------------------------------- /.changeset/immutable-config.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | Remove legacy config chain & node mutations. 5 | -------------------------------------------------------------------------------- /.changeset/khaki-bananas-camp.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Change the job creation error to specify that a job was created but it cannot start. 6 | -------------------------------------------------------------------------------- /.changeset/kind-chairs-brake.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | #internal support for standard capability job spec 6 | -------------------------------------------------------------------------------- /.changeset/late-pugs-jog.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fix a bug that would show all tasks in task list as completed 6 | -------------------------------------------------------------------------------- /.changeset/many-seahorses-protect.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixed a bug that caused RPC nodes to not be listed under Chains -> Nodes 6 | -------------------------------------------------------------------------------- /.changeset/mean-wasps-cover.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add OCR2 plugins selection for FMS 6 | -------------------------------------------------------------------------------- /.changeset/modern-balloons-sneeze.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Rename Feeds Manager to Job Distributor 6 | -------------------------------------------------------------------------------- /.changeset/nervous-cups-invite.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | chainconfig: attach chain type label to key bundle id in UI 6 | -------------------------------------------------------------------------------- /.changeset/purple-feet-nail.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | add new job type: BlockHeaderFeeder 6 | -------------------------------------------------------------------------------- /.changeset/purple-humans-explain.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fix chainlink-ci workflow by adding tag input to checkout specific tag/commit for operator-ui repo 6 | -------------------------------------------------------------------------------- /.changeset/quick-days-cross.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixes Node and Chain GQL queries which call the `CreatedAt` field which was removed 6 | -------------------------------------------------------------------------------- /.changeset/real-zoos-jump.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | chainconfg: make admin address optional 6 | -------------------------------------------------------------------------------- /.changeset/serious-turtles-fold.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Support APTOS in chain config 6 | -------------------------------------------------------------------------------- /.changeset/sharp-houses-applaud.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add deprecation warning for P2P.V1 6 | -------------------------------------------------------------------------------- /.changeset/sharp-rice-melt.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add support for using operator forwarder in OCR2 jobs managed by FMS 6 | -------------------------------------------------------------------------------- /.changeset/shiny-lobsters-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Add revoked jobs tab in feeds manager 6 | -------------------------------------------------------------------------------- /.changeset/short-tables-drop.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | New job type - Gateway 6 | -------------------------------------------------------------------------------- /.changeset/shy-ducks-reply.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixed a bug that caused the UI to go blank when a job was malformed 6 | -------------------------------------------------------------------------------- /.changeset/shy-moose-walk.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Removing notification for AllowSimplePasswords breaking change 6 | -------------------------------------------------------------------------------- /.changeset/silly-jokes-lay.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Replaced "ETH balance" with "Native token balance" 6 | -------------------------------------------------------------------------------- /.changeset/smart-rabbits-yell.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Remove depecration warnings for TelemetryIngress.URL, TelemtryIngress.ServerPubKey and P2P.V1 6 | -------------------------------------------------------------------------------- /.changeset/stale-mails-bake.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixes infinite loop issue on Sign Out 6 | -------------------------------------------------------------------------------- /.changeset/strong-ads-jump.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | #updated chain config: allow chain id and account address to be manually provided when no selections are available 6 | -------------------------------------------------------------------------------- /.changeset/sweet-queens-camp.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fixed a bug where the Task List would not be displayed correctly if a run was successfully completed. 6 | -------------------------------------------------------------------------------- /.changeset/sweet-turkeys-mate.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Display the Feeds Manager navigation in the mobile navigation drawer 6 | -------------------------------------------------------------------------------- /.changeset/tame-penguins-shake.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Adding notification for upcoming AllowSimplePasswords configuration breaking change in core v2.6.0 6 | -------------------------------------------------------------------------------- /.changeset/ten-crabs-dream.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Add OCR2 Key bundle creation 6 | -------------------------------------------------------------------------------- /.changeset/tender-trees-cover.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Add order field in the `Nodes` screen 6 | -------------------------------------------------------------------------------- /.changeset/thin-beers-peel.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Fix bug preventing selection of "Rows per page" in jobs/ID/runs page 6 | -------------------------------------------------------------------------------- /.changeset/twelve-chefs-juggle.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | Allow job deletion requests to be sent from FMS 6 | -------------------------------------------------------------------------------- /.changeset/twenty-radios-tease.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': minor 3 | --- 4 | 5 | Added support for the display and deletion of OCR version 2 (OCR2) keys 6 | -------------------------------------------------------------------------------- /.changeset/wise-bugs-smile.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@smartcontractkit/operator-ui': patch 3 | --- 4 | 5 | dynamic config for legacy vs. TOML; syntax highlighting; expansion panels 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/generated/** 3 | **/dist/** 4 | **/artifacts/** 5 | **/public/** 6 | **/build/** 7 | **/fixtures/** 8 | **/lib/** 9 | **/schema/** 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | `` 4 | 5 | ## Steps to Test 6 | 7 | 1. `yarn && yarn setup` 8 | 2. `yarn start` 9 | 3. ...etc 10 | 11 | # Checklist 12 | 13 | If this PR creates changes to the operator-ui itself, rather than tests, pipeline changes, etc. Then please create a changeset so that a new release is created, and the changelog is updated. See: https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md#what-is-a-changeset 14 | 15 | - [ ] This PR has an accompanying changeset if needed. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated 2 | src/types/* 3 | graphql.schema.json 4 | coverage 5 | node_modules 6 | artifacts 7 | tsconfig.tsbuildinfo 8 | .npmrc 9 | assets 10 | yarn-error.log 11 | *report.json 12 | 13 | # OS specific 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | smartcontractkit:registry=https://npm.pkg.github.com/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/generated/** 3 | **/dist/** 4 | **/artifacts/** 5 | **/public/** 6 | **/build/** 7 | **/coverage/** 8 | **/schema/** 9 | .yarn 10 | 11 | # Ignore TS definition and map files 12 | **/**.d.ts 13 | **/**.d.ts.map 14 | .github/**/*.yml 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | printWidth: 80, 5 | endOfLine: 'auto', 6 | tabWidth: 2, 7 | trailingComma: 'all', 8 | } 9 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript", 6 | "tsx": true, 7 | "jsx": true 8 | } 9 | }, 10 | "minify": false 11 | } 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.13.1 2 | -------------------------------------------------------------------------------- /@types/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Global { 3 | fetch: any 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /@types/graphlib-dot.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphlib-dot' 2 | -------------------------------------------------------------------------------- /@types/json-pretty-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'json-pretty-html' 2 | -------------------------------------------------------------------------------- /@types/react-copy-to-clipboard.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-copy-to-clipboard' 2 | -------------------------------------------------------------------------------- /@types/react-time-ago.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-time-ago' 2 | -------------------------------------------------------------------------------- /@types/redux-object.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'redux-object' 2 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // Jest cannot import CSS modules. We don't currently assert on any styles 2 | // so mock CSS and return nothing in the Jest test environment 3 | 4 | module.exports = {} 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 4 | }, 5 | moduleDirectories: [ 6 | 'node_modules', 7 | '/src/', 8 | '/support/', 9 | '/__tests__', 10 | ], 11 | setupFilesAfterEnv: ['/jest.setup.js'], 12 | globalSetup: './jest.globalSetup.js', 13 | testPathIgnorePatterns: [ 14 | '/dist/', 15 | '/tmp/', 16 | '/node_modules/', 17 | '/__tests__/.eslintrc.js', 18 | ], 19 | moduleNameMapper: { 20 | '^src/(.*)$': '/src/$1', 21 | '^support/(.*)$': '/support/$1', 22 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 23 | '/__mocks__/fileMock.js', 24 | '\\.(css|less|sass|scss)$': '/__mocks__/styleMock.js', 25 | }, 26 | transformIgnorePatterns: ['/node_modules/(?!(react-syntax-highlighter)/)'], 27 | testEnvironment: 'jsdom', 28 | testTimeout: 20000, 29 | collectCoverage: true, 30 | } 31 | -------------------------------------------------------------------------------- /jest.globalSetup.js: -------------------------------------------------------------------------------- 1 | // The normal setupFiles did not work since they run too late (jest: ^23.5.0). So it is mandatory to use the globalSetup file. 2 | // https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config 3 | module.exports = async () => { 4 | process.env.TZ = 'UTC' 5 | } 6 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import 'mock-local-storage' 2 | import JavascriptTimeAgo from 'javascript-time-ago' 3 | import en from 'javascript-time-ago/locale/en' 4 | import '@testing-library/jest-dom' 5 | import FetchMockStatic from 'fetch-mock' 6 | 7 | JavascriptTimeAgo.locale(en) 8 | 9 | global.fetch = FetchMockStatic.sandbox() 10 | global.fetch.config.overwriteRoutes = true 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/operator-ui/9db0cda85972a52c712c737be193a9d088c2f470/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /schema/type/configv2.graphql: -------------------------------------------------------------------------------- 1 | type ConfigV2Payload { 2 | user: String! 3 | effective: String! 4 | } 5 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "release": "node --loader=ts-node/esm ./release.mts" 8 | }, 9 | "dependencies": { 10 | "@changesets/read": "^0.5.7", 11 | "ts-node": "^10.9.1", 12 | "typescript": "^4.8.3", 13 | "zx": "^7.0.8" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/assets/:asset", "destination": "/:asset" }] 3 | } 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=smartcontractkit_operator-ui 2 | sonar.sources=. 3 | sonar.sourceEncoding=UTF-8 4 | 5 | # Full exclusions from the static analysis 6 | sonar.exclusions=\ 7 | **/node_modules/**/*,\ 8 | **/webpack/**/*,\ 9 | **/*mocks*/**/*,\ 10 | **/bin/**/*,\ 11 | **/coverage/**/*,\ 12 | **/generated/**/*,\ 13 | **/fixtures/**/*,\ 14 | **/docs/**/*,\ 15 | **/tools/**/*,\ 16 | **/*report.xml,\ 17 | **/*.config.ts,\ 18 | **/*.txt,\ 19 | **/*.abi,\ 20 | **/*.bin 21 | 22 | # Coverage exclusions 23 | sonar.coverage.exclusions=\ 24 | **/scripts/**/*,\ 25 | **/support/**/*,\ 26 | **/*.test.*,\ 27 | **/*.setup.js 28 | 29 | # Tests' root folder, inclusions (tests to check and count) and exclusions 30 | sonar.tests=. 31 | sonar.test.inclusions=**/*.test.* 32 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import createStore from './createStore' 4 | import './index.css' 5 | import Layout from './Layout' 6 | import { setPersistUrl } from './utils/storage' 7 | 8 | const SIGNIN_PATH = '/signin' 9 | 10 | const store = createStore() 11 | 12 | store.subscribe(() => { 13 | const prevURL = store.getState().notifications.currentUrl 14 | if (prevURL && prevURL !== SIGNIN_PATH) { 15 | setPersistUrl(prevURL) 16 | } 17 | }) 18 | 19 | const App = () => { 20 | return ( 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default App 28 | -------------------------------------------------------------------------------- /src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Route, 4 | Switch, 5 | Redirect, 6 | BrowserRouter as Router, 7 | } from 'react-router-dom' 8 | import CssBaseline from '@material-ui/core/CssBaseline' 9 | import Private from './Private' 10 | import { useOperatorUiSelector } from 'reducers' 11 | import SignIn from 'pages/SignIn' 12 | 13 | const Layout = () => { 14 | const redirectTo = useOperatorUiSelector((state) => state.redirect.to) 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {redirectTo && } 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default Layout 34 | -------------------------------------------------------------------------------- /src/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, RouteProps, Redirect, useLocation } from 'react-router-dom' 3 | import { useDispatch } from 'react-redux' 4 | import { useOperatorUiSelector } from 'reducers' 5 | import { RouterActionType } from 'reducers/actions' 6 | 7 | export const PrivateRoute = (props: RouteProps) => { 8 | const dispatch = useDispatch() 9 | const { pathname } = useLocation() 10 | 11 | React.useEffect(() => { 12 | dispatch({ 13 | type: RouterActionType.MATCH_ROUTE, 14 | pathname, 15 | }) 16 | }, [dispatch, pathname]) 17 | 18 | const authenticated = useOperatorUiSelector( 19 | (state) => state.authentication.allowed, 20 | ) 21 | 22 | if (authenticated) { 23 | return 24 | } 25 | 26 | return 27 | } 28 | 29 | export default PrivateRoute 30 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'utils/json-api-client' 2 | import { Sessions } from './sessions' 3 | import { V2 } from './v2' 4 | 5 | const api = new Api({ 6 | base: process.env.CHAINLINK_BASEURL, 7 | }) 8 | 9 | export const sessions = new Sessions(api) 10 | export const v2 = new V2(api) 11 | -------------------------------------------------------------------------------- /src/api/sessions.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import { Api } from 'utils/json-api-client' 3 | import * as models from 'core/store/models' 4 | import * as sessionsController from 'core/web/sessions_controller' 5 | 6 | /** 7 | * Create creates a session ID for the given user credentials 8 | * and returns it in a cookie. 9 | */ 10 | const CREATE_ENDPOINT = '/sessions' 11 | 12 | /** 13 | * Destroy erases the session ID for the sole API user. 14 | */ 15 | const DESTROY_ENDPOINT = '/sessions' 16 | 17 | export class Sessions { 18 | constructor(private api: Api) {} 19 | 20 | public createSession = ( 21 | sessionRequest: models.SessionRequest, 22 | ): Promise> => { 23 | return this.create(sessionRequest) 24 | } 25 | 26 | public destroySession = (): Promise< 27 | jsonapi.ApiResponse 28 | > => { 29 | return this.destroy() 30 | } 31 | 32 | private create = this.api.createResource< 33 | models.SessionRequest, 34 | sessionsController.Session 35 | >(CREATE_ENDPOINT) 36 | 37 | private destroy = this.api.deleteResource< 38 | undefined, 39 | sessionsController.Session 40 | >(DESTROY_ENDPOINT) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/v2/buildInfo.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | const SHOW_ENDPOINT = '/v2/build_info' 5 | 6 | export class BuildInfo { 7 | constructor(private api: jsonapi.Api) {} 8 | 9 | public show = (): Promise => 10 | //@ts-expect-error /v2/build_info doesn't conform to the typical jsonapi 11 | // response model, it just returns raw JSON back 12 | this.api.GET(SHOW_ENDPOINT)() 13 | } 14 | -------------------------------------------------------------------------------- /src/api/v2/bulkDeleteRuns.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | /** 5 | * Delete removes all runs given a query 6 | * 7 | * @example "/bulk_delete_runs" 8 | */ 9 | const DELETE_ENDPOINT = '/v2/bulk_delete_runs' 10 | 11 | export class BulkDeleteRuns { 12 | constructor(private api: jsonapi.Api) {} 13 | 14 | public bulkDeleteJobRuns = ( 15 | bulkDeleteRunRequest: models.BulkDeleteRunRequest, 16 | ): Promise> => { 17 | return this.destroy(bulkDeleteRunRequest) 18 | } 19 | 20 | private destroy = this.api.deleteResource( 21 | DELETE_ENDPOINT, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/api/v2/chains.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | export const ENDPOINT = '/v2/chains/:network' 5 | 6 | export class Chains { 7 | constructor(private api: jsonapi.Api) {} 8 | 9 | public getChains = ( 10 | network: string, 11 | ): Promise> => { 12 | return this.index(undefined, { network }) 13 | } 14 | 15 | private index = this.api.fetchResource< 16 | object, 17 | models.Chain[], 18 | { network: string } 19 | >(ENDPOINT) 20 | } 21 | -------------------------------------------------------------------------------- /src/api/v2/evmKeys.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | export const ENDPOINT = '/v2/keys/evm/chain' 5 | 6 | export class EVMKeys { 7 | constructor(private api: jsonapi.Api) {} 8 | 9 | public chain = ( 10 | request: models.EVMKeysChainRequest, 11 | ): Promise> => { 12 | const query = new URLSearchParams() 13 | 14 | query.append('address', request.address) 15 | query.append('evmChainID', request.evmChainID) 16 | if (request.abandon !== null) { 17 | query.append('abandon', String(request.abandon)) 18 | } 19 | if (request.enabled !== null) { 20 | query.append('enabled', String(request.enabled)) 21 | } 22 | 23 | const endpoint = ENDPOINT + '?' + query.toString() 24 | 25 | return this.api.createResource( 26 | endpoint, 27 | )() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/api/v2/index.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'utils/json-api-client' 2 | import { BulkDeleteRuns } from './bulkDeleteRuns' 3 | import { Chains } from './chains' 4 | import { EVMKeys } from './evmKeys' 5 | import { Jobs } from './jobs' 6 | import { LogConfig } from './logConfig' 7 | import { Nodes } from './nodes' 8 | import { WebAuthn } from './webauthn' 9 | import { BuildInfo } from './buildInfo' 10 | export class V2 { 11 | constructor(private api: Api) {} 12 | 13 | public buildInfo = new BuildInfo(this.api) 14 | public bulkDeleteRuns = new BulkDeleteRuns(this.api) 15 | public chains = new Chains(this.api) 16 | public logConfig = new LogConfig(this.api) 17 | public nodes = new Nodes(this.api) 18 | public jobs = new Jobs(this.api) 19 | public webauthn = new WebAuthn(this.api) 20 | public evmKeys = new EVMKeys(this.api) 21 | } 22 | -------------------------------------------------------------------------------- /src/api/v2/jobs.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | 3 | export const ENDPOINT = '/v2/jobs' 4 | const RUN_JOB_ENDPOINT = `${ENDPOINT}/:specId/runs` 5 | 6 | // Jobs represents the v2 jobs 7 | export class Jobs { 8 | constructor(private api: jsonapi.Api) {} 9 | 10 | public createJobRunV2 = ( 11 | id: string, 12 | pipelineInput: string, 13 | ): Promise> => { 14 | return this.post(pipelineInput, { specId: id }) 15 | } 16 | 17 | private post = this.api.createResource< 18 | string, 19 | null, 20 | { 21 | specId: string 22 | } 23 | >(RUN_JOB_ENDPOINT, true) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/v2/logConfig.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | /** 5 | * Show returns the whitelist of config variables 6 | * 7 | * @example "/config" 8 | */ 9 | const ENDPOINT = '/v2/log' 10 | 11 | export class LogConfig { 12 | constructor(private api: jsonapi.Api) {} 13 | 14 | /** 15 | * Get log configuration variables 16 | */ 17 | public getLogConfig = (): Promise> => { 18 | return this.show() 19 | } 20 | 21 | public updateLogConfig = ( 22 | request: models.LogConfigRequest, 23 | ): Promise> => { 24 | return this.update(request) 25 | } 26 | 27 | private show = this.api.fetchResource( 28 | ENDPOINT, 29 | ) 30 | 31 | private update = this.api.updateResource< 32 | models.LogConfigRequest, 33 | models.LogConfig 34 | >(ENDPOINT) 35 | } 36 | -------------------------------------------------------------------------------- /src/api/v2/nodes.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | export const ENDPOINT = '/v2/nodes' 5 | 6 | export class Nodes { 7 | constructor(private api: jsonapi.Api) {} 8 | 9 | public getNodes = (): Promise> => { 10 | return this.index() 11 | } 12 | 13 | public createNode = ( 14 | request: models.CreateNodeRequest, 15 | ): Promise> => { 16 | return this.create(request) 17 | } 18 | 19 | private index = this.api.fetchResource(ENDPOINT) 20 | 21 | private create = this.api.createResource< 22 | models.CreateNodeRequest, 23 | models.Node 24 | >(ENDPOINT) 25 | } 26 | -------------------------------------------------------------------------------- /src/api/v2/webauthn.ts: -------------------------------------------------------------------------------- 1 | import * as jsonapi from 'utils/json-api-client' 2 | import * as models from 'core/store/models' 3 | 4 | const REGISTRATION_ENDPOINT = '/v2/enroll_webauthn' 5 | 6 | export class WebAuthn { 7 | constructor(private api: jsonapi.Api) {} 8 | 9 | public beginKeyRegistration = ( 10 | request: models.BeginWebAuthnRegistrationV2Request, 11 | ): Promise> => { 12 | return this.create(request) 13 | } 14 | 15 | public finishKeyRegistration = ( 16 | request: models.FinishWebAuthnRegistrationV2Request, 17 | ): Promise> => { 18 | return this.put(request) 19 | } 20 | 21 | private create = this.api.fetchResource< 22 | models.BeginWebAuthnRegistrationV2Request, 23 | models.BeginWebAuthnRegistrationV2 24 | >(REGISTRATION_ENDPOINT) 25 | 26 | private put = this.api.createResource< 27 | models.FinishWebAuthnRegistrationV2Request, 28 | models.FinishWebAuthnRegistrationV2 29 | >(REGISTRATION_ENDPOINT) 30 | } 31 | -------------------------------------------------------------------------------- /src/apollo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloClient, 3 | defaultDataIdFromObject, 4 | HttpLink, 5 | InMemoryCache, 6 | } from '@apollo/client' 7 | import generatedIntrospection from 'src/types/generated/possibleTypes' 8 | 9 | const baseURL = process.env.CHAINLINK_BASEURL ?? location.origin 10 | 11 | const httpLink = new HttpLink({ 12 | uri: `${baseURL}/query`, 13 | credentials: 'include', 14 | }) 15 | 16 | export const client = new ApolloClient({ 17 | cache: new InMemoryCache({ 18 | possibleTypes: generatedIntrospection.possibleTypes, 19 | // we need to explicitly handle the uniqueness of chain object because 20 | // ID is not unique across as different network can have the same ID 21 | // which confuses the caching of apollo client. 22 | // the code below is to override the handling of uniqueness for Chain type. 23 | dataIdFromObject(responseObject) { 24 | switch (responseObject.__typename) { 25 | case 'Chain': 26 | if (!responseObject.network) { 27 | throw new Error( 28 | 'Due to Chain ID not being unique across chain, ensure network is fetched too', 29 | ) 30 | } 31 | return `Chain:${responseObject.network}:${responseObject.id}` 32 | default: 33 | return defaultDataIdFromObject(responseObject) 34 | } 35 | }, 36 | }), 37 | link: httpLink, 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/BaseLink.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MemoryRouter } from 'react-router-dom' 3 | import { render, screen } from '@testing-library/react' 4 | import BaseLink from '../../src/components/BaseLink' 5 | 6 | const { getByRole, getByText } = screen 7 | 8 | const renderBaseLink = (link: React.ReactNode) => 9 | render({link}) 10 | 11 | describe('components/BaseLink', () => { 12 | it('renders an anchor', () => { 13 | renderBaseLink(My Link) 14 | 15 | expect(getByText('My Link')).toBeInTheDocument() 16 | expect(getByRole('link')).toHaveAttribute('href', '/foo') 17 | }) 18 | 19 | it('can render an id', () => { 20 | renderBaseLink( 21 | 22 | My Link 23 | , 24 | ) 25 | 26 | expect(getByRole('link')).toHaveAttribute('id', 'my-id') 27 | }) 28 | 29 | it('can render a css class', () => { 30 | renderBaseLink( 31 | 32 | My Link 33 | , 34 | ) 35 | 36 | expect(getByRole('link')).toHaveAttribute('class', 'my-css-class') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/BaseLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | interface Props { 5 | children: React.ReactNode 6 | href: string 7 | id?: string 8 | className?: string 9 | onClick?: (event: React.MouseEvent) => void 10 | } 11 | 12 | const BaseLink = ({ children, href, id, className, onClick }: Props) => ( 13 | 14 | {children} 15 | 16 | ) 17 | 18 | export default BaseLink 19 | -------------------------------------------------------------------------------- /src/components/CardTitle.tsx: -------------------------------------------------------------------------------- 1 | import CardContent from '@material-ui/core/CardContent' 2 | import Divider from '@material-ui/core/Divider' 3 | import Typography from '@material-ui/core/Typography' 4 | import React from 'react' 5 | 6 | interface Props { 7 | children: string 8 | divider?: boolean 9 | } 10 | 11 | export const CardTitle = ({ children, divider = false }: Props) => { 12 | return ( 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | {divider && } 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withStyles } from '@material-ui/core/styles' 3 | 4 | const styles = (theme) => ({ 5 | content: { 6 | padding: theme.spacing.unit * 5, 7 | }, 8 | }) 9 | 10 | const Content = ({ children, classes }) => { 11 | return
{children}
12 | } 13 | 14 | export default withStyles(styles)(Content) 15 | -------------------------------------------------------------------------------- /src/components/Copy/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CopyToClipboard } from 'react-copy-to-clipboard' 4 | 5 | import Button from '../Button' 6 | import Tooltip from '@material-ui/core/Tooltip' 7 | 8 | interface Props { 9 | data: string 10 | title: string 11 | } 12 | 13 | export const CopyButton: React.FC = ({ data, title }) => { 14 | const [copied, setCopied] = React.useState(false) 15 | 16 | return ( 17 | setCopied(true)}> 18 | setCopied(false)}> 19 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Copy/CopyIconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CopyToClipboard } from 'react-copy-to-clipboard' 4 | 5 | import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined' 6 | import IconButton from '@material-ui/core/IconButton' 7 | import { 8 | createStyles, 9 | withStyles, 10 | WithStyles, 11 | Theme, 12 | } from '@material-ui/core/styles' 13 | import Tooltip from '@material-ui/core/Tooltip' 14 | 15 | const styles = (theme: Theme) => 16 | createStyles({ 17 | button: { 18 | width: 27, 19 | height: 27, 20 | minHeight: 27, 21 | marginLeft: theme.spacing.unit, 22 | marginRight: theme.spacing.unit, 23 | }, 24 | icon: { 25 | fontSize: 18, 26 | }, 27 | }) 28 | 29 | export const CopyIconButton = withStyles(styles)( 30 | ({ classes, data }: WithStyles & { data: string }) => { 31 | const [copied, setCopied] = React.useState(false) 32 | 33 | return ( 34 | setCopied(true)}> 35 | setCopied(false)}> 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /src/components/D3Chart/D3GraphTooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Stratify } from 'utils/parseDot' 3 | import Typography from '@material-ui/core/Typography' 4 | 5 | interface Props { 6 | data: Stratify 7 | } 8 | 9 | export const D3Tooltip: React.FC = ({ data }) => { 10 | const attribs: Array = [] 11 | 12 | if (data?.attributes) { 13 | Object.keys(data.attributes).forEach((keyName) => { 14 | attribs.push( 15 |
16 | 17 | {keyName}: {data.attributes?.[keyName]} 18 | 19 |
, 20 | ) 21 | }) 22 | } 23 | 24 | return ( 25 |
26 | {data && ( 27 | 28 | {data.id} 29 | 30 | )} 31 | {attribs} 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Feedback/Loading.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from 'support/test-utils' 3 | import { Loading } from './Loading' 4 | 5 | const { queryByRole } = screen 6 | 7 | describe('Loading', () => { 8 | it('shows a loading spinner', () => { 9 | render() 10 | 11 | expect(queryByRole('progressbar')).toBeInTheDocument() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/components/Feedback/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import CircularProgress from '@material-ui/core/CircularProgress' 4 | import Grid from '@material-ui/core/Grid' 5 | import { Theme, withStyles, WithStyles } from '@material-ui/core/styles' 6 | 7 | const styles = (theme: Theme) => ({ 8 | root: { 9 | margin: theme.spacing.unit * 2, 10 | }, 11 | gridItem: { 12 | display: 'flex', 13 | justifyContent: 'center', 14 | }, 15 | }) 16 | 17 | interface Props extends WithStyles {} 18 | 19 | export const Loading = withStyles(styles)(({ classes }: Props) => ( 20 | 21 | 22 | 23 | 24 | 25 | )) 26 | -------------------------------------------------------------------------------- /src/components/Form/ChainTypes.ts: -------------------------------------------------------------------------------- 1 | export const ChainTypes = { 2 | EVM: 'EVM', 3 | APTOS: 'APTOS', 4 | SOLANA: 'SOLANA', 5 | STARKNET: 'STARKNET', 6 | COSMOS: 'COSMOS', 7 | TRON: 'TRON', 8 | TON: 'TON', 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Heading/Heading1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | 6 | const styles = () => { 7 | return createStyles({ 8 | root: { 9 | fontSize: 30, 10 | }, 11 | }) 12 | } 13 | 14 | interface Props extends WithStyles {} 15 | 16 | export const Heading1 = withStyles(styles)( 17 | ({ children, classes }: React.PropsWithChildren) => ( 18 | 19 | {children} 20 | 21 | ), 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/Heading/Heading2.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | 6 | const styles = () => { 7 | return createStyles({ 8 | root: { 9 | fontSize: 24, 10 | }, 11 | }) 12 | } 13 | 14 | interface Props extends WithStyles {} 15 | 16 | export const Heading2 = withStyles(styles)( 17 | ({ children, classes }: React.PropsWithChildren) => ( 18 | 19 | {children} 20 | 21 | ), 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/Icons/Close.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/no-unknown-property: 0 */ 2 | import React, { FC, SVGProps } from 'react' 3 | 4 | const Close: FC> = (props) => ( 5 | 14 | 15 | 21 | 29 | 30 | ) 31 | 32 | export default Close 33 | -------------------------------------------------------------------------------- /src/components/Icons/Error.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/no-unknown-property: 0 */ 2 | import React, { FC, SVGProps } from 'react' 3 | 4 | const ErrorIcon: FC> = (props) => ( 5 | 6 | 10 | 11 | 17 | 18 | ) 19 | 20 | export default ErrorIcon 21 | -------------------------------------------------------------------------------- /src/components/Icons/JobRunStatusIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from '@testing-library/react' 4 | 5 | import { JobRunStatusIcon } from './JobRunStatusIcon' 6 | 7 | const { getByTestId } = screen 8 | 9 | describe('JobRunStatusIcon', () => { 10 | // Some default values for testing 11 | const dimensions = { width: 20, height: 20 } 12 | 13 | it('renders the completed icon', () => { 14 | render() 15 | expect(getByTestId('completed')).toBeInTheDocument() 16 | }) 17 | 18 | it('renders the errored icon', () => { 19 | render() 20 | expect(getByTestId('errored')).toBeInTheDocument() 21 | }) 22 | 23 | it('renders the running icon', () => { 24 | render() 25 | expect(getByTestId('running')).toBeInTheDocument() 26 | }) 27 | 28 | it('renders the suspended icon', () => { 29 | render() 30 | expect(getByTestId('suspended')).toBeInTheDocument() 31 | }) 32 | 33 | it('renders no icon when unknown', () => { 34 | const { container } = render( 35 | , 36 | ) 37 | 38 | expect(container).toBeEmptyDOMElement() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/components/Icons/JobRunStatusIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ErrorIcon from './Error' 4 | import { JobRunStatus } from 'src/types/generated/graphql' 5 | import ListIcon from 'components/Icons/ListIcon' 6 | import PendingIcon from './Pending' 7 | import SuccessIcon from './Success' 8 | 9 | interface Props { 10 | status: JobRunStatus 11 | width?: number 12 | height?: number 13 | } 14 | 15 | export const JobRunStatusIcon = ({ status, width, height }: Props) => { 16 | switch (status) { 17 | case 'COMPLETED': 18 | return ( 19 | 20 | ) 21 | case 'ERRORED': 22 | return 23 | case 'RUNNING': 24 | return 25 | case 'SUSPENDED': 26 | return 27 | default: 28 | return null 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Icons/Pending.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/no-unknown-property: 0 */ 2 | import React, { FC, SVGProps } from 'react' 3 | 4 | const Pending: FC> = (props) => ( 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 25 | 26 | 30 | 31 | 32 | 33 | ) 34 | 35 | export default Pending 36 | -------------------------------------------------------------------------------- /src/components/Icons/Success.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/no-unknown-property: 0 */ 2 | import React, { FC, SVGProps } from 'react' 3 | 4 | const Success: FC> = (props) => ( 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | ) 30 | 31 | export default Success 32 | -------------------------------------------------------------------------------- /src/components/Icons/TaskRunStatusIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from '@testing-library/react' 4 | 5 | import { TaskRunStatusIcon } from './TaskRunStatusIcon' 6 | import { TaskRunStatus } from 'src/utils/taskRunStatus' 7 | 8 | const { getByTestId } = screen 9 | 10 | describe('TaskRunStatusIcon', () => { 11 | // Some default values for testing 12 | const dimensions = { width: 20, height: 20 } 13 | 14 | it('renders the completed icon', () => { 15 | render( 16 | , 17 | ) 18 | expect(getByTestId('complete-run-icon')).toBeInTheDocument() 19 | }) 20 | 21 | it('renders the errored icon', () => { 22 | render() 23 | expect(getByTestId('error-run-icon')).toBeInTheDocument() 24 | }) 25 | 26 | it('renders the pending icon', () => { 27 | render() 28 | expect(getByTestId('pending-run-icon')).toBeInTheDocument() 29 | }) 30 | 31 | it('renders the default icon', () => { 32 | render() 33 | expect(getByTestId('default-run-icon')).toBeInTheDocument() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Icons/TaskRunStatusIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ErrorIcon from './Error' 4 | import PendingIcon from './Pending' 5 | import ListIcon from './ListIcon' 6 | import SuccessIcon from './Success' 7 | import { TaskRunStatus } from 'src/utils/taskRunStatus' 8 | 9 | interface Props { 10 | status: TaskRunStatus 11 | width?: number 12 | height?: number 13 | } 14 | 15 | export const TaskRunStatusIcon = ({ status, width, height }: Props) => { 16 | switch (status) { 17 | case TaskRunStatus.COMPLETE: 18 | return ( 19 | 24 | ) 25 | case TaskRunStatus.ERROR: 26 | return ( 27 | 28 | ) 29 | case TaskRunStatus.PENDING: 30 | return ( 31 | 36 | ) 37 | default: 38 | return ( 39 | 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | import Typography from '@material-ui/core/Typography' 4 | import { withStyles } from '@material-ui/core/styles' 5 | 6 | const styles = (theme) => ({ 7 | wrapper: { 8 | marginTop: theme.spacing.unit * 5, 9 | }, 10 | text: { 11 | textAlign: 'center', 12 | }, 13 | }) 14 | 15 | const Loading = ({ classes }) => ( 16 | 17 | 18 | 19 | Loading... 20 | 21 | 22 | 23 | ) 24 | 25 | export default withStyles(styles)(Loading) 26 | -------------------------------------------------------------------------------- /src/components/LoadingBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import LinearProgress from '@material-ui/core/LinearProgress' 4 | import { withStyles } from '@material-ui/core/styles' 5 | 6 | const styles = (theme) => ({ 7 | root: { 8 | flexGrow: 1, 9 | height: 3, 10 | overflow: 'hidden', 11 | }, 12 | colorPrimary: { 13 | backgroundColor: theme.palette.background.default, 14 | }, 15 | barColorPrimary: { 16 | backgroundColor: theme.palette.primary.main, 17 | }, 18 | }) 19 | 20 | const LoadingBar = ({ classes, fetchCount }) => { 21 | const progressClasses = { 22 | colorPrimary: classes.colorPrimary, 23 | barColorPrimary: classes.barColorPrimary, 24 | } 25 | 26 | return ( 27 |
28 | {fetchCount > 0 && ( 29 | 30 | )} 31 |
32 | ) 33 | } 34 | 35 | LoadingBar.propTypes = { 36 | fetchCount: PropTypes.number.isRequired, 37 | } 38 | 39 | export default withStyles(styles)(LoadingBar) 40 | -------------------------------------------------------------------------------- /src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles' 3 | 4 | const styles = createStyles({ 5 | animate: { 6 | animation: 'spin 4s linear infinite', 7 | }, 8 | '@keyframes spin': { 9 | '100%': { 10 | transform: 'rotate(360deg)', 11 | }, 12 | }, 13 | }) 14 | 15 | interface Props extends WithStyles { 16 | src: string 17 | width?: number 18 | height?: number 19 | spin?: boolean 20 | alt?: string 21 | } 22 | 23 | const UnstyledImage = ({ 24 | src, 25 | width, 26 | height, 27 | alt, 28 | classes, 29 | spin = false, 30 | }: Props) => { 31 | return ( 32 | {alt} 39 | ) 40 | } 41 | 42 | const Image = withStyles(styles)(UnstyledImage) 43 | 44 | interface Props { 45 | src: string 46 | width?: number 47 | height?: number 48 | alt?: string 49 | } 50 | 51 | export const Logo: React.FC = (props) => { 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Logos/Hexagon.js: -------------------------------------------------------------------------------- 1 | import { Logo } from 'components/Logo' 2 | import PropTypes from 'prop-types' 3 | import React from 'react' 4 | import src from '../../images/icon-logo-blue.svg' 5 | 6 | const Hexagon = (props) => { 7 | return 8 | } 9 | 10 | Hexagon.propTypes = { 11 | width: PropTypes.number, 12 | height: PropTypes.number, 13 | } 14 | 15 | export default Hexagon 16 | -------------------------------------------------------------------------------- /src/components/Logos/Main.js: -------------------------------------------------------------------------------- 1 | import { Logo } from 'components/Logo' 2 | import PropTypes from 'prop-types' 3 | import React from 'react' 4 | import src from '../../images/chainlink-operator-logo.svg' 5 | 6 | const Main = (props) => { 7 | return 8 | } 9 | 10 | Main.propTypes = { 11 | width: PropTypes.number, 12 | height: PropTypes.number, 13 | } 14 | 15 | export default Main 16 | -------------------------------------------------------------------------------- /src/components/Logos/NoContent.js: -------------------------------------------------------------------------------- 1 | import { Logo } from 'components/Logo' 2 | import PropTypes from 'prop-types' 3 | import React from 'react' 4 | import src from '../../images/no-activity-icon.svg' 5 | 6 | const NoContent = (props) => { 7 | return 8 | } 9 | 10 | NoContent.propTypes = { 11 | width: PropTypes.number, 12 | height: PropTypes.number, 13 | } 14 | 15 | export default NoContent 16 | -------------------------------------------------------------------------------- /src/components/MenuItemLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem' 4 | 5 | interface MenuItemLinkProps extends MenuItemProps { 6 | to: string 7 | replace?: boolean 8 | } 9 | 10 | export const MenuItemLink = (props: MenuItemLinkProps) => { 11 | return ( 12 | 13 | {props.children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Notifications/DefaultError.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | msg: string 3 | } 4 | 5 | export default function DefaultError({ msg }: Props) { 6 | return msg 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Notifications/UnhandledError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const UnhandledError = () => ( 4 | 5 | Unhandled error. Please help us by opening a{' '} 6 | 7 | bug report 8 | 9 | 10 | ) 11 | 12 | export default UnhandledError 13 | -------------------------------------------------------------------------------- /src/components/Search/SearchTextField.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | 6 | import { SearchTextField } from './SearchTextField' 7 | 8 | const { getByRole } = screen 9 | 10 | describe('SearchTextField', () => { 11 | it('renders the text field', () => { 12 | const handleChange = jest.fn() 13 | 14 | render( 15 | , 20 | ) 21 | 22 | const textbox = getByRole('textbox') 23 | expect(textbox).toHaveAttribute('placeholder', 'Search...') 24 | expect(textbox).toHaveAttribute('value', '') 25 | 26 | userEvent.paste(textbox, 'foo') 27 | 28 | expect(handleChange).toHaveBeenCalledWith('foo') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/Syntax/PrettyJson.css: -------------------------------------------------------------------------------- 1 | .json-pretty { 2 | font-family: Roboto Mono, Menlo, Monaco, Consolas, 'Courier New', monospace; 3 | font-weight: normal; 4 | font-size: 14px; 5 | line-height: 16px; 6 | letter-spacing: 0; 7 | color: #d4d4d4; 8 | text-align: left; 9 | margin: 0; 10 | overflow: auto; 11 | } 12 | 13 | .json-selected { 14 | background-color: rgba(139, 191, 228, 0.19999999999999996); 15 | } 16 | 17 | .json-string { 18 | color: #6caedd; 19 | } 20 | 21 | .json-key { 22 | color: #ec5f67; 23 | } 24 | 25 | .json-boolean { 26 | color: #99c794; 27 | } 28 | 29 | .json-number { 30 | color: #99c794; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Syntax/PrettyJson.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import jsonPrettyHtml from 'json-pretty-html' 4 | import './PrettyJson.css' 5 | 6 | interface Props { 7 | object: object 8 | } 9 | 10 | // TODO - Look to standardise syntax highlighting so we can reduce dependencies 11 | export const PrettyJson: React.FC = ({ object }) => ( 12 |
16 | ) 17 | -------------------------------------------------------------------------------- /src/components/Tab/TabLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import Tab, { TabProps } from '@material-ui/core/Tab' 4 | 5 | interface TabLinkProps extends TabProps { 6 | to: string 7 | } 8 | 9 | export const TabLink = (props: TabLinkProps) => { 10 | return ( 11 | 12 | {props.children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Table.ts: -------------------------------------------------------------------------------- 1 | import { createStyles, Theme } from '@material-ui/core/styles' 2 | 3 | // Contains styles to make a table row linkable 4 | // 5 | // This has been placed in the components folder because we ideally want to 6 | // wrap the link in a reusable components, however using Material UI 3 7 | // 'withStyles' leads to small freeze when navigating to the link, due to 8 | // the slowness of JSS. When we migrate to MUI 4/5 we can implement this with 9 | // makeStyles which doesn't have those problems 10 | export const tableStyles = (theme: Theme) => 11 | createStyles({ 12 | cell: { 13 | paddingTop: theme.spacing.unit * 2, 14 | paddingBottom: theme.spacing.unit * 2, 15 | }, 16 | row: { 17 | transform: 'scale(1)', 18 | }, 19 | link: { 20 | '&::before': { 21 | content: "''", 22 | position: 'absolute', 23 | top: 0, 24 | left: 0, 25 | width: '100%', 26 | height: '100%', 27 | }, 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/TableRow/ErrorRow.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import { ErrorRow } from './ErrorRow' 5 | 6 | const { getAllByRole, queryAllByRole, queryByText } = screen 7 | 8 | describe('ErrorRow', () => { 9 | function renderComponent(msg?: string) { 10 | render( 11 | 12 | 13 | 14 | 15 |
, 16 | ) 17 | } 18 | 19 | it('renders an error', () => { 20 | renderComponent('error message') 21 | 22 | expect(getAllByRole('row')).toHaveLength(1) 23 | expect(queryByText('error message')).toBeInTheDocument() 24 | }) 25 | 26 | it('renders nothing', () => { 27 | renderComponent() 28 | 29 | expect(queryAllByRole('row')).toHaveLength(0) 30 | expect(queryByText('error message')).toBeNull() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/components/TableRow/ErrorRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TableCell from '@material-ui/core/TableCell' 4 | import TableRow from '@material-ui/core/TableRow' 5 | 6 | export const ErrorRow = ({ msg }: { msg?: string }) => { 7 | if (!msg) { 8 | return null 9 | } 10 | 11 | return ( 12 | 13 | 14 | {msg} 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/TableRow/LoadingRow.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import { LoadingRow } from './LoadingRow' 5 | 6 | const { getAllByRole, queryAllByRole, queryByTestId } = screen 7 | 8 | describe('LoadingRow', () => { 9 | function renderComponent(visible: boolean) { 10 | render( 11 | 12 | 13 | 14 | 15 |
, 16 | ) 17 | } 18 | 19 | it('renders an loading spinner', () => { 20 | renderComponent(true) 21 | 22 | expect(getAllByRole('row')).toHaveLength(1) 23 | expect(queryByTestId('loading-spinner')).toBeInTheDocument() 24 | }) 25 | 26 | it('renders nothing', () => { 27 | renderComponent(false) 28 | 29 | expect(queryAllByRole('row')).toHaveLength(0) 30 | expect(queryByTestId('loading-spinner')).toBeNull() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/components/TableRow/LoadingRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import CircularProgress from '@material-ui/core/CircularProgress' 4 | import { 5 | createStyles, 6 | Theme, 7 | withStyles, 8 | WithStyles, 9 | } from '@material-ui/core/styles' 10 | import TableCell from '@material-ui/core/TableCell' 11 | import TableRow from '@material-ui/core/TableRow' 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | cell: { 16 | padding: theme.spacing.unit * 2, 17 | }, 18 | }) 19 | 20 | interface Props extends WithStyles { 21 | visible: boolean 22 | } 23 | 24 | export const LoadingRow = withStyles(styles)(({ classes, visible }: Props) => { 25 | if (!visible) { 26 | return null 27 | } 28 | 29 | return ( 30 | 31 | {/* Sets a high column count to insure this is always centered regardless 32 | of the number of columns in the table. 33 | */} 34 | 35 | 36 | 37 | 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/TableRow/NoContentRow.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import { NoContentRow } from './NoContentRow' 5 | 6 | const { getAllByRole, queryAllByRole, queryByText } = screen 7 | 8 | describe('ErrorRow', () => { 9 | function renderComponent(visible: boolean, children?: React.ReactNode) { 10 | render( 11 | 12 | 13 | {children} 14 | 15 |
, 16 | ) 17 | } 18 | 19 | it('renders default text', () => { 20 | renderComponent(true) 21 | 22 | expect(getAllByRole('row')).toHaveLength(1) 23 | expect(queryByText('No entries to show')).toBeInTheDocument() 24 | }) 25 | 26 | it('renders custom child', () => { 27 | renderComponent(true, 'custom message') 28 | 29 | expect(getAllByRole('row')).toHaveLength(1) 30 | expect(queryByText('custom message')).toBeInTheDocument() 31 | }) 32 | 33 | it('renders nothing', () => { 34 | renderComponent(false) 35 | 36 | expect(queryAllByRole('row')).toHaveLength(0) 37 | expect(queryByText('error message')).toBeNull() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/TableRow/NoContentRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TableCell from '@material-ui/core/TableCell' 4 | import TableRow from '@material-ui/core/TableRow' 5 | 6 | export const NoContentRow: React.FC<{ visible: boolean }> = ({ 7 | children, 8 | visible, 9 | }) => { 10 | if (!visible) { 11 | return null 12 | } 13 | 14 | return ( 15 | 16 | 17 | {children ? children : 'No entries to show'} 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/TimeAgo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactTimeAgo from 'react-time-ago' 3 | import moment from 'moment' 4 | import { Tooltip } from './Tooltip' 5 | 6 | interface Props { 7 | children: string 8 | tooltip: boolean 9 | } 10 | 11 | export const localizedTimestamp = (creationDate: string): string => 12 | creationDate && moment(creationDate).format() 13 | 14 | export const TimeAgo: React.FC = ({ children, tooltip = false }) => { 15 | const date = Date.parse(children) 16 | const ago = 17 | 18 | if (tooltip) { 19 | return ( 20 | 21 | {ago} 22 | 23 | ) 24 | } 25 | 26 | return ago 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createStyles, 3 | Theme, 4 | withStyles, 5 | WithStyles, 6 | } from '@material-ui/core/styles' 7 | import MuiTooltip from '@material-ui/core/Tooltip' 8 | import React from 'react' 9 | 10 | const styles = ({ palette, shadows, typography }: Theme) => 11 | createStyles({ 12 | lightTooltip: { 13 | background: palette.primary.contrastText, 14 | // @ts-expect-error color might be overwritten 15 | color: palette.text.primary, 16 | boxShadow: shadows[24], 17 | ...typography.h6, 18 | }, 19 | }) 20 | 21 | interface Props extends WithStyles { 22 | children: React.ReactElement 23 | title: string 24 | } 25 | 26 | const UnstyledTooltip = ({ title, children, classes }: Props) => { 27 | return ( 28 | 29 | {children} 30 | 31 | ) 32 | } 33 | 34 | export const Tooltip = withStyles(styles)(UnstyledTooltip) 35 | -------------------------------------------------------------------------------- /src/core/store/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RunStatus is a string that represents the run status 3 | */ 4 | export enum RunStatus { 5 | IN_PROGRESS = 'in_progress', 6 | PENDING_INCOMING_CONFIRMATIONS = 'pending_incoming_confirmations', 7 | PENDING_CONNECTION = 'pending_connection', 8 | PENDING_BRIDGE = 'pending_bridge', 9 | PENDING_SLEEP = 'pending_sleep', 10 | ERRORED = 'errored', 11 | COMPLETED = 'completed', 12 | } 13 | -------------------------------------------------------------------------------- /src/createStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | applyMiddleware, 4 | createStore as reduxCreateStore, 5 | Middleware, 6 | Reducer, 7 | } from 'redux' 8 | import { composeWithDevTools } from 'redux-devtools-extension' 9 | import thunkMiddleware from 'redux-thunk' 10 | import { createExplorerConnectionMiddleware } from './middleware' 11 | import reducer from './reducers' 12 | const middleware: Middleware[] = [ 13 | thunkMiddleware, 14 | createExplorerConnectionMiddleware(), 15 | ] 16 | 17 | const composeEnhancers = composeWithDevTools({}) 18 | 19 | function createStoreWith( 20 | reducer: Reducer, 21 | middleware: Middleware[], 22 | ) { 23 | return reduxCreateStore( 24 | reducer, 25 | composeEnhancers(applyMiddleware(...[...middleware])), 26 | ) 27 | } 28 | export type StoreDispatch = ReturnType['dispatch'] 29 | const createStore = () => createStoreWith(reducer, middleware) 30 | 31 | export default createStore 32 | -------------------------------------------------------------------------------- /src/hooks/queries/useChainsQuery.ts: -------------------------------------------------------------------------------- 1 | import { gql, QueryHookOptions, useQuery } from '@apollo/client' 2 | 3 | export const CHAINS_PAYLOAD__RESULTS_FIELDS = gql` 4 | fragment ChainsPayload_ResultsFields on Chain { 5 | id 6 | enabled 7 | network 8 | } 9 | ` 10 | 11 | export const CHAINS_QUERY = gql` 12 | ${CHAINS_PAYLOAD__RESULTS_FIELDS} 13 | query FetchChains($offset: Int, $limit: Int) { 14 | chains(offset: $offset, limit: $limit) { 15 | results { 16 | ...ChainsPayload_ResultsFields 17 | } 18 | metadata { 19 | total 20 | } 21 | } 22 | } 23 | ` 24 | 25 | // useChainsQuery fetches the chains 26 | export const useChainsQuery = (opts: QueryHookOptions = {}) => { 27 | return useQuery(CHAINS_QUERY, opts) 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/queries/useEVMAccountsQuery.ts: -------------------------------------------------------------------------------- 1 | import { gql, QueryHookOptions, useQuery } from '@apollo/client' 2 | 3 | export const ETH_KEYS_PAYLOAD__RESULTS_FIELDS = gql` 4 | fragment ETHKeysPayload_ResultsFields on EthKey { 5 | address 6 | chain { 7 | id 8 | network 9 | } 10 | createdAt 11 | ethBalance 12 | isDisabled 13 | linkBalance 14 | } 15 | ` 16 | 17 | export const ETH_KEYS_QUERY = gql` 18 | ${ETH_KEYS_PAYLOAD__RESULTS_FIELDS} 19 | query FetchETHKeys { 20 | ethKeys { 21 | results { 22 | ...ETHKeysPayload_ResultsFields 23 | } 24 | } 25 | } 26 | ` 27 | 28 | // useEVMAccountsQuery fetches the EVM accounts. 29 | export const useEVMAccountsQuery = (opts: QueryHookOptions = {}) => { 30 | return useQuery(ETH_KEYS_QUERY, opts) 31 | } 32 | -------------------------------------------------------------------------------- /src/hooks/queries/useFeedsManagersQuery.ts: -------------------------------------------------------------------------------- 1 | import { gql, QueryHookOptions, useQuery } from '@apollo/client' 2 | 3 | export const FEEDS_MANAGERS_QUERY = gql` 4 | fragment FetchFeedsManagersPayload_ResultsFields on FeedsManager { 5 | __typename 6 | id 7 | name 8 | uri 9 | publicKey 10 | isConnectionActive 11 | createdAt 12 | disabledAt 13 | } 14 | query FetchFeedsManagers { 15 | feedsManagers { 16 | results { 17 | ...FetchFeedsManagersPayload_ResultsFields 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export const useFeedsManagersQuery = ( 24 | options?: QueryHookOptions, 25 | ) => { 26 | return useQuery(FEEDS_MANAGERS_QUERY, options) 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/queries/useOCRKeysQuery.ts: -------------------------------------------------------------------------------- 1 | import { gql, QueryHookOptions, useQuery } from '@apollo/client' 2 | 3 | export const OCR_KEY_BUNDLES_PAYLOAD__RESULTS_FIELDS = gql` 4 | fragment OCRKeyBundlesPayload_ResultsFields on OCRKeyBundle { 5 | id 6 | configPublicKey 7 | offChainPublicKey 8 | onChainSigningAddress 9 | } 10 | ` 11 | 12 | export const OCR_KEY_BUNDLES_QUERY = gql` 13 | ${OCR_KEY_BUNDLES_PAYLOAD__RESULTS_FIELDS} 14 | query FetchOCRKeyBundles { 15 | ocrKeyBundles { 16 | results { 17 | ...OCRKeyBundlesPayload_ResultsFields 18 | } 19 | } 20 | } 21 | ` 22 | 23 | // useOCRKeysQuery fetches the chains 24 | export const useOCRKeysQuery = (opts: QueryHookOptions = {}) => { 25 | return useQuery(OCR_KEY_BUNDLES_QUERY, opts) 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/queries/useP2PKeysQuery.ts: -------------------------------------------------------------------------------- 1 | import { gql, QueryHookOptions, useQuery } from '@apollo/client' 2 | 3 | export const P2P_KEYS_PAYLOAD__RESULTS_FIELDS = gql` 4 | fragment P2PKeysPayload_ResultsFields on P2PKey { 5 | id 6 | peerID 7 | publicKey 8 | } 9 | ` 10 | 11 | export const P2P_KEYS_QUERY = gql` 12 | ${P2P_KEYS_PAYLOAD__RESULTS_FIELDS} 13 | query FetchP2PKeys { 14 | p2pKeys { 15 | results { 16 | ...P2PKeysPayload_ResultsFields 17 | } 18 | } 19 | } 20 | ` 21 | 22 | // useP2PKeysQuery fetches the chains 23 | export const useP2PKeysQuery = (opts: QueryHookOptions = {}) => { 24 | return useQuery(P2P_KEYS_QUERY, opts) 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useErrorHandler.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import { AuthenticationError } from 'utils/json-api-client' 4 | import { useDispatch } from 'react-redux' 5 | import { receiveSignoutSuccess } from 'actionCreators' 6 | 7 | export const useErrorHandler = (): { 8 | error: unknown 9 | ErrorComponent: React.FC 10 | setError: React.Dispatch 11 | } => { 12 | const [error, setError] = React.useState() 13 | const history = useHistory() 14 | const dispatch = useDispatch() 15 | 16 | React.useEffect(() => { 17 | if (error instanceof AuthenticationError) { 18 | /** 19 | * Because sign in page is using redux to figure out whether the 20 | * user is logged in we need to dispatch a redux action. The reducer 21 | * updates the store and syncs with local storage (which is a bad 22 | * practice as it's a side-effect, but let's focus on solving one 23 | * problem at a time 😅). 24 | */ 25 | dispatch(receiveSignoutSuccess()) 26 | history.push('/signin') 27 | } 28 | }, [dispatch, error, history]) 29 | 30 | const ErrorComponent: React.FC = error 31 | ? () => ( 32 |
33 | Error:{' '} 34 | {error instanceof Error ? error.message : JSON.stringify(error)} 35 |
36 | ) 37 | : () => null 38 | 39 | return { error, ErrorComponent, setError } 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/useFeatureFlag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | 5 | export enum Feature { 6 | CSA = 'csa', 7 | FeedsManager = 'feeds_manager', 8 | } 9 | 10 | export const FEATURES_QUERY = gql` 11 | query FetchFeatures { 12 | features { 13 | ... on Features { 14 | __typename 15 | csa 16 | feedsManager 17 | } 18 | } 19 | } 20 | ` 21 | 22 | // useFeature is a hook which returns whether a feature is enabled/disabled. 23 | export function useFeatureFlag(feature: Feature): boolean { 24 | const { data } = useQuery( 25 | FEATURES_QUERY, 26 | { fetchPolicy: 'network-only' }, 27 | ) 28 | 29 | const [isEnabled, setIsEnabled] = React.useState(false) 30 | 31 | React.useEffect(() => { 32 | if (data?.features.__typename == 'Features') { 33 | switch (feature) { 34 | case Feature.CSA: 35 | setIsEnabled(data?.features.csa) 36 | 37 | break 38 | case Feature.FeedsManager: 39 | setIsEnabled(data?.features.feedsManager) 40 | 41 | break 42 | default: 43 | setIsEnabled(false) 44 | } 45 | } 46 | }, [data, feature]) 47 | 48 | return isEnabled 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/useLoadingPlaceholder.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import { useLoadingPlaceholder } from './useLoadingPlaceholder' 4 | 5 | const { queryByText } = screen 6 | 7 | describe('useLoadingPlaceholder', () => { 8 | it('renders "Loading..." text while loading', () => { 9 | const { LoadingPlaceholder } = useLoadingPlaceholder(true) 10 | render() 11 | 12 | expect(queryByText('Loading...')).toBeInTheDocument() 13 | }) 14 | 15 | it('defaults to false and renders an empty component', () => { 16 | const { LoadingPlaceholder } = useLoadingPlaceholder() 17 | render() 18 | 19 | expect(document.documentElement).toHaveTextContent('') 20 | }) 21 | 22 | it('exposes "isLoading" variable', () => { 23 | const { isLoading } = useLoadingPlaceholder(true) 24 | 25 | expect(isLoading).toBe(true) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/hooks/useLoadingPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Typography } from '@material-ui/core' 3 | 4 | export const useLoadingPlaceholder = ( 5 | isLoading = false, 6 | ): { 7 | isLoading: boolean 8 | LoadingPlaceholder: React.FC 9 | } => { 10 | const LoadingPlaceholder: React.FC = isLoading 11 | ? () => Loading... 12 | : () => null 13 | 14 | return { 15 | isLoading, 16 | LoadingPlaceholder, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useQueryParams.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Route } from 'react-router-dom' 4 | import { renderWithRouter, screen } from 'support/test-utils' 5 | 6 | import { useQueryParams } from './useQueryParams' 7 | 8 | const { getByText } = screen 9 | 10 | const StubComponent = () => { 11 | const qp = useQueryParams() 12 | 13 | return
{qp.toString()}
14 | } 15 | 16 | describe('useQueryParams', () => { 17 | it('extracts the query params', () => { 18 | renderWithRouter( 19 | 20 | 21 | , 22 | { initialEntries: ['/?foo=bar&baz=qux'] }, 23 | ) 24 | 25 | expect(getByText('foo=bar&baz=qux')).toBeInTheDocument() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/hooks/useQueryParams.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | 4 | // A custom hook that builds on useLocation to parse the query string. 5 | export const useQueryParams = () => { 6 | const { search } = useLocation() 7 | 8 | return React.useMemo(() => new URLSearchParams(search), [search]) 9 | } 10 | -------------------------------------------------------------------------------- /src/images/icon-logo-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Mono'); 2 | 3 | body { 4 | height: 100vh; 5 | min-height: 100vh; 6 | } 7 | 8 | #root { 9 | height: 100%; 10 | min-height: 100%; 11 | overflow-y: auto !important; 12 | } 13 | 14 | #task-list-graph-d3-graph-wrapper .task-run-icon-pending:hover{ 15 | filter:drop-shadow(0px 0px 2px gray); 16 | } 17 | #task-list-graph-d3-graph-wrapper .task-run-icon-success:hover{ 18 | filter:drop-shadow(0px 0px 2px green); 19 | } 20 | #task-list-graph-d3-graph-wrapper .task-run-icon-error:hover{ 21 | filter:drop-shadow(0px 0px 2px red); 22 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Operator UI 7 | 11 | 15 | 19 | 20 | Chainlink 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { MuiThemeProvider } from '@material-ui/core/styles' 2 | import JavascriptTimeAgo from 'javascript-time-ago' 3 | import en from 'javascript-time-ago/locale/en' 4 | import moment from 'moment' 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import { AppContainer } from 'react-hot-loader' 8 | import App from './App' 9 | import { theme } from './theme' 10 | import { ApolloProvider } from '@apollo/client' 11 | import { client } from './apollo' 12 | 13 | JavascriptTimeAgo.locale(en) 14 | moment.defaultFormat = 'YYYY-MM-DD h:mm:ss A' 15 | 16 | export default App 17 | 18 | if (typeof document !== 'undefined') { 19 | const renderMethod = module.hot ? ReactDOM.render : ReactDOM.hydrate 20 | 21 | const render = (Comp) => { 22 | renderMethod( 23 | 24 | 25 | 26 | 27 | 28 | 29 | , 30 | document.getElementById('root'), 31 | ) 32 | } 33 | 34 | render(App) 35 | // Hot Module Replacement 36 | if (module.hot) { 37 | // eslint-disable-next-line @typescript-eslint/no-var-requires 38 | module.hot.accept('./App', () => render(require('./App').default)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './explorerConnection' 2 | -------------------------------------------------------------------------------- /src/pages/Chains/ChainNodes.tsx: -------------------------------------------------------------------------------- 1 | import { CardTitle } from 'components/CardTitle' 2 | import { Card, Grid } from '@material-ui/core' 3 | import Content from 'components/Content' 4 | import React from 'react' 5 | import { ChainResource } from './Show' 6 | import ChainNodesList from './NodesList' 7 | import { Resource, Node } from 'core/store/models' 8 | 9 | export type NodeResource = Resource 10 | 11 | interface Props { 12 | nodes: NodeResource[] 13 | chain: ChainResource 14 | } 15 | 16 | export const ChainNodes = ({ nodes, chain }: Props) => { 17 | const filterByChainID = function (node: NodeResource): boolean { 18 | return node.attributes.chainID === chain.id 19 | } 20 | return ( 21 | 22 | {chain && ( 23 | 24 | 25 | 26 | Nodes 27 | 28 | 29 | 30 | 31 | )} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/Chains/NodeRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { NodeResource } from './ChainNodes' 4 | import { tableStyles } from 'components/Table' 5 | import Link from 'components/Link' 6 | 7 | import { withStyles, WithStyles } from '@material-ui/core/styles' 8 | import TableCell from '@material-ui/core/TableCell' 9 | import TableRow from '@material-ui/core/TableRow' 10 | 11 | interface Props extends WithStyles { 12 | node: NodeResource 13 | } 14 | 15 | export const NodeRow = withStyles(tableStyles)(({ node, classes }: Props) => { 16 | return ( 17 | 18 | 19 | 20 | {node.attributes.name} 21 | 22 | 23 | 24 | {node.attributes.chainID} 25 | 26 | {node.attributes.state} 27 | 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/pages/JobsIndex/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import { JobScreen } from 'screens/Job/JobScreen' 5 | import { JobsScreen } from 'screens/Jobs/JobsScreen' 6 | import { NewJobScreen } from 'screens/NewJob/NewJobScreen' 7 | 8 | export const JobsPage = function () { 9 | const { path } = useRouteMatch() 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NotFoundSVG from 'images/four-oh-four.js' 3 | import { withStyles } from '@material-ui/core/styles' 4 | 5 | const styles = () => ({ 6 | logo: { 7 | top: '30%', 8 | left: '50%', 9 | transform: 'translate(-50%, -30%)', 10 | position: 'absolute', 11 | }, 12 | }) 13 | 14 | const Logo = ({ classes }) => ( 15 |
16 | 17 |
18 | ) 19 | 20 | export default withStyles(styles)(Logo) 21 | -------------------------------------------------------------------------------- /src/pages/SignOut.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { receiveSignoutSuccess } from 'actionCreators' 4 | import { connect } from 'react-redux' 5 | 6 | export const SignOut = ({ receiveSignoutSuccess }) => { 7 | useEffect(() => { 8 | receiveSignoutSuccess() 9 | }, [receiveSignoutSuccess]) 10 | return 11 | } 12 | 13 | SignOut.propTypes = { 14 | receiveSignoutSuccess: PropTypes.func.isRequired, 15 | } 16 | 17 | export default connect(null, { receiveSignoutSuccess })(SignOut) 18 | -------------------------------------------------------------------------------- /src/pages/Transactions/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import { TransactionScreen } from 'screens/Transaction/TransactionScreen' 5 | import { TransactionsScreen } from 'screens/Transactions/TransactionsScreen' 6 | 7 | export const TransactionsPage = function () { 8 | const { path } = useRouteMatch() 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/bridges/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import { BridgeScreen } from '../../screens/Bridge/BridgeScreen' 5 | import { BridgesScreen } from '../../screens/Bridges/BridgesScreen' 6 | import { EditBridgeScreen } from '../../screens/EditBridge/EditBridgeScreen' 7 | import { NewBridgeScreen } from '../../screens/NewBridge/NewBridgeScreen' 8 | 9 | export const BridgesPage = function () { 10 | const { path } = useRouteMatch() 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/config/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ConfigurationScreen } from 'src/screens/Configuration/ConfigurationScreen' 4 | 5 | export const ConfigPage = () => { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { DashboardScreen } from 'src/screens/Dashboard/DashboardScreen' 4 | 5 | export const DashboardPage = function () { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/feeds_manager/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import Content from 'components/Content' 5 | import { JobDistributorsScreen } from 'src/screens/JobDistributors/JobDistributorsScreen' 6 | import { EditFeedsManagerScreen } from '../../screens/EditFeedsManager/EditFeedsManagerScreen' 7 | import { FeedsManagerScreen } from '../../screens/FeedsManager/FeedsManagerScreen' 8 | import { NewFeedsManagerScreen } from '../../screens/NewFeedsManager/NewFeedsManagerScreen' 9 | 10 | export const FeedsManagerPage = function () { 11 | const { path } = useRouteMatch() 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/job_proposals/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, useRouteMatch } from 'react-router-dom' 3 | 4 | import { JobProposalScreen } from 'src/screens/JobProposal/JobProposalScreen' 5 | 6 | export const JobProposalsPage = function () { 7 | const { path } = useRouteMatch() 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/job_runs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import { JobRunScreen } from 'screens/JobRun/JobRunScreen' 5 | import { JobRunsScreen } from 'screens/JobRuns/JobRunsScreen' 6 | 7 | export const JobRunsPage = function () { 8 | const { path } = useRouteMatch() 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/keys/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { KeyManagementScreen } from 'src/screens/KeyManagement/KeyManagementScreen' 4 | 5 | export const KeysPage = () => { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/nodes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import { NodeScreen } from 'screens/Node/NodeScreen' 5 | import { NodesScreen } from 'screens/Nodes/NodesScreen' 6 | 7 | export const NodesPage = function () { 8 | const { path } = useRouteMatch() 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/reducers.ts: -------------------------------------------------------------------------------- 1 | import { useSelector, TypedUseSelectorHook } from 'react-redux' 2 | import { combineReducers } from 'redux' 3 | import authentication from './reducers/authentication' 4 | import fetching from './reducers/fetching' 5 | import notifications from './reducers/notifications' 6 | import redirect from './reducers/redirect' 7 | import buildInfo from './reducers/buildInfo' 8 | const reducer = combineReducers({ 9 | authentication, 10 | fetching, 11 | notifications, 12 | redirect, 13 | buildInfo, 14 | }) 15 | 16 | export const INITIAL_STATE = reducer(undefined, { type: 'INITIAL_STATE' }) 17 | export type AppState = typeof INITIAL_STATE 18 | export const useOperatorUiSelector: TypedUseSelectorHook = useSelector 19 | 20 | export default reducer 21 | -------------------------------------------------------------------------------- /src/reducers/buildInfo.test.ts: -------------------------------------------------------------------------------- 1 | import reducer, { INITIAL_STATE } from '../../src/reducers' 2 | import { FetchBuildInfoActionType } from './actions' 3 | describe('reducers/buildInfo', () => { 4 | it('should set the buildInfo when a response arrives', () => { 5 | const buildInfoResponseAction = { 6 | type: FetchBuildInfoActionType.FETCH_BUILD_INFO_SUCCEEDED, 7 | buildInfo: { 8 | commitSHA: 'foo', 9 | version: 'bar', 10 | }, 11 | } as const 12 | 13 | const state = reducer(INITIAL_STATE, buildInfoResponseAction) 14 | expect(state.buildInfo.commitSHA).toEqual('foo') 15 | expect(state.buildInfo.version).toEqual('bar') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/reducers/buildInfo.ts: -------------------------------------------------------------------------------- 1 | import { BuildInfo } from 'core/store/models' 2 | import { Reducer } from 'redux' 3 | import { Actions, FetchBuildInfoActionType } from './actions' 4 | 5 | export type State = BuildInfo 6 | 7 | export const INITIAL_STATE: BuildInfo = { 8 | commitSHA: 'unknown', 9 | version: 'unknown', 10 | } 11 | 12 | const reducer: Reducer = (state = INITIAL_STATE, action) => { 13 | switch (action.type) { 14 | case FetchBuildInfoActionType.FETCH_BUILD_INFO_SUCCEEDED: { 15 | return { ...action.buildInfo } 16 | } 17 | } 18 | return state 19 | } 20 | 21 | export default reducer 22 | -------------------------------------------------------------------------------- /src/reducers/fetching.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux' 2 | import { Actions, RouterActionType } from './actions' 3 | 4 | export interface State { 5 | count: number 6 | } 7 | 8 | const INITIAL_STATE: State = { 9 | count: 0, 10 | } 11 | 12 | enum FetchPrefix { 13 | REQUEST = 'REQUEST_', 14 | RECEIVE = 'RECEIVE_', 15 | RESPONSE = 'RESPONSE_', 16 | } 17 | 18 | const reducer: Reducer = ( 19 | state = INITIAL_STATE, 20 | action: Actions, 21 | ) => { 22 | if (!action.type) { 23 | return state 24 | } 25 | 26 | if (action.type.startsWith(FetchPrefix.REQUEST)) { 27 | return { ...state, count: state.count + 1 } 28 | } else if (action.type.startsWith(FetchPrefix.RECEIVE)) { 29 | return { ...state, count: Math.max(state.count - 1, 0) } 30 | } else if (action.type.startsWith(FetchPrefix.RESPONSE)) { 31 | return { ...state, count: Math.max(state.count - 1, 0) } 32 | } else if (action.type === RouterActionType.REDIRECT) { 33 | return { ...state, count: 0 } 34 | } 35 | return state 36 | } 37 | 38 | export default reducer 39 | -------------------------------------------------------------------------------- /src/reducers/redirect.test.ts: -------------------------------------------------------------------------------- 1 | import reducer, { INITIAL_STATE } from '../../src/reducers' 2 | import { RouterActionType, RedirectAction } from '../../src/reducers/actions' 3 | 4 | describe('reducers/redirect', () => { 5 | const redirectAction: RedirectAction = { 6 | type: RouterActionType.REDIRECT, 7 | to: '/foo', 8 | } 9 | 10 | it('REDIRECT sets "to" as the given url', () => { 11 | const state = reducer(INITIAL_STATE, redirectAction) 12 | expect(state.redirect.to).toEqual('/foo') 13 | }) 14 | 15 | it('MATCH_ROUTE clears "to"', () => { 16 | let state = reducer(INITIAL_STATE, redirectAction) 17 | expect(state.redirect.to).toBeDefined() 18 | 19 | state = reducer(state, { 20 | type: RouterActionType.MATCH_ROUTE, 21 | pathname: '/any', 22 | }) 23 | expect(state.redirect.to).toBeUndefined() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/reducers/redirect.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux' 2 | import { Actions, RouterActionType } from './actions' 3 | 4 | export interface State { 5 | to?: string 6 | } 7 | 8 | const INITIAL_STATE: State = { 9 | to: undefined, 10 | } 11 | 12 | const reducer: Reducer = (state = INITIAL_STATE, action) => { 13 | switch (action.type) { 14 | case RouterActionType.REDIRECT: 15 | return { ...state, to: action.to } 16 | case RouterActionType.MATCH_ROUTE: 17 | return { ...state, to: undefined } 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | export default reducer 24 | -------------------------------------------------------------------------------- /src/screens/Bridges/BridgeRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { withStyles, WithStyles } from '@material-ui/core/styles' 4 | import TableCell from '@material-ui/core/TableCell' 5 | import TableRow from '@material-ui/core/TableRow' 6 | import Typography from '@material-ui/core/Typography' 7 | 8 | import Link from 'components/Link' 9 | import { tableStyles } from 'components/Table' 10 | 11 | interface Props extends WithStyles { 12 | bridge: BridgesPayload_ResultsFields 13 | } 14 | 15 | export const BridgeRow = withStyles(tableStyles)( 16 | ({ bridge, classes }: Props) => { 17 | return ( 18 | 19 | 20 | 21 | {bridge.name} 22 | 23 | 24 | 25 | {bridge.url} 26 | 27 | 28 | {bridge.confirmations} 29 | 30 | 31 | 32 | {bridge.minimumContractPayment} 33 | 34 | 35 | 36 | ) 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /src/screens/Chains/BetaAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | createStyles, 5 | Theme, 6 | withStyles, 7 | WithStyles, 8 | } from '@material-ui/core/styles' 9 | import Paper from '@material-ui/core/Paper' 10 | import Typography from '@material-ui/core/Typography' 11 | import WarningIcon from '@material-ui/icons//Warning' 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | paper: { 16 | backgroundColor: theme.palette.primary.light, 17 | padding: theme.spacing.unit * 2, 18 | display: 'flex', 19 | }, 20 | icon: { 21 | marginRight: theme.spacing.unit, 22 | }, 23 | }) 24 | 25 | interface Props extends WithStyles {} 26 | 27 | export const BetaAlert = withStyles(styles)(({ classes }: Props) => { 28 | return ( 29 | 30 | 31 | Multi-chain functionality is in Beta 32 | 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /src/screens/Chains/ChainRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { tableStyles } from 'components/Table' 4 | import Link from 'components/Link' 5 | 6 | import { withStyles, WithStyles } from '@material-ui/core/styles' 7 | import TableCell from '@material-ui/core/TableCell' 8 | import TableRow from '@material-ui/core/TableRow' 9 | 10 | interface Props extends WithStyles { 11 | chain: ChainsPayload_ResultsFields 12 | } 13 | 14 | export const ChainRow = withStyles(tableStyles)(({ chain, classes }: Props) => { 15 | return ( 16 | 17 | {chain.network} 18 | 19 | 23 | {chain.id} 24 | 25 | 26 | 27 | {chain.enabled.toString()} 28 | 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /src/screens/Chains/ChainsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ChainsView } from './ChainsView' 4 | import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler' 5 | import { Loading } from 'src/components/Feedback/Loading' 6 | import { useQueryParams } from 'src/hooks/useQueryParams' 7 | import { useChainsQuery } from 'src/hooks/queries/useChainsQuery' 8 | 9 | export const ChainsScreen = () => { 10 | const qp = useQueryParams() 11 | const page = parseInt(qp.get('page') || '1', 10) 12 | const pageSize = parseInt(qp.get('per') || '50', 10) 13 | 14 | const { data, loading, error } = useChainsQuery({ 15 | variables: { offset: (page - 1) * pageSize, limit: pageSize }, 16 | fetchPolicy: 'network-only', 17 | }) 18 | 19 | if (loading) { 20 | return 21 | } 22 | 23 | if (error) { 24 | return 25 | } 26 | 27 | if (data) { 28 | return ( 29 | 35 | ) 36 | } 37 | 38 | return null 39 | } 40 | -------------------------------------------------------------------------------- /src/screens/Configuration/ConfigurationScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ConfigurationView } from './ConfigurationView' 4 | 5 | export const ConfigurationScreen = () => { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/screens/Configuration/ConfigurationView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { BuildInfoProvider, renderWithRouter, screen } from 'support/test-utils' 4 | import { MockedProvider, MockedResponse } from '@apollo/client/testing' 5 | import { ConfigurationView } from './ConfigurationView' 6 | 7 | const { findByText } = screen 8 | 9 | describe('ConfigurationView', () => { 10 | it('renders the cards', async () => { 11 | const mocks: MockedResponse[] = [] 12 | 13 | renderWithRouter( 14 | 15 | 16 | 17 | 18 | , 19 | ) 20 | 21 | expect(await findByText('Node')).toBeInTheDocument() 22 | expect(await findByText('Job Runs')).toBeInTheDocument() 23 | expect(await findByText('Logging')).toBeInTheDocument() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/screens/Configuration/ConfigurationView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | import Content from 'components/Content' 4 | 5 | import { LoggingCard } from 'src/pages/Configuration/LoggingCard' 6 | import { JobRuns } from 'src/pages/Configuration/JobRuns' 7 | 8 | import { ConfigurationV2Card } from './ConfigurationV2Card/ConfigurationV2Card' 9 | import { NodeInfoCard } from './NodeInfoCard/NodeInfoCard' 10 | 11 | export const ConfigurationView = () => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/Configuration/NodeInfoCard/NodeInfoCard.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BuildInfoProvider, render, screen } from 'support/test-utils' 3 | 4 | import { NodeInfoCard } from './NodeInfoCard' 5 | 6 | const { queryByText } = screen 7 | 8 | describe('NodeInfoCard', () => { 9 | it('renders the node info card', () => { 10 | render( 11 | 12 | 13 | , 14 | ) 15 | 16 | expect(queryByText(/version/i)).toBeInTheDocument() 17 | expect(queryByText('1.0.0')).toBeInTheDocument() 18 | 19 | expect(queryByText(/sha/i)).toBeInTheDocument() 20 | expect( 21 | queryByText('6989a388ef26d981e771fec6710dc65bcc8fb5af'), 22 | ).toBeInTheDocument() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/screens/Dashboard/AccountBalance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | 5 | import { 6 | AccountBalanceCard, 7 | ACCOUNT_BALANCES_PAYLOAD__RESULTS_FIELDS, 8 | } from './AccountBalanceCard' 9 | 10 | export const ACCOUNT_BALANCES_QUERY = gql` 11 | ${ACCOUNT_BALANCES_PAYLOAD__RESULTS_FIELDS} 12 | query FetchAccountBalances { 13 | ethKeys { 14 | results { 15 | ...AccountBalancesPayload_ResultsFields 16 | } 17 | } 18 | } 19 | ` 20 | 21 | export const AccountBalance = () => { 22 | const { data, loading, error } = useQuery< 23 | FetchAccountBalances, 24 | FetchAccountBalancesVariables 25 | >(ACCOUNT_BALANCES_QUERY, { 26 | fetchPolicy: 'cache-and-network', 27 | }) 28 | 29 | return ( 30 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/Dashboard/Activity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | 5 | import { 6 | ActivityCard, 7 | RECENT_JOB_RUNS_PAYLOAD__RESULTS_FIELDS, 8 | } from './ActivityCard' 9 | 10 | const RECENT_JOB_RUNS_SIZE = 5 11 | 12 | export const RECENT_JOB_RUNS_QUERY = gql` 13 | ${RECENT_JOB_RUNS_PAYLOAD__RESULTS_FIELDS} 14 | query FetchRecentJobRuns($offset: Int, $limit: Int) { 15 | jobRuns(offset: $offset, limit: $limit) { 16 | results { 17 | ...RecentJobRunsPayload_ResultsFields 18 | } 19 | metadata { 20 | total 21 | } 22 | } 23 | } 24 | ` 25 | 26 | export const Activity = () => { 27 | const { data, loading, error } = useQuery< 28 | FetchRecentJobRuns, 29 | FetchRecentJobRunsVariables 30 | >(RECENT_JOB_RUNS_QUERY, { 31 | variables: { offset: 0, limit: RECENT_JOB_RUNS_SIZE }, 32 | fetchPolicy: 'cache-and-network', 33 | }) 34 | 35 | return ( 36 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/screens/Dashboard/BuildInfoFooter.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BuildInfoProvider, render, screen } from 'support/test-utils' 3 | import { BuildInfoFooter } from './BuildInfoFooter' 4 | 5 | const { getByRole, queryByText } = screen 6 | 7 | describe('BuildInfoFooter', () => { 8 | it('renders the footer', () => { 9 | render( 10 | 11 | 12 | , 13 | ) 14 | expect(queryByText(/chainlink node 1\.0\.0 at commit/i)).toBeInTheDocument() 15 | expect( 16 | getByRole('link', { name: '6989a388ef26d981e771fec6710dc65bcc8fb5af' }), 17 | ).toHaveAttribute( 18 | 'href', 19 | 'https://github.com/smartcontractkit/chainlink/commit/6989a388ef26d981e771fec6710dc65bcc8fb5af', 20 | ) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/screens/Dashboard/DashboardScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DashboardView } from './DashboardView' 3 | 4 | export const DashboardScreen = () => { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/screens/Dashboard/DashboardView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { BuildInfoProvider, renderWithRouter, screen } from 'support/test-utils' 3 | import { MockedProvider, MockedResponse } from '@apollo/client/testing' 4 | 5 | import { DashboardView } from './DashboardView' 6 | 7 | const { findByText } = screen 8 | 9 | describe('DashboardView', () => { 10 | it('renders the cards', async () => { 11 | const mocks: MockedResponse[] = [] 12 | 13 | renderWithRouter( 14 | 15 | 16 | 17 | 18 | , 19 | ) 20 | 21 | expect(await findByText('Activity')).toBeInTheDocument() 22 | expect(await findByText('Account Balances')).toBeInTheDocument() 23 | expect(await findByText('Recent Jobs')).toBeInTheDocument() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/screens/Dashboard/DashboardView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | 4 | import Content from 'components/Content' 5 | 6 | import { AccountBalance } from './AccountBalance' 7 | import { Activity } from './Activity' 8 | import { BuildInfoFooter } from './BuildInfoFooter' 9 | import { RecentJobs } from './RecentJobs' 10 | 11 | export const DashboardView = () => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/screens/Dashboard/RecentJobRow.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Route } from 'react-router-dom' 4 | import { renderWithRouter, screen } from 'support/test-utils' 5 | 6 | import { RecentJobRow } from './RecentJobRow' 7 | import { buildRecentJob } from 'support/factories/gql/fetchRecentJobs' 8 | import userEvent from '@testing-library/user-event' 9 | 10 | const { getByRole, queryByText } = screen 11 | 12 | describe('RecentJobRow', () => { 13 | function renderComponent(job: RecentJobsPayload_ResultsFields) { 14 | renderWithRouter( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | Job Page 25 | , 26 | ) 27 | } 28 | 29 | it('renders a row', () => { 30 | const job = buildRecentJob() 31 | 32 | renderComponent(job) 33 | 34 | expect(queryByText(job.name)).toBeInTheDocument() 35 | expect(queryByText('1 minute ago')).toBeInTheDocument() 36 | }) 37 | 38 | it('navigates to the job page', () => { 39 | const job = buildRecentJob() 40 | 41 | renderComponent(job) 42 | 43 | userEvent.click(getByRole('link', { name: job.name })) 44 | 45 | expect(queryByText('Job Page')).toBeInTheDocument() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/screens/Dashboard/RecentJobs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | 5 | import { 6 | RecentJobsCard, 7 | RECENT_JOBS_PAYLOAD__RESULTS_FIELDS, 8 | } from './RecentJobsCard' 9 | 10 | const RECENT_JOBS_SIZE = 5 11 | 12 | export const RECENT_JOBS_QUERY = gql` 13 | ${RECENT_JOBS_PAYLOAD__RESULTS_FIELDS} 14 | query FetchRecentJobs($offset: Int, $limit: Int) { 15 | jobs(offset: $offset, limit: $limit) { 16 | results { 17 | ...RecentJobsPayload_ResultsFields 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export const RecentJobs = () => { 24 | const { data, loading, error } = useQuery< 25 | FetchRecentJobs, 26 | FetchRecentJobsVariables 27 | >(RECENT_JOBS_QUERY, { 28 | variables: { offset: 0, limit: RECENT_JOBS_SIZE }, 29 | fetchPolicy: 'cache-and-network', 30 | }) 31 | 32 | return ( 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/screens/EditFeedsManager/EditFeedsManagerView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render, screen } from 'support/test-utils' 3 | 4 | import { EditFeedsManagerView } from './EditFeedsManagerView' 5 | import { buildFeedsManager } from 'support/factories/gql/fetchFeedsManagers' 6 | 7 | const { getByTestId, getByText } = screen 8 | 9 | describe('EditFeedsManagerView', () => { 10 | it('renders with initial values', () => { 11 | const handleSubmit = jest.fn() 12 | const manager = buildFeedsManager() 13 | 14 | render() 15 | 16 | expect(getByText('Edit Job Distributor')).toBeInTheDocument() 17 | expect(getByTestId('feeds-manager-form')).toHaveFormValues({ 18 | name: 'Chainlink Feeds Manager', 19 | uri: manager.uri, 20 | publicKey: manager.publicKey, 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/screens/EditFeedsManager/EditFeedsManagerView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import CardHeader from '@material-ui/core/CardHeader' 6 | import Grid from '@material-ui/core/Grid' 7 | 8 | import { 9 | FeedsManagerForm, 10 | Props as FormProps, 11 | } from 'components/Form/FeedsManagerForm' 12 | 13 | type Props = { 14 | data: FetchFeedsManagersPayload_ResultsFields 15 | } & Pick 16 | 17 | export const EditFeedsManagerView: React.FC = ({ data, onSubmit }) => { 18 | const initialValues = { 19 | name: data.name, 20 | uri: data.uri, 21 | publicKey: data.publicKey, 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/screens/FeedsManager/FeedsManagerView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { renderWithRouter, screen } from 'support/test-utils' 4 | 5 | import { MockedProvider } from '@apollo/client/testing' 6 | import { buildFeedsManagerResultFields } from 'support/factories/gql/fetchFeedsManagersWithProposals' 7 | import { FeedsManagerView } from './FeedsManagerView' 8 | 9 | const { findByText } = screen 10 | 11 | describe('FeedsManagerView', () => { 12 | it('renders the feeds manager view', async () => { 13 | const mgr = buildFeedsManagerResultFields() 14 | 15 | renderWithRouter( 16 | 17 | {}} 20 | onEnable={() => {}} 21 | /> 22 | , 23 | ) 24 | 25 | expect(await findByText('Job Distributors')).toBeInTheDocument() 26 | expect(await findByText('Job Proposals')).toBeInTheDocument() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/screens/FeedsManager/FeedsManagerView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Grid from '@material-ui/core/Grid' 4 | 5 | import { Heading1 } from 'src/components/Heading/Heading1' 6 | import { Heading2 } from 'src/components/Heading/Heading2' 7 | import { FeedsManagerCard } from './FeedsManagerCard' 8 | import { JobProposalsCard } from './JobProposalsCard' 9 | import { SupportedChainsCard } from './SupportedChainsCard' 10 | 11 | interface Props { 12 | manager: FeedsManagerPayload_ResultsFields 13 | onEnable: () => void 14 | onDisable: () => void 15 | } 16 | 17 | export const FeedsManagerView: React.FC = ({ 18 | manager, 19 | onDisable, 20 | onEnable, 21 | }) => { 22 | return ( 23 | 24 | 25 | Job Distributors 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | Job Proposals 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/screens/Job/RunJobDialog.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | import userEvent from '@testing-library/user-event' 5 | 6 | import { RunJobDialog } from './RunJobDialog' 7 | 8 | const { getByRole, queryByText } = screen 9 | 10 | describe('RunJobDialog', () => { 11 | let handleOnClose: jest.Mock 12 | let handleOnRun: jest.Mock 13 | 14 | function renderComponent() { 15 | render( 16 | , 17 | ) 18 | } 19 | 20 | beforeEach(() => { 21 | handleOnClose = jest.fn() 22 | handleOnRun = jest.fn() 23 | }) 24 | 25 | it('renders the dialog', () => { 26 | renderComponent() 27 | 28 | expect(queryByText('Pipeline Input')) 29 | }) 30 | 31 | it('can close the dialog', () => { 32 | renderComponent() 33 | 34 | userEvent.click(getByRole('button', { name: /close/i })) 35 | 36 | expect(handleOnClose).toHaveBeenCalled() 37 | }) 38 | 39 | it('submits the form', () => { 40 | renderComponent() 41 | 42 | userEvent.type(getByRole('textbox'), '{someinput}') 43 | userEvent.click(getByRole('button', { name: /run job/i })) 44 | 45 | expect(handleOnRun).toHaveBeenCalled() 46 | expect(handleOnClose).toHaveBeenCalled() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/screens/Job/TabDefinition.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Route } from 'react-router-dom' 4 | import { renderWithRouter, screen } from 'support/test-utils' 5 | import userEvent from '@testing-library/user-event' 6 | 7 | import { buildJob } from 'support/factories/gql/fetchJob' 8 | import { TabDefinition } from './TabDefinition' 9 | 10 | const { getByRole, getByTestId, queryByText } = screen 11 | 12 | describe('TabOverview', () => { 13 | function renderComponent(job: JobPayload_Fields) { 14 | renderWithRouter( 15 | <> 16 | 17 | 18 | 19 | , 20 | { initialEntries: ['/jobs/1/definition'] }, 21 | ) 22 | } 23 | 24 | it('renders the definition', () => { 25 | const job = buildJob() 26 | 27 | renderComponent(job) 28 | 29 | expect(getByTestId('definition').textContent).toMatchSnapshot() 30 | }) 31 | 32 | it('can copy the definition', () => { 33 | // The copy package used window.prompt to copy to clipboard 34 | window.prompt = jest.fn() 35 | 36 | const job = buildJob() 37 | 38 | renderComponent(job) 39 | 40 | userEvent.click(getByRole('button', { name: /copy/i })) 41 | 42 | expect(queryByText(/copied!/i)).toBeInTheDocument() 43 | expect(window.prompt).toHaveBeenCalled() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/screens/Job/TabDefinition.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 3 | import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism' 4 | 5 | import Card from '@material-ui/core/Card' 6 | import CardContent from '@material-ui/core/CardContent' 7 | import CardHeader from '@material-ui/core/CardHeader' 8 | import Typography from '@material-ui/core/Typography' 9 | 10 | import { CopyButton } from 'src/components/Copy/CopyButton' 11 | import { generateJobDefinition } from './generateJobDefinition' 12 | 13 | interface Props { 14 | job: JobPayload_Fields 15 | } 16 | 17 | export const TabDefinition = ({ job }: Props) => { 18 | const { definition } = generateJobDefinition(job) 19 | 20 | return ( 21 | 22 | } 25 | /> 26 | 27 | 28 | 33 | {definition} 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/screens/Job/__snapshots__/TabDefinition.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TabOverview renders the definition 1`] = ` 4 | "type = "directrequest" 5 | schemaVersion = 1 6 | name = "direct request job" 7 | externalJobID = "00000000-0000-0000-0000-0000000000001" 8 | gasLimit = 1_000 9 | forwardingAllowed = false 10 | maxTaskDuration = "10s" 11 | contractAddress = "0x0000000000000000000000000000000000000000" 12 | evmChainID = "42" 13 | minIncomingConfirmations = 3 14 | minContractPaymentLinkJuels = "100000000000000" 15 | requesters = [ "0x59bbE8CFC79c76857fE0eC27e67E4957370d72B5" ] 16 | observationSource = """ 17 | fetch [type=http method=POST url="http://localhost:8001" requestData="{\\\\"hi\\\\": \\\\"hello\\\\"}"]; 18 | parse [type=jsonparse path="data,result"]; 19 | multiply [type=multiply times=100]; 20 | fetch -> parse -> multiply; 21 | """ 22 | 23 | " 24 | `; 25 | -------------------------------------------------------------------------------- /src/screens/JobDistributors/JobDistributorsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler' 4 | import { Loading } from 'src/components/Feedback/Loading' 5 | import { useFeedsManagersQuery } from 'src/hooks/queries/useFeedsManagersQuery' 6 | import { JobDistributorsView } from './JobDistributorsView' 7 | 8 | export const JobDistributorsScreen = () => { 9 | const { data, loading, error } = useFeedsManagersQuery({ 10 | fetchPolicy: 'cache-and-network', 11 | }) 12 | 13 | if (loading) { 14 | return 15 | } 16 | 17 | if (error) { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/screens/JobRun/ErrorsCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { ErrorsCard } from './ErrorsCard' 6 | 7 | const { queryByText } = screen 8 | 9 | describe('ErrorsCard', () => { 10 | it('renders the errors', async () => { 11 | render() 12 | 13 | expect(queryByText('Errors')).toBeInTheDocument() 14 | expect(queryByText('error 1')).toBeInTheDocument() 15 | expect(queryByText('error 2')).toBeInTheDocument() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/screens/JobRun/ErrorsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import CardHeader from '@material-ui/core/CardHeader' 5 | import Typography from '@material-ui/core/Typography' 6 | 7 | interface Props { 8 | errors: ReadonlyArray 9 | } 10 | 11 | export const ErrorsCard: React.FC = ({ errors }) => { 12 | return ( 13 | 14 | 15 |
    16 | {errors.map((error, index) => ( 17 |
  • 18 | {error} 19 |
  • 20 | ))} 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/screens/JobRun/JSONCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { buildRun } from 'support/factories/gql/fetchJobRun' 3 | 4 | import { render, screen } from 'support/test-utils' 5 | 6 | import { JSONCard } from './JSONCard' 7 | 8 | const { getByTestId } = screen 9 | 10 | describe('JSONCard', () => { 11 | it('renders the run as json', () => { 12 | const start = '2020-01-03T22:45:00.166261Z' 13 | const end1m = '2020-01-03T22:46:00.166261Z' 14 | 15 | const run = buildRun({ 16 | createdAt: start, 17 | finishedAt: end1m, 18 | }) 19 | 20 | render() 21 | 22 | expect(getByTestId('pretty-json').textContent).toMatchSnapshot() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/screens/JobRun/JSONCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | 6 | import { PrettyJson } from 'src/components/Syntax/PrettyJson' 7 | 8 | interface Props { 9 | run: JobRunPayload_Fields 10 | } 11 | 12 | export const JSONCard: React.FC = ({ run }) => { 13 | // The run inputs are returned as a string instead of JSON, but we want to 14 | // display it as JSON. 15 | const obj = React.useMemo(() => { 16 | const { inputs, outputs, taskRuns, ...rest } = run 17 | 18 | let inputsObj = {} 19 | try { 20 | inputsObj = JSON.parse(inputs) 21 | } catch (e) { 22 | inputsObj = {} 23 | } 24 | 25 | return { 26 | ...rest, 27 | inputs: inputsObj, 28 | outputs, 29 | taskRuns, 30 | } 31 | }, [run]) 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/JobRun/JobRunScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | import { useParams } from 'react-router-dom' 5 | 6 | import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler' 7 | import { Loading } from 'src/components/Feedback/Loading' 8 | import { JobRunView, JOB_RUN_PAYLOAD_FIELDS } from './JobRunView' 9 | import NotFound from 'src/pages/NotFound' 10 | 11 | export const JOB_RUN_QUERY = gql` 12 | ${JOB_RUN_PAYLOAD_FIELDS} 13 | query FetchJobRun($id: ID!) { 14 | jobRun(id: $id) { 15 | __typename 16 | ... on JobRun { 17 | ...JobRunPayload_Fields 18 | } 19 | ... on NotFoundError { 20 | message 21 | } 22 | } 23 | } 24 | ` 25 | 26 | interface RouteParams { 27 | id: string 28 | } 29 | 30 | export const JobRunScreen = () => { 31 | const { id } = useParams() 32 | 33 | const { data, loading, error } = useQuery( 34 | JOB_RUN_QUERY, 35 | { variables: { id } }, 36 | ) 37 | 38 | if (loading) { 39 | return 40 | } 41 | 42 | if (error) { 43 | return 44 | } 45 | 46 | const payload = data?.jobRun 47 | switch (payload?.__typename) { 48 | case 'JobRun': 49 | return 50 | case 'NotFoundError': 51 | return 52 | default: 53 | return null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/screens/JobRun/StatusCard.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { StatusCard } from './StatusCard' 4 | import { render, screen } from 'support/test-utils' 5 | 6 | const { queryByText } = screen 7 | 8 | describe('components/StatusCard', () => { 9 | const start = '2020-01-03T22:45:00.166261Z' 10 | const end1m = '2020-01-03T22:46:00.166261Z' 11 | 12 | it('converts the given title to title case', () => { 13 | render() 14 | 15 | expect(queryByText('Completed')).toBeInTheDocument() 16 | }) 17 | 18 | it('displays the elapsed time for completed job runs', () => { 19 | render( 20 | , 21 | ) 22 | expect(queryByText('1m0s')).toBeInTheDocument() 23 | }) 24 | 25 | it('displays the elapsed time for errored job runs', () => { 26 | render() 27 | 28 | expect(queryByText('1m0s')).toBeInTheDocument() 29 | }) 30 | 31 | it('displays a live elapsed time for running job runs', () => { 32 | const now2m = '2020-01-03T22:47:00.166261Z' 33 | 34 | const spy = jest 35 | .spyOn(Date, 'now') 36 | .mockImplementation(() => new Date(now2m).valueOf()) 37 | 38 | render() 39 | 40 | expect(queryByText('2m0s')).toBeInTheDocument() 41 | 42 | spy.mockReset() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/screens/JobRun/TaskRunsCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { buildTaskRun } from 'support/factories/gql/fetchJobRun' 6 | import { TaskRunsCard } from './TaskRunsCard' 7 | 8 | const { queryByText } = screen 9 | 10 | describe('TaskRunsCard', () => { 11 | it('renders a job', () => { 12 | const observationSource = `fetch [type=bridge name="bridge-api0"]` 13 | const run = buildTaskRun({ 14 | dotID: 'fetch', 15 | }) 16 | 17 | render( 18 | , 19 | ) 20 | 21 | expect(queryByText(run.dotID)).toBeInTheDocument() 22 | expect(queryByText(run.type)).toBeInTheDocument() 23 | expect(queryByText(': bridge-api0')).toBeInTheDocument() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/screens/JobRun/TaskRunsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import { parseDot } from 'src/utils/parseDot' 5 | import { TaskRunItem } from './TaskRunItem' 6 | 7 | interface Props { 8 | taskRuns: ReadonlyArray 9 | observationSource?: string 10 | } 11 | 12 | export const TaskRunsCard = ({ taskRuns, observationSource }: Props) => { 13 | const items = React.useMemo(() => { 14 | const graph = parseDot(`digraph {${observationSource}}`) 15 | 16 | return graph.map((node) => { 17 | const taskRun = taskRuns.find(({ dotID }) => { 18 | return dotID === node.id 19 | }) 20 | 21 | if (!taskRun) { 22 | return undefined 23 | } 24 | 25 | return { 26 | ...taskRun, 27 | attrs: node.attributes, 28 | } 29 | }) 30 | }, [observationSource, taskRuns]) 31 | 32 | return ( 33 | 34 | {items.map((item, idx) => { 35 | return item ? : null 36 | })} 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/screens/JobRun/__snapshots__/JSONCard.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`JSONCard renders the run as json 1`] = `"{  "__typename": "JobRun",  "id": "1",  "allErrors": [  ],  "createdAt": "2020-01-03T22:45:00.166261Z",  "fatalErrors": [  ],  "finishedAt": "2020-01-03T22:46:00.166261Z",  "job": {    "id": "10",    "name": "job 1",    "observationSource": ""  },  "status": "COMPLETED",  "inputs": {  },  "outputs": [  ],  "taskRuns": [  ]}"`; 4 | -------------------------------------------------------------------------------- /src/screens/JobRuns/JobRunsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | 5 | import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler' 6 | import { JobRunsView, JOB_RUNS_PAYLOAD__RESULTS_FIELDS } from './JobRunsView' 7 | import { useQueryParams } from 'src/hooks/useQueryParams' 8 | 9 | export const JOB_RUNS_QUERY = gql` 10 | ${JOB_RUNS_PAYLOAD__RESULTS_FIELDS} 11 | query FetchJobRuns($offset: Int, $limit: Int) { 12 | jobRuns(offset: $offset, limit: $limit) { 13 | results { 14 | ...JobRunsPayload_ResultsFields 15 | } 16 | metadata { 17 | total 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export const JobRunsScreen = () => { 24 | const qp = useQueryParams() 25 | const page = parseInt(qp.get('page') || '1', 10) 26 | const pageSize = parseInt(qp.get('per') || '25', 10) 27 | 28 | const { data, loading, error } = useQuery< 29 | FetchJobRuns, 30 | FetchJobRunsVariables 31 | >(JOB_RUNS_QUERY, { 32 | variables: { offset: (page - 1) * pageSize, limit: pageSize }, 33 | fetchPolicy: 'cache-and-network', 34 | }) 35 | 36 | if (error) { 37 | return 38 | } 39 | 40 | return ( 41 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/screens/Jobs/JobRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { withStyles, WithStyles } from '@material-ui/core/styles' 4 | import TableCell from '@material-ui/core/TableCell' 5 | import TableRow from '@material-ui/core/TableRow' 6 | 7 | import { formatJobSpecType } from 'src/utils/formatJobSpecType' 8 | import { tableStyles } from 'components/Table' 9 | import { TimeAgo } from 'components/TimeAgo' 10 | import Link from 'components/Link' 11 | 12 | interface Props extends WithStyles { 13 | job: JobsPayload_ResultsFields 14 | } 15 | 16 | export const JobRow = withStyles(tableStyles)(({ job, classes }: Props) => { 17 | return ( 18 | 19 | 20 | 21 | {job.id} 22 | 23 | 24 | {job.name != '' ? job.name : '--'} 25 | {formatJobSpecType(job.spec.__typename)} 26 | {job.externalJobID} 27 | 28 | {job.createdAt} 29 | 30 | 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/CSAKeyRow.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { CSAKeyRow } from './CSAKeyRow' 6 | import { buildCSAKey } from 'support/factories/gql/fetchCSAKeys' 7 | 8 | const { queryByText } = screen 9 | 10 | describe('CSAKeyRow', () => { 11 | function renderComponent(csaKey: CsaKeysPayload_ResultsFields) { 12 | render( 13 | 14 | 15 | 16 | 17 |
, 18 | ) 19 | } 20 | 21 | it('renders a row', () => { 22 | const csaKey = buildCSAKey() 23 | 24 | renderComponent(csaKey) 25 | 26 | expect(queryByText(csaKey.publicKey)).toBeInTheDocument() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/CSAKeyRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TableCell from '@material-ui/core/TableCell' 4 | import TableRow from '@material-ui/core/TableRow' 5 | import Typography from '@material-ui/core/Typography' 6 | 7 | import { CopyIconButton } from 'src/components/Copy/CopyIconButton' 8 | 9 | interface Props { 10 | csaKey: CsaKeysPayload_ResultsFields 11 | } 12 | 13 | export const CSAKeyRow: React.FC = ({ csaKey }) => { 14 | return ( 15 | 16 | 17 | 18 | {csaKey.publicKey} 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/EVMAccountRow.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { EVMAccountRow } from './EVMAccountRow' 6 | import { buildETHKey } from 'support/factories/gql/fetchETHKeys' 7 | import { shortenHex } from 'src/utils/shortenHex' 8 | 9 | const { queryByText } = screen 10 | 11 | describe('EVMAccountRow', () => { 12 | function renderComponent(ethKey: EthKeysPayload_ResultsFields) { 13 | render( 14 | 15 | 16 | 17 | 18 |
, 19 | ) 20 | } 21 | 22 | it('renders a row', () => { 23 | const ethKey = buildETHKey() 24 | 25 | renderComponent(ethKey) 26 | 27 | expect( 28 | queryByText(shortenHex(ethKey.address, { start: 6, end: 6 })), 29 | ).toBeInTheDocument() 30 | expect(queryByText(ethKey.chain.id)).toBeInTheDocument() 31 | expect(queryByText('Enabled')).toBeInTheDocument() 32 | expect(queryByText('1.00000000')).toBeInTheDocument() 33 | expect(queryByText('0.100000000000000000')).toBeInTheDocument() 34 | expect(queryByText('1 minute ago')).toBeInTheDocument() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/EVMAccounts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EVMAccountsCard } from './EVMAccountsCard' 4 | import { useEVMAccountsQuery } from 'src/hooks/queries/useEVMAccountsQuery' 5 | 6 | export const EVMAccounts = () => { 7 | const { data, loading, error, refetch } = useEVMAccountsQuery({ 8 | fetchPolicy: 'cache-and-network', 9 | }) 10 | 11 | return ( 12 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/KeyBundle.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { KeyBundle } from './KeyBundle' 6 | 7 | const { getByText } = screen 8 | 9 | describe('pages/Keys/KeyBundle', () => { 10 | it('renders key bundle cell', async () => { 11 | const expectedPrimary = 'Primary information' 12 | const expectedSecondary = [ 13 | 'Secondary information 1', 14 | 'Secondary information 2', 15 | ] 16 | render( 17 | , 18 | ) 19 | 20 | expect(getByText(expectedPrimary)).toBeInTheDocument() 21 | expect(getByText(expectedSecondary[0])).toBeInTheDocument() 22 | expect(getByText(expectedSecondary[1])).toBeInTheDocument() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/KeyManagementScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { KeyManagementView } from './KeyManagementView' 4 | import { Feature, useFeatureFlag } from 'src/hooks/useFeatureFlag' 5 | 6 | export const KeyManagementScreen = () => { 7 | const isCSAKeysFeatureEnabled = useFeatureFlag(Feature.CSA) 8 | 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/KeyManagementView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Grid from '@material-ui/core/Grid' 4 | 5 | import Content from 'components/Content' 6 | import { EVMAccounts } from './EVMAccounts' 7 | import { NonEVMKeys } from './NonEVMKeys' 8 | import { CSAKeys } from './CSAKeys' 9 | import { OCRKeys } from './OCRKeys' 10 | import { OCR2Keys } from './OCR2Keys' 11 | import { P2PKeys } from './P2PKeys' 12 | 13 | interface Props { 14 | isCSAKeysFeatureEnabled: boolean 15 | } 16 | 17 | export const KeyManagementView: React.FC = ({ 18 | isCSAKeysFeatureEnabled, 19 | }) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {isCSAKeysFeatureEnabled && } 45 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/NonEVMKeyRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TableCell from '@material-ui/core/TableCell' 4 | import TableRow from '@material-ui/core/TableRow' 5 | import Typography from '@material-ui/core/Typography' 6 | 7 | import { CopyIconButton } from 'src/components/Copy/CopyIconButton' 8 | 9 | interface Props { 10 | chainKey: any 11 | fields: any[] 12 | } 13 | 14 | export const NonEVMKeyRow: React.FC = ({ chainKey, fields }) => { 15 | return ( 16 | 17 | {fields.map((field, idx) => ( 18 | 19 | 20 | {chainKey[field.key]}{' '} 21 | {field.copy && } 22 | 23 | 24 | ))} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/OCR2KeyBundleRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Button from 'src/components/Button' 4 | import TableCell from '@material-ui/core/TableCell' 5 | import TableRow from '@material-ui/core/TableRow' 6 | 7 | import { KeyBundle } from './KeyBundle' 8 | import { CopyIconButton } from 'src/components/Copy/CopyIconButton' 9 | 10 | interface Props { 11 | bundle: Ocr2KeyBundlesPayload_ResultsFields 12 | onDelete: () => void 13 | } 14 | 15 | /** 16 | * This row follows the form and structure of OCRKeyBundleRow but 17 | * uses the new data for keys from OCR2 18 | */ 19 | export const OCR2KeyBundleRow: React.FC = ({ bundle, onDelete }) => { 20 | return ( 21 | 22 | 23 | 26 | Key ID: {bundle.id} 27 | 28 | } 29 | secondary={[ 30 | <>Chain Type: {bundle.chainType}, 31 | <>Config Public Key: {bundle.configPublicKey}, 32 | <>On-Chain Public Key: {bundle.onChainPublicKey}, 33 | <>Off-Chain Public Key: {bundle.offChainPublicKey}, 34 | ]} 35 | /> 36 | 37 | 38 | 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/OCRKeyBundleRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Button from 'src/components/Button' 4 | import TableCell from '@material-ui/core/TableCell' 5 | import TableRow from '@material-ui/core/TableRow' 6 | 7 | import { KeyBundle } from './KeyBundle' 8 | import { CopyIconButton } from 'src/components/Copy/CopyIconButton' 9 | 10 | interface Props { 11 | bundle: OcrKeyBundlesPayload_ResultsFields 12 | onDelete: () => void 13 | } 14 | 15 | export const OCRKeyBundleRow: React.FC = ({ bundle, onDelete }) => { 16 | return ( 17 | 18 | 19 | 22 | Key ID: {bundle.id} 23 | 24 | } 25 | secondary={[ 26 | <>Config Public Key: {bundle.configPublicKey}, 27 | <>Signing Address: {bundle.onChainSigningAddress}, 28 | <>Off-Chain Public Key: {bundle.offChainPublicKey}, 29 | ]} 30 | /> 31 | 32 | 33 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/P2PKeyRow.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | import userEvent from '@testing-library/user-event' 5 | 6 | import { buildP2PKey } from 'support/factories/gql/fetchP2PKeys' 7 | import { P2PKeyRow } from './P2PKeyRow' 8 | 9 | const { getByRole, queryByText } = screen 10 | 11 | describe('P2PKeyRow', () => { 12 | let handleDelete: jest.Mock 13 | 14 | beforeEach(() => { 15 | handleDelete = jest.fn() 16 | }) 17 | 18 | function renderComponent(bundle: P2PKeysPayload_ResultsFields) { 19 | render( 20 | 21 | 22 | 23 | 24 |
, 25 | ) 26 | } 27 | 28 | it('renders a row', () => { 29 | const p2pKey = buildP2PKey() 30 | 31 | renderComponent(p2pKey) 32 | 33 | expect(queryByText(`Peer ID: ${p2pKey.peerID}`)).toBeInTheDocument() 34 | expect(queryByText(`Public Key: ${p2pKey.publicKey}`)).toBeInTheDocument() 35 | }) 36 | 37 | it('calls delete', () => { 38 | const p2pKey = buildP2PKey() 39 | 40 | renderComponent(p2pKey) 41 | 42 | userEvent.click(getByRole('button', { name: /delete/i })) 43 | 44 | expect(handleDelete).toHaveBeenCalled() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/screens/KeyManagement/notifications.tsx: -------------------------------------------------------------------------------- 1 | import { notifySuccessMsg } from 'actionCreators' 2 | 3 | // createSuccessNotification generates a action creator which provides a create 4 | // success message. 5 | export const createSuccessNotification = ({ 6 | keyType, 7 | keyValue, 8 | }: { 9 | keyType: string 10 | keyValue: string 11 | }) => notifySuccessMsg(`Successfully created ${keyType}: ${keyValue}`) 12 | 13 | // deleteSuccessNotification generates a action creator which provides a delete 14 | // success message. 15 | export const deleteSuccessNotification = ({ keyType }: { keyType: string }) => 16 | notifySuccessMsg(`Successfully deleted ${keyType} Key`) 17 | -------------------------------------------------------------------------------- /src/screens/NewBridge/NewBridgeView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { NewBridgeView } from './NewBridgeView' 6 | 7 | const { getByTestId, getByText } = screen 8 | 9 | describe('NewBridgeView', () => { 10 | it('renders with initial values', () => { 11 | const handleSubmit = jest.fn() 12 | 13 | render() 14 | 15 | expect(getByText('New Bridge')).toBeInTheDocument() 16 | expect(getByTestId('bridge-form')).toHaveFormValues({ 17 | name: '', 18 | url: '', 19 | minimumContractPayment: '0', 20 | confirmations: 0, 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/screens/NewBridge/NewBridgeView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import CardHeader from '@material-ui/core/CardHeader' 6 | import Grid from '@material-ui/core/Grid' 7 | 8 | import Content from 'components/Content' 9 | import { BridgeForm, Props as FormProps } from 'src/components/Form/BridgeForm' 10 | 11 | const initialValues = { 12 | name: '', 13 | url: '', 14 | minimumContractPayment: '0', 15 | confirmations: 0, 16 | } 17 | 18 | type Props = Pick 19 | 20 | export const NewBridgeView: React.FC = ({ onSubmit }) => { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/NewFeedsManager/NewFeedsManagerView.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { NewFeedsManagerView } from './NewFeedsManagerView' 6 | 7 | const { getByTestId, getByText } = screen 8 | 9 | describe('NewFeedsManagerView', () => { 10 | it('renders with initial values', () => { 11 | const handleSubmit = jest.fn() 12 | 13 | render() 14 | 15 | expect(getByText('Register Job Distributor')).toBeInTheDocument() 16 | expect(getByTestId('feeds-manager-form')).toHaveFormValues({ 17 | name: '', 18 | uri: '', 19 | publicKey: '', 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/screens/NewFeedsManager/NewFeedsManagerView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import CardHeader from '@material-ui/core/CardHeader' 6 | import Grid from '@material-ui/core/Grid' 7 | 8 | import { 9 | FeedsManagerForm, 10 | Props as FormProps, 11 | } from 'components/Form/FeedsManagerForm' 12 | 13 | const initialValues = { 14 | name: '', 15 | uri: '', 16 | publicKey: '', 17 | } 18 | 19 | type Props = Pick 20 | 21 | export const NewFeedsManagerView: React.FC = ({ onSubmit }) => { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/screens/NewJob/NewJobView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import debounce from 'lodash/debounce' 4 | import Grid from '@material-ui/core/Grid' 5 | 6 | import Content from 'components/Content' 7 | import { Props as FormProps } from 'components/Form/JobForm' 8 | import { NewJobFormCard } from './NewJobFormCard/NewJobFormCard' 9 | import { TaskListPreviewCard } from './TaskListPreviewCard/TaskListPreviewCard' 10 | 11 | type Props = Pick 12 | 13 | export const NewJobView: React.FC = ({ onSubmit }) => { 14 | const [toml, setTOML] = React.useState('') 15 | 16 | const handleTOMLChange = React.useCallback( 17 | (toml: string) => debounce(() => setTOML(toml), 500)(), 18 | [setTOML], 19 | ) 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/screens/NewJob/TaskListPreviewCard/TaskListPreviewCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { TaskListPreviewCard } from './TaskListPreviewCard' 6 | 7 | const { queryByText } = screen 8 | 9 | describe('TaskListPreviewCard', () => { 10 | function renderComponent(toml: string) { 11 | render() 12 | } 13 | 14 | it('renders the card', () => { 15 | renderComponent(`observationSource="ds1 [type=bridge name=voter_turnout];"`) 16 | 17 | expect(queryByText('Task List')).toBeInTheDocument() 18 | expect(queryByText('ds1')).toBeInTheDocument() 19 | }) 20 | 21 | it('has an empty TOML string', () => { 22 | renderComponent('') 23 | 24 | expect(queryByText('No Task Graph Found')).toBeInTheDocument() 25 | }) 26 | 27 | it('has an invalid TOML string', () => { 28 | renderComponent('invalidstring') 29 | 30 | expect(queryByText('No Task Graph Found')).toBeInTheDocument() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/screens/NewJob/TaskListPreviewCard/TaskListPreviewCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TOML from '@iarna/toml' 4 | import { TaskListCard } from 'src/components/Cards/TaskListCard' 5 | 6 | interface Props { 7 | toml: string 8 | } 9 | 10 | export const TaskListPreviewCard: React.FC = ({ toml }) => { 11 | const [observationSource, setObservationSource] = React.useState('') 12 | 13 | React.useEffect(() => { 14 | try { 15 | const spec = TOML.parse(toml) 16 | 17 | if (spec.observationSource) { 18 | setObservationSource((spec.observationSource as string).trim()) 19 | } else { 20 | setObservationSource('') 21 | } 22 | } catch (e) { 23 | setObservationSource('') 24 | } 25 | }, [toml]) 26 | 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /src/screens/Node/NodeCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { NodeCard } from './NodeCard' 6 | import { buildNodePayloadFields } from 'support/factories/gql/fetchNode' 7 | 8 | const { queryByText } = screen 9 | 10 | describe('NodeCard', () => { 11 | function renderComponent(node: NodePayload_Fields) { 12 | render() 13 | } 14 | 15 | it('renders a node', () => { 16 | const node = buildNodePayloadFields() 17 | 18 | renderComponent(node) 19 | 20 | expect(queryByText(node.chain.id)).toBeInTheDocument() 21 | expect(queryByText(node.httpURL)).toBeInTheDocument() 22 | expect(queryByText(node.wsURL)).toBeInTheDocument() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/screens/Node/NodeCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Grid from '@material-ui/core/Grid' 4 | 5 | import { 6 | DetailsCard, 7 | DetailsCardItemTitle, 8 | DetailsCardItemValue, 9 | } from 'src/components/Cards/DetailsCard' 10 | 11 | interface Props { 12 | node: NodePayload_Fields 13 | } 14 | 15 | export const NodeCard: React.FC = ({ node }) => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/Node/NodeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql, useQuery } from '@apollo/client' 4 | import { useParams } from 'react-router-dom' 5 | 6 | import { GraphqlErrorHandler } from 'src/components/ErrorHandler/GraphqlErrorHandler' 7 | import { Loading } from 'src/components/Feedback/Loading' 8 | import { NodeView, NODE_PAYLOAD_FIELDS } from './NodeView' 9 | import NotFound from 'src/pages/NotFound' 10 | 11 | export const NODE_QUERY = gql` 12 | ${NODE_PAYLOAD_FIELDS} 13 | query FetchNode($id: ID!) { 14 | node(id: $id) { 15 | __typename 16 | ... on Node { 17 | ...NodePayload_Fields 18 | } 19 | ... on NotFoundError { 20 | message 21 | } 22 | } 23 | } 24 | ` 25 | 26 | interface RouteParams { 27 | id: string 28 | } 29 | 30 | export const NodeScreen = () => { 31 | const { id } = useParams() 32 | 33 | const { data, loading, error } = useQuery( 34 | NODE_QUERY, 35 | { variables: { id } }, 36 | ) 37 | 38 | if (loading) { 39 | return 40 | } 41 | 42 | if (error) { 43 | return 44 | } 45 | 46 | const payload = data?.node 47 | switch (payload?.__typename) { 48 | case 'Node': 49 | return 50 | case 'NotFoundError': 51 | return 52 | default: 53 | return null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/screens/Node/NodeView.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from 'test-utils' 4 | 5 | import { NodeView } from './NodeView' 6 | import { buildNodePayloadFields } from 'support/factories/gql/fetchNode' 7 | 8 | const { getByRole } = screen 9 | 10 | describe('NodeView', () => { 11 | function renderComponent(node: NodePayload_Fields) { 12 | render() 13 | } 14 | 15 | it('renders the view', async () => { 16 | const node = buildNodePayloadFields() 17 | renderComponent(node) 18 | 19 | expect(getByRole('heading', { name: node.name })).toBeInTheDocument() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/screens/Node/NodeView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql } from '@apollo/client' 4 | 5 | import Grid from '@material-ui/core/Grid' 6 | 7 | import Content from 'components/Content' 8 | import { NodeCard } from './NodeCard' 9 | import { Heading1 } from 'src/components/Heading/Heading1' 10 | 11 | export const NODE_PAYLOAD_FIELDS = gql` 12 | fragment NodePayload_Fields on Node { 13 | id 14 | name 15 | chain { 16 | id 17 | network 18 | } 19 | httpURL 20 | wsURL 21 | state 22 | sendOnly 23 | order 24 | } 25 | ` 26 | 27 | interface Props { 28 | node: NodePayload_Fields 29 | } 30 | 31 | export const NodeView = ({ node }: Props) => { 32 | return ( 33 | <> 34 | 35 | 36 | 37 | {node.name} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/screens/Nodes/NodeRow.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Route } from 'react-router-dom' 4 | import { renderWithRouter, screen } from 'support/test-utils' 5 | import userEvent from '@testing-library/user-event' 6 | 7 | import { NodeRow } from './NodeRow' 8 | import { buildNode } from 'support/factories/gql/fetchNodes' 9 | 10 | const { findByText, getByRole, queryByText } = screen 11 | 12 | function renderComponent(node: NodesPayload_ResultsFields) { 13 | renderWithRouter( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | Link Success 24 | 25 | , 26 | ) 27 | } 28 | 29 | describe('NodeRow', () => { 30 | it('renders the row', () => { 31 | const node = buildNode() 32 | 33 | renderComponent(node) 34 | 35 | expect(queryByText('node1')).toBeInTheDocument() 36 | expect(queryByText('42')).toBeInTheDocument() 37 | }) 38 | 39 | it('links to the row details', async () => { 40 | const node = buildNode() 41 | 42 | renderComponent(node) 43 | 44 | const link = getByRole('link', { name: /1/i }) 45 | expect(link).toHaveAttribute('href', '/nodes/node1') 46 | 47 | userEvent.click(link) 48 | 49 | expect(await findByText('Link Success')).toBeInTheDocument() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/screens/Nodes/NodeRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { tableStyles } from 'components/Table' 4 | import Link from 'components/Link' 5 | 6 | import { withStyles, WithStyles } from '@material-ui/core/styles' 7 | import TableCell from '@material-ui/core/TableCell' 8 | import TableRow from '@material-ui/core/TableRow' 9 | 10 | interface Props extends WithStyles { 11 | node: NodesPayload_ResultsFields 12 | } 13 | 14 | export const NodeRow = withStyles(tableStyles)(({ node, classes }: Props) => { 15 | return ( 16 | 17 | 18 | 19 | {node.name} 20 | 21 | 22 | {node.chain.id} 23 | {node.state} 24 | {node.sendOnly ? 'SendOnly' : 'Primary'} 25 | {node.order ? node.order : 'NA'} 26 | 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /src/screens/Transaction/TransactionCard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { render, screen } from 'support/test-utils' 4 | 5 | import { buildEthTx } from 'support/factories/gql/fetchEthTransaction' 6 | import { TransactionCard } from './TransactionCard' 7 | import titleize from 'src/utils/titleize' 8 | 9 | const { queryByText } = screen 10 | 11 | describe('TransactionCard', () => { 12 | function renderComponent(tx: EthTransactionPayloadFields) { 13 | render() 14 | } 15 | 16 | it('renders a node', () => { 17 | const tx = buildEthTx() 18 | 19 | renderComponent(tx) 20 | 21 | expect(queryByText(tx.chain.id)).toBeInTheDocument() 22 | expect(queryByText(tx.data)).toBeInTheDocument() 23 | expect(queryByText(tx.from)).toBeInTheDocument() 24 | expect(queryByText(tx.gasLimit)).toBeInTheDocument() 25 | expect(queryByText(tx.gasPrice)).toBeInTheDocument() 26 | expect(queryByText(tx.hash)).toBeInTheDocument() 27 | expect(queryByText(tx.hex)).toBeInTheDocument() 28 | expect(queryByText(tx.nonce as string)).toBeInTheDocument() 29 | expect(queryByText(tx.sentAt as string)).toBeInTheDocument() 30 | expect(queryByText(titleize(tx.state))).toBeInTheDocument() 31 | expect(queryByText(tx.to)).toBeInTheDocument() 32 | expect(queryByText(tx.value)).toBeInTheDocument() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/screens/Transaction/TransactionView.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { render, screen } from 'test-utils' 4 | 5 | import { buildEthTx } from 'support/factories/gql/fetchEthTransaction' 6 | import { TransactionView } from './TransactionView' 7 | 8 | const { getByRole, getByText } = screen 9 | 10 | describe('TransactionView', () => { 11 | function renderComponent(tx: EthTransactionPayloadFields) { 12 | render() 13 | } 14 | 15 | it('renders the view', async () => { 16 | const tx = buildEthTx() 17 | renderComponent(tx) 18 | 19 | expect( 20 | getByRole('heading', { name: 'Transaction Details' }), 21 | ).toBeInTheDocument() 22 | expect(getByText(tx.hash)).toBeInTheDocument() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/screens/Transaction/TransactionView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { gql } from '@apollo/client' 4 | 5 | import Grid from '@material-ui/core/Grid' 6 | 7 | import Content from 'src/components/Content' 8 | import { Heading1 } from 'src/components/Heading/Heading1' 9 | import { TransactionCard } from './TransactionCard' 10 | 11 | export const ETH_TRANSACTION_PAYLOAD_FIELDS = gql` 12 | fragment EthTransactionPayloadFields on EthTransaction { 13 | chain { 14 | id 15 | network 16 | } 17 | data 18 | from 19 | gasLimit 20 | gasPrice 21 | hash 22 | hex 23 | nonce 24 | sentAt 25 | state 26 | to 27 | value 28 | } 29 | ` 30 | 31 | interface Props { 32 | tx: EthTransactionPayloadFields 33 | } 34 | 35 | export const TransactionView: React.FC = ({ tx }) => { 36 | return ( 37 | 38 | 39 | 40 | Transaction Details 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/screens/Transactions/TransactionRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { withStyles, WithStyles } from '@material-ui/core/styles' 4 | import TableCell from '@material-ui/core/TableCell' 5 | import TableRow from '@material-ui/core/TableRow' 6 | 7 | import { shortenHex } from 'src/utils/shortenHex' 8 | import { tableStyles } from 'components/Table' 9 | import Link from 'components/Link' 10 | 11 | interface Props extends WithStyles { 12 | tx: EthTransactionsPayload_ResultsFields 13 | } 14 | 15 | export const TransactionRow = withStyles(tableStyles)( 16 | ({ tx, classes }: Props) => { 17 | return ( 18 | 19 | 20 | 21 | {shortenHex(tx.hash, { start: 8, end: 8 })} 22 | 23 | 24 | {tx.chain.id} 25 | {shortenHex(tx.from, { start: 8, end: 8 })} 26 | {shortenHex(tx.to, { start: 8, end: 8 })} 27 | {tx.nonce} 28 | {tx.sentAt || '--'} 29 | 30 | ) 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /src/selectors/buildInfo.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from 'reducers' 2 | 3 | export const selectBuildInfo = ({ buildInfo }: Pick) => 4 | buildInfo 5 | -------------------------------------------------------------------------------- /src/selectors/fetchCount.test.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../../src/reducers' 2 | import fetchCountSelector from '../../src/selectors/fetchCount' 3 | 4 | describe('selectors - fetchCount', () => { 5 | it('returns the value of the counter', () => { 6 | const state: Pick = { fetching: { count: 1 } } 7 | 8 | expect(fetchCountSelector(state)).toEqual(1) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/selectors/fetchCount.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from 'reducers' 2 | 3 | export default ({ fetching }: Pick) => fetching.count 4 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const GWEI_PER_TOKEN = 1e9 2 | export const WEI_PER_TOKEN = 1e18 3 | -------------------------------------------------------------------------------- /src/utils/formatJobSpecType.test.ts: -------------------------------------------------------------------------------- 1 | import { formatJobSpecType } from './formatJobSpecType' 2 | 3 | describe('formatJobSpecType', () => { 4 | it("removes 'Spec' suffix as a default", () => { 5 | expect(formatJobSpecType('KeeperSpec')).toEqual('Keeper') 6 | }) 7 | 8 | it('formats Direct Request as a special case', () => { 9 | expect(formatJobSpecType('DirectRequestSpec')).toEqual('Direct Request') 10 | }) 11 | 12 | it('formats Flux Monitor as a special case', () => { 13 | expect(formatJobSpecType('FluxMonitorSpec')).toEqual('Flux Monitor') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/utils/formatJobSpecType.ts: -------------------------------------------------------------------------------- 1 | // formatJobSpecType formats the typename of a spec to a readable format. 2 | export const formatJobSpecType = (typename: string) => { 3 | switch (typename) { 4 | case 'DirectRequestSpec': 5 | return 'Direct Request' 6 | case 'FluxMonitorSpec': 7 | return 'Flux Monitor' 8 | default: 9 | return typename.replace(/Spec$/, '') 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/inputErrors.ts: -------------------------------------------------------------------------------- 1 | import { InputErrors } from 'src/types/generated/graphql' 2 | 3 | export const parseInputErrors = (payload: InputErrors) => { 4 | return payload.errors.reduce((obj, item) => { 5 | const key = item['path'].replace(/^input\//, '') 6 | 7 | return { 8 | ...obj, 9 | [key]: item.message, 10 | } 11 | }, {}) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/json-api-client/fetchWithTimeout.test.ts: -------------------------------------------------------------------------------- 1 | import { fetchWithTimeout } from './fetchWithTimeout' 2 | 3 | describe('fetchWithTimeout', () => { 4 | it('rejects fetch requests after timeout period', () => { 5 | const timeoutResponse = new Promise((res) => 6 | setTimeout(() => res(200), 100), 7 | ) 8 | global.fetch.getOnce('/test', timeoutResponse) 9 | 10 | return expect(fetchWithTimeout('/test', {}, 1)).rejects.toThrow('timeout') 11 | }) 12 | 13 | it('resolves fetch requests before timeout period', () => { 14 | const timeoutResponse = new Promise((res) => setTimeout(() => res(200), 1)) 15 | global.fetch.getOnce('/test', timeoutResponse) 16 | 17 | return expect(fetchWithTimeout('/test', {}, 100)).resolves.toMatchObject({ 18 | status: 200, 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/json-api-client/fetchWithTimeout.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch' 2 | 3 | export function fetchWithTimeout( 4 | url: string, 5 | options: Parameters[1], 6 | timeout = 20000, 7 | ): Promise { 8 | return Promise.race([ 9 | fetch(url, options), 10 | new Promise((_, reject) => 11 | setTimeout(() => reject(new Error('timeout')), timeout), 12 | ) as any as Response, 13 | ]) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/json-api-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors' 2 | export * from './fetchWithTimeout' 3 | export * from './transport/http' 4 | export * from './transport/json' 5 | -------------------------------------------------------------------------------- /src/utils/json-api-client/transport/http.test.ts: -------------------------------------------------------------------------------- 1 | import { createUrl } from './http' 2 | 3 | describe('http tests', () => { 4 | describe('createUrl', () => { 5 | // each element is in the format of 6 | // [expected, base, path, query?] 7 | const cases = [ 8 | ['http://explorer:3001/foo', 'http://explorer:3001', 'foo', undefined], 9 | ['http://explorer:3001/foo', 'http://explorer:3001', '/foo', undefined], 10 | [ 11 | 'http://explorer:3001/foo', 12 | 'http://explorer:3001/ignore/this/path', 13 | '/foo', 14 | undefined, 15 | ], 16 | [ 17 | 'http://explorer:3001/foo?bar=baz&boing=boing', 18 | 'http://explorer:3001', 19 | 'foo', 20 | { bar: 'baz', boing: 'boing' }, 21 | ], 22 | [ 23 | 'http://explorer:3001/foo?stinky=false', 24 | 'http://explorer:3001', 25 | 'foo', 26 | { stinky: false, shouldNotExist: undefined, shouldNotExist2: null }, 27 | ], 28 | [ 29 | 'http://explorer:3001/jobs/170?page=1&size=10', 30 | 'http://explorer:3001', 31 | 'jobs/170', 32 | { page: 1, size: 10 }, 33 | ], 34 | ] 35 | 36 | it.each(cases as any[])( 37 | '%s\nbase=%s\npath=%s\nquery=%o\n', 38 | (expected, b, p, q) => { 39 | expect(createUrl(b, p, q).toString()).toEqual(expected) 40 | }, 41 | ) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/utils/local-storage.ts: -------------------------------------------------------------------------------- 1 | import storage from 'local-storage-fallback' 2 | 3 | export function get(key: string): string | null { 4 | return storage.getItem(`chainlink.${key}`) 5 | } 6 | 7 | export function set(key: string, val: string): void { 8 | storage.setItem(`chainlink.${key}`, val) 9 | } 10 | 11 | export function remove(key: string): void { 12 | storage.removeItem(`chainlink.${key}`) 13 | } 14 | 15 | export function getJson(key: string): any { 16 | const stored = get(key) 17 | const obj = {} 18 | 19 | if (stored) { 20 | try { 21 | return JSON.parse(stored) 22 | } catch (e) { 23 | // continue regardless of error 24 | } 25 | } 26 | 27 | return obj 28 | } 29 | 30 | export function setJson(key: string, obj: any): void { 31 | const json = JSON.stringify(obj) 32 | set(key, json) 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/matchRouteAndMapDispatchToProps.js: -------------------------------------------------------------------------------- 1 | // react-static does not support react-router-redux, connected-react-router or 2 | // provide an escape hatch. 3 | // https://github.com/nozzle/react-static/issues/211#issuecomment-389695521 4 | // 5 | // We need to handle this manually. mapDispatchToProps provides the props of 6 | // the matched URL. We can dispatch this url and parse it in a reducer. 7 | import { bindActionCreators } from 'redux' 8 | 9 | const matchRouteAndMapDispatchToProps = (actionCreators) => (dispatch) => { 10 | return bindActionCreators(actionCreators, dispatch) 11 | } 12 | 13 | export default matchRouteAndMapDispatchToProps 14 | -------------------------------------------------------------------------------- /src/utils/parseDot.ts: -------------------------------------------------------------------------------- 1 | import graphlibDot from 'graphlib-dot' 2 | 3 | export type Stratify = { 4 | id: string 5 | parentIds: string[] 6 | attributes?: { [key: string]: string } 7 | } 8 | 9 | type Edge = { 10 | v: string 11 | w: string 12 | } 13 | 14 | export function parseDot(dot: string): Stratify[] { 15 | // We want to permit the use of angle brackets to make the 16 | // specs more readable for multi-line task attributes. The backend 17 | // dot parsing library supports angle brackets which do not contain 18 | // valid HTML inside them, but the frontend dot library graphlibDot does not. 19 | // Since the dot parsing on the frontend is merely used to display the graph nodes 20 | // its fine to omit the angle bracket attributes. 21 | const dotNoAngleBrackets = dot.replace(/\w+\s*=\s*<([^>]|[\r\n])*>/g, '') 22 | const digraph = graphlibDot.read(dotNoAngleBrackets) 23 | const edges = digraph.edges() 24 | 25 | return digraph.nodes().map((id: string) => { 26 | const nodeInformation: Stratify = { 27 | id, 28 | parentIds: edges 29 | .filter((edge: Edge) => edge.w === id) 30 | .map((edge: Edge) => edge.v), 31 | } 32 | 33 | if (Object.keys(digraph.node(id)).length > 0) { 34 | nodeInformation.attributes = digraph.node(id) 35 | } 36 | 37 | return nodeInformation 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/shortenHex.test.ts: -------------------------------------------------------------------------------- 1 | import { shortenHex } from './shortenHex' 2 | 3 | describe('shortenHex', () => { 4 | it('shortens the hex with default values', () => { 5 | expect(shortenHex('0x123456789abcdef')).toEqual('0x1234...cdef') 6 | }) 7 | 8 | it('shortens the hex with custom values', () => { 9 | expect(shortenHex('0x123456789abcdef', { start: 2, end: 2 })).toEqual( 10 | '0x...ef', 11 | ) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/utils/shortenHex.ts: -------------------------------------------------------------------------------- 1 | export function shortenHex( 2 | value: string, 3 | { 4 | start = 6, 5 | end = 4, 6 | }: { 7 | start?: number 8 | end?: number 9 | } = {}, 10 | ) { 11 | return value.substring(0, start) + '...' + value.substring(value.length - end) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { getAuthentication, setAuthentication } from '../../src/utils/storage' 2 | 3 | describe('utils/storage', () => { 4 | beforeEach(() => { 5 | localStorage.clear() 6 | }) 7 | 8 | describe('getAuthentication', () => { 9 | it('returns a JS object for JSON stored as "chainlink.authentication" in localStorage', () => { 10 | localStorage.setItem('chainlink.authentication', '{"allowed":true}') 11 | expect(getAuthentication()).toEqual({ allowed: true }) 12 | }) 13 | }) 14 | 15 | describe('setAuthentication', () => { 16 | it('saves the JS object as JSON under the key "chainlink.authentication" in localStorage', () => { 17 | setAuthentication({ allowed: true }) 18 | expect(localStorage.getItem('chainlink.authentication')).toEqual( 19 | '{"allowed":true}', 20 | ) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import * as storage from 'utils/local-storage' 2 | 3 | const PERSIST_URL = 'persistURL' 4 | 5 | export function getPersistUrl(): string { 6 | return storage.get(PERSIST_URL) || '' 7 | } 8 | 9 | export function setPersistUrl(url: string): void { 10 | storage.set(PERSIST_URL, url) 11 | } 12 | 13 | export interface Auth { 14 | allowed?: boolean 15 | } 16 | 17 | export function getAuthentication(): Auth { 18 | return storage.getJson('authentication') 19 | } 20 | 21 | export function setAuthentication(auth: Auth): void { 22 | storage.setJson('authentication', auth) 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/taskRunStatus.ts: -------------------------------------------------------------------------------- 1 | // A task run status is inferred from the state of a task run as we are not 2 | // provided a status from the API. 3 | export enum TaskRunStatus { 4 | UNKNOWN = 'UNKNOWN', 5 | PENDING = 'PENDING', 6 | ERROR = 'ERROR', 7 | COMPLETE = 'COMPLETE', 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/titleize.js: -------------------------------------------------------------------------------- 1 | // insight from - https://github.com/sindresorhus/titleize 2 | export default (input) => { 3 | if (typeof input !== 'string') { 4 | return input 5 | } 6 | 7 | return input 8 | .toLowerCase() 9 | .replace(/_/g, ' ') 10 | .replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase()) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/titleize.test.js: -------------------------------------------------------------------------------- 1 | import titleize from 'utils/titleize' 2 | 3 | describe('Titleizes strings', () => { 4 | it('Capitalizes first words of the sentence', () => { 5 | const brokenCase = 'cApiTAlS hErE, LoweRcaseS There' 6 | const brokenCaseTitleized = 'Capitals Here, Lowercases There' 7 | expect(titleize(brokenCase)).toEqual(brokenCaseTitleized) 8 | }) 9 | it('Converts underscores into spaces', () => { 10 | const underscoreString = 'Pending_Run_Success' 11 | const underscoreStringTitleized = 'Pending Run Success' 12 | expect(titleize(underscoreString)).toEqual(underscoreStringTitleized) 13 | }) 14 | it('Capitalizes first words and converts underscores into spaces', () => { 15 | const brokenCaseWithUnderscores = 'job_error_now' 16 | const brokenCaseWithUnderscoresCorrect = 'Job Error Now' 17 | expect(titleize(brokenCaseWithUnderscores)).toEqual( 18 | brokenCaseWithUnderscoresCorrect, 19 | ) 20 | }) 21 | it('Does not converts non string values ', () => { 22 | const date = new Date() 23 | expect(titleize(1)).toEqual(1) 24 | expect(titleize(undefined)).toEqual(undefined) 25 | expect(titleize(date)).toEqual(date) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/utils/tokens/link.test.ts: -------------------------------------------------------------------------------- 1 | import { fromJuels } from './link' 2 | 3 | describe('fromJuels', () => { 4 | it('converts juels to LINK', () => { 5 | expect(fromJuels('1010000000000000001')).toEqual('1.01000000') 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/utils/tokens/link.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'bignumber.js' 2 | 3 | export const JUELS_PER_TOKEN = 1e18 4 | 5 | // fromJuels converts a string value in juels to LINK 6 | export const fromJuels = (val: string): string => { 7 | const juels = new BigNumber(val) 8 | 9 | return juels.dividedBy(JUELS_PER_TOKEN).toFixed(8) 10 | } 11 | -------------------------------------------------------------------------------- /support/factories/gql/fetchAccountBalances.ts: -------------------------------------------------------------------------------- 1 | // buildETHKey builds a eth key for the FetchETHKeys query. 2 | export function buildETHKey( 3 | overrides?: Partial, 4 | ): AccountBalancesPayload_ResultsFields { 5 | return { 6 | __typename: 'EthKey', 7 | address: '0x0000000000000000000000000000000000000001', 8 | chain: { 9 | __typename: 'Chain', 10 | id: '42', 11 | network: 'evm', 12 | }, 13 | ethBalance: '0.100000000000000000', 14 | isDisabled: false, 15 | linkBalance: '1000000000000000000', 16 | ...overrides, 17 | } 18 | } 19 | 20 | // buildETHKeys builds a list of eth keys. 21 | export function buildETHKeys(): ReadonlyArray { 22 | return [ 23 | buildETHKey(), 24 | buildETHKey({ 25 | address: '0x0000000000000000000000000000000000000002', 26 | }), 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /support/factories/gql/fetchBridge.ts: -------------------------------------------------------------------------------- 1 | // buildBridgePayloadFields builds the bridge fields. 2 | export function buildBridgePayloadFields( 3 | overrides?: Partial, 4 | ): BridgePayload_Fields { 5 | return { 6 | __typename: 'Bridge', 7 | id: 'bridge-api', 8 | name: 'bridge-api', 9 | url: 'http://bridge.com', 10 | confirmations: 1, 11 | minimumContractPayment: '0', 12 | outgoingToken: 'outgoing1', 13 | ...overrides, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /support/factories/gql/fetchBridges.ts: -------------------------------------------------------------------------------- 1 | // buildBridge builds a Bridge for the FetchBridges query. 2 | export function buildBridge( 3 | overrides?: Partial, 4 | ): BridgesPayload_ResultsFields { 5 | return { 6 | __typename: 'Bridge', 7 | id: 'bridge-api', 8 | name: 'bridge-api', 9 | url: 'http://bridge.com', 10 | confirmations: 1, 11 | minimumContractPayment: '0', 12 | ...overrides, 13 | } 14 | } 15 | 16 | // buildsBridges builds a list of bridges. 17 | export function buildBridges(): ReadonlyArray { 18 | return [ 19 | buildBridge({ 20 | id: 'bridge-api1', 21 | name: 'bridge-api1', 22 | url: 'http://bridge1.com', 23 | confirmations: 1, 24 | minimumContractPayment: '100', 25 | }), 26 | buildBridge({ 27 | id: 'bridge-api2', 28 | name: 'bridge-api2', 29 | url: 'http://bridge2.com', 30 | confirmations: 2, 31 | minimumContractPayment: '200', 32 | }), 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /support/factories/gql/fetchCSAKeys.ts: -------------------------------------------------------------------------------- 1 | // buildCSAKey builds a CSA Key for the FetchCSAKeys query. 2 | export function buildCSAKey( 3 | overrides?: Partial, 4 | ): CsaKeysPayload_ResultsFields { 5 | return { 6 | __typename: 'CSAKey', 7 | id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 8 | publicKey: 9 | 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 10 | ...overrides, 11 | } 12 | } 13 | 14 | // buildCSAKeys builds a list of csa keys. 15 | export function buildCSAKeys(): ReadonlyArray { 16 | return [ 17 | buildCSAKey({ 18 | id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 19 | publicKey: 20 | 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 21 | }), 22 | buildCSAKey({ 23 | id: 'e09c2e1444322d91cfb9b8576ce5895e54dc5caef37c5aff4accca9272412f5b', 24 | publicKey: 25 | 'e09c2e1444322d91cfb9b8576ce5895e54dc5caef37c5aff4accca9272412f5b', 26 | }), 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /support/factories/gql/fetchChains.ts: -------------------------------------------------------------------------------- 1 | // buildChains builds a chain for the FetchChains query. 2 | export function buildChain( 3 | overrides?: Partial, 4 | ): ChainsPayload_ResultsFields { 5 | return { 6 | __typename: 'Chain', 7 | id: '5', 8 | enabled: true, 9 | network: 'EVM', 10 | ...overrides, 11 | } 12 | } 13 | 14 | // buildsChains builds a list of chains. 15 | export function buildChains(): ReadonlyArray { 16 | return [ 17 | buildChain({ 18 | id: '5', 19 | enabled: true, 20 | }), 21 | buildChain({ 22 | id: '42', 23 | enabled: true, 24 | }), 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /support/factories/gql/fetchETHKeys.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | // buildETHKey builds a eth key for the FetchETHKeys query. 4 | export function buildETHKey( 5 | overrides?: Partial, 6 | ): EthKeysPayload_ResultsFields { 7 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 8 | 9 | return { 10 | __typename: 'EthKey', 11 | address: '0x0000000000000000000000000000000000000001', 12 | chain: { 13 | id: '42', 14 | network: 'evm', 15 | }, 16 | createdAt: minuteAgo, 17 | ethBalance: '0.100000000000000000', 18 | isDisabled: false, 19 | linkBalance: '1000000000000000000', 20 | ...overrides, 21 | } 22 | } 23 | 24 | // buildETHKeys builds a list of eth keys. 25 | export function buildETHKeys(): ReadonlyArray { 26 | return [ 27 | buildETHKey(), 28 | buildETHKey({ 29 | address: '0x0000000000000000000000000000000000000002', 30 | }), 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /support/factories/gql/fetchEthTransaction.ts: -------------------------------------------------------------------------------- 1 | // buildEthTx builds a eth transaction for the FetchEthTransaction query. 2 | export function buildEthTx( 3 | overrides?: Partial, 4 | ): EthTransactionPayloadFields { 5 | return { 6 | __typename: 'EthTransaction', 7 | chain: { 8 | id: '42', 9 | network: 'evm', 10 | }, 11 | data: '0x', 12 | from: '0x0000000000000000000000000000000000000001', 13 | gasLimit: '21000', 14 | gasPrice: '2500000008', 15 | hash: '0x1111111111111111', 16 | hex: '0xf', 17 | nonce: '0', 18 | sentAt: '1000', 19 | state: 'confirmed', 20 | to: '0x0000000000000000000000000000000000000002', 21 | value: '0.020000000000000000', 22 | ...overrides, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /support/factories/gql/fetchEthTransactions.ts: -------------------------------------------------------------------------------- 1 | // buildEthTx builds a eth transactions for the FetchEthTransactions query. 2 | export function buildEthTx( 3 | overrides?: Partial, 4 | ): EthTransactionsPayload_ResultsFields { 5 | return { 6 | __typename: 'EthTransaction', 7 | chain: { 8 | id: '42', 9 | network: 'evm', 10 | }, 11 | from: '0x0000000000000000000000000000000000000001', 12 | hash: '0x1111111111111111', 13 | to: '0x0000000000000000000000000000000000000002', 14 | nonce: '0', 15 | sentAt: '1000', 16 | ...overrides, 17 | } 18 | } 19 | 20 | // buildEthTxs builds a list of eth keys. 21 | export function buildEthTxs(): ReadonlyArray { 22 | return [ 23 | buildEthTx(), 24 | buildEthTx({ 25 | from: '0x0000000000000000000000000000000000000003', 26 | hash: '0x2222222222222222', 27 | to: '0x0000000000000000000000000000000000000004', 28 | nonce: '1', 29 | sentAt: '1001', 30 | }), 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /support/factories/gql/fetchFeedsManagers.ts: -------------------------------------------------------------------------------- 1 | // buildFeedsManager builds a feeds manager for the FetchFeedsManagers query. 2 | export function buildFeedsManager( 3 | overrides?: Partial, 4 | ): FetchFeedsManagersPayload_ResultsFields { 5 | return { 6 | __typename: 'FeedsManager', 7 | id: '1', 8 | name: 'Chainlink Feeds Manager', 9 | uri: 'localhost:8080', 10 | publicKey: '1111', 11 | isConnectionActive: false, 12 | createdAt: new Date(), 13 | disabledAt: new Date(), 14 | ...overrides, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /support/factories/gql/fetchJobProposal.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | // buildJobProposal builds a job proposal for the FetchJobProposal query. 4 | export function buildJobProposal( 5 | overrides?: Partial, 6 | ): JobProposalPayloadFields { 7 | return { 8 | __typename: 'JobProposal', 9 | id: '1', 10 | remoteUUID: '00000000-0000-0000-0000-000000000001', 11 | status: 'PENDING', 12 | externalJobID: null, 13 | specs: [buildJobProposalSpec()], 14 | pendingUpdate: false, 15 | ...overrides, 16 | } 17 | } 18 | 19 | // buildJobProposalSpec builds a job proposal spec for the FetchJobProposal query. 20 | export function buildJobProposalSpec( 21 | overrides?: Partial, 22 | ): JobProposal_SpecsFields { 23 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 24 | 25 | return { 26 | __typename: 'JobProposalSpec', 27 | id: '1', 28 | definition: "name='spec'", 29 | status: 'PENDING', 30 | version: 1, 31 | createdAt: minuteAgo, 32 | ...overrides, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /support/factories/gql/fetchJobRun.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | export function buildRun( 4 | overrides?: Partial, 5 | ): JobRunPayload_Fields { 6 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 7 | const twoMinutesAgo = isoDate(Date.now() - MINUTE_MS * 2) 8 | 9 | return { 10 | __typename: 'JobRun', 11 | id: '1', 12 | allErrors: [], 13 | createdAt: twoMinutesAgo, 14 | fatalErrors: [], 15 | finishedAt: minuteAgo, 16 | inputs: '', 17 | job: { 18 | id: '10', 19 | name: 'job 1', 20 | observationSource: '', 21 | }, 22 | outputs: [], 23 | status: 'COMPLETED', 24 | taskRuns: [], 25 | ...overrides, 26 | } 27 | } 28 | 29 | export function buildTaskRun( 30 | overrides?: Partial, 31 | ): JobRunPayload_TaskRunsFields { 32 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 33 | const twoMinutesAgo = isoDate(Date.now() - MINUTE_MS * 2) 34 | 35 | return { 36 | __typename: 'TaskRun', 37 | id: '00000000-0000-0000-0000-000000000001', 38 | createdAt: twoMinutesAgo, 39 | dotID: 'parse_request', 40 | error: 'data: parameter is empty', 41 | finishedAt: minuteAgo, 42 | output: 'null', 43 | type: 'jsonparse', 44 | ...overrides, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /support/factories/gql/fetchJobRuns.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | export function buildRun( 4 | overrides?: Partial, 5 | ): JobRunsPayload_ResultsFields { 6 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 7 | 8 | return { 9 | __typename: 'JobRun', 10 | id: '1', 11 | allErrors: [], 12 | createdAt: minuteAgo, 13 | finishedAt: minuteAgo, 14 | status: 'COMPLETED', 15 | job: { 16 | id: '10', 17 | }, 18 | ...overrides, 19 | } 20 | } 21 | 22 | export function buildRuns(): ReadonlyArray { 23 | const twoMinutesAgo = isoDate(Date.now() - 2 * MINUTE_MS) 24 | 25 | return [ 26 | buildRun(), 27 | buildRun({ 28 | id: '2', 29 | allErrors: ['error'], 30 | createdAt: twoMinutesAgo, 31 | finishedAt: twoMinutesAgo, 32 | status: 'ERRORED', 33 | job: { 34 | id: '20', 35 | }, 36 | }), 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /support/factories/gql/fetchJobs.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | // buildJob builds a job for the FetchJobs query. 4 | export function buildJob( 5 | overrides?: Partial, 6 | ): JobsPayload_ResultsFields { 7 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 8 | 9 | return { 10 | __typename: 'Job', 11 | id: '1', 12 | name: 'job 1', 13 | externalJobID: '00000000-0000-0000-0000-000000000001', 14 | spec: { 15 | __typename: 'FluxMonitorSpec', 16 | }, 17 | createdAt: minuteAgo, 18 | ...overrides, 19 | } 20 | } 21 | 22 | // buildsJobs builds a list of jobs. 23 | export function buildJobs(): ReadonlyArray { 24 | return [ 25 | buildJob({ 26 | id: '1', 27 | name: 'job 1', 28 | externalJobID: '00000000-0000-0000-0000-000000000001', 29 | spec: { 30 | __typename: 'FluxMonitorSpec', 31 | }, 32 | }), 33 | buildJob({ 34 | id: '2', 35 | name: 'job 2', 36 | externalJobID: '00000000-0000-0000-0000-000000000002', 37 | spec: { 38 | __typename: 'OCRSpec', 39 | contractAddress: '0x0000000000000000000000000000000000000001', 40 | keyBundleID: 'keybundleid', 41 | transmitterAddress: 'transmitteraddress', 42 | }, 43 | }), 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /support/factories/gql/fetchNode.ts: -------------------------------------------------------------------------------- 1 | // buildNodePayloadFields builds the node fields. 2 | export function buildNodePayloadFields( 3 | overrides?: Partial, 4 | ): NodePayload_Fields { 5 | return { 6 | __typename: 'Node', 7 | id: '1', 8 | name: 'node1', 9 | httpURL: 'https://node1.com', 10 | wsURL: 'wss://node1.com', 11 | chain: { 12 | id: '42', 13 | network: 'evm', 14 | }, 15 | state: '', 16 | sendOnly: false, 17 | ...overrides, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /support/factories/gql/fetchNodes.ts: -------------------------------------------------------------------------------- 1 | // buildNode builds a node for the FetchNodes query. 2 | export function buildNode( 3 | overrides?: Partial, 4 | ): NodesPayload_ResultsFields { 5 | return { 6 | __typename: 'Node', 7 | id: '1', 8 | name: 'node1', 9 | chain: { 10 | id: '42', 11 | network: 'evm', 12 | }, 13 | state: '', 14 | sendOnly: false, 15 | ...overrides, 16 | } 17 | } 18 | 19 | // buildNodes builds a list of nodes. 20 | export function buildNodes(): ReadonlyArray { 21 | return [ 22 | buildNode({ 23 | id: '1', 24 | name: 'node1', 25 | chain: { 26 | id: '42', 27 | network: 'evm', 28 | }, 29 | order: 32, 30 | }), 31 | buildNode({ 32 | id: '2', 33 | name: 'node2', 34 | chain: { 35 | id: '5', 36 | network: 'evm', 37 | }, 38 | }), 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /support/factories/gql/fetchNonEVMKeys.ts: -------------------------------------------------------------------------------- 1 | // buildAptosKey builds a Aptos Key for the FetchNonEVMKeys query. 2 | export function buildAptosKey( 3 | overrides?: Partial, 4 | ): AptosKeysPayload_ResultsFields { 5 | return { 6 | __typename: 'AptosKey', 7 | id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 8 | account: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 9 | ...overrides, 10 | } 11 | } 12 | 13 | // buildAptosKeys builds a list of aptos keys. 14 | export function buildAptosKeys(): ReadonlyArray { 15 | return [ 16 | buildAptosKey({ 17 | id: 'aa67b61969793d51a3008cffba147bf57f1c89c423e32ce93ec9471d21e4231d', 18 | account: 19 | 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 20 | }), 21 | buildAptosKey({ 22 | id: 'e09c2e1444322d91cfb9b8576ce5895e54dc5caef37c5aff4accca9272412f5b', 23 | account: 24 | 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 25 | }), 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /support/factories/gql/fetchOCRKeyBundles.ts: -------------------------------------------------------------------------------- 1 | // buildOCRKeyBundle builds a ocr key bundle for the FetchOCRKeyBundles query. 2 | export function buildOCRKeyBundle( 3 | overrides?: Partial, 4 | ): OcrKeyBundlesPayload_ResultsFields { 5 | return { 6 | __typename: 'OCRKeyBundle', 7 | id: '0cb6a1cb6c83bf0a94ba4b18ddc3385c7a52823795fc4a2424a42109258b7641', 8 | configPublicKey: 9 | 'ocrcfg_2d19a30110a75886162ca1b0fc2d812377f917c386f2bdd746f89100ba79b364', 10 | onChainSigningAddress: 'ocrsad_0x04Ab7a825E09ab3bcf7697e8aAaB4A565Be9D2b5', 11 | offChainPublicKey: 12 | 'ocroff_d94746863f04e638e6e0328433cf8e874fa1d26cf1662e7d14b723e983e0465e', 13 | ...overrides, 14 | } 15 | } 16 | 17 | // buildOCRKeyBundles builds a list of ocr key bundles. 18 | export function buildOCRKeyBundles(): ReadonlyArray { 19 | return [ 20 | buildOCRKeyBundle(), 21 | buildOCRKeyBundle({ 22 | id: '4fadc92ce0b3deff6b2e2ef49cfc26cc39f8500818ad6591fb68f6c6ad0bb0dc', 23 | configPublicKey: 24 | 'ocrcfg_e990a63c766c2f9d3ca33500b52a356afd16ca0cf9f3eeefe7c88026daf11d78', 25 | onChainSigningAddress: 26 | 'ocrsad_0x2eB9410b954cbB18A83653B73cEaBa123dB19E9D', 27 | offChainPublicKey: 28 | 'ocroff_f8014d2c2cc7730285d3fc7f2f0e5d8b3189675dfbd5dff68eb4a057f50afaf8', 29 | }), 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /support/factories/gql/fetchP2PKeys.ts: -------------------------------------------------------------------------------- 1 | // buildP2PKey builds a p2p key for the FetchP2PKeys query. 2 | export function buildP2PKey( 3 | overrides?: Partial, 4 | ): P2PKeysPayload_ResultsFields { 5 | return { 6 | __typename: 'P2PKey', 7 | id: '12D3KooWQTF8qHapWg89jsVucDvZittNUAGdkBph8ZgTuMAq7Ftk', 8 | peerID: 'p2p_12D3KooWQTF8qHapWg89jsVucDvZittNUAGdkBph8ZgTuMAq7Ftk', 9 | publicKey: 10 | 'd976279be41a66b1192c1ba065d8bc6ba95a3777271009de99de177ce559fd41', 11 | ...overrides, 12 | } 13 | } 14 | 15 | // buildP2PKeys builds a list of p2p keys. 16 | export function buildP2PKeys(): ReadonlyArray { 17 | return [ 18 | buildP2PKey(), 19 | buildP2PKey({ 20 | id: '12D3KooWNkPvkVkT3tRB179fjxdudwW6JRf4EZs8gJM6sDXPazEy', 21 | peerID: 'p2p_12D3KooWNkPvkVkT3tRB179fjxdudwW6JRf4EZs8gJM6sDXPazEy', 22 | publicKey: 23 | 'c023986ca3ad70b4f06893d0a9b5b0a338578160dce173aacfb159dda3a54876', 24 | }), 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /support/factories/gql/fetchRecentJobRuns.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | export function buildRun( 4 | overrides?: Partial, 5 | ): RecentJobRunsPayload_ResultsFields { 6 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 7 | 8 | return { 9 | __typename: 'JobRun', 10 | id: '1', 11 | allErrors: [], 12 | createdAt: minuteAgo, 13 | finishedAt: minuteAgo, 14 | job: { 15 | id: '10', 16 | }, 17 | status: 'COMPLETED', 18 | ...overrides, 19 | } 20 | } 21 | 22 | export function buildRuns(): ReadonlyArray { 23 | const twoMinutesAgo = isoDate(Date.now() - 2 * MINUTE_MS) 24 | 25 | return [ 26 | buildRun(), 27 | buildRun({ 28 | id: '2', 29 | createdAt: twoMinutesAgo, 30 | finishedAt: twoMinutesAgo, 31 | status: 'COMPLETED', 32 | job: { 33 | id: '20', 34 | }, 35 | }), 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /support/factories/gql/fetchRecentJobs.ts: -------------------------------------------------------------------------------- 1 | import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' 2 | 3 | // buildRecentJob builds a job for the FetchRecentJobs query. 4 | export function buildRecentJob( 5 | overrides?: Partial, 6 | ): RecentJobsPayload_ResultsFields { 7 | const minuteAgo = isoDate(Date.now() - MINUTE_MS) 8 | 9 | return { 10 | __typename: 'Job', 11 | id: '1', 12 | name: 'job 1', 13 | createdAt: minuteAgo, 14 | ...overrides, 15 | } 16 | } 17 | 18 | // buildRecentJobs builds a list of recent jobs. 19 | export function buildRecentJobs(): ReadonlyArray { 20 | return [ 21 | buildRecentJob({ 22 | id: '1', 23 | name: 'job 1', 24 | }), 25 | buildRecentJob({ 26 | id: '2', 27 | name: 'job 2', 28 | }), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /support/factories/jsonApiLogConfig.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from 'utils/json-api-client' 2 | import { LogConfig } from 'core/store/models' 3 | 4 | export function logConfigFactory(config: Partial) { 5 | return { 6 | data: { 7 | id: 'log', 8 | type: 'logs', 9 | attributes: { 10 | serviceName: config.serviceName || ['Global', 'IsSqlEnabled'], 11 | logLevel: config.logLevel || ['info', 'true'], 12 | defaultLogLevel: config.defaultLogLevel || 'info', 13 | }, 14 | }, 15 | } as ApiResponse 16 | } 17 | -------------------------------------------------------------------------------- /support/test-helpers/globPath.js: -------------------------------------------------------------------------------- 1 | export default (path) => { 2 | return `glob:${process.env.CHAINLINK_BASEURL || 'http://localhost'}${path}*` 3 | } 4 | -------------------------------------------------------------------------------- /support/test-helpers/isoDate.js: -------------------------------------------------------------------------------- 1 | export const MINUTE_MS = 60 * 1000 2 | export const TWO_MINUTES_MS = MINUTE_MS * 2 3 | 4 | export default (i) => new Date(i).toISOString() 5 | -------------------------------------------------------------------------------- /support/test-helpers/partialAsFull.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A test helper that allows one to only partially satisfy a given type, and the function will 3 | * return the same value, but type casted as a full type. 4 | * 5 | * Useful for tests that require only a small slice of a given object to test conditions. 6 | * 7 | * @param val The partial value of a given type to be mocked as the full value 8 | */ 9 | export function partialAsFull(val: Partial): T { 10 | return val as T 11 | } 12 | -------------------------------------------------------------------------------- /support/test-helpers/wait.ts: -------------------------------------------------------------------------------- 1 | import { screen, waitForElementToBeRemoved } from 'support/test-utils' 2 | 3 | export const waitForLoading = () => { 4 | return waitForElementToBeRemoved(() => screen.queryByRole('progressbar')) 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "baseUrl": ".", 6 | "paths": { 7 | "*": ["*", "src/*", "support/*", "@types/*"] 8 | }, 9 | "noEmit": true, 10 | "allowJs": true 11 | }, 12 | "include": ["src", "@types", "__tests__", "support"] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const webpack = require('webpack') 3 | const webpackBase = require('./webpack.config') 4 | 5 | module.exports = Object.assign(webpackBase, { 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | port: 3000, 9 | static: './artifacts', 10 | historyApiFallback: true, 11 | hot: true, 12 | }, 13 | plugins: [...webpackBase.plugins, new webpack.HotModuleReplacementPlugin()], 14 | }) 15 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const CompressionPlugin = require('compression-webpack-plugin') 3 | const webpackBase = require('./webpack.config') 4 | const TerserPlugin = require('terser-webpack-plugin') 5 | module.exports = Object.assign(webpackBase, { 6 | output: { 7 | ...webpackBase.output, 8 | publicPath: '/assets/', // JS files are served from `/assets` by web 9 | }, 10 | plugins: [...webpackBase.plugins, new CompressionPlugin({})], 11 | optimization: { 12 | minimize: true, 13 | minimizer: [ 14 | new TerserPlugin({ 15 | minify: TerserPlugin.swcMinify, 16 | // `terserOptions` options will be passed to `swc` (`@swc/core`) 17 | // Link to options - https://swc.rs/docs/config-js-minify 18 | terserOptions: {}, 19 | }), 20 | ], 21 | }, 22 | }) 23 | --------------------------------------------------------------------------------