From 3760f41fc712d06f7fa5de8a792b76904cb051c0 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 20 Aug 2020 10:25:18 +0200 Subject: [PATCH] Grouping in network selector (#3454) --- .../apps-config/src/settings/endpoints.ts | 6 +- packages/apps/src/Endpoints/Endpoint.tsx | 49 -------- packages/apps/src/Endpoints/Group.tsx | 79 ++++++++++++ packages/apps/src/Endpoints/Network.tsx | 16 +-- packages/apps/src/Endpoints/index.tsx | 113 +++++++++++------- packages/apps/src/Endpoints/types.ts | 3 +- 6 files changed, 159 insertions(+), 107 deletions(-) delete mode 100644 packages/apps/src/Endpoints/Endpoint.tsx create mode 100644 packages/apps/src/Endpoints/Group.tsx diff --git a/packages/apps-config/src/settings/endpoints.ts b/packages/apps-config/src/settings/endpoints.ts index 633c17e7b4..917071abdf 100644 --- a/packages/apps-config/src/settings/endpoints.ts +++ b/packages/apps-config/src/settings/endpoints.ts @@ -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('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('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('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('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('rpc.header.dev', 'Development', { ns: 'apps-config' }), value: '' diff --git a/packages/apps/src/Endpoints/Endpoint.tsx b/packages/apps/src/Endpoints/Endpoint.tsx deleted file mode 100644 index 4dac09f516..0000000000 --- a/packages/apps/src/Endpoints/Endpoint.tsx +++ /dev/null @@ -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 { - return ( -
-
{header}
- {networks.map((network, index): React.ReactNode => ( - - ))} -
- ); -} - -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; - } -`); diff --git a/packages/apps/src/Endpoints/Group.tsx b/packages/apps/src/Endpoints/Group.tsx new file mode 100644 index 0000000000..d6f0a83f59 --- /dev/null +++ b/packages/apps/src/Endpoints/Group.tsx @@ -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 { + const _setGroup = useCallback( + () => setGroup(isSelected ? -1 : index), + [index, isSelected, setGroup] + ); + + return ( +
+
+ + {header} +
+ {isSelected && ( + <> +
+ {networks.map((network, index): React.ReactNode => ( + + ))} +
+ {children} + + )} +
+ ); +} + +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; + } +`); diff --git a/packages/apps/src/Endpoints/Network.tsx b/packages/apps/src/Endpoints/Network.tsx index 816eb1fd1c..fc156a32e9 100644 --- a/packages/apps/src/Endpoints/Network.tsx +++ b/packages/apps/src/Endpoints/Network.tsx @@ -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 ( -
+
- { +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 { const { t } = useTranslation(); const linkOptions = createEndpoints(t); - const [endpoints, setEndpoints] = useState(combineEndpoints(linkOptions)); - const [{ apiUrl, hasUrlChanged, isUrlValid }, setApiUrl] = useState({ apiUrl: uiSettings.get().apiUrl, hasUrlChanged: false, isUrlValid: true }); + const [groups, setGroups] = useState(combineEndpoints(linkOptions)); + const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState(extractUrlState(uiSettings.get().apiUrl, groups)); const [storedCustomEndpoints, setStoredCustomEndpoints] = useState(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 => ( - ( + + setGroup={_changeGroup} + value={group} + > + {group.isDevelopment && ( +
+ ('custom endpoint')} + onChange={_onChangeCustom} + value={apiUrl} + /> + {isSavedCustomEndpoint + ?
+ )} +
))} -
- ('custom endpoint')} - onChange={_onChangeCustom} - value={apiUrl} - /> - {isSavedCustomEndpoint - ?
); } 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; } diff --git a/packages/apps/src/Endpoints/types.ts b/packages/apps/src/Endpoints/types.ts index 633e39690b..a733131d66 100644 --- a/packages/apps/src/Endpoints/types.ts +++ b/packages/apps/src/Endpoints/types.ts @@ -4,8 +4,9 @@ import React from 'react'; -export interface Endpoint { +export interface Group { header: React.ReactNode; + isDevelopment?: boolean; networks: Network[]; }