From f7ecc456064a4c4a444c5e07b43f49cadedde9f4 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 29 Jul 2020 13:59:30 +0200 Subject: [PATCH] Endpoint sidebar (#3282) * Endpoint sidebar * Collapsible * Selection of new url * Sidebar icon override * url validity * Remove network selection from settings * Updates with custom --- .../apps-config/src/settings/endpoints.ts | 13 +- packages/apps/src/Apps.tsx | 6 +- packages/apps/src/SideBar/ChainInfo.tsx | 6 +- packages/apps/src/SideBar/Endpoints.tsx | 242 ++++++++++++++++++ packages/apps/src/SideBar/Item.tsx | 2 +- packages/apps/src/SideBar/NodeInfo.tsx | 5 +- packages/apps/src/SideBar/index.tsx | 32 ++- packages/apps/src/index.tsx | 8 - packages/apps/src/modals/Network.tsx | 35 --- packages/page-settings/src/General.tsx | 118 ++++----- packages/page-settings/src/SelectUrl.tsx | 134 ---------- packages/page-settings/src/util.tsx | 2 +- packages/react-components/src/Sidebar.tsx | 12 +- 13 files changed, 336 insertions(+), 279 deletions(-) create mode 100644 packages/apps/src/SideBar/Endpoints.tsx delete mode 100644 packages/apps/src/modals/Network.tsx delete mode 100644 packages/page-settings/src/SelectUrl.tsx diff --git a/packages/apps-config/src/settings/endpoints.ts b/packages/apps-config/src/settings/endpoints.ts index 0b1b5dd42c..26210751ca 100644 --- a/packages/apps-config/src/settings/endpoints.ts +++ b/packages/apps-config/src/settings/endpoints.ts @@ -22,7 +22,7 @@ function createDev (t: TFunction): LinkOption[] { dnslink: 'local', info: 'local', text: t('rpc.local', 'Local Node (Own, 127.0.0.1:9944)', { ns: 'apps-config' }), - value: 'ws://127.0.0.1:9944/' + value: 'ws://127.0.0.1:9944' } ]; } @@ -45,19 +45,20 @@ function createLive (t: TFunction): LinkOption[] { dnslink: 'kusama', info: 'kusama', text: t('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }), - value: 'wss://kusama-rpc.polkadot.io/' + 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/' + 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' }), - value: 'wss://kusama.polkadot.cloud.ava.do/' + value: 'wss://kusama.polkadot.cloud.ava.do' }, { dnslink: 'centrifuge', @@ -101,12 +102,12 @@ function createTest (t: TFunction): LinkOption[] { { info: 'substrate', text: t('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }), - value: 'wss://substrate-rpc.parity.io/' + value: 'wss://substrate-rpc.parity.io' }, { info: 'nodle', text: t('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }), - value: 'wss://arcadia1.nodleprotocol.io/' + value: 'wss://arcadia1.nodleprotocol.io' }, { info: 'datahighway', diff --git a/packages/apps/src/Apps.tsx b/packages/apps/src/Apps.tsx index f0995e28e5..9ad6d32c30 100644 --- a/packages/apps/src/Apps.tsx +++ b/packages/apps/src/Apps.tsx @@ -45,16 +45,16 @@ function Apps ({ className = '' }: Props): React.ReactElement { isMenu: window.innerWidth < SIDEBAR_MENU_THRESHOLD }); const uiHighlight = useMemo( - (): string | undefined => getSystemChainColor(systemChain, systemName), + () => getSystemChainColor(systemChain, systemName), [systemChain, systemName] ); const _collapse = useCallback( - (): void => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: !sidebar.isCollapsed })), + () => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: !sidebar.isCollapsed })), [] ); const _toggleMenu = useCallback( - (): void => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: false, isMenuOpen: true })), + () => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: false, isMenuOpen: true })), [] ); const _handleResize = useCallback( diff --git a/packages/apps/src/SideBar/ChainInfo.tsx b/packages/apps/src/SideBar/ChainInfo.tsx index 0e417ee81e..d3d67e75db 100644 --- a/packages/apps/src/SideBar/ChainInfo.tsx +++ b/packages/apps/src/SideBar/ChainInfo.tsx @@ -14,10 +14,11 @@ import { useTranslation } from '../translate'; interface Props { className?: string; + isToggled?: boolean; onClick?: () => void; } -function ChainInfo ({ className = '', onClick }: Props): React.ReactElement { +function ChainInfo ({ className = '', isToggled, onClick }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const runtimeVersion = useCall(api.rpc.state.subscribeRuntimeVersion, []); @@ -38,7 +39,7 @@ function ChainInfo ({ className = '', onClick }: Props): React.ReactElement @@ -75,6 +76,7 @@ export default React.memo(styled(ChainInfo)` .ui--Icon.dropdown { flex: 0; margin: 0; + width: 1rem; } > div.info { diff --git a/packages/apps/src/SideBar/Endpoints.tsx b/packages/apps/src/SideBar/Endpoints.tsx new file mode 100644 index 0000000000..edaf710788 --- /dev/null +++ b/packages/apps/src/SideBar/Endpoints.tsx @@ -0,0 +1,242 @@ +// 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 { Option } from '@polkadot/apps-config/settings/types'; + +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; +import { createEndpoints } from '@polkadot/apps-config/settings'; +import { ChainImg, Icon, Input, Sidebar, Toggle } from '@polkadot/react-components'; +import uiSettings from '@polkadot/ui-settings'; + +import { useTranslation } from '../translate'; + +interface Endpoint { + header: React.ReactNode; + networks: { + icon?: string; + name: string; + providers: { + name: string; + url: string; + }[] + }[]; +} + +interface Props { + className?: string; + offset?: number | string; + onClose: () => void; +} + +interface UrlState { + apiUrl: string; + hasUrlChanged: boolean; + isUrlValid: boolean; +} + +function textToParts (text: string): [string, string, string] { + const [first, remainder] = text.replace(')', '').split(' ('); + const [middle, last] = remainder.split(', '); + + return [first, middle, last]; +} + +function isValidUrl (url: string): boolean { + return ( + // some random length... we probably want to parse via some lib + (url.length >= 7) && + // check that it starts with a valid ws identifier + (url.startsWith('ws://') || url.startsWith('wss://')) + ); +} + +function combineEndpoints (endpoints: Option[]): Endpoint[] { + return endpoints.reduce((result: Endpoint[], e): Endpoint[] => { + if (e.isHeader) { + result.push({ header: e.text, networks: [] }); + } else { + const [name, , providerName] = textToParts(e.text as string); + const prev = result[result.length - 1]; + const prov = { name: providerName, url: e.value as string }; + + if (prev.networks[prev.networks.length - 1] && name === prev.networks[prev.networks.length - 1].name) { + prev.networks[prev.networks.length - 1].providers.push(prov); + } else { + prev.networks.push({ + icon: e.info, + name, + providers: [prov] + }); + } + } + + return result; + }, []); +} + +function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement { + const { t } = useTranslation(); + const [endpoints] = useState(combineEndpoints(createEndpoints(t))); + const [{ apiUrl, hasUrlChanged, isUrlValid }, setApiUrl] = useState({ apiUrl: uiSettings.get().apiUrl, hasUrlChanged: false, isUrlValid: true }); + const [openIndex, setOpenIndex] = useState(''); + + const _setApiUrl = useCallback( + (apiUrl: string) => () => setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: true }), + [] + ); + + const _setOpenIndex = useCallback( + (index: string) => () => setOpenIndex((openIndex) => openIndex === index ? '' : index), + [] + ); + + const _onChangeCustom = useCallback( + (apiUrl: string) => setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: isValidUrl(apiUrl) }), + [] + ); + + const _onClose = useCallback( + (): void => { + if (hasUrlChanged && isUrlValid) { + const settings = uiSettings.get(); + + uiSettings.set({ ...settings, apiUrl }); + window.location.reload(); + } + + onClose(); + }, + [apiUrl, hasUrlChanged, isUrlValid, onClose] + ); + + return ( + + {endpoints.map(({ header, networks }, typeIndex): React.ReactNode => ( +
+
{header}
+ {networks.map(({ icon, name, providers }, netIndex): React.ReactNode => { + const isSelected = providers.some(({ url }) => url === apiUrl); + const index = `${typeIndex}:${netIndex}`; + const isOpen = openIndex === index; + + return ( +
+
+ +
{name}
+ {!isSelected && ( + + )} +
+ {providers.map(({ name, url }): React.ReactNode => ( + + ))} +
+ ); + })} +
+ ))} + ('Custom url')} + onChange={_onChangeCustom} + value={apiUrl} + /> +
+ ); +} + +export default React.memo(styled(Endpoints)` + .endpointCustom { + margin-top: 0.5rem; + } + + .endpointType { + &+.endpointType { + margin-top: 1rem; + } + } + + .endpointGroup { + border-radius: 0.25rem; + cursor: pointer; + padding: 0.375rem; + + &.isOpen, + &.isSelected { + .endpointProvider { + display: block; + } + } + + &:hover { + background: #fffefd; + } + } + + .endpointHeader { + font-size: 0.78571429em; + font-weight: 700; + line-height: 1; + padding: 0.5rem 0 1rem; + text-transform: uppercase; + } + + .endpointIcon { + height: 32px; + margin-right: 0.75rem; + width: 32px; + } + + .endpointProvider { + display: none; + padding: 0.25rem; + text-align: right; + } + + .endpointSection { + align-items: center; + display: flex; + justify-content: flex-start; + position: relative; + + .endpointOpen { + position: absolute; + right: 0.5rem; + top: 0.5rem; + } + + &+.endpointProvider { + margin-top: -0.5rem; + } + } +`); diff --git a/packages/apps/src/SideBar/Item.tsx b/packages/apps/src/SideBar/Item.tsx index abcd801144..f50f74281d 100644 --- a/packages/apps/src/SideBar/Item.tsx +++ b/packages/apps/src/SideBar/Item.tsx @@ -65,7 +65,7 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement { - setHasSudo(!!sudoKey && allAccounts.some((address): boolean => sudoKey.eq(address))); + setHasSudo(!!sudoKey && allAccounts.some((address) => sudoKey.eq(address))); }, [allAccounts, sudoKey]); useEffect((): void => { diff --git a/packages/apps/src/SideBar/NodeInfo.tsx b/packages/apps/src/SideBar/NodeInfo.tsx index 787c3a2900..297093b3b8 100644 --- a/packages/apps/src/SideBar/NodeInfo.tsx +++ b/packages/apps/src/SideBar/NodeInfo.tsx @@ -41,12 +41,9 @@ export default React.memo(styled(NodeInfo)` > div { margin-bottom: -0.125em; + > div { display: inline-block; } - - &.spacer { - margin-bottom: 0.5rem; - } } `); diff --git a/packages/apps/src/SideBar/index.tsx b/packages/apps/src/SideBar/index.tsx index 7cc71f1bfe..73779f70ff 100644 --- a/packages/apps/src/SideBar/index.tsx +++ b/packages/apps/src/SideBar/index.tsx @@ -10,9 +10,9 @@ import createRoutes from '@polkadot/apps-routing'; import { Button, ChainImg, Icon, Menu, media } from '@polkadot/react-components'; import { SIDEBAR_MENU_THRESHOLD } from '../constants'; -import NetworkModal from '../modals/Network'; import { useTranslation } from '../translate'; import ChainInfo from './ChainInfo'; +import Endpoints from './Endpoints'; import Item from './Item'; import NodeInfo from './NodeInfo'; @@ -25,7 +25,11 @@ interface Props { toggleMenu: () => void; } -function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuOpen, toggleMenu }: Props): React.ReactElement { +const SIDE_BORDER_WIDTH = '0.65rem'; +const MENU_WIDTH_COLLAPSED = '4.2rem'; +const MENU_WIDTH_EXPANDED = '12rem'; + +function AppsSidebar ({ className = '', collapse, handleResize, isCollapsed, isMenuOpen, toggleMenu }: Props): React.ReactElement { const { t } = useTranslation(); const [modals, setModals] = useState>( createRoutes(t).reduce((result: Record, route): Record => { @@ -57,6 +61,12 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO className={`toggleImg ${isMenuOpen ? 'closed' : 'open delayed'}`} onClick={toggleMenu} /> + {modals.network && ( + + )} {routing.map((route): React.ReactNode => ( route?.Modal ? route.Modal && modals[route.name] @@ -69,16 +79,16 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO :
: null ))} - {modals.network && ( - - )}
- + {routing.map((route, index): React.ReactNode => ( route ? ( @@ -142,19 +152,17 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO ); } -const sideBorderWidth = '0.65rem'; - -export default React.memo(styled(SideBar)` +export default React.memo(styled(AppsSidebar)` display: flex; position: relative; z-index: 300; &.collapsed { - width: 4.2rem; + width: ${MENU_WIDTH_COLLAPSED}; } &.expanded { - width: 12rem; + width: ${MENU_WIDTH_EXPANDED}; } .apps--SideBar { @@ -169,7 +177,7 @@ export default React.memo(styled(SideBar)` width: 100%; .apps--SideBar-border { - border-top: ${sideBorderWidth} solid transparent; + border-top: ${SIDE_BORDER_WIDTH} solid transparent; position: absolute; left: 0; right: 0; diff --git a/packages/apps/src/index.tsx b/packages/apps/src/index.tsx index 1654bc4541..f595ef795c 100644 --- a/packages/apps/src/index.tsx +++ b/packages/apps/src/index.tsx @@ -10,7 +10,6 @@ import '@polkadot/react-components/i18n'; import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { HashRouter } from 'react-router-dom'; -import store from 'store'; import { ThemeProvider } from 'styled-components'; import { Api } from '@polkadot/react-api'; import Queue from '@polkadot/react-components/Status/Queue'; @@ -28,13 +27,6 @@ if (!rootElement) { throw new Error(`Unable to find element with id '${rootId}'`); } -// cleanups for old/unused storage items -store.each((_, key): void => { - if (key.startsWith('hooks:sessionSlashes:')) { - store.remove(key); - } -}); - ReactDOM.render( diff --git a/packages/apps/src/modals/Network.tsx b/packages/apps/src/modals/Network.tsx deleted file mode 100644 index 7e1a2ed169..0000000000 --- a/packages/apps/src/modals/Network.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2017-2020 @polkadot/app-accounts 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 React from 'react'; -import { Modal } from '@polkadot/react-components'; -import General from '@polkadot/app-settings/General'; - -import { useTranslation } from '../translate'; - -interface Props { - className?: string; - onClose: () => void; -} - -function NetworkModal ({ className = '', onClose }: Props): React.ReactElement { - const { t } = useTranslation(); - - return ( - ('Select Network')} - size='large' - > - - - - - ); -} - -export default React.memo(NetworkModal); diff --git a/packages/page-settings/src/General.tsx b/packages/page-settings/src/General.tsx index 8938fc8f74..eaf07a981f 100644 --- a/packages/page-settings/src/General.tsx +++ b/packages/page-settings/src/General.tsx @@ -7,22 +7,19 @@ import { Option } from '@polkadot/apps-config/settings/types'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { createLanguages, createSs58 } from '@polkadot/apps-config/settings'; import { isLedgerCapable } from '@polkadot/react-api'; -import { Button, ButtonCancel, Dropdown, Modal } from '@polkadot/react-components'; +import { Button, Dropdown } from '@polkadot/react-components'; import uiSettings, { SettingsStruct } from '@polkadot/ui-settings'; import { useTranslation } from './translate'; import { createIdenticon, createOption, save, saveAndReload } from './util'; -import SelectUrl from './SelectUrl'; interface Props { className?: string; - isModalContent?: boolean; - onClose: () => void; } const ledgerConnOptions = uiSettings.availableLedgerConn; -function General ({ className = '', isModalContent, onClose }: Props): React.ReactElement { +function General ({ className = '' }: Props): React.ReactElement { const { t } = useTranslation(); // tri-state: null = nothing changed, false = no reload, true = reload required const [changed, setChanged] = useState(null); @@ -58,7 +55,7 @@ function General ({ className = '', isModalContent, onClose }: Props): React.Rea [] ); const _saveAndReload = useCallback( - (): void => saveAndReload(settings), + () => saveAndReload(settings), [settings] ); const _save = useCallback( @@ -70,75 +67,56 @@ function General ({ className = '', isModalContent, onClose }: Props): React.Rea ); const { i18nLang, icon, ledgerConn, prefix, uiMode } = settings; - const networkSelector = ; return (
- {isModalContent - ? ( - - {networkSelector} - - {t('The RPC node can be selected from the pre-defined list or manually entered, depending on the chain you wish to connect to.')} - - - ) - : networkSelector - } - {!isModalContent && ( - <> -
- ('Override the default ss58 prefix for address generation')} - label={t('address prefix')} - onChange={_handleChange('prefix')} - options={prefixOptions} - /> -
-
- ('Override the default identity icon display with a specific theme')} - label={t('default icon theme')} - onChange={_handleChange('icon')} - options={iconOptions} - /> -
-
- ('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')} - label={t('interface operation mode')} - onChange={_handleChange('uiMode')} - options={uiSettings.availableUIModes} - /> -
- {isLedgerCapable() && ( -
- ('Manage your connection to Ledger S')} - label={t('manage hardware connections')} - onChange={_handleChange('ledgerConn')} - options={ledgerConnOptions} - /> -
- )} -
- ('default interface language')} - onChange={_handleChange('i18nLang')} - options={translateLanguages} - /> -
- +
+ ('Override the default ss58 prefix for address generation')} + label={t('address prefix')} + onChange={_handleChange('prefix')} + options={prefixOptions} + /> +
+
+ ('Override the default identity icon display with a specific theme')} + label={t('default icon theme')} + onChange={_handleChange('icon')} + options={iconOptions} + /> +
+
+ ('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')} + label={t('interface operation mode')} + onChange={_handleChange('uiMode')} + options={uiSettings.availableUIModes} + /> +
+ {isLedgerCapable() && ( +
+ ('Manage your connection to Ledger S')} + label={t('manage hardware connections')} + onChange={_handleChange('ledgerConn')} + options={ledgerConnOptions} + /> +
)} +
+ ('default interface language')} + onChange={_handleChange('i18nLang')} + options={translateLanguages} + /> +
- {isModalContent && ( - - )}