Feat/722: Network switcher in console2 (#1073)

* feat: add network switcher dropdown to the trading app

* fix: refactor label text

* fix: format

* fix: paddings

* fix: use theme spacing instead of px
This commit is contained in:
botond 2022-08-25 15:23:57 +01:00 committed by GitHub
parent eb2f4fd27c
commit eaea6a38ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 463 additions and 16 deletions

View File

@ -10,7 +10,7 @@ const TRANCHES_URLS: { [N in Networks]: string } = {
MAINNET: 'https://static.vega.xyz/assets/mainnet-tranches.json',
TESTNET: 'https://static.vega.xyz/assets/testnet-tranches.json',
STAGNET: 'https://static.vega.xyz/assets/stagnet1-tranches.json',
STAGNET3: 'https://static.vega.xyz/assets/stagnet2-tranches.json',
STAGNET3: 'https://static.vega.xyz/assets/stagnet3-tranches.json',
DEVNET: 'https://static.vega.xyz/assets/devnet-tranches.json',
CUSTOM: 'https://static.vega.xyz/assets/testnet-tranches.json',
};

View File

@ -1,8 +1,9 @@
import { useRouter } from 'next/router';
import { Vega } from '../icons/vega';
import Link from 'next/link';
import { t } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/react-helpers';
import { Vega } from '../icons/vega';
import { useGlobalStore } from '../../stores/global';
export const Navbar = () => {
@ -10,12 +11,15 @@ export const Navbar = () => {
const tradingPath = marketId ? `/markets/${marketId}` : '/';
return (
<nav className="flex items-center">
<Link href="/" passHref={true}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a className="px-[26px]">
<Vega className="fill-white" />
</a>
</Link>
<div className="flex items-center h-full">
<Link href="/" passHref={true}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a className="px-24">
<Vega className="fill-white" />
</a>
</Link>
<NetworkSwitcher />
</div>
{[
{
name: t('Trading'),

View File

@ -1,3 +1,4 @@
export * from './network-loader';
export * from './network-switcher';
export * from './node-switcher';
export * from './node-switcher-dialog';

View File

@ -0,0 +1 @@
export * from './network-switcher';

View File

@ -0,0 +1,227 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '../../hooks/use-environment';
import {
NetworkSwitcher,
envNameMapping,
envTriggerMapping,
envDescriptionMapping,
} from './';
import { Networks } from '../../';
jest.mock('../../hooks/use-environment');
describe('Network switcher', () => {
it.each`
network | label
${Networks.CUSTOM} | ${envTriggerMapping[Networks.CUSTOM]}
${Networks.DEVNET} | ${envTriggerMapping[Networks.DEVNET]}
${Networks.STAGNET} | ${envTriggerMapping[Networks.STAGNET]}
${Networks.STAGNET3} | ${envTriggerMapping[Networks.STAGNET3]}
${Networks.TESTNET} | ${envTriggerMapping[Networks.TESTNET]}
${Networks.MAINNET} | ${envTriggerMapping[Networks.MAINNET]}
`(
'displays the correct selection label for $network',
({ network, label }) => {
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: network,
VEGA_NETWORKS: {},
}));
render(<NetworkSwitcher />);
expect(screen.getByRole('button')).toHaveTextContent(label);
}
);
it('displays mainnet and testnet on the default dropdown view', () => {
const mainnetUrl = 'https://main.net';
const testnetUrl = 'https://test.net';
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.DEVNET,
VEGA_NETWORKS: {
[Networks.MAINNET]: mainnetUrl,
[Networks.TESTNET]: testnetUrl,
},
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
expect(menuitems[0]).not.toHaveTextContent(t('current'));
expect(menuitems[1]).not.toHaveTextContent(t('current'));
expect(menuitems[0]).not.toHaveTextContent(t('not available'));
expect(menuitems[1]).not.toHaveTextContent(t('not available'));
expect(menuitems[2]).toHaveTextContent(t('Advanced'));
const links = screen.getAllByRole('link');
expect(links[0]).toHaveAttribute('href', mainnetUrl);
expect(links[1]).toHaveAttribute('href', testnetUrl);
});
it('displays the correct selected network on the default dropdown view', () => {
const mainnetUrl = 'https://main.net';
const testnetUrl = 'https://test.net';
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.MAINNET,
VEGA_NETWORKS: {
[Networks.MAINNET]: mainnetUrl,
[Networks.TESTNET]: testnetUrl,
},
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
});
it('displays the correct selected network on the default dropdown view when it does not have an associated url', () => {
const testnetUrl = 'https://test.net';
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.MAINNET,
VEGA_NETWORKS: {
[Networks.MAINNET]: undefined,
[Networks.TESTNET]: testnetUrl,
},
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
});
it('displays the correct state for a network without url on the default dropdown view', () => {
const testnetUrl = 'https://test.net';
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.TESTNET,
VEGA_NETWORKS: {
[Networks.MAINNET]: undefined,
[Networks.TESTNET]: testnetUrl,
},
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('not available'));
});
it('displays the advanced view in the correct state', () => {
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
[Networks.CUSTOM]: undefined,
[Networks.MAINNET]: 'https://main.net',
[Networks.TESTNET]: 'https://test.net',
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.STAGNET]: 'https://stag.net',
[Networks.DEVNET]: 'https://dev.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.DEVNET,
VEGA_NETWORKS,
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
fireEvent.click(screen.getByRole('menuitem', { name: t('Advanced') }));
[
Networks.MAINNET,
Networks.TESTNET,
Networks.STAGNET3,
Networks.STAGNET,
Networks.DEVNET,
].forEach((network) => {
expect(
screen.getByRole('link', { name: envNameMapping[network] })
).toHaveAttribute('href', VEGA_NETWORKS[network]);
expect(
screen.getByText(envDescriptionMapping[network])
).toBeInTheDocument();
});
});
it('labels the selected network in the advanced view', () => {
const selectedNetwork = Networks.DEVNET;
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
[Networks.CUSTOM]: undefined,
[Networks.MAINNET]: 'https://main.net',
[Networks.TESTNET]: 'https://test.net',
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.STAGNET]: 'https://stag.net',
[Networks.DEVNET]: 'https://dev.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: selectedNetwork,
VEGA_NETWORKS,
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
fireEvent.click(screen.getByRole('menuitem', { name: t('Advanced') }));
const label = screen.getByText(`(${t('current')})`);
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[selectedNetwork]
);
});
it('labels unavailable networks view in the correct state', () => {
const selectedNetwork = Networks.DEVNET;
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
[Networks.CUSTOM]: undefined,
[Networks.MAINNET]: undefined,
[Networks.TESTNET]: 'https://test.net',
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.STAGNET]: 'https://stag.net',
[Networks.DEVNET]: 'https://dev.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: selectedNetwork,
VEGA_NETWORKS,
}));
render(<NetworkSwitcher />);
fireEvent.click(screen.getByRole('button'));
fireEvent.click(screen.getByRole('menuitem', { name: t('Advanced') }));
const label = screen.getByText(`(${t('not available')})`);
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[Networks.MAINNET]
);
});
});

View File

@ -0,0 +1,183 @@
import classNames from 'classnames';
import { Fragment, useState, useCallback } from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { t } from '@vegaprotocol/react-helpers';
import {
Link,
Icon,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '../../hooks/use-environment';
import { Networks } from '../../types';
export const envNameMapping: Record<Networks, string> = {
[Networks.CUSTOM]: t('Custom'),
[Networks.DEVNET]: t('Devnet'),
[Networks.STAGNET]: t('Stagnet'),
[Networks.STAGNET3]: t('Stagnet3'),
[Networks.TESTNET]: t('Fairground testnet'),
[Networks.MAINNET]: t('Mainnet'),
};
export const envTriggerMapping: Record<Networks, string> = {
...envNameMapping,
[Networks.TESTNET]: t('Fairground'),
};
export const envDescriptionMapping: Record<Networks, string> = {
[Networks.CUSTOM]: '',
[Networks.DEVNET]: t('The latest Vega code auto-deployed'),
[Networks.STAGNET]: t('A staging environment with trading'),
[Networks.STAGNET3]: t(
'A testnet that simulates validators coming and going'
),
[Networks.TESTNET]: t(
'Public testnet run by the Vega team, often used for incentives'
),
[Networks.MAINNET]: t('The vega mainnet'),
};
const standardNetworkKeys = [Networks.MAINNET, Networks.TESTNET];
const advancedNetworkKeys = [
Networks.MAINNET,
Networks.TESTNET,
Networks.STAGNET3,
Networks.STAGNET,
Networks.DEVNET,
];
type NetworkLabelProps = {
isCurrent: boolean;
isAvailable: boolean;
};
const getLabelText = ({
isCurrent = false,
isAvailable = false,
}: NetworkLabelProps) => {
if (isCurrent) {
return ` (${t('current')})`;
}
if (!isAvailable) {
return ` (${t('not available')})`;
}
return '';
};
const NetworkLabel = ({
isCurrent = false,
isAvailable = false,
}: NetworkLabelProps) => (
<span className="text-white-80 dark:text-white-80">
{getLabelText({ isCurrent, isAvailable })}
</span>
);
export const NetworkSwitcher = () => {
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const [isOpen, setOpen] = useState(false);
const [isAdvancedView, setAdvancedView] = useState(false);
const handleOpen = useCallback(
(isOpen: boolean) => {
setOpen(isOpen);
if (!isOpen) {
setAdvancedView(false);
}
},
[setOpen, setAdvancedView]
);
const menuItemClasses = 'pt-12 pb-12 pl-16 pr-16 h-auto';
return (
<DropdownMenu open={isOpen} onOpenChange={handleOpen}>
<DropdownMenuPrimitive.Trigger
className={classNames('h-full outline-none mr-16 text-white px-16', {
'bg-dropdown-bg-dark': isOpen,
})}
onClick={() => handleOpen(!isOpen)}
>
<span className="mr-8">{envTriggerMapping[VEGA_ENV]}</span>
<Icon name="chevron-down" />
</DropdownMenuPrimitive.Trigger>
<DropdownMenuContent
align="start"
className="bg-dropdown-bg-dark border-none"
>
{!isAdvancedView && (
<>
{standardNetworkKeys.map((key) => (
<DropdownMenuItem
key={key}
data-testid="network-item"
disabled={!VEGA_NETWORKS[key]}
className={classNames(menuItemClasses, {
'text-white': !!VEGA_NETWORKS[key],
'cursor-not-allowed text-white-80 dark:text-white-80':
!VEGA_NETWORKS[key],
})}
>
<a
href={VEGA_NETWORKS[key]}
className="h-full block no-underline"
>
{envNameMapping[key]}
<NetworkLabel
isCurrent={VEGA_ENV === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</a>
</DropdownMenuItem>
))}
<DropdownMenuItem
className={classNames(
menuItemClasses,
'text-white-80 dark:text-white-80'
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setAdvancedView(true);
}}
>
{t('Advanced')}
</DropdownMenuItem>
</>
)}
{isAdvancedView && (
<div className="grid py-12 px-16 grid-cols-[repeat(2,_minmax(0,_auto))] gap-y-12 gap-x-16">
{advancedNetworkKeys.map((key) => (
<Fragment key={key}>
<div className="py-8" data-testid="network-item-advanced">
<Link
href={VEGA_NETWORKS[key]}
className={classNames({
'text-white': !!VEGA_NETWORKS[key],
'cursor-not-allowed text-white-80 dark:text-white-80':
!VEGA_NETWORKS[key],
})}
>
{envNameMapping[key]}
</Link>
<NetworkLabel
isCurrent={VEGA_ENV === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</div>
<span
className="text-white py-8"
data-testid="network-item-description"
>
{envDescriptionMapping[key]}
</span>
</Fragment>
))}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -7,8 +7,8 @@ import createMockClient from '../../hooks/mocks/apollo-client';
import { STATS_QUERY } from '../../utils/request-node';
import { NodeSwitcher } from './node-switcher';
import { getErrorByType } from '../../utils/validate-node';
import type { Configuration, NodeData } from '../../';
import { Networks, ErrorType, CUSTOM_NODE_KEY } from '../../';
import type { Configuration, NodeData } from '../../types';
import { Networks, ErrorType, CUSTOM_NODE_KEY } from '../../types';
type NodeDataProp = 'responseTime' | 'block' | 'chain' | 'ssl';

View File

@ -195,7 +195,7 @@ describe('throws error', () => {
});
expect(result).not.toThrow(
`Error processing the vega app environment:
- NX_VEGA_ENV is invalid, received "SOMETHING" instead of: CUSTOM | TESTNET | STAGNET | STAGNET2 | DEVNET | MAINNET`
- NX_VEGA_ENV is invalid, received "SOMETHING" instead of: CUSTOM | TESTNET | STAGNET | DEVNET | MAINNET`
);
});
});

View File

@ -192,7 +192,7 @@ it.each`
${Networks.DEVNET} | ${'https://ropsten.etherscan.io'} | ${'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
${Networks.TESTNET} | ${'https://ropsten.etherscan.io'} | ${'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
${Networks.STAGNET} | ${'https://ropsten.etherscan.io'} | ${'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
${Networks.STAGNET2} | ${'https://ropsten.etherscan.io'} | ${'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
${Networks.STAGNET3} | ${'https://ropsten.etherscan.io'} | ${'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
${Networks.MAINNET} | ${'https://etherscan.io'} | ${'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'}
`(
'uses correct default ethereum connection variables in $env',

View File

@ -6,3 +6,24 @@ import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
// Required by radix-ui/react-dropdown-menu
global.DOMRect = class DOMRect {
bottom = 0;
left = 0;
right = 0;
top = 0;
constructor(
public x = 0,
public y = 0,
public width = 0,
public height = 0
) {}
static fromRect(other?: DOMRectInit): DOMRect {
return new DOMRect(other?.x, other?.y, other?.width, other?.height);
}
toJSON() {
return JSON.stringify(this);
}
};

View File

@ -104,6 +104,12 @@ export const compileEnvironment = (
return acc;
}, {} as Environment);
const networkOverride = environment.VEGA_ENV
? {
[environment.VEGA_ENV]: isBrowser ? window.location.origin : undefined,
}
: {};
return {
// @ts-ignore enable using default object props
ETHERSCAN_URL: getDefaultEtherscanUrl(environment['VEGA_ENV']),
@ -112,5 +118,9 @@ export const compileEnvironment = (
environment['VEGA_ENV']
),
...environment,
VEGA_NETWORKS: {
...networkOverride,
...environment.VEGA_NETWORKS,
},
};
};