Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
829d3112e1 | ||
|
|
73cda26e17 | ||
|
|
f34ddaa7b9 | ||
|
|
20371f56f7 | ||
|
|
261ec8c5e4 | ||
|
|
d577080a86 | ||
|
|
c5deddb6d8 | ||
|
|
4acf2a7f8c | ||
|
|
fdfae74524 | ||
|
|
7f9c1178c4 | ||
|
|
b633dbc049 | ||
|
|
92315181e1 | ||
|
|
9b7c26386b | ||
|
|
312db3ea0f | ||
|
|
4d9bcde560 | ||
|
|
e5fc3d1bc4 | ||
|
|
799cb04cae | ||
|
|
e03ba3256f | ||
|
|
0c6212e82b | ||
|
|
71ae12007b | ||
|
|
ccc4d39aec | ||
|
|
5d4ce7e2f2 | ||
|
|
3f77986885 |
@@ -11,3 +11,5 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
|||||||
|
|
||||||
#Test configuration variables
|
#Test configuration variables
|
||||||
CYPRESS_FAIRGROUND=false
|
CYPRESS_FAIRGROUND=false
|
||||||
|
|
||||||
|
NX_GIT_ORIGIN_URL=git@github.com:vegaprotocol/frontend-monorepo.git
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET":"https://dev.
|
|||||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
|
|
||||||
|
NX_GIT_ORIGIN_URL=git@github.com:vegaprotocol/frontend-monorepo.git
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
|
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||||
@@ -7,3 +8,4 @@ NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
|||||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
||||||
NX_USE_ENV_OVERRIDES=1
|
NX_USE_ENV_OVERRIDES=1
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||||
|
NX_GIT_ORIGIN_URL=git@github.com:vegaprotocol/frontend-monorepo.git
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
|||||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||||
|
|
||||||
|
NX_GIT_ORIGIN_URL=git@github.com:vegaprotocol/frontend-monorepo.git
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*"],
|
"ignorePatterns": ["!**/*", "__generated__"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './network-loader';
|
export * from './network-loader';
|
||||||
export * from './network-switcher';
|
export * from './network-switcher';
|
||||||
export * from './network-switcher-dialog';
|
export * from './network-switcher-dialog';
|
||||||
|
export * from './node-switcher';
|
||||||
|
export * from './node-switcher-dialog';
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './network-loader';
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { NetworkLoader } from './network-loader';
|
||||||
@@ -1,128 +1,8 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ApolloClient } from '@apollo/client';
|
import type { ApolloClient } from '@apollo/client';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
import {
|
|
||||||
Callout,
|
|
||||||
Intent,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Loader,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import { useEnvironment } from '../../hooks';
|
import { useEnvironment } from '../../hooks';
|
||||||
import type { ConfigStatus } from '../../types';
|
|
||||||
|
|
||||||
type MessageComponentProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusMessage = ({ children }: MessageComponentProps) => (
|
|
||||||
<div className="flex items-center fixed bottom-0 right-0 px-16 bg-intent-highlight text-black">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type ErrorComponentProps = MessageComponentProps & {
|
|
||||||
children?: ReactNode;
|
|
||||||
showTryAgain?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Error = ({ children, showTryAgain }: ErrorComponentProps) => (
|
|
||||||
<div>
|
|
||||||
<div className="mb-16">{children}</div>
|
|
||||||
{showTryAgain && (
|
|
||||||
<Button
|
|
||||||
className="mt-8"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
{t('Try again')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type StatusComponentProps = {
|
|
||||||
status: ConfigStatus;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusComponent = ({ status, children }: StatusComponentProps) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'error-loading-config':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error>
|
|
||||||
{t('There was an error fetching the network configuration.')}
|
|
||||||
</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'error-validating-config':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error>
|
|
||||||
{t('The network configuration for the app is invalid.')}
|
|
||||||
</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'error-loading-node':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error showTryAgain>{t('Failed to connect to a data node.')}</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'idle':
|
|
||||||
case 'loading-config':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Loader size="small" forceTheme="light" />
|
|
||||||
<span className="ml-8">{t('Loading configuration...')}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'loading-node':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Loader size="small" forceTheme="light" />
|
|
||||||
<span className="ml-8">{t('Finding a node...')}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'success':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Icon name="antenna" />
|
|
||||||
<span className="ml-8">{t("You're connected!")}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type NetworkLoaderProps<T> = {
|
type NetworkLoaderProps<T> = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -135,9 +15,7 @@ export function NetworkLoader<T>({
|
|||||||
children,
|
children,
|
||||||
createClient,
|
createClient,
|
||||||
}: NetworkLoaderProps<T>) {
|
}: NetworkLoaderProps<T>) {
|
||||||
// this is to prevent an error rendering callouts on the server side
|
const { VEGA_URL } = useEnvironment();
|
||||||
const [canShowCallout, setShowCallout] = useState(false);
|
|
||||||
const { configStatus, VEGA_URL } = useEnvironment();
|
|
||||||
|
|
||||||
const client = useMemo(() => {
|
const client = useMemo(() => {
|
||||||
if (VEGA_URL) {
|
if (VEGA_URL) {
|
||||||
@@ -146,16 +24,12 @@ export function NetworkLoader<T>({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [VEGA_URL, createClient]);
|
}, [VEGA_URL, createClient]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setShowCallout(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return canShowCallout ? (
|
return (
|
||||||
<div className="h-full min-h-screen flex items-center justify-center">
|
<div className="h-full min-h-screen flex items-center justify-center">
|
||||||
<StatusComponent status={configStatus}>{skeleton}</StatusComponent>
|
{skeleton}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './node-switcher-dialog';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { NodeSwitcher } from '../node-switcher';
|
||||||
|
|
||||||
|
type NodeSwitcherDialogProps = ComponentProps<typeof NodeSwitcher> & {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
setDialogOpen: (dialogOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeSwitcherDialog = ({
|
||||||
|
config,
|
||||||
|
initialErrorType,
|
||||||
|
dialogOpen,
|
||||||
|
setDialogOpen,
|
||||||
|
onConnect,
|
||||||
|
}: NodeSwitcherDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={dialogOpen} >
|
||||||
|
<NodeSwitcher
|
||||||
|
config={config}
|
||||||
|
initialErrorType={initialErrorType}
|
||||||
|
onConnect={(url) => {
|
||||||
|
onConnect(url);
|
||||||
|
setDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: BlockHeightStats
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface BlockHeightStats_statistics {
|
||||||
|
__typename: "Statistics";
|
||||||
|
/**
|
||||||
|
* Current block number
|
||||||
|
*/
|
||||||
|
blockHeight: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockHeightStats {
|
||||||
|
/**
|
||||||
|
* get statistics about the vega node
|
||||||
|
*/
|
||||||
|
statistics: BlockHeightStats_statistics;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './node-switcher';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
type LayoutCellProps = {
|
||||||
|
hasError?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayoutCell = ({
|
||||||
|
hasError,
|
||||||
|
isLoading,
|
||||||
|
children,
|
||||||
|
}: LayoutCellProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('px-8 text-right', {
|
||||||
|
'text-danger': !isLoading && hasError,
|
||||||
|
'text-white-60 dark:text-black-60': isLoading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? t('Checking') : children || '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type LayoutRowProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayoutRow = ({ children }: LayoutRowProps) => {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 py-8 w-full h-[42px] grid-cols-[minmax(200px,_1fr),_150px_125px_100px]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import type { BlockHeightStats } from './__generated__/BlockHeightStats';
|
||||||
|
|
||||||
|
type NodeBlockHeightProps = {
|
||||||
|
value?: number;
|
||||||
|
setValue: (value: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const POLL_INTERVAL = 3000;
|
||||||
|
|
||||||
|
const BLOCK_HEIGHT_QUERY = gql`
|
||||||
|
query BlockHeightStats {
|
||||||
|
statistics {
|
||||||
|
blockHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const NodeBlockHeight = ({ value, setValue }: NodeBlockHeightProps) => {
|
||||||
|
const { data, startPolling, stopPolling } = useQuery<BlockHeightStats>(
|
||||||
|
BLOCK_HEIGHT_QUERY,
|
||||||
|
{
|
||||||
|
pollInterval: POLL_INTERVAL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStartPoll = () => startPolling(POLL_INTERVAL);
|
||||||
|
const handleStopPoll = () => stopPolling();
|
||||||
|
window.addEventListener('blur', handleStopPoll);
|
||||||
|
window.addEventListener('focus', handleStartPoll);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('blur', handleStopPoll);
|
||||||
|
window.removeEventListener('focus', handleStartPoll);
|
||||||
|
};
|
||||||
|
}, [startPolling, stopPolling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.statistics?.blockHeight) {
|
||||||
|
setValue(Number(data.statistics.blockHeight));
|
||||||
|
}
|
||||||
|
}, [setValue, data?.statistics?.blockHeight]);
|
||||||
|
|
||||||
|
return <span>{value ?? '-'}</span>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
type NodeErrorProps = {
|
||||||
|
headline?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeError = ({ headline, message }: NodeErrorProps) => {
|
||||||
|
if (!headline && !message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="p-16 my-16 border border-danger">
|
||||||
|
<p className="font-bold">{headline}</p>
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { NodeData } from '../../types';
|
||||||
|
import type createClient from '../../utils/apollo-client';
|
||||||
|
import { LayoutRow } from './layout-row';
|
||||||
|
import { LayoutCell } from './layout-cell';
|
||||||
|
import { NodeBlockHeight } from './node-block-height';
|
||||||
|
|
||||||
|
type NodeStatsContentProps = {
|
||||||
|
data?: NodeData;
|
||||||
|
highestBlock: number;
|
||||||
|
setBlock: (value: number) => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResponseTimeDisplayValue = (
|
||||||
|
responseTime?: NodeData['responseTime']
|
||||||
|
) => {
|
||||||
|
if (typeof responseTime?.value === 'number') {
|
||||||
|
return `${Number(responseTime.value).toFixed(2)}ms`;
|
||||||
|
}
|
||||||
|
if (responseTime?.hasError) {
|
||||||
|
return t('n/a');
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBlockDisplayValue = (
|
||||||
|
block: NodeData['block'] | undefined,
|
||||||
|
setBlock: (block: number) => void
|
||||||
|
) => {
|
||||||
|
if (block?.value) {
|
||||||
|
return <NodeBlockHeight value={block?.value} setValue={setBlock} />;
|
||||||
|
}
|
||||||
|
if (block?.hasError) {
|
||||||
|
return t('n/a');
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSslDisplayValue = (ssl?: NodeData['ssl']) => {
|
||||||
|
if (ssl?.value) {
|
||||||
|
return t('Yes');
|
||||||
|
}
|
||||||
|
if (ssl?.hasError) {
|
||||||
|
return t('No');
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeStatsContent = ({
|
||||||
|
// @ts-ignore Allow defaulting to an empty object
|
||||||
|
data = {},
|
||||||
|
highestBlock,
|
||||||
|
setBlock,
|
||||||
|
children,
|
||||||
|
}: NodeStatsContentProps) => {
|
||||||
|
return (
|
||||||
|
<LayoutRow>
|
||||||
|
{children}
|
||||||
|
<LayoutCell
|
||||||
|
isLoading={data.responseTime?.isLoading}
|
||||||
|
hasError={data.responseTime?.hasError}
|
||||||
|
>
|
||||||
|
{getResponseTimeDisplayValue(data.responseTime)}
|
||||||
|
</LayoutCell>
|
||||||
|
<LayoutCell
|
||||||
|
isLoading={data.block?.isLoading}
|
||||||
|
hasError={
|
||||||
|
data.block?.hasError ||
|
||||||
|
(!!data.block?.value && highestBlock > data.block.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getBlockDisplayValue(data.block, setBlock)}
|
||||||
|
</LayoutCell>
|
||||||
|
<LayoutCell isLoading={data.ssl?.isLoading} hasError={data.ssl?.hasError}>
|
||||||
|
{getSslDisplayValue(data.ssl)}
|
||||||
|
</LayoutCell>
|
||||||
|
</LayoutRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type WrapperProps = {
|
||||||
|
client?: ReturnType<typeof createClient>;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = ({ client, children }: WrapperProps) => {
|
||||||
|
if (client) {
|
||||||
|
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeStatsProps = {
|
||||||
|
data?: NodeData;
|
||||||
|
client?: ReturnType<typeof createClient>;
|
||||||
|
highestBlock: number;
|
||||||
|
setBlock: (value: number) => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeStats = ({
|
||||||
|
data,
|
||||||
|
client,
|
||||||
|
highestBlock,
|
||||||
|
children,
|
||||||
|
setBlock,
|
||||||
|
}: NodeStatsProps) => {
|
||||||
|
return (
|
||||||
|
<Wrapper client={client}>
|
||||||
|
<NodeStatsContent
|
||||||
|
data={data}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={setBlock}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NodeStatsContent>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Link,
|
||||||
|
Radio,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useEnvironment } from '../../hooks/use-environment';
|
||||||
|
import { useNodes } from '../../hooks/use-nodes';
|
||||||
|
import {
|
||||||
|
getIsNodeLoading,
|
||||||
|
getIsNodeDisabled,
|
||||||
|
getIsFormDisabled,
|
||||||
|
getErrorByData,
|
||||||
|
getErrorByType,
|
||||||
|
} from '../../utils/validate-node';
|
||||||
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
|
import type { Configuration, NodeData, ErrorType } from '../../types';
|
||||||
|
import { LayoutRow } from './layout-row';
|
||||||
|
import { LayoutCell } from './layout-cell';
|
||||||
|
import { NodeError } from './node-error';
|
||||||
|
import { NodeStats } from './node-stats';
|
||||||
|
|
||||||
|
type NodeSwitcherProps = {
|
||||||
|
error?: string;
|
||||||
|
config: Configuration;
|
||||||
|
initialErrorType?: ErrorType;
|
||||||
|
onConnect: (url: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultNode = (urls: string[], currentUrl?: string) => {
|
||||||
|
return currentUrl && urls.includes(currentUrl) ? currentUrl : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHighestBlock = (state: Record<string, NodeData>) => {
|
||||||
|
return Object.keys(state).reduce((acc, node) => {
|
||||||
|
const value = Number(state[node].block.value);
|
||||||
|
return value ? Math.max(acc, value) : acc;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeSwitcher = ({ config, initialErrorType, onConnect }: NodeSwitcherProps) => {
|
||||||
|
const { VEGA_ENV, VEGA_URL } = useEnvironment();
|
||||||
|
const [networkError, setNetworkError] = useState(getErrorByType(initialErrorType, VEGA_ENV, VEGA_URL));
|
||||||
|
const [customNodeText, setCustomNodeText] = useState('');
|
||||||
|
const [nodeRadio, setNodeRadio] = useState(
|
||||||
|
getDefaultNode(config.hosts, VEGA_URL)
|
||||||
|
);
|
||||||
|
const { state, clients, customNode, setCustomNode, updateNodeBlock } =
|
||||||
|
useNodes(config);
|
||||||
|
const highestBlock = useMemo(() => getHighestBlock(state), [state]);
|
||||||
|
|
||||||
|
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
||||||
|
if (node) {
|
||||||
|
onConnect(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubmitDisabled = getIsFormDisabled(
|
||||||
|
nodeRadio,
|
||||||
|
customNodeText,
|
||||||
|
VEGA_ENV,
|
||||||
|
state
|
||||||
|
);
|
||||||
|
const currentNodeError = getErrorByData(
|
||||||
|
VEGA_ENV,
|
||||||
|
nodeRadio && customNode && customNode === customNodeText
|
||||||
|
? state[nodeRadio]
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-black dark:text-white min-w-[800px]">
|
||||||
|
<NodeError {...(currentNodeError || networkError)} />
|
||||||
|
<form onSubmit={() => onSubmit(nodeRadio)}>
|
||||||
|
<p className="text-body-large font-bold mt-16 mb-32">
|
||||||
|
{t('Select a GraphQL node to connect to:')}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<LayoutRow>
|
||||||
|
<div />
|
||||||
|
<LayoutCell>{t('Response time')}</LayoutCell>
|
||||||
|
<LayoutCell>{t('Block')}</LayoutCell>
|
||||||
|
<LayoutCell>{t('SSL')}</LayoutCell>
|
||||||
|
</LayoutRow>
|
||||||
|
<RadioGroup
|
||||||
|
className="block"
|
||||||
|
value={nodeRadio}
|
||||||
|
onChange={(value) => {
|
||||||
|
setNodeRadio(value)
|
||||||
|
setNetworkError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
{config.hosts.map((node, index) => (
|
||||||
|
<NodeStats
|
||||||
|
key={index}
|
||||||
|
data={state[node]}
|
||||||
|
client={clients[node]}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={(block) =>
|
||||||
|
updateNodeBlock(node, Math.max(block, highestBlock))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Radio
|
||||||
|
id={`node-url-${index}`}
|
||||||
|
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
|
value={node}
|
||||||
|
label={node}
|
||||||
|
disabled={getIsNodeDisabled(VEGA_ENV, state[node])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NodeStats>
|
||||||
|
))}
|
||||||
|
<NodeStats
|
||||||
|
data={state[CUSTOM_NODE_KEY]}
|
||||||
|
client={clients[CUSTOM_NODE_KEY]}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={(block) =>
|
||||||
|
updateNodeBlock(
|
||||||
|
CUSTOM_NODE_KEY,
|
||||||
|
Math.max(block, highestBlock)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex w-full">
|
||||||
|
<Radio
|
||||||
|
id={`node-url-custom`}
|
||||||
|
value={CUSTOM_NODE_KEY}
|
||||||
|
label={
|
||||||
|
nodeRadio === CUSTOM_NODE_KEY || !!customNode
|
||||||
|
? ''
|
||||||
|
: t('Other')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{(customNodeText || nodeRadio === CUSTOM_NODE_KEY) && (
|
||||||
|
<div className="flex w-full gap-8">
|
||||||
|
<Input
|
||||||
|
placeholder="https://"
|
||||||
|
value={customNodeText}
|
||||||
|
hasError={
|
||||||
|
!!customNodeText &&
|
||||||
|
!!(currentNodeError?.headline || currentNodeError?.message)
|
||||||
|
}
|
||||||
|
onChange={(e) => setCustomNodeText(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Link onClick={() => {
|
||||||
|
setNetworkError(null);
|
||||||
|
setCustomNode(customNodeText)
|
||||||
|
}}>
|
||||||
|
{getIsNodeLoading(state[CUSTOM_NODE_KEY])
|
||||||
|
? t('Checking')
|
||||||
|
: t('Check')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</NodeStats>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full mt-16"
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{t('Connect')}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// import { ApolloClient, InMemoryCache, DocumentNode } from '@apollo/client';
|
||||||
|
import { STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from '../../utils/request-node';
|
||||||
|
import type { Statistics } from '../../utils/__generated__/Statistics';
|
||||||
|
import type { BlockTime } from '../../utils/__generated__/BlockTime';
|
||||||
|
import { Networks } from '../../types';
|
||||||
|
import { createMockClient, RequestHandlerResponse } from 'mock-apollo-client';
|
||||||
|
|
||||||
|
export type MockRequestConfig = {
|
||||||
|
hasError?: boolean;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockClientProps = {
|
||||||
|
network?: Networks,
|
||||||
|
query?: MockRequestConfig;
|
||||||
|
subscription?: MockRequestConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMockBusEventsResult = (): BlockTime => ({
|
||||||
|
busEvents: [{
|
||||||
|
__typename: 'BusEvent',
|
||||||
|
eventId: '0',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMockStatisticsResult = (env: Networks = Networks.TESTNET): Statistics => ({
|
||||||
|
statistics: {
|
||||||
|
__typename: 'Statistics',
|
||||||
|
chainId: `${env.toLowerCase()}-0123`,
|
||||||
|
blockHeight: '11',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getHandler <T>({ hasError, delay = 0 }: MockRequestConfig = {}, result: T) {
|
||||||
|
return () => new Promise<RequestHandlerResponse<T>>((resolve, reject) => {
|
||||||
|
console.log('EXECUTING!!', delay)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('EXECUTING TIMEOUT!!')
|
||||||
|
if (hasError) {
|
||||||
|
reject(new Error('Failed to execute query.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('RESOLVING!', result)
|
||||||
|
resolve({ data: result });
|
||||||
|
}, delay)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({ network, query, subscription }: MockClientProps) {
|
||||||
|
const mockClient = createMockClient();
|
||||||
|
|
||||||
|
mockClient.setRequestHandler(STATS_QUERY, getHandler(query, getMockStatisticsResult(network)));
|
||||||
|
mockClient.setRequestHandler(TIME_UPDATE_SUBSCRIPTION, getHandler(subscription, getMockBusEventsResult()));
|
||||||
|
|
||||||
|
return mockClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const getMockQueryResult = (env: Networks): Statistics => ({
|
||||||
|
// statistics: {
|
||||||
|
// __typename: 'Statistics',
|
||||||
|
// chainId: `${env.toLowerCase()}-0123`,
|
||||||
|
// blockHeight: '11',
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const MOCK_SUBSCRIPTION_RESULT: BlockTime = {
|
||||||
|
// busEvents: [{
|
||||||
|
// __typename: 'BusEvent',
|
||||||
|
// eventId: '0',
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// type QueryMockProps<T> = MockLinkConfig & {
|
||||||
|
// query: DocumentNode,
|
||||||
|
// data: T,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function getQueryMock <T> ({ query, data, hasError, delay }: QueryMockProps<T>) {
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// query,
|
||||||
|
// },
|
||||||
|
// delay,
|
||||||
|
// result: { data, newData: () => data },
|
||||||
|
// error: hasError ? new Error('Error executing query') : undefined,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// export default function createMockClient ({
|
||||||
|
// network = Networks.TESTNET,
|
||||||
|
// query,
|
||||||
|
// subscription,
|
||||||
|
// }: MockClientProps) {
|
||||||
|
// return new ApolloClient({
|
||||||
|
// cache: new InMemoryCache(),
|
||||||
|
// link: new MockLink([
|
||||||
|
// getQueryMock({ ...query, query: STATS_QUERY, data: getMockQueryResult(network) }),
|
||||||
|
// getQueryMock({ ...subscription, query: TIME_UPDATE_SUBSCRIPTION, data: MOCK_SUBSCRIPTION_RESULT }),
|
||||||
|
// ]),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
@@ -1,18 +1,52 @@
|
|||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
import type { EnvironmentWithOptionalUrl } from './use-config';
|
import type { EnvironmentWithOptionalUrl } from './use-config';
|
||||||
import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config';
|
import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config';
|
||||||
|
import createClient from '../utils/apollo-client';
|
||||||
|
import type { MockRequestConfig } from './mocks/apollo-client';
|
||||||
|
import createMockClient from './mocks/apollo-client';
|
||||||
import { Networks } from '../types';
|
import { Networks } from '../types';
|
||||||
|
import * as requestNodeModule from '../utils/request-node';
|
||||||
|
|
||||||
type HostMapping = Record<string, number | Error>;
|
jest.mock('../utils/request-node');
|
||||||
|
jest.mock('../utils/apollo-client');
|
||||||
|
|
||||||
const mockHostsMap: HostMapping = {
|
type HostMapping = Record<
|
||||||
'https://host1.com': 300,
|
string,
|
||||||
'https://host2.com': 500,
|
{
|
||||||
'https://host3.com': 100,
|
query?: MockRequestConfig;
|
||||||
'https://host4.com': 650,
|
subscription?: MockRequestConfig;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const mockHostMap: HostMapping = {
|
||||||
|
'https://host1.com': {
|
||||||
|
query: {
|
||||||
|
delay: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'https://host2.com': {
|
||||||
|
query: {
|
||||||
|
delay: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'https://host3.com': {
|
||||||
|
query: {
|
||||||
|
delay: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'https://host4.com': {
|
||||||
|
query: {
|
||||||
|
delay: 650,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hostList = Object.keys(mockHostsMap);
|
const getQuickestHost = (hostMap: HostMapping) => {
|
||||||
|
return Object.keys(hostMap).sort(
|
||||||
|
(host1, host2) =>
|
||||||
|
(hostMap[host1].query?.delay ?? 0) - (hostMap[host2].query?.delay ?? 0)
|
||||||
|
)[0];
|
||||||
|
};
|
||||||
|
|
||||||
const mockEnvironment: EnvironmentWithOptionalUrl = {
|
const mockEnvironment: EnvironmentWithOptionalUrl = {
|
||||||
VEGA_ENV: Networks.TESTNET,
|
VEGA_ENV: Networks.TESTNET,
|
||||||
@@ -20,6 +54,10 @@ const mockEnvironment: EnvironmentWithOptionalUrl = {
|
|||||||
VEGA_NETWORKS: {},
|
VEGA_NETWORKS: {},
|
||||||
ETHEREUM_PROVIDER_URL: 'https://ethereum.provider',
|
ETHEREUM_PROVIDER_URL: 'https://ethereum.provider',
|
||||||
ETHERSCAN_URL: 'https://etherscan.url',
|
ETHERSCAN_URL: 'https://etherscan.url',
|
||||||
|
GIT_BRANCH: 'test',
|
||||||
|
GIT_ORIGIN_URL: 'https://github.com/test/repo',
|
||||||
|
GIT_COMMIT_HASH: 'abcde01234',
|
||||||
|
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupFetch(configUrl: string, hostMap: HostMapping) {
|
function setupFetch(configUrl: string, hostMap: HostMapping) {
|
||||||
@@ -32,43 +70,40 @@ function setupFetch(configUrl: string, hostMap: HostMapping) {
|
|||||||
} as Response);
|
} as Response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostUrls.includes(url as string)) {
|
|
||||||
const value = hostMap[url as string];
|
|
||||||
return new Promise<Response>((resolve, reject) => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({ ok: true } as Response);
|
|
||||||
}, value);
|
|
||||||
} else {
|
|
||||||
reject(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
} as Response);
|
} as Response);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
global.fetch = jest.fn();
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
const mockUpdate = jest.fn();
|
const onUpdate = jest.fn();
|
||||||
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
onUpdate.mockClear();
|
||||||
mockUpdate.mockClear();
|
onError.mockClear();
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
global.fetch.mockReset();
|
global.fetch.mockReset();
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
global.fetch.mockImplementation(
|
global.fetch.mockImplementation(
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMap)
|
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostMap)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
// createClient.mockImplementation((baseUrl) =>
|
||||||
|
// createMockClient({
|
||||||
|
// ...mockHostMap[baseUrl],
|
||||||
|
// network: mockEnvironment.VEGA_ENV,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -77,235 +112,346 @@ afterAll(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('useConfig hook', () => {
|
describe('useConfig hook', () => {
|
||||||
it('has an initial success state when the environment already has a URL', async () => {
|
it('sets config for environment with VEGA_URL', async () => {
|
||||||
const mockEnvWithUrl = {
|
const mockEnvWithUrl = {
|
||||||
...mockEnvironment,
|
...mockEnvironment,
|
||||||
VEGA_URL: 'https://some.url/query',
|
VEGA_URL: Object.keys(mockHostMap)[0],
|
||||||
};
|
};
|
||||||
const { result } = renderHook(() => useConfig(mockEnvWithUrl, mockUpdate));
|
|
||||||
|
|
||||||
expect(fetch).not.toHaveBeenCalled();
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates the environment with a host url from the network configuration', async () => {
|
|
||||||
const allowedStatuses = [
|
|
||||||
'idle',
|
|
||||||
'loading-config',
|
|
||||||
'loading-node',
|
|
||||||
'success',
|
|
||||||
];
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
useConfig(mockEnvWithUrl, onUpdate, onError)
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
expect(result.current.config).toEqual({ hosts: Object.keys(mockHostMap) });
|
||||||
result.all.forEach((state) => {
|
expect(onUpdate).not.toHaveBeenCalled();
|
||||||
expect(allowedStatuses).toContain('status' in state && state.status);
|
expect(onError).not.toHaveBeenCalled();
|
||||||
});
|
|
||||||
|
|
||||||
// fetches config
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
// calls each node
|
|
||||||
hostList.forEach((url) => {
|
|
||||||
expect(fetch).toHaveBeenCalledWith(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
// updates the environment
|
|
||||||
expect(hostList).toContain(mockUpdate.mock.calls[0][0]({}).VEGA_URL);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the host from the configuration which responds first', async () => {
|
it('sets config for environment without VEGA_URL and updates environment with the node completing validation first', async () => {
|
||||||
const shortestResponseTime = Object.values(mockHostsMap).sort()[0];
|
jest
|
||||||
const expectedHost = hostList.find((url: keyof typeof mockHostsMap) => {
|
.spyOn(requestNodeModule, 'requestNode')
|
||||||
return mockHostsMap[url] === shortestResponseTime;
|
.mockImplementation((url: any, callbacks: any) => {
|
||||||
});
|
callbacks.onStatsSuccess({
|
||||||
|
statistics: { chainId: 'testnet-43f9ed', blockHeight: '100' },
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores failing hosts and uses one which returns a success response', async () => {
|
|
||||||
const mockHostsMapScoped = {
|
|
||||||
'https://host1.com': 350,
|
|
||||||
'https://host2.com': new Error('Server error'),
|
|
||||||
'https://host3.com': 230,
|
|
||||||
'https://host4.com': new Error('Server error'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation(
|
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when the config cannot be accessed', async () => {
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
|
||||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
|
||||||
return Promise.reject(new Error('Server error'));
|
|
||||||
}
|
|
||||||
return Promise.resolve({ ok: true } as Response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-loading-config');
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when the config is not valid', async () => {
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
|
||||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
|
||||||
return Promise.resolve({
|
|
||||||
ok: true,
|
|
||||||
json: () => Promise.resolve({ some: 'data' }),
|
|
||||||
});
|
});
|
||||||
}
|
callbacks.onSubscriptionSuccess();
|
||||||
return Promise.resolve({ ok: true } as Response);
|
return createClient(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const mockEnvWithUrl = {
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
...mockEnvironment,
|
||||||
);
|
VEGA_URL: undefined,
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-validating-config');
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when no hosts can be accessed', async () => {
|
|
||||||
const mockHostsMapScoped = {
|
|
||||||
'https://host1.com': new Error('Server error'),
|
|
||||||
'https://host2.com': new Error('Server error'),
|
|
||||||
'https://host3.com': new Error('Server error'),
|
|
||||||
'https://host4.com': new Error('Server error'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation(
|
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
useConfig(mockEnvWithUrl, onUpdate, onError)
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-loading-node');
|
expect(result.current.config).toEqual({
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
hosts: Object.keys(mockHostMap),
|
||||||
});
|
});
|
||||||
|
expect(onUpdate).toHaveBeenCalledWith(
|
||||||
it('caches the list of networks', async () => {
|
expect.objectContaining({
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
VEGA_URL: 'https://host1.com',
|
||||||
|
})
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
fetch.mockClear();
|
|
||||||
|
|
||||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run2.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run2.result.current.status).toBe('success');
|
|
||||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('caches the list of networks between runs', async () => {
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
fetch.mockClear();
|
|
||||||
|
|
||||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run2.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run2.result.current.status).toBe('success');
|
|
||||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
|
|
||||||
window.localStorage.setItem(LOCAL_STORAGE_NETWORK_KEY, '{not:{valid:{json');
|
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
|
||||||
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
LOCAL_STORAGE_NETWORK_KEY,
|
|
||||||
JSON.stringify({ invalid: 'data' })
|
|
||||||
);
|
);
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
expect(onError).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
expect(consoleSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns an unset config when no config url is provided', async () => {
|
||||||
|
const mockEnvWithUrl = {
|
||||||
|
...mockEnvironment,
|
||||||
|
VEGA_CONFIG_URL: undefined,
|
||||||
|
};
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConfig(mockEnvWithUrl, onUpdate, onError)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current.config).toBe(undefined);
|
||||||
|
expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(onError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call the error callback when the provided VEGA_URL passes the node validation', async () => {
|
||||||
|
const mockEnvWithUrl = {
|
||||||
|
...mockEnvironment,
|
||||||
|
VEGA_CONFIG_URL: undefined,
|
||||||
|
VEGA_URL: Object.keys(mockHostMap)[0],
|
||||||
|
};
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConfig(mockEnvWithUrl, onUpdate, onError)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current.config).toBe(undefined);
|
||||||
|
expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(onError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('updates the VEGA_ENV ', async () => {
|
||||||
|
// const mockEnvWithUrl = {
|
||||||
|
// ...mockEnvironment,
|
||||||
|
// VEGA_URL: Object.keys(mockHostMap)[0],
|
||||||
|
// };
|
||||||
|
// const { result } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvWithUrl, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// expect(result.error).toBe(undefined);
|
||||||
|
// expect(result.current.config).toBe(undefined);
|
||||||
|
// expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
// expect(onError).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
|
||||||
|
//
|
||||||
|
// it('updates the environment with a host url from the network configuration', async () => {
|
||||||
|
// const allowedStatuses = [
|
||||||
|
// 'idle',
|
||||||
|
// 'loading-config',
|
||||||
|
// 'loading-node',
|
||||||
|
// 'success',
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// // fetches config
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
// // calls each node
|
||||||
|
// hostList.forEach((url) => {
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(url);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // updates the environment
|
||||||
|
// expect(hostList).toContain(onUpdate.mock.calls[0][0]({}).VEGA_URL);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('uses the host from the configuration which responds first', async () => {
|
||||||
|
// const shortestResponseTime = Object.values(mockHostMap).sort()[0];
|
||||||
|
// const expectedHost = hostList.find((url: keyof typeof mockHostMap) => {
|
||||||
|
// return mockHostMap[url] === shortestResponseTime;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(onUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('ignores failing hosts and uses one which returns a success response', async () => {
|
||||||
|
// const mockHostMapScoped = {
|
||||||
|
// 'https://host1.com': {
|
||||||
|
// query: {
|
||||||
|
// delay: 350,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// 'https://host2.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// 'https://host3.com': {
|
||||||
|
// query: {
|
||||||
|
// delay: 230,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// 'https://host4.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// global.fetch.mockImplementation(
|
||||||
|
// setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostMapScoped)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(onUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com');
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('returns the correct error status for when the config cannot be accessed', async () => {
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// global.fetch.mockImplementation((url: RequestInfo) => {
|
||||||
|
// if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
||||||
|
// return Promise.reject(new Error('Server error'));
|
||||||
|
// }
|
||||||
|
// return Promise.resolve({ ok: true } as Response);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('returns the correct error status for when the config is not valid', async () => {
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// global.fetch.mockImplementation((url: RequestInfo) => {
|
||||||
|
// if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
||||||
|
// return Promise.resolve({
|
||||||
|
// ok: true,
|
||||||
|
// json: () => Promise.resolve({ some: 'data' }),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return Promise.resolve({ ok: true } as Response);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('returns the correct error status for when no hosts can be accessed', async () => {
|
||||||
|
// const mockHostMapScoped = {
|
||||||
|
// 'https://host1.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// 'https://host2.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// 'https://host3.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// 'https://host4.com': {
|
||||||
|
// query: {
|
||||||
|
// hasError: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// global.fetch.mockImplementation(
|
||||||
|
// setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostMapScoped)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(onUpdate).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('caches the list of networks', async () => {
|
||||||
|
// const run1 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
//
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// fetch.mockClear();
|
||||||
|
//
|
||||||
|
// const run2 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run2.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('caches the list of networks between runs', async () => {
|
||||||
|
// const run1 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
//
|
||||||
|
// // @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
// fetch.mockClear();
|
||||||
|
//
|
||||||
|
// const run2 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run2.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
|
||||||
|
// window.localStorage.setItem(LOCAL_STORAGE_NETWORK_KEY, '{not:{valid:{json');
|
||||||
|
// const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||||
|
//
|
||||||
|
// const run1 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
// expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
//
|
||||||
|
// consoleWarnSpy.mockRestore();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
||||||
|
// window.localStorage.setItem(
|
||||||
|
// LOCAL_STORAGE_NETWORK_KEY,
|
||||||
|
// JSON.stringify({ invalid: 'data' })
|
||||||
|
// );
|
||||||
|
// const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||||
|
//
|
||||||
|
// const run1 = renderHook(() =>
|
||||||
|
// useConfig(mockEnvironment, onUpdate, onError)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
// jest.runAllTimers();
|
||||||
|
// await run1.waitForNextUpdate();
|
||||||
|
//
|
||||||
|
// expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
// expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
//
|
||||||
|
// consoleSpy.mockRestore();
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,59 @@
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||||
import type { Environment, Configuration, ConfigStatus } from '../types';
|
import { ErrorType } from '../types';
|
||||||
|
import type { Environment, Configuration, Networks } from '../types';
|
||||||
import { validateConfiguration } from '../utils/validate-configuration';
|
import { validateConfiguration } from '../utils/validate-configuration';
|
||||||
import { promiseRaceToSuccess } from '../utils/promise-race-success';
|
import { promiseRaceToSuccess } from '../utils/promise-race-success';
|
||||||
|
import { requestNode } from '../utils/request-node';
|
||||||
|
import { getHasInvalidChain } from '../utils/validate-node';
|
||||||
|
|
||||||
export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig';
|
export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig';
|
||||||
|
|
||||||
export type EnvironmentWithOptionalUrl = Partial<Environment> &
|
export type EnvironmentWithOptionalUrl = Partial<Environment> &
|
||||||
Omit<Environment, 'VEGA_URL'>;
|
Omit<Environment, 'VEGA_URL'>;
|
||||||
|
|
||||||
const requestToNode = async (url: string, index: number): Promise<number> => {
|
const requestToNode =
|
||||||
const response = await fetch(url);
|
(
|
||||||
if (!response.ok) {
|
env: Networks,
|
||||||
throw new Error(`Failed connecting to node: ${url}.`);
|
setSubscriptionStatus: (url: string, status: boolean) => void
|
||||||
|
) =>
|
||||||
|
async (url: string): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
requestNode(url, {
|
||||||
|
onStatsSuccess: (data) => {
|
||||||
|
if (getHasInvalidChain(env, data.statistics.chainId)) {
|
||||||
|
reject(ErrorType.INVALID_NETWORK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(url);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
onStatsFailure: () => {
|
||||||
|
reject(ErrorType.CONNECTION_ERROR);
|
||||||
|
},
|
||||||
|
onSubscriptionSuccess: () => {
|
||||||
|
setSubscriptionStatus(url, true);
|
||||||
|
},
|
||||||
|
onSubscriptionFailure: () => {
|
||||||
|
setSubscriptionStatus(url, false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const compileHosts = (hosts: string[], envUrl?: string) => {
|
||||||
|
if (envUrl && !hosts.includes(envUrl)) {
|
||||||
|
return [...hosts, envUrl];
|
||||||
}
|
}
|
||||||
return index;
|
return hosts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCachedConfig = () => {
|
const getCachedConfig = (env: Networks) => {
|
||||||
const value = LocalStorage.getItem(LOCAL_STORAGE_NETWORK_KEY);
|
const value = LocalStorage.getItem(LOCAL_STORAGE_NETWORK_KEY);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(value) as Configuration;
|
const config = JSON.parse(value)[env] as Configuration;
|
||||||
const hasError = validateConfiguration(config);
|
const hasError = validateConfiguration(config);
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
@@ -34,7 +64,7 @@ const getCachedConfig = () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
LocalStorage.removeItem(LOCAL_STORAGE_NETWORK_KEY);
|
LocalStorage.removeItem(LOCAL_STORAGE_NETWORK_KEY);
|
||||||
console.warn(
|
console.warn(
|
||||||
'Malformed data found for network configuration. Removed and continuing...'
|
'Malformed data found for network configuration. Removed cached configuration, continuing...'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,79 +74,103 @@ const getCachedConfig = () => {
|
|||||||
|
|
||||||
export const useConfig = (
|
export const useConfig = (
|
||||||
environment: EnvironmentWithOptionalUrl,
|
environment: EnvironmentWithOptionalUrl,
|
||||||
updateEnvironment: Dispatch<SetStateAction<Environment>>
|
updateEnvironment: (env: Partial<Environment>) => void,
|
||||||
|
onError: (errorType: ErrorType) => void
|
||||||
) => {
|
) => {
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
const [subscriptionStatusMap, setSubscriptionStatusMap] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
const [config, setConfig] = useState<Configuration | undefined>(
|
const [config, setConfig] = useState<Configuration | undefined>(
|
||||||
getCachedConfig()
|
getCachedConfig(environment.VEGA_ENV)
|
||||||
);
|
|
||||||
const [status, setStatus] = useState<ConfigStatus>(
|
|
||||||
!environment.VEGA_URL ? 'idle' : 'success'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config && status === 'idle') {
|
(async () => {
|
||||||
(async () => {
|
if (!config && environment.VEGA_CONFIG_URL) {
|
||||||
setStatus('loading-config');
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(environment.VEGA_CONFIG_URL ?? '');
|
const response = await fetch(environment.VEGA_CONFIG_URL);
|
||||||
const configData: Configuration = await response.json();
|
const configData: Configuration = await response.json();
|
||||||
|
|
||||||
if (validateConfiguration(configData)) {
|
if (validateConfiguration(configData)) {
|
||||||
setStatus('error-validating-config');
|
onError(ErrorType.CONFIG_VALIDATION_ERROR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig({ hosts: configData.hosts });
|
const hosts = compileHosts(configData.hosts, environment.VEGA_URL);
|
||||||
|
|
||||||
|
setConfig({ hosts });
|
||||||
LocalStorage.setItem(
|
LocalStorage.setItem(
|
||||||
LOCAL_STORAGE_NETWORK_KEY,
|
LOCAL_STORAGE_NETWORK_KEY,
|
||||||
JSON.stringify({ hosts: configData.hosts })
|
JSON.stringify({
|
||||||
|
[environment.VEGA_ENV]: {
|
||||||
|
hosts,
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus('error-loading-config');
|
onError(ErrorType.CONFIG_LOAD_ERROR);
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
}
|
})();
|
||||||
// load config only once per runtime
|
// load config only once per runtime
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [environment.VEGA_CONFIG_URL, !!config, status, setStatus, setConfig]);
|
}, [environment.VEGA_CONFIG_URL, !!config, onError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
(async () => {
|
||||||
config &&
|
if (environment.VEGA_URL && !verified) {
|
||||||
!['loading-node', 'success', 'error-loading-node'].includes(status)
|
|
||||||
) {
|
|
||||||
(async () => {
|
|
||||||
setStatus('loading-node');
|
|
||||||
|
|
||||||
// if there's only one configured node to choose from, set is as the env url
|
|
||||||
if (config.hosts.length === 1) {
|
|
||||||
setStatus('success');
|
|
||||||
updateEnvironment((prevEnvironment) => ({
|
|
||||||
...prevEnvironment,
|
|
||||||
VEGA_URL: config.hosts[0],
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// when there are multiple possible hosts, set the env url to the node which responds first
|
|
||||||
try {
|
try {
|
||||||
const requests = config.hosts.map(requestToNode);
|
await requestToNode(environment.VEGA_ENV, (index, status) => {
|
||||||
const index = await promiseRaceToSuccess(requests);
|
setSubscriptionStatusMap((state) => ({
|
||||||
setStatus('success');
|
...state,
|
||||||
updateEnvironment((prevEnvironment) => ({
|
[index]: status,
|
||||||
...prevEnvironment,
|
}));
|
||||||
VEGA_URL: config.hosts[index],
|
})(environment.VEGA_URL);
|
||||||
}));
|
setVerified(true);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
setStatus('error-loading-node');
|
if (err in ErrorType) {
|
||||||
|
onError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onError(ErrorType.CONNECTION_ERROR);
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
}
|
|
||||||
|
if (config && !environment.VEGA_URL) {
|
||||||
|
try {
|
||||||
|
const requests = config.hosts.map(
|
||||||
|
requestToNode(environment.VEGA_ENV, (url, status) => {
|
||||||
|
setSubscriptionStatusMap((state) => ({
|
||||||
|
...state,
|
||||||
|
[url]: status,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('START RACE');
|
||||||
|
const node = await promiseRaceToSuccess(requests);
|
||||||
|
|
||||||
|
setVerified(true);
|
||||||
|
updateEnvironment({
|
||||||
|
VEGA_URL: node,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
onError(ErrorType.CONNECTION_ERROR_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
// load config only once per runtime
|
// load config only once per runtime
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [status, !!config, setStatus, updateEnvironment]);
|
}, [environment.VEGA_URL, verified, !!config, updateEnvironment]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (subscriptionStatusMap[environment.VEGA_URL ?? ''] === false) {
|
||||||
|
onError(ErrorType.SSL_ERROR);
|
||||||
|
}
|
||||||
|
}, [onError, subscriptionStatusMap[environment.VEGA_URL ?? '']]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import type { ComponentProps } from 'react';
|
// having the node switcher dialog in the environment provider breaks the test renderer
|
||||||
|
// workaround based on: https://github.com/facebook/react/issues/11565
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import type { EnvironmentState } from './use-environment';
|
import type { EnvironmentState } from './use-environment';
|
||||||
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
||||||
import { Networks } from '../types';
|
import { Networks } from '../types';
|
||||||
|
|
||||||
|
jest.mock('react-dom', () => ({
|
||||||
|
...jest.requireActual('react-dom'),
|
||||||
|
createPortal: (node: ReactNode) => node,
|
||||||
|
}));
|
||||||
|
|
||||||
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
||||||
return <EnvironmentProvider {...props} />;
|
return <EnvironmentProvider {...props} />;
|
||||||
};
|
};
|
||||||
@@ -29,7 +36,6 @@ const mockFetch = (url: RequestInfo) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockEnvironmentState: EnvironmentState = {
|
const mockEnvironmentState: EnvironmentState = {
|
||||||
configStatus: 'success',
|
|
||||||
VEGA_URL: 'https://vega.xyz',
|
VEGA_URL: 'https://vega.xyz',
|
||||||
VEGA_ENV: Networks.TESTNET,
|
VEGA_ENV: Networks.TESTNET,
|
||||||
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
||||||
@@ -44,12 +50,13 @@ const mockEnvironmentState: EnvironmentState = {
|
|||||||
GIT_ORIGIN_URL: 'https://github.com/test/repo',
|
GIT_ORIGIN_URL: 'https://github.com/test/repo',
|
||||||
GIT_COMMIT_HASH: 'abcde01234',
|
GIT_COMMIT_HASH: 'abcde01234',
|
||||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||||
|
setNodeSwitcherOpen: noop,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockReset();
|
global.fetch.mockReset();
|
||||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockImplementation(mockFetch);
|
global.fetch.mockImplementation(mockFetch);
|
||||||
|
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
@@ -57,12 +64,12 @@ beforeEach(() => {
|
|||||||
process.env['NX_VEGA_URL'] = mockEnvironmentState.VEGA_URL;
|
process.env['NX_VEGA_URL'] = mockEnvironmentState.VEGA_URL;
|
||||||
process.env['NX_VEGA_ENV'] = mockEnvironmentState.VEGA_ENV;
|
process.env['NX_VEGA_ENV'] = mockEnvironmentState.VEGA_ENV;
|
||||||
process.env['NX_VEGA_CONFIG_URL'] = mockEnvironmentState.VEGA_CONFIG_URL;
|
process.env['NX_VEGA_CONFIG_URL'] = mockEnvironmentState.VEGA_CONFIG_URL;
|
||||||
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
|
||||||
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
|
||||||
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
|
||||||
process.env['NX_VEGA_NETWORKS'] = JSON.stringify(
|
process.env['NX_VEGA_NETWORKS'] = JSON.stringify(
|
||||||
mockEnvironmentState.VEGA_NETWORKS
|
mockEnvironmentState.VEGA_NETWORKS
|
||||||
);
|
);
|
||||||
|
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
||||||
|
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
||||||
|
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
||||||
process.env['NX_GIT_BRANCH'] = mockEnvironmentState.GIT_BRANCH;
|
process.env['NX_GIT_BRANCH'] = mockEnvironmentState.GIT_BRANCH;
|
||||||
process.env['NX_GIT_ORIGIN_URL'] = mockEnvironmentState.GIT_ORIGIN_URL;
|
process.env['NX_GIT_ORIGIN_URL'] = mockEnvironmentState.GIT_ORIGIN_URL;
|
||||||
process.env['NX_GIT_COMMIT_HASH'] = mockEnvironmentState.GIT_COMMIT_HASH;
|
process.env['NX_GIT_COMMIT_HASH'] = mockEnvironmentState.GIT_COMMIT_HASH;
|
||||||
@@ -78,9 +85,13 @@ afterAll(() => {
|
|||||||
delete process.env['NX_VEGA_URL'];
|
delete process.env['NX_VEGA_URL'];
|
||||||
delete process.env['NX_VEGA_ENV'];
|
delete process.env['NX_VEGA_ENV'];
|
||||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||||
|
delete process.env['NX_VEGA_NETWORKS'];
|
||||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||||
delete process.env['NX_ETHERSCAN_URL'];
|
delete process.env['NX_ETHERSCAN_URL'];
|
||||||
delete process.env['NX_VEGA_NETWORKS'];
|
delete process.env['NX_GIT_BRANCH'];
|
||||||
|
delete process.env['NX_GIT_ORIGIN_URL'];
|
||||||
|
delete process.env['NX_GIT_COMMIT_HASH'];
|
||||||
|
delete process.env['NX_GITHUB_FEEDBACK_URL'];
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useEnvironment hook', () => {
|
describe('useEnvironment hook', () => {
|
||||||
@@ -89,7 +100,10 @@ describe('useEnvironment hook', () => {
|
|||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
});
|
});
|
||||||
expect(result.error).toBe(undefined);
|
expect(result.error).toBe(undefined);
|
||||||
expect(result.current).toEqual(mockEnvironmentState);
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows for the VEGA_CONFIG_URL to be missing when there is a VEGA_URL present', () => {
|
it('allows for the VEGA_CONFIG_URL to be missing when there is a VEGA_URL present', () => {
|
||||||
@@ -101,6 +115,7 @@ describe('useEnvironment hook', () => {
|
|||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_CONFIG_URL: undefined,
|
VEGA_CONFIG_URL: undefined,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,6 +130,7 @@ describe('useEnvironment hook', () => {
|
|||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_URL: MOCK_HOST,
|
VEGA_URL: MOCK_HOST,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,6 +143,7 @@ describe('useEnvironment hook', () => {
|
|||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_NETWORKS: {},
|
VEGA_NETWORKS: {},
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,6 +177,7 @@ describe('useEnvironment hook', () => {
|
|||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_NETWORKS: {},
|
VEGA_NETWORKS: {},
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
@@ -211,6 +229,7 @@ describe('useEnvironment hook', () => {
|
|||||||
VEGA_ENV: env,
|
VEGA_ENV: env,
|
||||||
ETHEREUM_PROVIDER_URL: providerUrl,
|
ETHEREUM_PROVIDER_URL: providerUrl,
|
||||||
ETHERSCAN_URL: etherscanUrl,
|
ETHERSCAN_URL: etherscanUrl,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState, createContext, useContext } from 'react';
|
import { useEffect, useState, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import { NodeSwitcherDialog } from '../components/node-switcher-dialog';
|
||||||
import { useConfig } from './use-config';
|
import { useConfig } from './use-config';
|
||||||
import { compileEnvironment } from '../utils/compile-environment';
|
import { compileEnvironment } from '../utils/compile-environment';
|
||||||
import { validateEnvironment } from '../utils/validate-environment';
|
import { validateEnvironment } from '../utils/validate-environment';
|
||||||
import type { Environment, RawEnvironment, ConfigStatus } from '../types';
|
import type { Environment, RawEnvironment } from '../types';
|
||||||
|
import type { ErrorType } from '../types';
|
||||||
|
|
||||||
type EnvironmentProviderProps = {
|
type EnvironmentProviderProps = {
|
||||||
definitions?: Partial<RawEnvironment>;
|
definitions?: Partial<RawEnvironment>;
|
||||||
@@ -12,7 +14,7 @@ type EnvironmentProviderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EnvironmentState = Environment & {
|
export type EnvironmentState = Environment & {
|
||||||
configStatus: ConfigStatus;
|
setNodeSwitcherOpen: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentContext = createContext({} as EnvironmentState);
|
const EnvironmentContext = createContext({} as EnvironmentState);
|
||||||
@@ -21,10 +23,22 @@ export const EnvironmentProvider = ({
|
|||||||
definitions,
|
definitions,
|
||||||
children,
|
children,
|
||||||
}: EnvironmentProviderProps) => {
|
}: EnvironmentProviderProps) => {
|
||||||
|
const [networkError, setNetworkError] = useState<undefined | ErrorType>();
|
||||||
|
const [isNodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||||
const [environment, updateEnvironment] = useState<Environment>(
|
const [environment, updateEnvironment] = useState<Environment>(
|
||||||
compileEnvironment(definitions)
|
compileEnvironment(definitions)
|
||||||
);
|
);
|
||||||
const { status: configStatus } = useConfig(environment, updateEnvironment);
|
const { config } = useConfig(
|
||||||
|
environment,
|
||||||
|
(env: Partial<Environment>) => {
|
||||||
|
updateEnvironment((curr) => ({ ...curr, ...env }));
|
||||||
|
},
|
||||||
|
(errorType) => {
|
||||||
|
setNetworkError(errorType);
|
||||||
|
setNodeSwitcherOpen(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('config', config);
|
||||||
|
|
||||||
const errorMessage = validateEnvironment(environment);
|
const errorMessage = validateEnvironment(environment);
|
||||||
|
|
||||||
@@ -32,8 +46,28 @@ export const EnvironmentProvider = ({
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNodeSwitcherOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnvironmentContext.Provider value={{ ...environment, configStatus }}>
|
<EnvironmentContext.Provider
|
||||||
|
value={{
|
||||||
|
...environment,
|
||||||
|
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{config && (
|
||||||
|
<NodeSwitcherDialog
|
||||||
|
dialogOpen={isNodeSwitcherOpen}
|
||||||
|
initialErrorType={networkError}
|
||||||
|
setDialogOpen={setNodeSwitcherOpen}
|
||||||
|
config={config}
|
||||||
|
onConnect={(url) =>
|
||||||
|
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</EnvironmentContext.Provider>
|
</EnvironmentContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
|
import { ApolloClient } from '@apollo/client';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from '../utils/request-node';
|
||||||
|
import createClient from '../utils/apollo-client';
|
||||||
|
import { CUSTOM_NODE_KEY } from '../types';
|
||||||
|
import { useNodes } from './use-nodes';
|
||||||
|
|
||||||
|
jest.mock('../utils/apollo-client');
|
||||||
|
|
||||||
|
export const MOCK_STATISTICS_QUERY_RESULT = {
|
||||||
|
blockHeight: '11',
|
||||||
|
chainId: 'testnet_01234',
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockClient {
|
||||||
|
constructor ({
|
||||||
|
failStats = false,
|
||||||
|
failSubscription = false,
|
||||||
|
}: { failStats?: boolean; failSubscription?: boolean } = {}) {
|
||||||
|
const provider = new MockedProvider({
|
||||||
|
mocks: [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: STATS_QUERY,
|
||||||
|
},
|
||||||
|
result: failStats
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
data: {
|
||||||
|
statistics: {
|
||||||
|
__typename: 'Statistics',
|
||||||
|
...MOCK_STATISTICS_QUERY_RESULT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
},
|
||||||
|
result: failSubscription
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
data: {
|
||||||
|
busEvents: {
|
||||||
|
eventId: 'time-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return provider.state.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_DURATION = 1073;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
url: '',
|
||||||
|
responseTime: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.performance.getEntriesByName = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((url: string) => [
|
||||||
|
{
|
||||||
|
entryType: 'resource',
|
||||||
|
name: url,
|
||||||
|
startTime: 0,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
duration: MOCK_DURATION,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockReturnValue(new MockClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// @ts-ignore allow deleting the spy function after we're done with the tests
|
||||||
|
delete window.performance.getEntriesByName;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useNodes hook', () => {
|
||||||
|
it('returns the default state when empty config provided', () => {
|
||||||
|
const { result } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state).toEqual({
|
||||||
|
custom: initialState,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets loading state while waiting for the results', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useNodes({ hosts: [url] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[url]).toEqual({
|
||||||
|
url,
|
||||||
|
responseTime: {
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets statistics results', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[url].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: MOCK_STATISTICS_QUERY_RESULT.chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[url].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: MOCK_DURATION,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets subscription result', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when host in not a valid url', async () => {
|
||||||
|
const url = 'not-url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block.hasError).toBe(true);
|
||||||
|
expect(result.current.state[url].chain.hasError).toBe(true);
|
||||||
|
expect(result.current.state[url].responseTime.hasError).toBe(true);
|
||||||
|
expect(result.current.state[url].responseTime.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when statistics request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockReturnValue(new MockClient({ failStats: true }));
|
||||||
|
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[url].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[url].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when subscription request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockReturnValue(new MockClient({ failSubscription: true }));
|
||||||
|
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating block values', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block.value).toEqual(
|
||||||
|
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeBlock(url, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block.value).toEqual(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when calling the block update on a non-existing node', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [url] }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[url].block.value).toEqual(
|
||||||
|
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeBlock('https://non-existing.url', 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state['https://non-existing.url']).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets custom node and client', async () => {
|
||||||
|
const customUrl = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY]).toEqual(initialState);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setCustomNode(customUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].block.value).toEqual(
|
||||||
|
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
||||||
|
);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].chain.value).toEqual(
|
||||||
|
MOCK_STATISTICS_QUERY_RESULT.chainId
|
||||||
|
);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].responseTime.value).toEqual(
|
||||||
|
MOCK_DURATION
|
||||||
|
);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].ssl.value).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node has an invalid url', async () => {
|
||||||
|
const customUrl = 'not-url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY]).toEqual(initialState);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setCustomNode(customUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].url).toBe(customUrl);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].block.hasError).toBe(true);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].chain.hasError).toBe(true);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].responseTime.hasError).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].ssl.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node statistics request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockReturnValue(new MockClient({ failStats: true }));
|
||||||
|
|
||||||
|
const customUrl = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY]).toEqual(initialState);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setCustomNode(customUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node subscription fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockReturnValue(new MockClient({ failSubscription: true }));
|
||||||
|
|
||||||
|
const customUrl = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY]).toEqual(initialState);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setCustomNode(customUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[CUSTOM_NODE_KEY].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes a collection of clients', async () => {
|
||||||
|
const url1 = 'https://some.url';
|
||||||
|
const url2 = 'https://some-other.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes({ hosts: [url1, url2] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.clients[url1]).toBeInstanceOf(ApolloClient);
|
||||||
|
expect(result.current.clients[url2]).toBeInstanceOf(ApolloClient);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes a client for the custom node', async () => {
|
||||||
|
const customUrl = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setCustomNode(customUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.clients[CUSTOM_NODE_KEY]).toBeInstanceOf(
|
||||||
|
ApolloClient
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import type { Dispatch } from 'react';
|
||||||
|
import { useState, useEffect, useReducer } from 'react';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import type createClient from '../utils/apollo-client';
|
||||||
|
import { initializeNode } from '../utils/initialize-node';
|
||||||
|
import type { NodeData, Configuration } from '../types';
|
||||||
|
import { CUSTOM_NODE_KEY } from '../types';
|
||||||
|
|
||||||
|
type StatisticsPayload = {
|
||||||
|
block: NodeData['block']['value'];
|
||||||
|
chain: NodeData['chain']['value'];
|
||||||
|
responseTime: NodeData['responseTime']['value'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ACTIONS {
|
||||||
|
GET_STATISTICS,
|
||||||
|
GET_STATISTICS_SUCCESS,
|
||||||
|
GET_STATISTICS_FAILURE,
|
||||||
|
CHECK_SUBSCRIPTION,
|
||||||
|
CHECK_SUBSCRIPTION_SUCCESS,
|
||||||
|
CHECK_SUBSCRIPTION_FAILURE,
|
||||||
|
UPDATE_BLOCK,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType<T extends ACTIONS, P = undefined> = {
|
||||||
|
type: T;
|
||||||
|
node: string;
|
||||||
|
payload?: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS, { url: string }>
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS_FAILURE>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION, { url: string }>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_SUCCESS>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_FAILURE>
|
||||||
|
| ActionType<ACTIONS.UPDATE_BLOCK, number>;
|
||||||
|
|
||||||
|
function withData<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function withError<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNodeData = (url?: string): NodeData => ({
|
||||||
|
url: url ?? '',
|
||||||
|
responseTime: withData(),
|
||||||
|
block: withData(),
|
||||||
|
ssl: withData(),
|
||||||
|
chain: withData(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getInitialState = (config: Configuration) =>
|
||||||
|
config.hosts.reduce<Record<string, NodeData>>(
|
||||||
|
(acc, url) => ({
|
||||||
|
...acc,
|
||||||
|
[url]: getNodeData(url),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
[CUSTOM_NODE_KEY]: getNodeData(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ClientCollection = Record<
|
||||||
|
string,
|
||||||
|
undefined | ReturnType<typeof createClient>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ClientData = {
|
||||||
|
clients: ClientCollection;
|
||||||
|
subscriptions: ReturnType<typeof initializeNode>['unsubscribe'][];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeNodes = (dispatch: Dispatch<Action>, nodes: string[]) => {
|
||||||
|
return nodes.reduce<ClientData>(
|
||||||
|
(acc, node) => {
|
||||||
|
const { client, unsubscribe } = initializeNode(dispatch, node);
|
||||||
|
Object.assign(acc.clients, { [node]: client });
|
||||||
|
acc.subscriptions.push(unsubscribe);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clients: {},
|
||||||
|
subscriptions: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state: Record<string, NodeData>, action: Action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ACTIONS.GET_STATISTICS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].url = action.payload?.url ?? '';
|
||||||
|
state[action.node].block.isLoading = true;
|
||||||
|
state[action.node].chain.isLoading = true;
|
||||||
|
state[action.node].responseTime.isLoading = true;
|
||||||
|
});
|
||||||
|
case ACTIONS.GET_STATISTICS_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block = withData(action.payload?.block);
|
||||||
|
state[action.node].chain = withData(action.payload?.chain);
|
||||||
|
state[action.node].responseTime = withData(
|
||||||
|
action.payload?.responseTime
|
||||||
|
);
|
||||||
|
});
|
||||||
|
case ACTIONS.GET_STATISTICS_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block = withError();
|
||||||
|
state[action.node].chain = withError();
|
||||||
|
state[action.node].responseTime = withError();
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].url = action.payload?.url ?? '';
|
||||||
|
state[action.node].ssl.isLoading = true;
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].ssl = withData(true);
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].ssl = withError();
|
||||||
|
});
|
||||||
|
case ACTIONS.UPDATE_BLOCK:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block.value = action.payload;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNodes = (config: Configuration) => {
|
||||||
|
const reinitCacheKey = config.hosts.join(';');
|
||||||
|
const [clients, setClients] = useState<ClientCollection>({});
|
||||||
|
const [customNode, setCustomNode] = useState<undefined | string>();
|
||||||
|
const [state, dispatch] = useReducer(reducer, getInitialState(config));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { clients, subscriptions } = initializeNodes(dispatch, config.hosts);
|
||||||
|
setClients(clients);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
// use primitive cache key to prevent infinite rerender loop
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [reinitCacheKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customNode) {
|
||||||
|
const { client, unsubscribe } = initializeNode(
|
||||||
|
dispatch,
|
||||||
|
CUSTOM_NODE_KEY,
|
||||||
|
customNode
|
||||||
|
);
|
||||||
|
setClients((clients) => ({
|
||||||
|
...clients,
|
||||||
|
[CUSTOM_NODE_KEY]: client,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [customNode]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
clients,
|
||||||
|
customNode,
|
||||||
|
setCustomNode,
|
||||||
|
updateNodeBlock: (node: string, value: number) =>
|
||||||
|
dispatch({ type: ACTIONS.UPDATE_BLOCK, payload: value, node }),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -6,6 +6,18 @@ import { Networks, ENV_KEYS } from './utils/validate-environment';
|
|||||||
|
|
||||||
export { ENV_KEYS, Networks };
|
export { ENV_KEYS, Networks };
|
||||||
|
|
||||||
|
export const CUSTOM_NODE_KEY = 'custom';
|
||||||
|
|
||||||
|
export enum ErrorType {
|
||||||
|
INVALID_URL,
|
||||||
|
INVALID_NETWORK,
|
||||||
|
SSL_ERROR,
|
||||||
|
CONNECTION_ERROR,
|
||||||
|
CONNECTION_ERROR_ALL,
|
||||||
|
CONFIG_LOAD_ERROR,
|
||||||
|
CONFIG_VALIDATION_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
export type Environment = z.infer<typeof envSchema> & {
|
export type Environment = z.infer<typeof envSchema> & {
|
||||||
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
||||||
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
||||||
@@ -17,11 +29,16 @@ export type RawEnvironment = Record<EnvKey, string>;
|
|||||||
|
|
||||||
export type Configuration = z.infer<typeof configSchema>;
|
export type Configuration = z.infer<typeof configSchema>;
|
||||||
|
|
||||||
export type ConfigStatus =
|
type NodeCheck<T> = {
|
||||||
| 'idle'
|
isLoading: boolean;
|
||||||
| 'success'
|
hasError: boolean;
|
||||||
| 'loading-config'
|
value?: T;
|
||||||
| 'loading-node'
|
};
|
||||||
| 'error-loading-config'
|
|
||||||
| 'error-validating-config'
|
export type NodeData = {
|
||||||
| 'error-loading-node';
|
url: string;
|
||||||
|
ssl: NodeCheck<boolean>;
|
||||||
|
block: NodeCheck<number>;
|
||||||
|
responseTime: NodeCheck<number>;
|
||||||
|
chain: NodeCheck<string>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL subscription operation: BlockTime
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface BlockTime_busEvents {
|
||||||
|
__typename: "BusEvent";
|
||||||
|
/**
|
||||||
|
* the id for this event
|
||||||
|
*/
|
||||||
|
eventId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockTime {
|
||||||
|
/**
|
||||||
|
* Subscribe to event data from the event bus
|
||||||
|
*/
|
||||||
|
busEvents: BlockTime_busEvents[] | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: Statistics
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Statistics_statistics {
|
||||||
|
__typename: "Statistics";
|
||||||
|
/**
|
||||||
|
* Current chain id
|
||||||
|
*/
|
||||||
|
chainId: string;
|
||||||
|
/**
|
||||||
|
* Current block number
|
||||||
|
*/
|
||||||
|
blockHeight: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Statistics {
|
||||||
|
/**
|
||||||
|
* get statistics about the vega node
|
||||||
|
*/
|
||||||
|
statistics: Statistics_statistics;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from '../request-node';
|
||||||
|
|
||||||
|
export const MOCK_STATISTICS_QUERY_RESULT = {
|
||||||
|
blockHeight: '11',
|
||||||
|
chainId: 'testnet_01234',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(STATS_QUERY)
|
||||||
|
|
||||||
|
export class MockClient {
|
||||||
|
constructor ({
|
||||||
|
failStats = false,
|
||||||
|
failSubscription = false,
|
||||||
|
}: { failStats?: boolean; failSubscription?: boolean } = {}) {
|
||||||
|
const provider = new MockedProvider({
|
||||||
|
mocks: [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: STATS_QUERY,
|
||||||
|
},
|
||||||
|
result: failStats
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
data: {
|
||||||
|
statistics: {
|
||||||
|
__typename: 'Statistics',
|
||||||
|
...MOCK_STATISTICS_QUERY_RESULT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
},
|
||||||
|
result: failSubscription
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
data: {
|
||||||
|
busEvents: {
|
||||||
|
eventId: 'time-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return provider.state.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockClient = jest.fn().mockReturnValue(new MockClient());
|
||||||
|
|
||||||
|
export default createMockClient;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
from,
|
||||||
|
split,
|
||||||
|
ApolloLink,
|
||||||
|
HttpLink,
|
||||||
|
InMemoryCache,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||||
|
import { getMainDefinition } from '@apollo/client/utilities';
|
||||||
|
import { createClient as createWSClient } from 'graphql-ws';
|
||||||
|
import { onError } from '@apollo/client/link/error';
|
||||||
|
import { RetryLink } from '@apollo/client/link/retry';
|
||||||
|
|
||||||
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
|
||||||
|
export default function createClient(base?: string) {
|
||||||
|
if (!base) {
|
||||||
|
throw new Error('Base must be passed into createClient!');
|
||||||
|
}
|
||||||
|
const gqlPath = 'query';
|
||||||
|
const urlHTTP = new URL(gqlPath, base);
|
||||||
|
const urlWS = new URL(gqlPath, base);
|
||||||
|
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
||||||
|
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
||||||
|
|
||||||
|
const retryLink = new RetryLink({
|
||||||
|
delay: {
|
||||||
|
initial: 300,
|
||||||
|
max: 10000,
|
||||||
|
jitter: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpLink = new HttpLink({
|
||||||
|
uri: urlHTTP.href,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsLink = isBrowser
|
||||||
|
? new GraphQLWsLink(
|
||||||
|
createWSClient({
|
||||||
|
url: urlWS.href,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: new ApolloLink((operation, forward) => forward(operation));
|
||||||
|
|
||||||
|
const splitLink = isBrowser
|
||||||
|
? split(
|
||||||
|
({ query }) => {
|
||||||
|
const definition = getMainDefinition(query);
|
||||||
|
return (
|
||||||
|
definition.kind === 'OperationDefinition' &&
|
||||||
|
definition.operation === 'subscription'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
wsLink,
|
||||||
|
httpLink
|
||||||
|
)
|
||||||
|
: httpLink;
|
||||||
|
|
||||||
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||||
|
if (graphQLErrors) console.log(graphQLErrors);
|
||||||
|
if (networkError) console.log(networkError);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ApolloClient({
|
||||||
|
link: from([errorLink, retryLink, splitLink]),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Dispatch } from 'react';
|
||||||
|
import { ACTIONS } from '../hooks/use-nodes';
|
||||||
|
import type { Action } from '../hooks/use-nodes';
|
||||||
|
import { requestNode } from './request-node';
|
||||||
|
|
||||||
|
const getResponseTime = (url: string) => {
|
||||||
|
const requests = window.performance.getEntriesByName(url);
|
||||||
|
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeNode = (
|
||||||
|
dispatch: Dispatch<Action>,
|
||||||
|
node: string,
|
||||||
|
nodeUrl?: string
|
||||||
|
) => {
|
||||||
|
let isMounted = true;
|
||||||
|
const url = nodeUrl ?? node;
|
||||||
|
|
||||||
|
dispatch({ type: ACTIONS.GET_STATISTICS, node, payload: { url } });
|
||||||
|
dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION, node, payload: { url } });
|
||||||
|
|
||||||
|
const client = requestNode(url, {
|
||||||
|
onStatsSuccess: data => {
|
||||||
|
isMounted &&
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_STATISTICS_SUCCESS,
|
||||||
|
node,
|
||||||
|
payload: {
|
||||||
|
chain: data.statistics.chainId,
|
||||||
|
block: Number(data.statistics.blockHeight),
|
||||||
|
responseTime: getResponseTime(url),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStatsFailure: () => {
|
||||||
|
isMounted && dispatch({ type: ACTIONS.GET_STATISTICS_FAILURE, node });
|
||||||
|
},
|
||||||
|
onSubscriptionSuccess: () => {
|
||||||
|
isMounted &&
|
||||||
|
dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_SUCCESS, node });
|
||||||
|
},
|
||||||
|
onSubscriptionFailure: () => {
|
||||||
|
isMounted &&
|
||||||
|
dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
unsubscribe: () => {
|
||||||
|
client?.stop();
|
||||||
|
isMounted = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import createClient from './apollo-client';
|
||||||
|
import type { Statistics } from './__generated__/Statistics';
|
||||||
|
|
||||||
|
export const STATS_QUERY = gql`
|
||||||
|
query Statistics {
|
||||||
|
statistics {
|
||||||
|
chainId
|
||||||
|
blockHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
||||||
|
subscription BlockTime {
|
||||||
|
busEvents(types: TimeUpdate, batchSize: 1) {
|
||||||
|
eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Callbacks = {
|
||||||
|
onStatsSuccess: (data: Statistics) => void,
|
||||||
|
onStatsFailure: () => void,
|
||||||
|
onSubscriptionSuccess: () => void,
|
||||||
|
onSubscriptionFailure: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestNode = (
|
||||||
|
url: string,
|
||||||
|
{
|
||||||
|
onStatsSuccess,
|
||||||
|
onStatsFailure,
|
||||||
|
onSubscriptionSuccess,
|
||||||
|
onSubscriptionFailure,
|
||||||
|
}: Callbacks
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (err) {
|
||||||
|
onStatsFailure();
|
||||||
|
onSubscriptionFailure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient(url);
|
||||||
|
|
||||||
|
client
|
||||||
|
.query<Statistics>({
|
||||||
|
query: STATS_QUERY,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log('GOT SOME DATA!!')
|
||||||
|
onStatsSuccess(res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('DATA ERROR!!')
|
||||||
|
onStatsFailure();
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscription = client
|
||||||
|
.subscribe({
|
||||||
|
query: TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
errorPolicy: 'all',
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next() {
|
||||||
|
onSubscriptionSuccess();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
onSubscriptionFailure();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
@@ -14,20 +14,13 @@ export enum Networks {
|
|||||||
|
|
||||||
const schemaObject = {
|
const schemaObject = {
|
||||||
VEGA_URL: z.optional(z.string()),
|
VEGA_URL: z.optional(z.string()),
|
||||||
VEGA_EXPLORER_URL: z.optional(z.string()),
|
|
||||||
VEGA_CONFIG_URL: z.optional(z.string()),
|
VEGA_CONFIG_URL: z.optional(z.string()),
|
||||||
ETHEREUM_PROVIDER_URL: z.string().url({
|
|
||||||
message:
|
|
||||||
'The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url',
|
|
||||||
}),
|
|
||||||
ETHERSCAN_URL: z.string().url({
|
|
||||||
message: 'The NX_ETHERSCAN_URL environment variable must be a valid url',
|
|
||||||
}),
|
|
||||||
GIT_BRANCH: z.string(),
|
GIT_BRANCH: z.string(),
|
||||||
GIT_COMMIT_HASH: z.string(),
|
GIT_COMMIT_HASH: z.string(),
|
||||||
GIT_ORIGIN_URL: z.string(),
|
GIT_ORIGIN_URL: z.string(),
|
||||||
GITHUB_FEEDBACK_URL: z.optional(z.string()),
|
GITHUB_FEEDBACK_URL: z.optional(z.string()),
|
||||||
VEGA_ENV: z.nativeEnum(Networks),
|
VEGA_ENV: z.nativeEnum(Networks),
|
||||||
|
VEGA_EXPLORER_URL: z.optional(z.string()),
|
||||||
VEGA_NETWORKS: z
|
VEGA_NETWORKS: z
|
||||||
.object(
|
.object(
|
||||||
Object.keys(Networks).reduce(
|
Object.keys(Networks).reduce(
|
||||||
@@ -43,6 +36,13 @@ const schemaObject = {
|
|||||||
Networks
|
Networks
|
||||||
).join(' | ')}`,
|
).join(' | ')}`,
|
||||||
}),
|
}),
|
||||||
|
ETHEREUM_PROVIDER_URL: z.string().url({
|
||||||
|
message:
|
||||||
|
'The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url',
|
||||||
|
}),
|
||||||
|
ETHERSCAN_URL: z.string().url({
|
||||||
|
message: 'The NX_ETHERSCAN_URL environment variable must be a valid url',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ENV_KEYS = Object.keys(schemaObject) as Array<
|
export const ENV_KEYS = Object.keys(schemaObject) as Array<
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { CUSTOM_NODE_KEY, ErrorType } from '../types';
|
||||||
|
import type { Networks, NodeData } from '../types';
|
||||||
|
|
||||||
|
export const getIsNodeLoading = ({
|
||||||
|
chain,
|
||||||
|
responseTime,
|
||||||
|
block,
|
||||||
|
ssl,
|
||||||
|
}: NodeData) => {
|
||||||
|
return (
|
||||||
|
chain.isLoading ||
|
||||||
|
responseTime.isLoading ||
|
||||||
|
block.isLoading ||
|
||||||
|
ssl.isLoading
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHasInvalidChain = (env: Networks, chain?: string) => {
|
||||||
|
return !(chain?.includes(env.toLowerCase()) ?? false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHasInvalidUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsNodeDisabled = (env: Networks, data: NodeData) => {
|
||||||
|
return (
|
||||||
|
getIsNodeLoading(data) ||
|
||||||
|
getHasInvalidChain(env, data.chain.value) ||
|
||||||
|
getHasInvalidUrl(data.url) ||
|
||||||
|
data.chain.hasError ||
|
||||||
|
data.responseTime.hasError ||
|
||||||
|
data.block.hasError ||
|
||||||
|
data.ssl.hasError
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsFormDisabled = (
|
||||||
|
currentNode: string | undefined,
|
||||||
|
inputText: string,
|
||||||
|
env: Networks,
|
||||||
|
state: Record<string, NodeData>
|
||||||
|
) => {
|
||||||
|
if (!currentNode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentNode === CUSTOM_NODE_KEY &&
|
||||||
|
inputText !== state[CUSTOM_NODE_KEY].url
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = state[currentNode];
|
||||||
|
return getIsNodeDisabled(env, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorByType = (
|
||||||
|
errorType: ErrorType | undefined,
|
||||||
|
env: Networks,
|
||||||
|
url?: string
|
||||||
|
) => {
|
||||||
|
switch (errorType) {
|
||||||
|
case ErrorType.INVALID_URL:
|
||||||
|
return {
|
||||||
|
headline: t('Error: invalid url'),
|
||||||
|
message: t(url ? `${url} is not a valid url.` : ''),
|
||||||
|
};
|
||||||
|
case ErrorType.INVALID_NETWORK:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: incorrect network`),
|
||||||
|
message: t(`This node is not on the ${env} network.`),
|
||||||
|
};
|
||||||
|
case ErrorType.SSL_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: the node you are reading from does not have SSL`),
|
||||||
|
message: t(
|
||||||
|
url
|
||||||
|
? `${url} does not have SSL. SSL is required to subscribe to data.`
|
||||||
|
: ''
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONNECTION_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't connect to node`),
|
||||||
|
message: t(url ? `There was an error connecting to ${url}.` : ''),
|
||||||
|
};
|
||||||
|
case ErrorType.CONNECTION_ERROR_ALL:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't connect to any of the nodes on the network`),
|
||||||
|
message: t(
|
||||||
|
`Please try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONFIG_VALIDATION_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(
|
||||||
|
`Error: the configuration found for the network ${env} is invalid`
|
||||||
|
),
|
||||||
|
message: t(
|
||||||
|
`Please try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONFIG_LOAD_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't load network configuration`),
|
||||||
|
message: t(
|
||||||
|
`You can try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorByData = (env: Networks, data?: NodeData) => {
|
||||||
|
if (data && !getIsNodeLoading(data)) {
|
||||||
|
if (getHasInvalidChain(env, data.chain.value)) {
|
||||||
|
return getErrorByType(ErrorType.INVALID_NETWORK, env, data.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getHasInvalidUrl(data.url)) {
|
||||||
|
return getErrorByType(ErrorType.INVALID_URL, env, data.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.chain.hasError ||
|
||||||
|
data.responseTime.hasError ||
|
||||||
|
data.block.hasError
|
||||||
|
) {
|
||||||
|
return getErrorByType(ErrorType.CONNECTION_ERROR, env, data.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ssl.hasError) {
|
||||||
|
return getErrorByType(ErrorType.SSL_ERROR, env, data.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"**/*.spec.js",
|
"**/*.spec.js",
|
||||||
"**/*.test.jsx",
|
"**/*.test.jsx",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
"**/*.d.ts"
|
"**/*.d.ts",
|
||||||
|
"**/__mocks__/*.tsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,25 @@ import type { ReactNode } from 'react';
|
|||||||
interface RadioGroupProps {
|
interface RadioGroupProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RadioGroup = ({ children, onChange, name }: RadioGroupProps) => {
|
export const RadioGroup = ({
|
||||||
|
children,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
}: RadioGroupProps) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
|
value={value}
|
||||||
name={name}
|
name={name}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="flex flex-row gap-24"
|
className={classNames('flex flex-row gap-24', className)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</RadioGroupPrimitive.Root>
|
</RadioGroupPrimitive.Root>
|
||||||
@@ -26,20 +34,30 @@ interface RadioProps {
|
|||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
labelClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Radio = ({ id, value, label, disabled, hasError }: RadioProps) => {
|
export const Radio = ({
|
||||||
const wrapperClasses = classNames('flex flex-row gap-8 items-center', {
|
id,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
labelClassName,
|
||||||
|
disabled,
|
||||||
|
hasError,
|
||||||
|
}: RadioProps) => {
|
||||||
|
const wrapperClasses = classNames('relative pl-[25px]', {
|
||||||
'opacity-50': disabled,
|
'opacity-50': disabled,
|
||||||
});
|
});
|
||||||
const itemClasses = classNames(
|
const itemClasses = classNames(
|
||||||
'flex justify-center items-center',
|
'flex justify-center items-center',
|
||||||
|
// 'absolute top-0 left-0',
|
||||||
'w-[17px] h-[17px] rounded-full border',
|
'w-[17px] h-[17px] rounded-full border',
|
||||||
'focus:outline-none focus-visible:outline-none',
|
'focus:outline-none focus-visible:outline-none',
|
||||||
'focus-visible:shadow-vega-pink dark:focus-visible:shadow-vega-yellow',
|
'focus-visible:shadow-vega-pink dark:focus-visible:shadow-vega-yellow',
|
||||||
'dark:bg-white-25',
|
'dark:bg-white-25',
|
||||||
|
labelClassName,
|
||||||
{
|
{
|
||||||
'border-black-60 dark:border-white-60': !hasError,
|
'border-black-60 dark:border-white-60': !hasError,
|
||||||
'border-danger dark:border-danger': hasError,
|
'border-danger dark:border-danger': hasError,
|
||||||
@@ -49,12 +67,14 @@ export const Radio = ({ id, value, label, disabled, hasError }: RadioProps) => {
|
|||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
value={value}
|
value={value}
|
||||||
className={itemClasses}
|
className="absolute h-full w-[25px] top-0 left-0"
|
||||||
id={id}
|
id={id}
|
||||||
data-testid={id}
|
data-testid={id}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="w-[7px] h-[7px] bg-vega-pink dark:bg-vega-yellow rounded-full" />
|
<div className={itemClasses}>
|
||||||
|
<RadioGroupPrimitive.Indicator className="w-[7px] h-[7px] bg-vega-pink dark:bg-vega-yellow rounded-full" />
|
||||||
|
</div>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
<label htmlFor={id} className={disabled ? '' : 'cursor-pointer'}>
|
<label htmlFor={id} className={disabled ? '' : 'cursor-pointer'}>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"jest-canvas-mock": "^2.3.1",
|
"jest-canvas-mock": "^2.3.1",
|
||||||
"jest-websocket-mock": "^2.3.0",
|
"jest-websocket-mock": "^2.3.0",
|
||||||
"lint-staged": "^12.3.3",
|
"lint-staged": "^12.3.3",
|
||||||
|
"mock-apollo-client": "^1.2.0",
|
||||||
"npmlog": "^6.0.2",
|
"npmlog": "^6.0.2",
|
||||||
"nx": "13.10.1",
|
"nx": "13.10.1",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
|||||||
@@ -16451,6 +16451,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
|
mock-apollo-client@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mock-apollo-client/-/mock-apollo-client-1.2.0.tgz#72543df0d74577d29be1b34cecba8898c7e71451"
|
||||||
|
integrity sha512-zCVHv3p7zvUmen9zce9l965ZrI6rMbrm2/oqGaTerVYOaYskl/cVgTG/L7iIToTIpI7onk/f6tu8hxPXZdyy/g==
|
||||||
|
|
||||||
mock-socket@^9.1.0:
|
mock-socket@^9.1.0:
|
||||||
version "9.1.3"
|
version "9.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.3.tgz#bcb106c6b345001fa7619466fcf2f8f5a156b10f"
|
resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.3.tgz#bcb106c6b345001fa7619466fcf2f8f5a156b10f"
|
||||||
|
|||||||
Reference in New Issue
Block a user