Feat/676 node switcher hosts (#698)
* feat: add node swicther * chore: remove hook form from node switcher * feat: generate apollo types and add tests * fix: format * fix: types * fix: remove redundant wrapper * fix: layout styles * fix: add controlled value to radio group * fix: flaky node hook test * feat: hook in node switcher to the explorer & token footer info * fix: cache key handling for env config * fix: env type in tests * fix: format again * fix: use netlify git env vars * fix: remove commented styles * fix: replace clsx with classnames * fix: dialog sizes * fix: fetch config by default * fix: format * fix: dialog close
This commit is contained in:
parent
74bb4b9bf9
commit
c32dae6eb9
@ -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';
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './node-switcher-dialog';
|
@ -0,0 +1,27 @@
|
|||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { NodeSwitcher } from '../node-switcher';
|
||||||
|
|
||||||
|
type NodeSwitcherDialogProps = ComponentProps<typeof NodeSwitcher> & {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
toggleDialogOpen: (dialogOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeSwitcherDialog = ({
|
||||||
|
config,
|
||||||
|
dialogOpen,
|
||||||
|
toggleDialogOpen,
|
||||||
|
onConnect,
|
||||||
|
}: NodeSwitcherDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={dialogOpen} onChange={toggleDialogOpen}>
|
||||||
|
<NodeSwitcher
|
||||||
|
config={config}
|
||||||
|
onConnect={(url) => {
|
||||||
|
onConnect(url);
|
||||||
|
toggleDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
23
libs/environment/src/components/node-switcher/__generated__/BlockHeightStats.ts
generated
Normal file
23
libs/environment/src/components/node-switcher/__generated__/BlockHeightStats.ts
generated
Normal file
@ -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;
|
||||||
|
}
|
1
libs/environment/src/components/node-switcher/index.tsx
Normal file
1
libs/environment/src/components/node-switcher/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './node-switcher';
|
@ -0,0 +1,26 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
type LayoutCellProps = {
|
||||||
|
hasError?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayoutCell = ({
|
||||||
|
hasError,
|
||||||
|
isLoading,
|
||||||
|
children,
|
||||||
|
}: LayoutCellProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames('px-8 text-right', {
|
||||||
|
'text-danger': !isLoading && hasError,
|
||||||
|
'text-white-60 dark:text-black-60': isLoading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? t('Checking') : children || '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
13
libs/environment/src/components/node-switcher/layout-row.tsx
Normal file
13
libs/environment/src/components/node-switcher/layout-row.tsx
Normal file
@ -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 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,3 @@
|
|||||||
|
export const NodeError = () => {
|
||||||
|
return <div />;
|
||||||
|
};
|
142
libs/environment/src/components/node-switcher/node-stats.tsx
Normal file
142
libs/environment/src/components/node-switcher/node-stats.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { NodeData } from '../../types';
|
||||||
|
import { createClient } from '../../utils/apollo-client';
|
||||||
|
import { useNode } from '../../hooks/use-node';
|
||||||
|
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']) => {
|
||||||
|
if (block.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
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 = ({
|
||||||
|
data: { url, responseTime, block, ssl },
|
||||||
|
highestBlock,
|
||||||
|
setBlock,
|
||||||
|
children,
|
||||||
|
}: NodeStatsContentProps) => {
|
||||||
|
return (
|
||||||
|
<LayoutRow>
|
||||||
|
{children}
|
||||||
|
<LayoutCell
|
||||||
|
isLoading={responseTime.isLoading}
|
||||||
|
hasError={responseTime.hasError}
|
||||||
|
>
|
||||||
|
{getResponseTimeDisplayValue(responseTime)}
|
||||||
|
</LayoutCell>
|
||||||
|
<LayoutCell
|
||||||
|
isLoading={block.isLoading}
|
||||||
|
hasError={
|
||||||
|
block.hasError || (!!block.value && highestBlock > block.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{url && block.value && (
|
||||||
|
<NodeBlockHeight value={block.value} setValue={setBlock} />
|
||||||
|
)}
|
||||||
|
{getBlockDisplayValue(block)}
|
||||||
|
</LayoutCell>
|
||||||
|
<LayoutCell isLoading={ssl.isLoading} hasError={ssl.hasError}>
|
||||||
|
{getSslDisplayValue(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 = {
|
||||||
|
url?: string;
|
||||||
|
highestBlock: number;
|
||||||
|
setBlock: (value: number) => void;
|
||||||
|
render: (data: NodeData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeStats = ({
|
||||||
|
url,
|
||||||
|
highestBlock,
|
||||||
|
render,
|
||||||
|
setBlock,
|
||||||
|
}: NodeStatsProps) => {
|
||||||
|
const [client, setClient] = useState<
|
||||||
|
undefined | ReturnType<typeof createClient>
|
||||||
|
>();
|
||||||
|
const { state, reset, updateBlockState } = useNode(url, client);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client?.stop();
|
||||||
|
reset();
|
||||||
|
setClient(url ? createClient(url) : undefined);
|
||||||
|
return () => client?.stop();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const onHandleBlockChange = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
updateBlockState(value);
|
||||||
|
setBlock(value);
|
||||||
|
},
|
||||||
|
[updateBlockState, setBlock]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper client={client}>
|
||||||
|
<NodeStatsContent
|
||||||
|
data={state}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={onHandleBlockChange}
|
||||||
|
>
|
||||||
|
{render(state)}
|
||||||
|
</NodeStatsContent>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
113
libs/environment/src/components/node-switcher/node-switcher.tsx
Normal file
113
libs/environment/src/components/node-switcher/node-switcher.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { RadioGroup, Button, Radio } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useEnvironment } from '../../hooks/use-environment';
|
||||||
|
import type { Configuration, NodeData, Networks } 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;
|
||||||
|
onConnect: (url: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultNode = (urls: string[], currentUrl?: string) => {
|
||||||
|
return currentUrl && urls.includes(currentUrl) ? currentUrl : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIsLoading = ({ chain, responseTime, block, ssl }: NodeData) => {
|
||||||
|
return (
|
||||||
|
chain.isLoading ||
|
||||||
|
responseTime.isLoading ||
|
||||||
|
block.isLoading ||
|
||||||
|
ssl.isLoading
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHasMatchingChain = (env: Networks, chain?: string) => {
|
||||||
|
return chain?.includes(env.toLowerCase()) ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIsDisabled = (env: Networks, data: NodeData) => {
|
||||||
|
const { chain, responseTime, block, ssl } = data;
|
||||||
|
return (
|
||||||
|
!getHasMatchingChain(env, data.chain.value) ||
|
||||||
|
getIsLoading(data) ||
|
||||||
|
chain.hasError ||
|
||||||
|
responseTime.hasError ||
|
||||||
|
block.hasError ||
|
||||||
|
ssl.hasError
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => {
|
||||||
|
const { VEGA_ENV, VEGA_URL } = useEnvironment();
|
||||||
|
const [node, setNode] = useState(getDefaultNode(config.hosts, VEGA_URL));
|
||||||
|
const [highestBlock, setHighestBlock] = useState(0);
|
||||||
|
|
||||||
|
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
||||||
|
if (node) {
|
||||||
|
onConnect(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubmitDisabled = !node;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-black dark:text-white w-full">
|
||||||
|
<NodeError />
|
||||||
|
<form onSubmit={() => onSubmit(node)}>
|
||||||
|
<p className="text-body-large font-bold 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={node}
|
||||||
|
onChange={(value) => setNode(value)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{config.hosts.map((url, index) => (
|
||||||
|
<NodeStats
|
||||||
|
key={index}
|
||||||
|
url={url}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={(block) =>
|
||||||
|
setHighestBlock(Math.max(block, highestBlock))
|
||||||
|
}
|
||||||
|
render={(data) => (
|
||||||
|
<div>
|
||||||
|
<Radio
|
||||||
|
id={`node-url-${index}`}
|
||||||
|
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
|
value={url}
|
||||||
|
label={url}
|
||||||
|
disabled={getIsDisabled(VEGA_ENV, data)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full mt-16"
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{t('Connect')}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
23
libs/environment/src/hooks/__generated__/BlockTime.ts
generated
Normal file
23
libs/environment/src/hooks/__generated__/BlockTime.ts
generated
Normal file
@ -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;
|
||||||
|
}
|
27
libs/environment/src/hooks/__generated__/Statistics.ts
generated
Normal file
27
libs/environment/src/hooks/__generated__/Statistics.ts
generated
Normal file
@ -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;
|
||||||
|
}
|
@ -20,6 +20,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) {
|
||||||
@ -77,18 +81,6 @@ afterAll(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('useConfig hook', () => {
|
describe('useConfig hook', () => {
|
||||||
it('has an initial success state when the environment already has a URL', async () => {
|
|
||||||
const mockEnvWithUrl = {
|
|
||||||
...mockEnvironment,
|
|
||||||
VEGA_URL: 'https://some.url/query',
|
|
||||||
};
|
|
||||||
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 () => {
|
it('updates the environment with a host url from the network configuration', async () => {
|
||||||
const allowedStatuses = [
|
const allowedStatuses = [
|
||||||
'idle',
|
'idle',
|
||||||
@ -273,7 +265,10 @@ describe('useConfig hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
|
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');
|
window.localStorage.setItem(
|
||||||
|
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
||||||
|
'{not:{valid:{json'
|
||||||
|
);
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||||
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||||
@ -291,7 +286,7 @@ describe('useConfig hook', () => {
|
|||||||
|
|
||||||
it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
LOCAL_STORAGE_NETWORK_KEY,
|
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
||||||
JSON.stringify({ invalid: 'data' })
|
JSON.stringify({ invalid: 'data' })
|
||||||
);
|
);
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
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 type {
|
||||||
|
Environment,
|
||||||
|
Configuration,
|
||||||
|
ConfigStatus,
|
||||||
|
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';
|
||||||
|
|
||||||
@ -18,8 +23,11 @@ const requestToNode = async (url: string, index: number): Promise<number> => {
|
|||||||
return index;
|
return index;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCachedConfig = () => {
|
const getCacheKey = (env: Networks) => `${LOCAL_STORAGE_NETWORK_KEY}-${env}`;
|
||||||
const value = LocalStorage.getItem(LOCAL_STORAGE_NETWORK_KEY);
|
|
||||||
|
const getCachedConfig = (env: Networks) => {
|
||||||
|
const key = getCacheKey(env);
|
||||||
|
const value = LocalStorage.getItem(key);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
try {
|
||||||
@ -32,7 +40,7 @@ const getCachedConfig = () => {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LocalStorage.removeItem(LOCAL_STORAGE_NETWORK_KEY);
|
LocalStorage.removeItem(key);
|
||||||
console.warn(
|
console.warn(
|
||||||
'Malformed data found for network configuration. Removed and continuing...'
|
'Malformed data found for network configuration. Removed and continuing...'
|
||||||
);
|
);
|
||||||
@ -47,10 +55,10 @@ export const useConfig = (
|
|||||||
updateEnvironment: Dispatch<SetStateAction<Environment>>
|
updateEnvironment: Dispatch<SetStateAction<Environment>>
|
||||||
) => {
|
) => {
|
||||||
const [config, setConfig] = useState<Configuration | undefined>(
|
const [config, setConfig] = useState<Configuration | undefined>(
|
||||||
getCachedConfig()
|
getCachedConfig(environment.VEGA_ENV)
|
||||||
);
|
);
|
||||||
const [status, setStatus] = useState<ConfigStatus>(
|
const [status, setStatus] = useState<ConfigStatus>(
|
||||||
!environment.VEGA_URL ? 'idle' : 'success'
|
environment.VEGA_CONFIG_URL ? 'idle' : 'success'
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -68,7 +76,7 @@ export const useConfig = (
|
|||||||
|
|
||||||
setConfig({ hosts: configData.hosts });
|
setConfig({ hosts: configData.hosts });
|
||||||
LocalStorage.setItem(
|
LocalStorage.setItem(
|
||||||
LOCAL_STORAGE_NETWORK_KEY,
|
getCacheKey(environment.VEGA_ENV),
|
||||||
JSON.stringify({ hosts: configData.hosts })
|
JSON.stringify({ hosts: configData.hosts })
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -93,7 +101,7 @@ export const useConfig = (
|
|||||||
setStatus('success');
|
setStatus('success');
|
||||||
updateEnvironment((prevEnvironment) => ({
|
updateEnvironment((prevEnvironment) => ({
|
||||||
...prevEnvironment,
|
...prevEnvironment,
|
||||||
VEGA_URL: config.hosts[0],
|
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[0],
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -105,7 +113,7 @@ export const useConfig = (
|
|||||||
setStatus('success');
|
setStatus('success');
|
||||||
updateEnvironment((prevEnvironment) => ({
|
updateEnvironment((prevEnvironment) => ({
|
||||||
...prevEnvironment,
|
...prevEnvironment,
|
||||||
VEGA_URL: config.hosts[index],
|
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[index],
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus('error-loading-node');
|
setStatus('error-loading-node');
|
||||||
@ -118,5 +126,6 @@ export const useConfig = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
|
config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ 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(() => {
|
||||||
@ -81,18 +82,26 @@ afterAll(() => {
|
|||||||
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_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', () => {
|
||||||
it('transforms and exposes values from the environment', () => {
|
it('transforms and exposes values from the environment', async () => {
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
});
|
});
|
||||||
|
await waitForNextUpdate();
|
||||||
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', async () => {
|
||||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
@ -101,6 +110,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,18 +125,21 @@ 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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows for the VEGA_NETWORKS to be missing from the environment', () => {
|
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
||||||
delete process.env['NX_VEGA_NETWORKS'];
|
delete process.env['NX_VEGA_NETWORKS'];
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
});
|
});
|
||||||
|
await waitForNextUpdate();
|
||||||
expect(result.error).toBe(undefined);
|
expect(result.error).toBe(undefined);
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_NETWORKS: {},
|
VEGA_NETWORKS: {},
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -140,7 +153,7 @@ describe('useEnvironment hook', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a validation error when VEGA_ENV is not a valid network', () => {
|
it('throws a validation error when VEGA_ENV is not a valid network', async () => {
|
||||||
process.env['NX_VEGA_ENV'] = 'SOMETHING';
|
process.env['NX_VEGA_ENV'] = 'SOMETHING';
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
@ -150,23 +163,25 @@ describe('useEnvironment hook', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when VEGA_NETWORKS is not a valid json, prints a warning and continues without using the value from it', () => {
|
it('when VEGA_NETWORKS is not a valid json, prints a warning and continues without using the value from it', async () => {
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||||
process.env['NX_VEGA_NETWORKS'] = '{not:{valid:json';
|
process.env['NX_VEGA_NETWORKS'] = '{not:{valid:json';
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
});
|
});
|
||||||
|
await waitForNextUpdate();
|
||||||
expect(result.error).toBe(undefined);
|
expect(result.error).toBe(undefined);
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
VEGA_NETWORKS: {},
|
VEGA_NETWORKS: {},
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
consoleWarnSpy.mockRestore();
|
consoleWarnSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a validation error when VEGA_NETWORKS is has an invalid network as a key', () => {
|
it('throws a validation error when VEGA_NETWORKS is has an invalid network as a key', async () => {
|
||||||
process.env['NX_VEGA_NETWORKS'] = JSON.stringify({
|
process.env['NX_VEGA_NETWORKS'] = JSON.stringify({
|
||||||
NOT_A_NETWORK: 'https://somewhere.url',
|
NOT_A_NETWORK: 'https://somewhere.url',
|
||||||
});
|
});
|
||||||
@ -178,7 +193,7 @@ describe('useEnvironment hook', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a validation error when both VEGA_URL and VEGA_CONFIG_URL are missing in the environment', () => {
|
it('throws a validation error when both VEGA_URL and VEGA_CONFIG_URL are missing in the environment', async () => {
|
||||||
delete process.env['NX_VEGA_URL'];
|
delete process.env['NX_VEGA_URL'];
|
||||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result } = renderHook(() => useEnvironment(), {
|
||||||
@ -202,15 +217,17 @@ describe('useEnvironment hook', () => {
|
|||||||
process.env['NX_VEGA_ENV'] = env;
|
process.env['NX_VEGA_ENV'] = env;
|
||||||
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'];
|
||||||
const { result } = renderHook(() => useEnvironment(), {
|
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||||
wrapper: MockWrapper,
|
wrapper: MockWrapper,
|
||||||
});
|
});
|
||||||
|
await waitForNextUpdate();
|
||||||
expect(result.error).toBe(undefined);
|
expect(result.error).toBe(undefined);
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
...mockEnvironmentState,
|
...mockEnvironmentState,
|
||||||
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,6 +1,7 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState, createContext, useContext } from 'react';
|
import { 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';
|
||||||
@ -13,6 +14,7 @@ type EnvironmentProviderProps = {
|
|||||||
|
|
||||||
export type EnvironmentState = Environment & {
|
export type EnvironmentState = Environment & {
|
||||||
configStatus: ConfigStatus;
|
configStatus: ConfigStatus;
|
||||||
|
setNodeSwitcherOpen: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentContext = createContext({} as EnvironmentState);
|
const EnvironmentContext = createContext({} as EnvironmentState);
|
||||||
@ -21,10 +23,14 @@ export const EnvironmentProvider = ({
|
|||||||
definitions,
|
definitions,
|
||||||
children,
|
children,
|
||||||
}: EnvironmentProviderProps) => {
|
}: EnvironmentProviderProps) => {
|
||||||
|
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 { status: configStatus, config } = useConfig(
|
||||||
|
environment,
|
||||||
|
updateEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
const errorMessage = validateEnvironment(environment);
|
const errorMessage = validateEnvironment(environment);
|
||||||
|
|
||||||
@ -33,7 +39,23 @@ export const EnvironmentProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnvironmentContext.Provider value={{ ...environment, configStatus }}>
|
<EnvironmentContext.Provider
|
||||||
|
value={{
|
||||||
|
...environment,
|
||||||
|
configStatus,
|
||||||
|
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{config && (
|
||||||
|
<NodeSwitcherDialog
|
||||||
|
dialogOpen={isNodeSwitcherOpen}
|
||||||
|
toggleDialogOpen={setNodeSwitcherOpen}
|
||||||
|
config={config}
|
||||||
|
onConnect={(url) =>
|
||||||
|
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</EnvironmentContext.Provider>
|
</EnvironmentContext.Provider>
|
||||||
);
|
);
|
||||||
|
267
libs/environment/src/hooks/use-node.spec.tsx
Normal file
267
libs/environment/src/hooks/use-node.spec.tsx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
|
import { useNode, STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from './use-node';
|
||||||
|
|
||||||
|
const MOCK_DURATION = 1073;
|
||||||
|
|
||||||
|
const MOCK_STATISTICS_QUERY_RESULT = {
|
||||||
|
blockHeight: '11',
|
||||||
|
chainId: 'testnet_01234',
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockClient = ({
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.performance.getEntriesByName = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((url: string) => [
|
||||||
|
{
|
||||||
|
entryType: 'resource',
|
||||||
|
name: url,
|
||||||
|
startTime: 0,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
duration: MOCK_DURATION,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// @ts-ignore allow deleting the spy function after we're done with the tests
|
||||||
|
delete window.performance.getEntriesByName;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useNode hook', () => {
|
||||||
|
it('returns the default state when no arguments provided', () => {
|
||||||
|
const { result } = renderHook(() => useNode());
|
||||||
|
expect(result.current.state).toEqual(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the default state when no url provided', () => {
|
||||||
|
const client = createMockClient();
|
||||||
|
const { result } = renderHook(() => useNode(undefined, client));
|
||||||
|
expect(result.current.state).toEqual(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the default state when no client provided', () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result } = renderHook(() => useNode(url, undefined));
|
||||||
|
expect(result.current.state).toEqual({ ...initialState, url });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets loading state while waiting for the results', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient();
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useNode(url, client)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state).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 client = createMockClient();
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state.chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: MOCK_STATISTICS_QUERY_RESULT.chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state.responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: MOCK_DURATION,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets subscription result', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient();
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when statistics request fails', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient({ failStats: true });
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state.chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state.responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when subscription request fails', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient({ failSubscription: true });
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating block values', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient({ failSubscription: true });
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.block.value).toEqual(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateBlockState(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.block.value).toEqual(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows resetting the state to defaults', async () => {
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const client = createMockClient();
|
||||||
|
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state.block.value).toBe(
|
||||||
|
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
||||||
|
);
|
||||||
|
expect(result.current.state.chain.value).toBe(
|
||||||
|
MOCK_STATISTICS_QUERY_RESULT.chainId
|
||||||
|
);
|
||||||
|
expect(result.current.state.responseTime.value).toBe(MOCK_DURATION);
|
||||||
|
expect(result.current.state.ssl.value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state).toEqual({ ...initialState, url });
|
||||||
|
});
|
||||||
|
});
|
188
libs/environment/src/hooks/use-node.tsx
Normal file
188
libs/environment/src/hooks/use-node.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { useEffect, useReducer } from 'react';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import type { createClient } from '../utils/apollo-client';
|
||||||
|
import type { NodeData } from '../types';
|
||||||
|
import type { Statistics } from './__generated__/Statistics';
|
||||||
|
|
||||||
|
type StatisticsPayload = {
|
||||||
|
block: NodeData['block']['value'];
|
||||||
|
chain: NodeData['chain']['value'];
|
||||||
|
responseTime: NodeData['responseTime']['value'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STATS_QUERY = gql`
|
||||||
|
query Statistics {
|
||||||
|
statistics {
|
||||||
|
chainId
|
||||||
|
blockHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
||||||
|
subscription BlockTime {
|
||||||
|
busEvents(types: TimeUpdate, batchSize: 1) {
|
||||||
|
eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
enum ACTION {
|
||||||
|
GET_STATISTICS,
|
||||||
|
GET_STATISTICS_SUCCESS,
|
||||||
|
GET_STATISTICS_FAILURE,
|
||||||
|
CHECK_SUBSCRIPTION,
|
||||||
|
CHECK_SUBSCRIPTION_SUCCESS,
|
||||||
|
CHECK_SUBSCRIPTION_FAILURE,
|
||||||
|
UPDATE_BLOCK,
|
||||||
|
RESET_STATE,
|
||||||
|
}
|
||||||
|
|
||||||
|
function withData<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function withError<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialState = (url?: string): NodeData => ({
|
||||||
|
url: url ?? '',
|
||||||
|
responseTime: withData(),
|
||||||
|
block: withData(),
|
||||||
|
ssl: withData(),
|
||||||
|
chain: withData(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getResponseTime = (url: string) => {
|
||||||
|
const requests = window.performance.getEntriesByName(url);
|
||||||
|
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionType<T extends ACTION, P = undefined> = {
|
||||||
|
type: T;
|
||||||
|
payload?: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| ActionType<ACTION.GET_STATISTICS>
|
||||||
|
| ActionType<ACTION.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
||||||
|
| ActionType<ACTION.GET_STATISTICS_FAILURE>
|
||||||
|
| ActionType<ACTION.CHECK_SUBSCRIPTION>
|
||||||
|
| ActionType<ACTION.CHECK_SUBSCRIPTION_SUCCESS>
|
||||||
|
| ActionType<ACTION.CHECK_SUBSCRIPTION_FAILURE>
|
||||||
|
| ActionType<ACTION.UPDATE_BLOCK, number>
|
||||||
|
| ActionType<ACTION.RESET_STATE>;
|
||||||
|
|
||||||
|
const reducer = (state: NodeData, action: Action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ACTION.GET_STATISTICS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.block.isLoading = true;
|
||||||
|
state.chain.isLoading = true;
|
||||||
|
state.responseTime.isLoading = true;
|
||||||
|
});
|
||||||
|
case ACTION.GET_STATISTICS_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.block = withData(action.payload?.block);
|
||||||
|
state.chain = withData(action.payload?.chain);
|
||||||
|
state.responseTime = withData(action.payload?.responseTime);
|
||||||
|
});
|
||||||
|
case ACTION.GET_STATISTICS_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.block = withError();
|
||||||
|
state.chain = withError();
|
||||||
|
state.responseTime = withError();
|
||||||
|
});
|
||||||
|
case ACTION.CHECK_SUBSCRIPTION:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.ssl.isLoading = true;
|
||||||
|
});
|
||||||
|
case ACTION.CHECK_SUBSCRIPTION_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.ssl = withData(true);
|
||||||
|
});
|
||||||
|
case ACTION.CHECK_SUBSCRIPTION_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.ssl = withError();
|
||||||
|
});
|
||||||
|
case ACTION.UPDATE_BLOCK:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.block.value = action.payload;
|
||||||
|
});
|
||||||
|
case ACTION.RESET_STATE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state.responseTime = withData();
|
||||||
|
state.block = withData();
|
||||||
|
state.ssl = withData();
|
||||||
|
state.chain = withData();
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNode = (
|
||||||
|
url?: string,
|
||||||
|
client?: ReturnType<typeof createClient>
|
||||||
|
) => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, getInitialState(url));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (client && url) {
|
||||||
|
dispatch({ type: ACTION.GET_STATISTICS });
|
||||||
|
dispatch({ type: ACTION.CHECK_SUBSCRIPTION });
|
||||||
|
|
||||||
|
client
|
||||||
|
.query<Statistics>({
|
||||||
|
query: STATS_QUERY,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTION.GET_STATISTICS_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
chain: res.data.statistics.chainId,
|
||||||
|
block: Number(res.data.statistics.blockHeight),
|
||||||
|
responseTime: getResponseTime(url),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({ type: ACTION.GET_STATISTICS_FAILURE });
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscription = client
|
||||||
|
.subscribe({
|
||||||
|
query: TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
errorPolicy: 'all',
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next() {
|
||||||
|
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_SUCCESS });
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_FAILURE });
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [client, url, dispatch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
updateBlockState: (value: number) =>
|
||||||
|
dispatch({ type: ACTION.UPDATE_BLOCK, payload: value }),
|
||||||
|
reset: () => dispatch({ type: ACTION.RESET_STATE }),
|
||||||
|
};
|
||||||
|
};
|
@ -25,3 +25,17 @@ export type ConfigStatus =
|
|||||||
| 'error-loading-config'
|
| 'error-loading-config'
|
||||||
| 'error-validating-config'
|
| 'error-validating-config'
|
||||||
| 'error-loading-node';
|
| 'error-loading-node';
|
||||||
|
|
||||||
|
type NodeCheck<T> = {
|
||||||
|
isLoading: boolean;
|
||||||
|
hasError: boolean;
|
||||||
|
value?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeData = {
|
||||||
|
url: string;
|
||||||
|
ssl: NodeCheck<boolean>;
|
||||||
|
block: NodeCheck<number>;
|
||||||
|
responseTime: NodeCheck<number>;
|
||||||
|
chain: NodeCheck<string>;
|
||||||
|
};
|
||||||
|
71
libs/environment/src/utils/apollo-client.tsx
Normal file
71
libs/environment/src/utils/apollo-client.tsx
Normal file
@ -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 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(),
|
||||||
|
});
|
||||||
|
}
|
@ -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.optional(z.string()),
|
GIT_BRANCH: z.optional(z.string()),
|
||||||
GIT_COMMIT_HASH: z.optional(z.string()),
|
GIT_COMMIT_HASH: z.optional(z.string()),
|
||||||
GIT_ORIGIN_URL: z.optional(z.string()),
|
GIT_ORIGIN_URL: z.optional(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<
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
|
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
useEnvironment,
|
|
||||||
NetworkSwitcherDialog,
|
|
||||||
} from '@vegaprotocol/environment';
|
|
||||||
|
|
||||||
const getFeedbackLinks = (gitOriginUrl?: string) =>
|
const getFeedbackLinks = (gitOriginUrl?: string) =>
|
||||||
[
|
[
|
||||||
@ -15,28 +11,26 @@ const getFeedbackLinks = (gitOriginUrl?: string) =>
|
|||||||
].filter((link) => !!link.url);
|
].filter((link) => !!link.url);
|
||||||
|
|
||||||
export const NetworkInfo = () => {
|
export const NetworkInfo = () => {
|
||||||
const [isNetworkConfigOpen, setNetworkConfigOpen] = useState(false);
|
|
||||||
const {
|
const {
|
||||||
VEGA_URL,
|
VEGA_URL,
|
||||||
VEGA_NETWORKS,
|
|
||||||
GIT_COMMIT_HASH,
|
GIT_COMMIT_HASH,
|
||||||
GIT_ORIGIN_URL,
|
GIT_ORIGIN_URL,
|
||||||
GITHUB_FEEDBACK_URL,
|
GITHUB_FEEDBACK_URL,
|
||||||
ETHEREUM_PROVIDER_URL,
|
ETHEREUM_PROVIDER_URL,
|
||||||
|
setNodeSwitcherOpen,
|
||||||
} = useEnvironment();
|
} = useEnvironment();
|
||||||
const feedbackLinks = getFeedbackLinks(GITHUB_FEEDBACK_URL);
|
const feedbackLinks = getFeedbackLinks(GITHUB_FEEDBACK_URL);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-16">
|
<p className="mb-16">
|
||||||
{t('Reading network data from')}{' '}
|
{t('Reading network data from')}{' '}
|
||||||
<Lozenge className="text-black dark:text-white bg-white-60 dark:bg-black-60">
|
<Lozenge className="text-black dark:text-white bg-white-60 dark:bg-black-60">
|
||||||
{VEGA_URL}
|
{VEGA_URL}
|
||||||
</Lozenge>
|
</Lozenge>
|
||||||
. <Link onClick={() => setNetworkConfigOpen(true)}>{t('Edit')}</Link>
|
. <Link onClick={() => setNodeSwitcherOpen()}>{t('Edit')}</Link>
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-[1rem]">
|
<p className="mb-16">
|
||||||
{t('Reading Ethereum data from')}{' '}
|
{t('Reading Ethereum data from')}{' '}
|
||||||
<Lozenge className="text-black dark:text-white bg-white-60 dark:bg-black-60">
|
<Lozenge className="text-black dark:text-white bg-white-60 dark:bg-black-60">
|
||||||
{ETHEREUM_PROVIDER_URL}
|
{ETHEREUM_PROVIDER_URL}
|
||||||
@ -44,7 +38,7 @@ export const NetworkInfo = () => {
|
|||||||
.{' '}
|
.{' '}
|
||||||
</p>
|
</p>
|
||||||
{GIT_COMMIT_HASH && (
|
{GIT_COMMIT_HASH && (
|
||||||
<p className="mb-[1rem]">
|
<p className="mb-16">
|
||||||
{t('Version/commit hash')}:{' '}
|
{t('Version/commit hash')}:{' '}
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
@ -77,15 +71,5 @@ export const NetworkInfo = () => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<NetworkSwitcherDialog
|
|
||||||
dialogOpen={isNetworkConfigOpen}
|
|
||||||
setDialogOpen={setNetworkConfigOpen}
|
|
||||||
onConnect={({ network }) => {
|
|
||||||
if (VEGA_NETWORKS[network]) {
|
|
||||||
window.location.href = VEGA_NETWORKS[network] as string;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -26,7 +26,7 @@ export function Dialog({
|
|||||||
}: DialogProps) {
|
}: DialogProps) {
|
||||||
const contentClasses = classNames(
|
const contentClasses = classNames(
|
||||||
// Positions the modal in the center of screen
|
// Positions the modal in the center of screen
|
||||||
'z-20 fixed w-full md:w-[520px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
'z-20 fixed w-full md:w-[520px] lg:w-[1000px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||||
// Need to apply background and text colors again as content is rendered in a portal
|
// Need to apply background and text colors again as content is rendered in a portal
|
||||||
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
||||||
getIntentShadow(intent),
|
getIntentShadow(intent),
|
||||||
|
@ -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
|
||||||
name={name}
|
name={name}
|
||||||
|
value={value}
|
||||||
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,12 +34,20 @@ 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(
|
||||||
@ -40,6 +56,7 @@ export const Radio = ({ id, value, label, disabled, hasError }: RadioProps) => {
|
|||||||
'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 +66,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}
|
||||||
>
|
>
|
||||||
|
<div className={itemClasses}>
|
||||||
<RadioGroupPrimitive.Indicator className="w-[7px] h-[7px] bg-vega-pink dark:bg-vega-yellow rounded-full" />
|
<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}
|
||||||
|
Loading…
Reference in New Issue
Block a user