Redesign screen/page "List of networks" (#13560)

Co-authored-by: George Marshall <george.marshall@consensys.net>
Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
feature/default_network_editable
VSaric 3 years ago committed by GitHub
parent 8174aaa6b5
commit 8184b150cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 97
      .storybook/test-data.js
  2. 8
      app/_locales/en/messages.json
  3. 7
      shared/constants/network.js
  4. 114
      ui/pages/settings/networks-tab/custom-content-search/custom-content-search.js
  5. 23
      ui/pages/settings/networks-tab/custom-content-search/custom-content-search.stories.js
  6. 110
      ui/pages/settings/networks-tab/custom-content-search/custom-content-search.test.js
  7. 1
      ui/pages/settings/networks-tab/custom-content-search/index.js
  8. 109
      ui/pages/settings/networks-tab/index.scss
  9. 58
      ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
  10. 28
      ui/pages/settings/networks-tab/networks-list/network-list.stories.js
  11. 64
      ui/pages/settings/networks-tab/networks-list/networks-list.js
  12. 2
      ui/pages/settings/networks-tab/networks-list/networks-list.test.js
  13. 10
      ui/pages/settings/networks-tab/networks-tab-content/networks-tab-content.test.js
  14. 7
      ui/pages/settings/networks-tab/networks-tab.js

@ -13,6 +13,103 @@ const state = {
protocol: 'https:', protocol: 'https:',
url: 'https://metamask.github.io/test-dapp/', url: 'https://metamask.github.io/test-dapp/',
}, },
networkList: [
{
blockExplorerUrl: "https://etherscan.io",
chainId: "0x1",
iconColor: 'var(--mainnet)',
isATestNetwork: false,
labelKey: "mainnet",
providerType: "mainnet",
rpcUrl: "https://mainnet.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://ropsten.etherscan.io",
chainId: "0x3",
iconColor: 'var(--ropsten)',
isATestNetwork: true,
labelKey: "ropsten",
providerType: "ropsten",
rpcUrl: "https://ropsten.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://rinkeby.etherscan.io",
chainId: "0x4",
iconColor: 'var(--rinkeby)',
isATestNetwork: true,
labelKey: "rinkeby",
providerType: "rinkeby",
rpcUrl: "https://rinkeby.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://goerli.etherscan.io",
chainId: "0x5",
iconColor: 'var(--goerli)',
isATestNetwork: true,
labelKey: "goerli",
providerType: "goerli",
rpcUrl: "https://goerli.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://kovan.etherscan.io",
chainId: "0x2a",
iconColor: 'var(--kovan)',
isATestNetwork: true,
labelKey: "kovan",
providerType: "kovan",
rpcUrl: "https://kovan.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "",
chainId: "0x539",
iconColor: 'var(--localhost)',
isATestNetwork: true,
label: "Localhost 8545",
providerType: "rpc",
rpcUrl: "http://localhost:8545",
ticker: "ETH",
},
{
blockExplorerUrl: "https://bscscan.com",
chainId: "0x38",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Binance Smart Chain",
providerType: "rpc",
rpcUrl: "https://bsc-dataseed.binance.org/",
ticker: "BNB",
},
{
blockExplorerUrl: "https://cchain.explorer.avax.network/",
chainId: "0xa86a",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Avalanche",
providerType: "rpc",
rpcUrl: "https://api.avax.network/ext/bc/C/rpc",
ticker: "AVAX",
},
{
blockExplorerUrl: "https://polygonscan.com",
chainId: "0x89",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Polygon",
providerType: "rpc",
rpcUrl: "https://polygon-rpc.com",
ticker: "MATIC",
},
],
metamask: { metamask: {
tokenList: { tokenList: {
'0x6b175474e89094c44da98b954eedeac495271d0f': { '0x6b175474e89094c44da98b954eedeac495271d0f': {

@ -714,6 +714,9 @@
"custom": { "custom": {
"message": "Advanced" "message": "Advanced"
}, },
"customContentSearch": {
"message": "Search for a previously added network"
},
"customGas": { "customGas": {
"message": "Customize Gas" "message": "Customize Gas"
}, },
@ -2798,7 +2801,7 @@
"message": "Settings" "message": "Settings"
}, },
"settingsSearchMatchingNotFound": { "settingsSearchMatchingNotFound": {
"message": "No matching results found" "message": "No matching results found."
}, },
"shorthandVersion": { "shorthandVersion": {
"message": "v$1", "message": "v$1",
@ -3539,6 +3542,9 @@
"testFaucet": { "testFaucet": {
"message": "Test Faucet" "message": "Test Faucet"
}, },
"testNetworks": {
"message": "Test networks"
},
"theme": { "theme": {
"message": "Theme" "message": "Theme"
}, },

@ -130,6 +130,13 @@ export const CHAIN_ID_TO_RPC_URL_MAP = {
[LOCALHOST_CHAIN_ID]: LOCALHOST_RPC_URL, [LOCALHOST_CHAIN_ID]: LOCALHOST_RPC_URL,
}; };
export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
[MAINNET_CHAIN_ID]: ETH_TOKEN_IMAGE_URL,
[AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL,
[BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL,
[POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL,
};
export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values( export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
NETWORK_TYPE_TO_ID_MAP, NETWORK_TYPE_TO_ID_MAP,
).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => { ).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => {

@ -0,0 +1,114 @@
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import Fuse from 'fuse.js';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '../../../../components/ui/text-field';
import { I18nContext } from '../../../../contexts/i18n';
import SearchIcon from '../../../../components/ui/search-icon';
export default function CustomContentSearch({
onSearch,
error,
networksList,
searchQueryInput,
}) {
const t = useContext(I18nContext);
const [searchIconColor, setSearchIconColor] = useState(
'var(--color-icon-muted)',
);
const networksListArray = Object.values(networksList);
const networksSearchFuse = new Fuse(networksListArray, {
shouldSort: true,
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['label', 'labelKey'],
});
const handleSearch = async (searchQuery) => {
if (searchQuery === '') {
setSearchIconColor('var(--color-icon-muted)');
} else {
setSearchIconColor('var(--color-icon-default)');
}
const fuseSearchResult = networksSearchFuse.search(searchQuery);
const results = searchQuery ? [...fuseSearchResult] : networksListArray;
await onSearch({ searchQuery, results });
};
const renderStartAdornment = () => {
return (
<InputAdornment position="start">
<SearchIcon color={searchIconColor} />
</InputAdornment>
);
};
const renderEndAdornment = () => {
return (
<>
{searchQueryInput && (
<InputAdornment
className="imageclosectn"
position="end"
onClick={() => handleSearch('')}
>
<i
className="fa fa-times networks-tab__imageclose"
width="17"
heigth="17"
title="Close"
/>
</InputAdornment>
)}
</>
);
};
return (
<TextField
id="search-networks"
data-testid="search-networks"
placeholder={t('customContentSearch')}
type="text"
value={searchQueryInput}
onChange={(e) => handleSearch(e.target.value)}
error={error}
fullWidth
autoFocus
autoComplete="off"
classes={{
inputRoot: 'networks-tab__networks-list__custom-search-network',
}}
startAdornment={renderStartAdornment()}
endAdornment={renderEndAdornment()}
/>
);
}
CustomContentSearch.propTypes = {
/**
* The function searches the list of networks depending on
* the entered parameter and returns the entire list of
* networks when the user clicks on 'X' on the search tab
*/
onSearch: PropTypes.func,
/**
* An error message is displayed when a user searches for a specific
* network on the search tab and that network does not exist
* in the networks list
*/
error: PropTypes.string,
/**
* The list of networks available for search.
*/
networksList: PropTypes.array,
/**
* Search for a specific network(s) by label or labelKey
*/
searchQueryInput: PropTypes.string,
};

@ -0,0 +1,23 @@
import React from 'react';
import testData from '../../../../../.storybook/test-data';
import CustomContentSearch from './custom-content-search';
export default {
title: 'Pages/Settings/NetworksTab/CustomContentSearch',
id: __filename,
argTypes: {
error: {
control: 'text',
},
searchQueryInput: {
control: 'text',
},
onSearch: {
action: 'onSearch',
},
},
};
export const CustomContentSearchComponent = (args) => {
return <CustomContentSearch {...args} networksList={testData.networkList} />;
};

@ -0,0 +1,110 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import Fuse from 'fuse.js';
import configureStore from '../../../../store/store';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import testData from '../../../../../.storybook/test-data';
import CustomContentSearch from './custom-content-search';
function renderComponent({ componentProps = {} } = {}) {
const store = configureStore({});
return renderWithProvider(<CustomContentSearch {...componentProps} />, store);
}
describe('CustomContentSearch', () => {
it('should render custom content search correctly', () => {
const onSearch = jest.fn();
const wrapper = renderComponent({
componentProps: { onSearch, networksList: testData.networkList },
});
expect(wrapper.getByTestId('search-networks')).toBeDefined();
});
it('should check placeholder text in TextField input', () => {
const onSearch = jest.fn();
const wrapper = renderComponent({
componentProps: { onSearch, networksList: testData.networkList },
});
const { getByPlaceholderText } = wrapper;
expect(
getByPlaceholderText('Search for a previously added network'),
).toBeInTheDocument();
});
it('re-render the same component with different props', () => {
const onSearch = jest.fn();
const { rerender } = render(
<CustomContentSearch
onSearch={onSearch}
networksList={[]}
searchQueryInput=""
/>,
);
const input = screen.getByTestId('search-networks');
expect(input.value).toBe('');
rerender(
<CustomContentSearch
onSearch={onSearch}
networksList={[]}
searchQueryInput="Polygon"
/>,
);
expect(input.value).toBe('Polygon');
});
it('should call onSearch prop with input value', () => {
const onSearch = jest.fn();
const wrapper = renderComponent({
componentProps: {
onSearch,
networksList: [],
searchQueryInput: 'Avalanche',
},
});
const input = wrapper.getByTestId('search-networks');
fireEvent.change(input, { target: { value: 'Polygon' } });
expect(input.value).toBe('Avalanche');
});
it('should check if error is shown if search does not return any network from the list', () => {
const onSearch = jest.fn();
const networksSearchFuse = new Fuse(testData.networkList, {
keys: ['label', 'labelKey'],
});
const fuseSearchResult = networksSearchFuse.search('Optimism');
const wrapper = renderComponent({
componentProps: {
onSearch,
networksList: testData.networkList,
searchQueryInput: 'Optimism',
error: 'No matching results found.',
},
});
const input = wrapper.getByTestId('search-networks');
expect(fuseSearchResult).toHaveLength(0);
fireEvent.change(input, {
target: { error: 'No matching results found.' },
});
expect(input.error).toBe('No matching results found.');
});
it('should check if error is not shown if search return some network from the list', () => {
const onSearch = jest.fn();
const networksSearchFuse = new Fuse(testData.networkList, {
keys: ['label', 'labelKey'],
});
const fuseSearchResult = networksSearchFuse.search('ropsten');
const wrapper = renderComponent({
componentProps: {
onSearch,
networksList: testData.networkList,
searchQueryInput: 'Avalanche',
error: '',
},
});
const input = wrapper.getByTestId('search-networks');
expect(fuseSearchResult).toHaveLength(1);
fireEvent.change(input, { target: { error: '' } });
expect(input.error).toBe('');
});
});

@ -0,0 +1 @@
export { default } from './custom-content-search';

@ -1,10 +1,46 @@
@use "design-system";
.networks-tab { .networks-tab {
&__imageclose {
cursor: pointer;
color: var(--color-icon-default);
}
&__content { &__content {
display: flex; display: flex;
height: 100%; height: 100%;
max-width: 739px; max-width: 779px;
justify-content: space-between; justify-content: space-between;
&__custom-image {
border: 1px solid var(--color-border-default);
margin-inline-start: 8px;
}
&__check-icon {
margin-inline-end: 10px;
color: var(--color-success-default);
&__transparent {
color: transparent;
width: 16px;
margin-inline-end: 10px;
}
}
&__icon-with-fallback {
padding: 0 1px 2px 2px;
color: var(--color-primary-inverse); // TODO: design-tokens needs network colors
margin-inline-start: 8px;
@each $variant, $color in design-system.$color-map {
&--color-#{$variant} {
background: var($color);
color: var(--color-primary-inverse); // TODO: design-tokens needs network colors
}
}
}
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
margin-top: 0; margin-top: 0;
flex-direction: column; flex-direction: column;
@ -52,7 +88,9 @@
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
max-height: 465px; max-height: 465px;
max-width: 400px;
margin-top: 24px; margin-top: 24px;
padding-inline-start: 24px;
.page-container__footer { .page-container__footer {
border-top: none; border-top: none;
@ -74,6 +112,7 @@
align-items: center; align-items: center;
width: 90%; width: 90%;
margin-top: 10px; margin-top: 10px;
padding: 0;
} }
} }
@ -103,8 +142,38 @@
&__networks-list { &__networks-list {
flex: 0.5 0 auto; flex: 0.5 0 auto;
max-width: 343px; max-width: 350px;
margin-top: 24px; border-right: 1px solid var(--color-border-default);
&__custom-search-network {
margin-top: 24px;
}
.MuiInput-input {
font-size: 14px;
@media screen and (max-width: $break-small) {
font-size: 12px;
}
}
.MuiTextField-root {
padding-inline-end: 16px;
#search-networks-helper-text {
color: var(--color-text-alternative);
}
@media screen and (max-width: $break-small) {
padding: 0 24px 0 24px;
}
}
&__label {
@media screen and (max-width: $break-small) {
margin-inline-start: 58px;
}
}
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
flex: 1; flex: 1;
@ -149,9 +218,10 @@
&__networks-list-item { &__networks-list-item {
display: flex; display: flex;
padding: 13px 0 13px 17px; padding: 12px 24px 12px 0;
position: relative; position: relative;
align-items: center; align-items: center;
width: 311px;
.color-indicator { .color-indicator {
&:hover { &:hover {
@ -159,13 +229,12 @@
} }
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
margin: 0 4px 0 10px; margin: 0 4px 0 20px;
} }
} }
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
padding: 20px 23px 21px 17px; padding: 12px 0 12px 24px;
border-bottom: 1px solid var(--color-border-default);
max-width: 351px; max-width: 351px;
} }
} }
@ -191,35 +260,31 @@
} }
svg { svg {
margin-left: 10px; margin-inline-start: 15px;
padding-top: 3px; padding-top: 3px;
} }
}
&__networks-list-arrow {
display: none;
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
display: block; color: var(--color-text-default);
right: 10px;
cursor: pointer;
position: absolute;
margin: 0 5px;
[dir='rtl'] & {
transform: rotate(180deg);
}
} }
} }
&__networks-list-name--selected { &__networks-list-name--selected {
font-weight: bold; font-weight: bold;
color: var(--color-text-default); color: var(--color-text-default);
@media screen and (max-width: $break-small) {
font-weight: normal;
color: var(--color-text-default);
}
} }
&__networks-list-name--disabled { &__networks-list-name--disabled {
font-weight: 300;
color: var(--color-text-muted); color: var(--color-text-muted);
@media screen and (max-width: $break-small) {
color: var(--color-text-default);
}
} }
&__network-form-footer { &__network-form-footer {

@ -4,16 +4,19 @@ import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useI18nContext } from '../../../../hooks/useI18nContext';
import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network'; import {
import { SIZES } from '../../../../helpers/constants/design-system'; NETWORK_TYPE_RPC,
import ColorIndicator from '../../../../components/ui/color-indicator'; CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP,
} from '../../../../../shared/constants/network';
import LockIcon from '../../../../components/ui/lock-icon'; import LockIcon from '../../../../components/ui/lock-icon';
import IconCaretRight from '../../../../components/ui/icon/icon-caret-right'; import IconCheck from '../../../../components/ui/icon/icon-check';
import { NETWORKS_FORM_ROUTE } from '../../../../helpers/constants/routes'; import { NETWORKS_FORM_ROUTE } from '../../../../helpers/constants/routes';
import { setSelectedSettingsRpcUrl } from '../../../../store/actions'; import { setSelectedSettingsRpcUrl } from '../../../../store/actions';
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { getProvider } from '../../../../selectors'; import { getProvider } from '../../../../selectors';
import Identicon from '../../../../components/ui/identicon';
import UrlIcon from '../../../../components/ui/url-icon';
import { handleHooksSettingsRefs } from '../../../../helpers/utils/settings-search'; import { handleHooksSettingsRefs } from '../../../../helpers/utils/settings-search';
@ -22,6 +25,8 @@ const NetworksListItem = ({
networkIsSelected, networkIsSelected,
selectedRpcUrl, selectedRpcUrl,
networkIndex, networkIndex,
setSearchQuery,
setSearchedNetworks,
}) => { }) => {
const t = useI18nContext(); const t = useI18nContext();
const history = useHistory(); const history = useHistory();
@ -45,6 +50,9 @@ const NetworksListItem = ({
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType); (listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
const displayNetworkListItemAsSelected = const displayNetworkListItemAsSelected =
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider; listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
const isCurrentRpcTarget =
listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType;
const settingsRefs = useRef(); const settingsRefs = useRef();
useEffect(() => { useEffect(() => {
@ -57,17 +65,46 @@ const NetworksListItem = ({
key={`settings-network-list-item:${rpcUrl}`} key={`settings-network-list-item:${rpcUrl}`}
className="networks-tab__networks-list-item" className="networks-tab__networks-list-item"
onClick={() => { onClick={() => {
setSearchQuery('');
setSearchedNetworks([]);
dispatch(setSelectedSettingsRpcUrl(rpcUrl)); dispatch(setSelectedSettingsRpcUrl(rpcUrl));
if (!isFullScreen) { if (!isFullScreen) {
history.push(NETWORKS_FORM_ROUTE); history.push(NETWORKS_FORM_ROUTE);
} }
}} }}
> >
<ColorIndicator {isCurrentRpcTarget ? (
color={labelKey} <IconCheck color="var(--color-success-default)" />
type={ColorIndicator.TYPES.FILLED} ) : (
size={SIZES.LG} <div className="networks-tab__content__check-icon__transparent"></div>
/> )}
{network.chainId in CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP ? (
<Identicon
className="networks-tab__content__custom-image"
diameter={24}
image={CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[network.chainId]}
imageBorder
/>
) : (
!network.isATestNetwork && (
<UrlIcon
className="networks-tab__content__icon-with-fallback"
fallbackClassName="networks-tab__content__icon-with-fallback"
name={label}
/>
)
)}
{network.isATestNetwork && (
<UrlIcon
name={label || labelKey}
fallbackClassName={classnames(
'networks-tab__content__icon-with-fallback',
{
[`networks-tab__content__icon-with-fallback--color-${labelKey}`]: true,
},
)}
/>
)}
<div <div
className={classnames('networks-tab__networks-list-name', { className={classnames('networks-tab__networks-list-name', {
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected, 'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
@ -81,7 +118,6 @@ const NetworksListItem = ({
<LockIcon width="14px" height="17px" fill="var(--color-icon-muted)" /> <LockIcon width="14px" height="17px" fill="var(--color-icon-muted)" />
)} )}
</div> </div>
<IconCaretRight className="networks-tab__networks-list-arrow" />
</div> </div>
); );
}; };
@ -91,6 +127,8 @@ NetworksListItem.propTypes = {
networkIsSelected: PropTypes.bool, networkIsSelected: PropTypes.bool,
selectedRpcUrl: PropTypes.string, selectedRpcUrl: PropTypes.string,
networkIndex: PropTypes.number, networkIndex: PropTypes.number,
setSearchQuery: PropTypes.func,
setSearchedNetworks: PropTypes.func,
}; };
export default NetworksListItem; export default NetworksListItem;

@ -0,0 +1,28 @@
import React from 'react';
import testData from '../../../../../.storybook/test-data';
import NetworksList from './networks-list';
export default {
title: 'Pages/Settings/NetworksTab/NetworksList',
id: __filename,
argTypes: {
networkDefaultedToProvider: {
control: 'boolean',
},
networkIsSelected: {
control: 'boolean',
},
networksToRender: {
control: 'array',
},
},
args: {
networkDefaultedToProvider: false,
networkIsSelected: false,
networksToRender: testData.networkList,
},
};
export const NetworksListComponent = (args) => {
return <NetworksList {...args} />;
};

@ -1,6 +1,13 @@
import React from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import CustomContentSearch from '../custom-content-search';
import Typography from '../../../../components/ui/typography';
import {
COLORS,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
import NetworksListItem from '../networks-list-item'; import NetworksListItem from '../networks-list-item';
const NetworksList = ({ const NetworksList = ({
@ -9,6 +16,20 @@ const NetworksList = ({
networkDefaultedToProvider, networkDefaultedToProvider,
selectedRpcUrl, selectedRpcUrl,
}) => { }) => {
const t = useI18nContext();
const [searchedNetworks, setSearchedNetworks] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const searchedNetworksToRender =
searchedNetworks.length === 0 && searchQuery === ''
? networksToRender
: searchedNetworks;
const searchedNetworksToRenderThatAreNotTestNetworks = searchedNetworksToRender.filter(
(network) => !network.isATestNetwork,
);
const searchedNetworksToRenderThatAreTestNetworks = searchedNetworksToRender.filter(
(network) => network.isATestNetwork,
);
return ( return (
<div <div
className={classnames('networks-tab__networks-list', { className={classnames('networks-tab__networks-list', {
@ -16,13 +37,52 @@ const NetworksList = ({
networkIsSelected && !networkDefaultedToProvider, networkIsSelected && !networkDefaultedToProvider,
})} })}
> >
{networksToRender.map((network, index) => ( <CustomContentSearch
onSearch={({
searchQuery: newSearchQuery = '',
results: newResults = [],
}) => {
setSearchedNetworks(newResults);
setSearchQuery(newSearchQuery);
}}
error={
searchedNetworksToRender.length === 0
? t('settingsSearchMatchingNotFound')
: null
}
networksList={networksToRender}
searchQueryInput={searchQuery}
/>
{searchedNetworksToRenderThatAreNotTestNetworks.map((network, index) => (
<NetworksListItem
key={`settings-network-list:${network.rpcUrl}`}
network={network}
networkIsSelected={networkIsSelected}
selectedRpcUrl={selectedRpcUrl}
networkIndex={index}
setSearchQuery={setSearchQuery}
setSearchedNetworks={setSearchedNetworks}
/>
))}
{searchQuery === '' && (
<Typography
variant={TYPOGRAPHY.H6}
margin={[6, 0, 0, 9]}
color={COLORS.TEXT_MUTED}
className="networks-tab__networks-list__label"
>
{t('testNetworks')}
</Typography>
)}
{searchedNetworksToRenderThatAreTestNetworks.map((network, index) => (
<NetworksListItem <NetworksListItem
key={`settings-network-list:${network.rpcUrl}`} key={`settings-network-list:${network.rpcUrl}`}
network={network} network={network}
networkIsSelected={networkIsSelected} networkIsSelected={networkIsSelected}
selectedRpcUrl={selectedRpcUrl} selectedRpcUrl={selectedRpcUrl}
networkIndex={index} networkIndex={index}
setSearchQuery={setSearchQuery}
setSearchedNetworks={setSearchedNetworks}
/> />
))} ))}
</div> </div>

@ -25,6 +25,7 @@ const renderComponent = (props) => {
const defaultNetworks = defaultNetworksData.map((network) => ({ const defaultNetworks = defaultNetworksData.map((network) => ({
...network, ...network,
viewOnly: true, viewOnly: true,
isATestNetwork: true,
})); }));
const props = { const props = {
@ -32,6 +33,7 @@ const props = {
networkIsSelected: false, networkIsSelected: false,
networksToRender: defaultNetworks, networksToRender: defaultNetworks,
selectedRpcUrl: 'http://localhost:8545', selectedRpcUrl: 'http://localhost:8545',
isATestNetwork: true,
}; };
describe('NetworksList Component', () => { describe('NetworksList Component', () => {

@ -26,6 +26,7 @@ const renderComponent = (props) => {
const defaultNetworks = defaultNetworksData.map((network) => ({ const defaultNetworks = defaultNetworksData.map((network) => ({
...network, ...network,
viewOnly: true, viewOnly: true,
isATestNetwork: true,
})); }));
const props = { const props = {
@ -40,13 +41,16 @@ const props = {
blockExplorerUrl: '', blockExplorerUrl: '',
viewOnly: false, viewOnly: false,
rpcPrefs: {}, rpcPrefs: {},
isATestNetwork: true,
}, },
shouldRenderNetworkForm: true, shouldRenderNetworkForm: true,
}; };
describe('NetworksTabContent Component', () => { describe('NetworksTabContent Component', () => {
it('should render networks tab content correctly', async () => { it('should render networks tab content correctly', async () => {
const { queryByText, getByDisplayValue } = renderComponent(props); const { queryByText, getByDisplayValue, getAllByText } = renderComponent(
props,
);
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument(); expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
expect(queryByText('Ropsten Test Network')).toBeInTheDocument(); expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
@ -68,9 +72,7 @@ describe('NetworksTabContent Component', () => {
getByDisplayValue(props.selectedNetwork.chainId), getByDisplayValue(props.selectedNetwork.chainId),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(getByDisplayValue(props.selectedNetwork.ticker)).toBeInTheDocument(); expect(getByDisplayValue(props.selectedNetwork.ticker)).toBeInTheDocument();
expect( expect(getAllByText(props.selectedNetwork.blockExplorerUrl)).toBeDefined();
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
).toBeInTheDocument();
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), { fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
target: { value: 'LocalHost 8545' }, target: { value: 'LocalHost 8545' },

@ -16,7 +16,10 @@ import {
getNetworksTabSelectedRpcUrl, getNetworksTabSelectedRpcUrl,
getProvider, getProvider,
} from '../../../selectors'; } from '../../../selectors';
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network'; import {
NETWORK_TYPE_RPC,
TEST_CHAINS,
} from '../../../../shared/constants/network';
import { defaultNetworksData } from './networks-tab.constants'; import { defaultNetworksData } from './networks-tab.constants';
import NetworksTabContent from './networks-tab-content'; import NetworksTabContent from './networks-tab-content';
import NetworksForm from './networks-form'; import NetworksForm from './networks-form';
@ -25,6 +28,7 @@ import NetworksFormSubheader from './networks-tab-subheader';
const defaultNetworks = defaultNetworksData.map((network) => ({ const defaultNetworks = defaultNetworksData.map((network) => ({
...network, ...network,
viewOnly: true, viewOnly: true,
isATestNetwork: TEST_CHAINS.includes(network.chainId),
})); }));
const NetworksTab = ({ addNewNetwork }) => { const NetworksTab = ({ addNewNetwork }) => {
@ -50,6 +54,7 @@ const NetworksTab = ({ addNewNetwork }) => {
chainId: rpc.chainId, chainId: rpc.chainId,
ticker: rpc.ticker, ticker: rpc.ticker,
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '', blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
isATestNetwork: TEST_CHAINS.includes(rpc.chainId),
}; };
}); });

Loading…
Cancel
Save