Grouping in network selector (#3454)
This commit is contained in:
parent
e1c05917bd
commit
3760f41fc7
@ -10,6 +10,7 @@ import { CUSTOM_ENDPOINT_KEY } from './constants';
|
||||
export interface LinkOption extends Option {
|
||||
dnslink?: string;
|
||||
isChild?: boolean;
|
||||
isDevelopment?: boolean;
|
||||
}
|
||||
|
||||
interface EnvWindow {
|
||||
@ -60,7 +61,6 @@ function createLive (t: TFunction): LinkOption[] {
|
||||
value: 'wss://rpc.polkadot.io'
|
||||
},
|
||||
{
|
||||
dnslink: 'polkadot',
|
||||
info: 'polkadot',
|
||||
text: t<string>('rpc.polkadot.w3f', 'Polkadot (Live, hosted by Web3 Foundation)', { ns: 'apps-config' }),
|
||||
value: 'wss://cc1-1.polkadot.network'
|
||||
@ -72,13 +72,11 @@ function createLive (t: TFunction): LinkOption[] {
|
||||
value: 'wss://kusama-rpc.polkadot.io'
|
||||
},
|
||||
{
|
||||
dnslink: 'kusama',
|
||||
info: 'kusama',
|
||||
text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
|
||||
value: 'wss://cc3-5.kusama.network'
|
||||
},
|
||||
{
|
||||
dnslink: 'kusama',
|
||||
info: 'kusama',
|
||||
isDisabled: true,
|
||||
text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
|
||||
@ -92,7 +90,6 @@ function createLive (t: TFunction): LinkOption[] {
|
||||
value: 'wss://fullnode.centrifuge.io'
|
||||
},
|
||||
{
|
||||
dnslink: 'crab',
|
||||
info: 'crab',
|
||||
text: t<string>('rpc.crab', 'Crab (Darwinia Canary, hosted by Darwinia Network)', { ns: 'apps-config' }),
|
||||
value: 'wss://crab.darwinia.network'
|
||||
@ -233,6 +230,7 @@ export default function create (t: TFunction): LinkOption[] {
|
||||
},
|
||||
...createTest(t),
|
||||
{
|
||||
isDevelopment: true,
|
||||
isHeader: true,
|
||||
text: t<string>('rpc.header.dev', 'Development', { ns: 'apps-config' }),
|
||||
value: ''
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
// Copyright 2017-2020 @polkadot/apps authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { Endpoint } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Network from './Network';
|
||||
|
||||
interface Props {
|
||||
apiUrl: string;
|
||||
className?: string;
|
||||
setApiUrl: (apiUrl: string) => void;
|
||||
value: Endpoint;
|
||||
}
|
||||
|
||||
function EndpointDisplay ({ apiUrl, className, setApiUrl, value: { header, networks } }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='endpointHeader'>{header}</div>
|
||||
{networks.map((network, index): React.ReactNode => (
|
||||
<Network
|
||||
apiUrl={apiUrl}
|
||||
key={index}
|
||||
setApiUrl={setApiUrl}
|
||||
value={network}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(EndpointDisplay)`
|
||||
margin-top: 2rem;
|
||||
|
||||
&+& {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.endpointHeader {
|
||||
font-size: 0.78571429em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0.5rem 0 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`);
|
||||
79
packages/apps/src/Endpoints/Group.tsx
Normal file
79
packages/apps/src/Endpoints/Group.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2017-2020 @polkadot/apps authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { Group } from './types';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Icon } from '@polkadot/react-components';
|
||||
|
||||
import Network from './Network';
|
||||
|
||||
interface Props {
|
||||
apiUrl: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
setApiUrl: (apiUrl: string) => void;
|
||||
setGroup: (groupIndex: number) => void;
|
||||
value: Group;
|
||||
}
|
||||
|
||||
function GroupDisplay ({ apiUrl, children, className = '', index, isSelected, setApiUrl, setGroup, value: { header, networks } }: Props): React.ReactElement<Props> {
|
||||
const _setGroup = useCallback(
|
||||
() => setGroup(isSelected ? -1 : index),
|
||||
[index, isSelected, setGroup]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${className}${isSelected ? ' isSelected' : ''}`}>
|
||||
<div
|
||||
className='groupHeader'
|
||||
onClick={_setGroup}
|
||||
>
|
||||
<Icon icon={isSelected ? 'caret-up' : 'caret-down'} />
|
||||
{header}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<>
|
||||
<div className='groupNetworks'>
|
||||
{networks.map((network, index): React.ReactNode => (
|
||||
<Network
|
||||
apiUrl={apiUrl}
|
||||
key={index}
|
||||
setApiUrl={setApiUrl}
|
||||
value={network}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(GroupDisplay)`
|
||||
.groupHeader {
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.ui--Icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.groupNetworks {
|
||||
padding: 0.25rem 0 0.5rem 1rem;
|
||||
}
|
||||
`);
|
||||
@ -6,7 +6,7 @@ import { Network } from './types';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ChainImg, Icon } from '@polkadot/react-components';
|
||||
import { ChainImg } from '@polkadot/react-components';
|
||||
|
||||
import Url from './Url';
|
||||
|
||||
@ -29,16 +29,11 @@ function NetworkDisplay ({ apiUrl, className = '', setApiUrl, value: { icon, isC
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${className}${isSelected ? ' isSelected ui--highlight--before' : ''}`}>
|
||||
<div className={`${className}${isSelected ? ' isSelected ui--highlight--border' : ''}`}>
|
||||
<div
|
||||
className={`endpointSection${isChild ? ' isChild' : ''}`}
|
||||
onClick={_selectFirst}
|
||||
>
|
||||
<Icon
|
||||
className={`endpointSelected${isSelected ? ' ui--highlight--color' : ''}`}
|
||||
color='transparent'
|
||||
icon='check'
|
||||
/>
|
||||
<ChainImg
|
||||
className='endpointIcon'
|
||||
logo={icon === 'local' ? 'empty' : icon}
|
||||
@ -59,10 +54,11 @@ function NetworkDisplay ({ apiUrl, className = '', setApiUrl, value: { icon, isC
|
||||
}
|
||||
|
||||
export default React.memo(styled(NetworkDisplay)`
|
||||
border-left: 0.25rem solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
margin: 0 0 0.25rem 0;
|
||||
padding: 0.375rem;
|
||||
padding: 0.375rem 0.5rem 0.375rem 1rem;
|
||||
position: relative;
|
||||
|
||||
&.isSelected,
|
||||
@ -90,8 +86,4 @@ export default React.memo(styled(NetworkDisplay)`
|
||||
margin-top: -0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.endpointSelected {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import type { LinkOption } from '@polkadot/apps-config/settings/endpoints';
|
||||
import { Endpoint } from './types';
|
||||
import { Group } from './types';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
// ok, this seems to be an eslint bug, this _is_ a package import
|
||||
@ -16,7 +16,7 @@ import uiSettings from '@polkadot/ui-settings';
|
||||
import { isAscii } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import EndpointDisplay from './Endpoint';
|
||||
import GroupDisplay from './Group';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -26,6 +26,7 @@ interface Props {
|
||||
|
||||
interface UrlState {
|
||||
apiUrl: string;
|
||||
groupIndex: number;
|
||||
hasUrlChanged: boolean;
|
||||
isUrlValid: boolean;
|
||||
}
|
||||
@ -46,10 +47,10 @@ function isValidUrl (url: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function combineEndpoints (endpoints: LinkOption[]): Endpoint[] {
|
||||
return endpoints.reduce((result: Endpoint[], e): Endpoint[] => {
|
||||
function combineEndpoints (endpoints: LinkOption[]): Group[] {
|
||||
return endpoints.reduce((result: Group[], e): Group[] => {
|
||||
if (e.isHeader) {
|
||||
result.push({ header: e.text, networks: [] });
|
||||
result.push({ header: e.text, isDevelopment: e.isDevelopment, networks: [] });
|
||||
} else {
|
||||
const [name, , providerName] = textToParts(e.text as string);
|
||||
const prev = result[result.length - 1];
|
||||
@ -86,11 +87,30 @@ function getCustomEndpoints (): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function extractUrlState (apiUrl: string, groups: Group[]): UrlState {
|
||||
let groupIndex = groups.findIndex(({ networks }) =>
|
||||
networks.some(({ providers }) =>
|
||||
providers.some(({ url }) => url === apiUrl)
|
||||
)
|
||||
);
|
||||
|
||||
if (groupIndex === -1) {
|
||||
groupIndex = groups.findIndex(({ isDevelopment }) => isDevelopment);
|
||||
}
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
groupIndex,
|
||||
hasUrlChanged: uiSettings.get().apiUrl !== apiUrl,
|
||||
isUrlValid: isValidUrl(apiUrl)
|
||||
};
|
||||
}
|
||||
|
||||
function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const linkOptions = createEndpoints(t);
|
||||
const [endpoints, setEndpoints] = useState(combineEndpoints(linkOptions));
|
||||
const [{ apiUrl, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>({ apiUrl: uiSettings.get().apiUrl, hasUrlChanged: false, isUrlValid: true });
|
||||
const [groups, setGroups] = useState(combineEndpoints(linkOptions));
|
||||
const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>(extractUrlState(uiSettings.get().apiUrl, groups));
|
||||
const [storedCustomEndpoints, setStoredCustomEndpoints] = useState<string[]>(getCustomEndpoints());
|
||||
const isKnownUrl = useMemo(() => {
|
||||
let result = false;
|
||||
@ -124,6 +144,11 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
return result;
|
||||
}, [apiUrl, storedCustomEndpoints]);
|
||||
|
||||
const _changeGroup = useCallback(
|
||||
(groupIndex: number) => setApiUrl((state) => ({ ...state, groupIndex })),
|
||||
[]
|
||||
);
|
||||
|
||||
const _saveApiEndpoint = () => {
|
||||
try {
|
||||
localStorage.setItem(CUSTOM_ENDPOINT_KEY, JSON.stringify([...storedCustomEndpoints, apiUrl]));
|
||||
@ -141,7 +166,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
|
||||
try {
|
||||
localStorage.setItem(CUSTOM_ENDPOINT_KEY, JSON.stringify(newStoredCurstomEndpoints));
|
||||
setEndpoints(combineEndpoints(createEndpoints(t)));
|
||||
setGroups(combineEndpoints(createEndpoints(t)));
|
||||
setStoredCustomEndpoints(getCustomEndpoints());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -150,8 +175,8 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
};
|
||||
|
||||
const _setApiUrl = useCallback(
|
||||
(apiUrl: string) => setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: true }),
|
||||
[]
|
||||
(apiUrl: string) => setApiUrl(extractUrlState(apiUrl, groups)),
|
||||
[groups]
|
||||
);
|
||||
|
||||
const _onChangeCustom = useCallback(
|
||||
@ -160,9 +185,9 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
apiUrl = punycode.toASCII(apiUrl);
|
||||
}
|
||||
|
||||
setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: isValidUrl(apiUrl) });
|
||||
setApiUrl(extractUrlState(apiUrl, groups));
|
||||
},
|
||||
[]
|
||||
[groups]
|
||||
);
|
||||
|
||||
const _onApply = useCallback(
|
||||
@ -190,42 +215,50 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
||||
onClose={onClose}
|
||||
position='left'
|
||||
>
|
||||
{endpoints.map((endpoint, index): React.ReactNode => (
|
||||
<EndpointDisplay
|
||||
{groups.map((group, index): React.ReactNode => (
|
||||
<GroupDisplay
|
||||
apiUrl={apiUrl}
|
||||
index={index}
|
||||
isSelected={groupIndex === index}
|
||||
key={index}
|
||||
setApiUrl={_setApiUrl}
|
||||
value={endpoint}
|
||||
/>
|
||||
setGroup={_changeGroup}
|
||||
value={group}
|
||||
>
|
||||
{group.isDevelopment && (
|
||||
<div className='endpointCustomWrapper'>
|
||||
<Input
|
||||
className='endpointCustom'
|
||||
isError={!isUrlValid}
|
||||
isFull
|
||||
label={t<string>('custom endpoint')}
|
||||
onChange={_onChangeCustom}
|
||||
value={apiUrl}
|
||||
/>
|
||||
{isSavedCustomEndpoint
|
||||
? <Button
|
||||
className='customButton'
|
||||
icon='trash-alt'
|
||||
onClick={_removeApiEndpoint}
|
||||
/>
|
||||
: <Button
|
||||
className='customButton'
|
||||
icon='save'
|
||||
isDisabled={!isUrlValid || isKnownUrl}
|
||||
onClick={_saveApiEndpoint}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</GroupDisplay>
|
||||
))}
|
||||
<div className='endpointCustomWrapper'>
|
||||
<Input
|
||||
className='endpointCustom'
|
||||
isError={!isUrlValid}
|
||||
isFull
|
||||
label={t<string>('custom endpoint')}
|
||||
onChange={_onChangeCustom}
|
||||
value={apiUrl}
|
||||
/>
|
||||
{isSavedCustomEndpoint
|
||||
? <Button
|
||||
className='customButton'
|
||||
icon='trash-alt'
|
||||
onClick={_removeApiEndpoint}
|
||||
/>
|
||||
: <Button
|
||||
className='customButton'
|
||||
icon='save'
|
||||
isDisabled={!isUrlValid || isKnownUrl}
|
||||
onClick={_saveApiEndpoint}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Endpoints)`
|
||||
padding-top: 3.5rem;
|
||||
|
||||
.customButton {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
@ -233,8 +266,6 @@ export default React.memo(styled(Endpoints)`
|
||||
}
|
||||
|
||||
.endpointCustom {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
input {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export interface Endpoint {
|
||||
export interface Group {
|
||||
header: React.ReactNode;
|
||||
isDevelopment?: boolean;
|
||||
networks: Network[];
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user