Grouping in network selector (#3454)

This commit is contained in:
Jaco Greeff 2020-08-20 10:25:18 +02:00 committed by GitHub
parent e1c05917bd
commit 3760f41fc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 159 additions and 107 deletions

View File

@ -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: ''

View File

@ -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;
}
`);

View 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;
}
`);

View File

@ -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;
}
`);

View File

@ -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;
}

View File

@ -4,8 +4,9 @@
import React from 'react';
export interface Endpoint {
export interface Group {
header: React.ReactNode;
isDevelopment?: boolean;
networks: Network[];
}