From be9a5a343713100b3f93743125d0820730eefda0 Mon Sep 17 00:00:00 2001 From: Edd Date: Wed, 13 Mar 2024 17:37:58 +0000 Subject: [PATCH] feat(explorer): add market specific oracle page (#5986) --- .../src/app/components/oracle-table/index.tsx | 159 ++++++++++++++++++ .../routes/markets/market-oracles-page.tsx | 42 +++++ .../routes/oracles/OraclesForMarkets.graphql | 33 ++++ .../__generated__/OraclesForMarkets.ts | 72 +++++++- .../src/app/routes/oracles/home/index.tsx | 154 +---------------- .../explorer/src/app/routes/router-config.tsx | 49 ++++-- 6 files changed, 344 insertions(+), 165 deletions(-) create mode 100644 apps/explorer/src/app/components/oracle-table/index.tsx create mode 100644 apps/explorer/src/app/routes/markets/market-oracles-page.tsx diff --git a/apps/explorer/src/app/components/oracle-table/index.tsx b/apps/explorer/src/app/components/oracle-table/index.tsx new file mode 100644 index 000000000..47196a236 --- /dev/null +++ b/apps/explorer/src/app/components/oracle-table/index.tsx @@ -0,0 +1,159 @@ +import compact from 'lodash/compact'; +import { MarketLink } from '../links'; +import { type MarketState, MarketStateMapping } from '@vegaprotocol/types'; +import OracleLink from '../links/oracle-link/oracle-link'; +import type { + ExplorerOracleForMarketQuery, + ExplorerOracleFormMarketsQuery, +} from '../../routes/oracles/__generated__/OraclesForMarkets'; +import { useState } from 'react'; + +export type OraclesTableProps = { + data?: ExplorerOracleFormMarketsQuery | ExplorerOracleForMarketQuery; +}; + +const cellSpacing = 'px-3'; + +export function OraclesTable({ data }: OraclesTableProps) { + const [hoveredOracle, setHoveredOracle] = useState(''); + + return ( + + + + + + + + + + + + {data?.marketsConnection?.edges + ? data.marketsConnection.edges.map((o) => { + let hasSeenOracleReports = false; + let settlementOracle = '-'; + let settlementOracleStatus = '-'; + let terminationOracle = '-'; + let terminationOracleStatus = '-'; + + const id = o?.node.id; + if (!id) { + return null; + } + + if ( + o.node.tradableInstrument.instrument.product.__typename === + 'Future' + ) { + settlementOracle = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementData.id; + terminationOracle = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForTradingTermination.id; + settlementOracleStatus = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementData.status; + terminationOracleStatus = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForTradingTermination.status; + } else if ( + o.node.tradableInstrument.instrument.product.__typename === + 'Perpetual' + ) { + settlementOracle = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementData.id; + terminationOracle = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementSchedule.id; + settlementOracleStatus = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementData.status; + terminationOracleStatus = + o.node.tradableInstrument.instrument.product + .dataSourceSpecForSettlementSchedule.status; + } + const oracleInformationUnfiltered = + data?.oracleSpecsConnection?.edges?.map((e) => + e && e.node ? e.node : undefined + ) || []; + + const oracleInformation = compact(oracleInformationUnfiltered) + .filter( + (o) => + o.dataConnection.edges && + o.dataConnection.edges.length > 0 && + (o.dataSourceSpec.spec.id === settlementOracle || + o.dataSourceSpec.spec.id === terminationOracle) + ) + .at(0); + if (oracleInformation) { + hasSeenOracleReports = true; + } + + const oracleList = `${settlementOracle} ${terminationOracle}`; + + return ( + 0 && + oracleList.indexOf(hoveredOracle) > -1 + ? 'bg-gray-100 dark:bg-gray-800' + : '' + } + data-testid="oracle-details" + data-oracles={oracleList} + > + + + + + + + ); + }) + : null} + +
MarketTypeStateSettlementTermination
+ + + {o.node.tradableInstrument.instrument.product.__typename} + + {MarketStateMapping[o.node.state as MarketState]} + 0 && + hoveredOracle === settlementOracle + ? `indent-1 ${cellSpacing}` + : cellSpacing + } + > + setHoveredOracle(settlementOracle)} + onMouseOut={() => setHoveredOracle('')} + /> + 0 && + hoveredOracle === terminationOracle + ? `indent-1 ${cellSpacing}` + : cellSpacing + } + > + setHoveredOracle(terminationOracle)} + onMouseOut={() => setHoveredOracle('')} + /> +
+ ); +} diff --git a/apps/explorer/src/app/routes/markets/market-oracles-page.tsx b/apps/explorer/src/app/routes/markets/market-oracles-page.tsx new file mode 100644 index 000000000..8cceb0f84 --- /dev/null +++ b/apps/explorer/src/app/routes/markets/market-oracles-page.tsx @@ -0,0 +1,42 @@ +import { t } from '@vegaprotocol/i18n'; +import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { useParams } from 'react-router-dom'; +import { useScrollToLocation } from '../../hooks/scroll-to-location'; +import { useDocumentTitle } from '../../hooks/use-document-title'; +import { PageTitle } from '../../components/page-helpers/page-title'; +import { useExplorerOracleForMarketQuery } from '../oracles/__generated__/OraclesForMarkets'; +import { OraclesTable } from '../../components/oracle-table'; + +type Params = { marketId: string }; + +export const MarketOraclesPage = () => { + useScrollToLocation(); + + const { marketId } = useParams(); + const { data, error, loading } = useExplorerOracleForMarketQuery({ + errorPolicy: 'ignore', + variables: { + id: marketId || '1', + }, + }); + + useDocumentTitle([marketId ? marketId : 'market', 'Oracles for Market']); + + return ( +
+ + + + +
+ ); +}; diff --git a/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql b/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql index 7116f6356..3412843d8 100644 --- a/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql +++ b/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql @@ -120,3 +120,36 @@ query ExplorerOracleFormMarkets { } } } + +query ExplorerOracleForMarket($id: ID!) { + marketsConnection(id: $id) { + edges { + node { + ...ExplorerOracleForMarketsMarket + } + } + } + oracleSpecsConnection { + edges { + node { + dataSourceSpec { + ...ExplorerOracleDataSourceSpec + } + dataConnection(pagination: { last: 1 }) { + edges { + node { + externalData { + data { + data { + name + value + } + } + } + } + } + } + } + } + } +} diff --git a/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts b/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts index 0a1a543c1..9eafd558b 100644 --- a/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts +++ b/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts @@ -16,6 +16,13 @@ export type ExplorerOracleFormMarketsQueryVariables = Types.Exact<{ [key: string export type ExplorerOracleFormMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } } }> } | null, oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string, sourceChainId: number } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; +export type ExplorerOracleForMarketQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']; +}>; + + +export type ExplorerOracleForMarketQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } } }> } | null, oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string, sourceChainId: number } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; + export const ExplorerOracleFutureFragmentDoc = gql` fragment ExplorerOracleFuture on Future { dataSourceSpecForSettlementData { @@ -172,4 +179,67 @@ export function useExplorerOracleFormMarketsLazyQuery(baseOptions?: Apollo.LazyQ } export type ExplorerOracleFormMarketsQueryHookResult = ReturnType; export type ExplorerOracleFormMarketsLazyQueryHookResult = ReturnType; -export type ExplorerOracleFormMarketsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type ExplorerOracleFormMarketsQueryResult = Apollo.QueryResult; +export const ExplorerOracleForMarketDocument = gql` + query ExplorerOracleForMarket($id: ID!) { + marketsConnection(id: $id) { + edges { + node { + ...ExplorerOracleForMarketsMarket + } + } + } + oracleSpecsConnection { + edges { + node { + dataSourceSpec { + ...ExplorerOracleDataSourceSpec + } + dataConnection(pagination: {last: 1}) { + edges { + node { + externalData { + data { + data { + name + value + } + } + } + } + } + } + } + } + } +} + ${ExplorerOracleForMarketsMarketFragmentDoc} +${ExplorerOracleDataSourceSpecFragmentDoc}`; + +/** + * __useExplorerOracleForMarketQuery__ + * + * To run a query within a React component, call `useExplorerOracleForMarketQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerOracleForMarketQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useExplorerOracleForMarketQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useExplorerOracleForMarketQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ExplorerOracleForMarketDocument, options); + } +export function useExplorerOracleForMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ExplorerOracleForMarketDocument, options); + } +export type ExplorerOracleForMarketQueryHookResult = ReturnType; +export type ExplorerOracleForMarketLazyQueryHookResult = ReturnType; +export type ExplorerOracleForMarketQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/explorer/src/app/routes/oracles/home/index.tsx b/apps/explorer/src/app/routes/oracles/home/index.tsx index 9ef8f4eac..3b660cb43 100644 --- a/apps/explorer/src/app/routes/oracles/home/index.tsx +++ b/apps/explorer/src/app/routes/oracles/home/index.tsx @@ -1,17 +1,10 @@ -import compact from 'lodash/compact'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { RouteTitle } from '../../../components/route-title'; import { t } from '@vegaprotocol/i18n'; import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useScrollToLocation } from '../../../hooks/scroll-to-location'; import { useExplorerOracleFormMarketsQuery } from '../__generated__/OraclesForMarkets'; -import { MarketLink } from '../../../components/links'; -import { OracleLink } from '../../../components/links/oracle-link/oracle-link'; -import { useState } from 'react'; -import { MarketStateMapping } from '@vegaprotocol/types'; -import type { MarketState } from '@vegaprotocol/types'; - -const cellSpacing = 'px-3'; +import { OraclesTable } from '../../../components/oracle-table'; const Oracles = () => { const { data, loading, error } = useExplorerOracleFormMarketsQuery({ @@ -21,8 +14,6 @@ const Oracles = () => { useDocumentTitle(['Oracles']); useScrollToLocation(); - const [hoveredOracle, setHoveredOracle] = useState(''); - return (
{t('Oracles')} @@ -38,148 +29,7 @@ const Oracles = () => { data.oracleSpecsConnection.edges?.length === 0 } > - - - - - - - - - - - - {data?.marketsConnection?.edges - ? data.marketsConnection.edges.map((o) => { - let hasSeenOracleReports = false; - let settlementOracle = '-'; - let settlementOracleStatus = '-'; - let terminationOracle = '-'; - let terminationOracleStatus = '-'; - - const id = o?.node.id; - if (!id) { - return null; - } - - if ( - o.node.tradableInstrument.instrument.product.__typename === - 'Future' - ) { - settlementOracle = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementData.id; - terminationOracle = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForTradingTermination.id; - settlementOracleStatus = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementData.status; - terminationOracleStatus = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForTradingTermination.status; - } else if ( - o.node.tradableInstrument.instrument.product.__typename === - 'Perpetual' - ) { - settlementOracle = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementData.id; - terminationOracle = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementSchedule.id; - settlementOracleStatus = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementData.status; - terminationOracleStatus = - o.node.tradableInstrument.instrument.product - .dataSourceSpecForSettlementSchedule.status; - } - const oracleInformationUnfiltered = - data?.oracleSpecsConnection?.edges?.map((e) => - e && e.node ? e.node : undefined - ) || []; - - const oracleInformation = compact(oracleInformationUnfiltered) - .filter( - (o) => - o.dataConnection.edges && - o.dataConnection.edges.length > 0 && - (o.dataSourceSpec.spec.id === settlementOracle || - o.dataSourceSpec.spec.id === terminationOracle) - ) - .at(0); - if (oracleInformation) { - hasSeenOracleReports = true; - } - - const oracleList = `${settlementOracle} ${terminationOracle}`; - - return ( - 0 && - oracleList.indexOf(hoveredOracle) > -1 - ? 'bg-gray-100 dark:bg-gray-800' - : '' - } - data-testid="oracle-details" - data-oracles={oracleList} - > - - - - - - - ); - }) - : null} - -
MarketTypeStateSettlementTermination
- - - { - o.node.tradableInstrument.instrument.product - .__typename - } - - {MarketStateMapping[o.node.state as MarketState]} - 0 && - hoveredOracle === settlementOracle - ? `indent-1 ${cellSpacing}` - : cellSpacing - } - > - setHoveredOracle(settlementOracle)} - onMouseOut={() => setHoveredOracle('')} - /> - 0 && - hoveredOracle === terminationOracle - ? `indent-1 ${cellSpacing}` - : cellSpacing - } - > - - setHoveredOracle(terminationOracle) - } - onMouseOut={() => setHoveredOracle('')} - /> -
+
); diff --git a/apps/explorer/src/app/routes/router-config.tsx b/apps/explorer/src/app/routes/router-config.tsx index f39b87daa..a48d6c8a4 100644 --- a/apps/explorer/src/app/routes/router-config.tsx +++ b/apps/explorer/src/app/routes/router-config.tsx @@ -5,9 +5,8 @@ import Home from './home'; import OraclePage from './oracles'; import Oracles from './oracles/home'; import { Oracle } from './oracles/id'; -import Party from './parties'; import { Parties } from './parties/home'; -import { Party as PartySingle } from './parties/id'; +import { Party } from './parties/id'; import { ValidatorsPage } from './validators'; import Genesis from './genesis'; import { Block } from './blocks/id'; @@ -17,6 +16,7 @@ import { TxsList } from './txs/home'; import { t } from '@vegaprotocol/i18n'; import { Routes } from './route-names'; import { NetworkParameters } from './network-parameters'; +import { Outlet } from 'react-router-dom'; import type { Params, RouteObject } from 'react-router-dom'; import { Link } from 'react-router-dom'; import { MarketPage, MarketsPage } from './markets'; @@ -31,6 +31,7 @@ import { Disclaimer } from './pages/disclaimer'; import { useFeatureFlags } from '@vegaprotocol/environment'; import RestrictedPage from './restricted'; import { NetworkTreasury } from './treasury'; +import { MarketOraclesPage } from './markets/market-oracles-page'; export type Navigable = { path: string; @@ -67,7 +68,7 @@ export const useRouterConfig = () => { ? [ { path: Routes.PARTIES, - element: , + element: , handle: { name: t('Parties'), text: t('Parties'), @@ -80,12 +81,12 @@ export const useRouterConfig = () => { }, { path: ':party', - element: , + element: , children: [ { index: true, - element: , + element: , handle: { breadcrumb: (params: Params) => ( @@ -96,7 +97,7 @@ export const useRouterConfig = () => { }, { path: 'assets', - element: , + element: , handle: { breadcrumb: (params: Params) => ( @@ -199,12 +200,36 @@ export const useRouterConfig = () => { }, { path: ':marketId', - element: , - handle: { - breadcrumb: (params: Params) => ( - - ), - }, + element: , + children: [ + { + index: true, + element: , + handle: { + breadcrumb: (params: Params) => ( + + ), + }, + }, + { + path: 'oracles', + element: , + handle: { + breadcrumb: (params: Params) => ( + + ), + }, + children: [ + { + index: true, + element: , + handle: { + breadcrumb: (params: Params) => t('Oracles'), + }, + }, + ], + }, + ], }, ], },