Endpoint sidebar (#3282)
* Endpoint sidebar * Collapsible * Selection of new url * Sidebar icon override * url validity * Remove network selection from settings * Updates with custom
This commit is contained in:
parent
78d7006cd8
commit
f7ecc45606
@ -22,7 +22,7 @@ function createDev (t: TFunction): LinkOption[] {
|
||||
dnslink: 'local',
|
||||
info: 'local',
|
||||
text: t<string>('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<string>('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<string>('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<string>('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<string>('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<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
|
||||
value: 'wss://arcadia1.nodleprotocol.io/'
|
||||
value: 'wss://arcadia1.nodleprotocol.io'
|
||||
},
|
||||
{
|
||||
info: 'datahighway',
|
||||
|
||||
@ -45,16 +45,16 @@ function Apps ({ className = '' }: Props): React.ReactElement<Props> {
|
||||
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(
|
||||
|
||||
@ -14,10 +14,11 @@ import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isToggled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function ChainInfo ({ className = '', onClick }: Props): React.ReactElement<Props> {
|
||||
function ChainInfo ({ className = '', isToggled, onClick }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const runtimeVersion = useCall<RuntimeVersion>(api.rpc.state.subscribeRuntimeVersion, []);
|
||||
@ -38,7 +39,7 @@ function ChainInfo ({ className = '', onClick }: Props): React.ReactElement<Prop
|
||||
</div>
|
||||
<Icon
|
||||
className='dropdown'
|
||||
icon='caret-down'
|
||||
icon={isToggled ? 'caret-right' : 'caret-down'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,6 +76,7 @@ export default React.memo(styled(ChainInfo)`
|
||||
.ui--Icon.dropdown {
|
||||
flex: 0;
|
||||
margin: 0;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
> div.info {
|
||||
|
||||
242
packages/apps/src/SideBar/Endpoints.tsx
Normal file
242
packages/apps/src/SideBar/Endpoints.tsx
Normal file
@ -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<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [endpoints] = useState(combineEndpoints(createEndpoints(t)));
|
||||
const [{ apiUrl, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>({ 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 (
|
||||
<Sidebar
|
||||
className={className}
|
||||
closeIcon={(hasUrlChanged && isUrlValid) ? 'sync' : 'times'}
|
||||
offset={offset}
|
||||
onClose={_onClose}
|
||||
position='left'
|
||||
>
|
||||
{endpoints.map(({ header, networks }, typeIndex): React.ReactNode => (
|
||||
<div
|
||||
className='endpointType'
|
||||
key={typeIndex}
|
||||
>
|
||||
<div className='endpointHeader'>{header}</div>
|
||||
{networks.map(({ icon, name, providers }, netIndex): React.ReactNode => {
|
||||
const isSelected = providers.some(({ url }) => url === apiUrl);
|
||||
const index = `${typeIndex}:${netIndex}`;
|
||||
const isOpen = openIndex === index;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`endpointGroup${isOpen ? ' isOpen' : ''}${isSelected ? ' isSelected' : ''}`}
|
||||
key={index}
|
||||
onClick={_setOpenIndex(index)}
|
||||
>
|
||||
<div className='endpointSection'>
|
||||
<ChainImg
|
||||
className='endpointIcon'
|
||||
logo={icon === 'local' ? 'empty' : icon}
|
||||
/>
|
||||
<div className='endpointValue'>{name}</div>
|
||||
{!isSelected && (
|
||||
<Icon
|
||||
className='endpointOpen'
|
||||
icon={isOpen ? 'caret-up' : 'caret-down'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{providers.map(({ name, url }): React.ReactNode => (
|
||||
<Toggle
|
||||
className='endpointProvider'
|
||||
key={url}
|
||||
label={name}
|
||||
onChange={_setApiUrl(url)}
|
||||
value={apiUrl === url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<Input
|
||||
className='endpointCustom'
|
||||
isError={!isUrlValid}
|
||||
isFull
|
||||
label={t<string>('Custom url')}
|
||||
onChange={_onChangeCustom}
|
||||
value={apiUrl}
|
||||
/>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@ -65,7 +65,7 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement<Props
|
||||
const count = (route.useCounter || DUMMY_COUNTER)();
|
||||
|
||||
useEffect((): void => {
|
||||
setHasSudo(!!sudoKey && allAccounts.some((address): boolean => sudoKey.eq(address)));
|
||||
setHasSudo(!!sudoKey && allAccounts.some((address) => sudoKey.eq(address)));
|
||||
}, [allAccounts, sudoKey]);
|
||||
|
||||
useEffect((): void => {
|
||||
|
||||
@ -41,12 +41,9 @@ export default React.memo(styled(NodeInfo)`
|
||||
|
||||
> div {
|
||||
margin-bottom: -0.125em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.spacer {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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<Props> {
|
||||
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<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [modals, setModals] = useState<Record<string, boolean>>(
|
||||
createRoutes(t).reduce((result: Record<string, boolean>, route): Record<string, boolean> => {
|
||||
@ -57,6 +61,12 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
|
||||
className={`toggleImg ${isMenuOpen ? 'closed' : 'open delayed'}`}
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
{modals.network && (
|
||||
<Endpoints
|
||||
offset={isCollapsed ? MENU_WIDTH_COLLAPSED : MENU_WIDTH_EXPANDED}
|
||||
onClose={_toggleModal('network')}
|
||||
/>
|
||||
)}
|
||||
{routing.map((route): React.ReactNode => (
|
||||
route?.Modal
|
||||
? route.Modal && modals[route.name]
|
||||
@ -69,16 +79,16 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
|
||||
: <div key={route.name} />
|
||||
: null
|
||||
))}
|
||||
{modals.network && (
|
||||
<NetworkModal onClose={_toggleModal('network')}/>
|
||||
)}
|
||||
<div className='apps--SideBar'>
|
||||
<Menu
|
||||
secondary
|
||||
vertical
|
||||
>
|
||||
<div className='apps--SideBar-Scroll'>
|
||||
<ChainInfo onClick={_toggleModal('network')} />
|
||||
<ChainInfo
|
||||
isToggled={modals.network}
|
||||
onClick={_toggleModal('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;
|
||||
|
||||
@ -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(
|
||||
<Suspense fallback='...'>
|
||||
<ThemeProvider theme={theme}>
|
||||
|
||||
@ -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<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t<string>('Select Network')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<General
|
||||
isModalContent
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(NetworkModal);
|
||||
@ -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<Props> {
|
||||
function General ({ className = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
// tri-state: null = nothing changed, false = no reload, true = reload required
|
||||
const [changed, setChanged] = useState<boolean | null>(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 = <SelectUrl onChange={_handleChange('apiUrl')} />;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{isModalContent
|
||||
? (
|
||||
<Modal.Columns>
|
||||
<Modal.Column>{networkSelector}</Modal.Column>
|
||||
<Modal.Column>
|
||||
{t<string>('The RPC node can be selected from the pre-defined list or manually entered, depending on the chain you wish to connect to.')}
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
)
|
||||
: networkSelector
|
||||
}
|
||||
{!isModalContent && (
|
||||
<>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={prefix}
|
||||
help={t<string>('Override the default ss58 prefix for address generation')}
|
||||
label={t<string>('address prefix')}
|
||||
onChange={_handleChange('prefix')}
|
||||
options={prefixOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={icon}
|
||||
help={t<string>('Override the default identity icon display with a specific theme')}
|
||||
label={t<string>('default icon theme')}
|
||||
onChange={_handleChange('icon')}
|
||||
options={iconOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={uiMode}
|
||||
help={t<string>('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')}
|
||||
label={t<string>('interface operation mode')}
|
||||
onChange={_handleChange('uiMode')}
|
||||
options={uiSettings.availableUIModes}
|
||||
/>
|
||||
</div>
|
||||
{isLedgerCapable() && (
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={ledgerConn}
|
||||
help={t<string>('Manage your connection to Ledger S')}
|
||||
label={t<string>('manage hardware connections')}
|
||||
onChange={_handleChange('ledgerConn')}
|
||||
options={ledgerConnOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={i18nLang}
|
||||
label={t<string>('default interface language')}
|
||||
onChange={_handleChange('i18nLang')}
|
||||
options={translateLanguages}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={prefix}
|
||||
help={t<string>('Override the default ss58 prefix for address generation')}
|
||||
label={t<string>('address prefix')}
|
||||
onChange={_handleChange('prefix')}
|
||||
options={prefixOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={icon}
|
||||
help={t<string>('Override the default identity icon display with a specific theme')}
|
||||
label={t<string>('default icon theme')}
|
||||
onChange={_handleChange('icon')}
|
||||
options={iconOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={uiMode}
|
||||
help={t<string>('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')}
|
||||
label={t<string>('interface operation mode')}
|
||||
onChange={_handleChange('uiMode')}
|
||||
options={uiSettings.availableUIModes}
|
||||
/>
|
||||
</div>
|
||||
{isLedgerCapable() && (
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={ledgerConn}
|
||||
help={t<string>('Manage your connection to Ledger S')}
|
||||
label={t<string>('manage hardware connections')}
|
||||
onChange={_handleChange('ledgerConn')}
|
||||
options={ledgerConnOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={i18nLang}
|
||||
label={t<string>('default interface language')}
|
||||
onChange={_handleChange('i18nLang')}
|
||||
options={translateLanguages}
|
||||
/>
|
||||
</div>
|
||||
<Button.Group>
|
||||
{isModalContent && (
|
||||
<ButtonCancel onClick={onClose} />
|
||||
)}
|
||||
<Button
|
||||
icon='save'
|
||||
isDisabled={changed === null}
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
// Copyright 2017-2020 @polkadot/app-settings 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 { TFunction } from 'i18next';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { createEndpoints } from '@polkadot/apps-config/settings';
|
||||
import { Dropdown, Input, Toggle } from '@polkadot/react-components';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
|
||||
import { useTranslation } from './translate';
|
||||
import { createOption } from './util';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onChange: (url: string) => void;
|
||||
}
|
||||
|
||||
interface StateUrl {
|
||||
isValid: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface State extends StateUrl {
|
||||
isCustom: boolean;
|
||||
}
|
||||
|
||||
// check the validity of the url
|
||||
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://'))
|
||||
);
|
||||
}
|
||||
|
||||
// sanitize a url and check it for validity
|
||||
function makeUrl (_url: string): StateUrl {
|
||||
const url = _url.trim();
|
||||
const isValid = isValidUrl(url);
|
||||
|
||||
return { isValid, url };
|
||||
}
|
||||
|
||||
// this allows us to retrieve the initial state by reading the settings and the applying
|
||||
// validation on-top of the values retrieved
|
||||
function getInitialState (t: TFunction): State {
|
||||
const url = uiSettings.get().apiUrl;
|
||||
|
||||
return {
|
||||
isCustom: createEndpoints(t).reduce((isCustom: boolean, { value }): boolean => {
|
||||
return isCustom && value !== url;
|
||||
}, true),
|
||||
isValid: isValidUrl(url),
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
function SelectUrl ({ className = '', onChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [info, setInfo] = useState(getInitialState(t));
|
||||
const { isCustom, isValid, url } = info;
|
||||
const translatedEndpoints = useMemo(
|
||||
() => createEndpoints(t).map((option) => createOption(option, ['local'])),
|
||||
[t]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange && info.isValid && onChange(info.url);
|
||||
// the issue here is that the onChange callback changes each and every render... so Houston, we have
|
||||
// a desperate issue here :(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [info]);
|
||||
|
||||
const _onChangeUrl = useCallback(
|
||||
(url: string) => setInfo((info: State) => ({ ...info, ...makeUrl(url) })),
|
||||
[]
|
||||
);
|
||||
|
||||
const _onChangeCustom = useCallback(
|
||||
(isCustom: boolean) => setInfo({
|
||||
...makeUrl(
|
||||
isCustom
|
||||
? info.url
|
||||
: (createEndpoints(t).find(({ value }) => !!value) || { value: 'ws://127.0.0.1:9944' }).value as string
|
||||
),
|
||||
isCustom
|
||||
}),
|
||||
[info, t]
|
||||
);
|
||||
|
||||
const help = t<string>('Select the remote endpoint, either from the dropdown on manual entered via the custom toggle');
|
||||
const label = t<string>('remote node/endpoint to connect to');
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='ui--row'>{
|
||||
isCustom
|
||||
? <Input
|
||||
defaultValue={url}
|
||||
help={help}
|
||||
isError={!isValid}
|
||||
label={label}
|
||||
onChange={_onChangeUrl}
|
||||
/>
|
||||
: <Dropdown
|
||||
defaultValue={url}
|
||||
help={help}
|
||||
label={label}
|
||||
onChange={_onChangeUrl}
|
||||
options={translatedEndpoints}
|
||||
/>
|
||||
}</div>
|
||||
<Toggle
|
||||
className='settings--customToggle'
|
||||
label={t<string>('custom endpoint')}
|
||||
onChange={_onChangeCustom}
|
||||
value={isCustom}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(SelectUrl)`
|
||||
position: relative;
|
||||
|
||||
.settings--customToggle {
|
||||
position: absolute;
|
||||
bottom: 1.375rem;
|
||||
right: 3.5rem;
|
||||
}
|
||||
`);
|
||||
@ -71,7 +71,7 @@ export function save (settings: SettingsStruct): void {
|
||||
export function saveAndReload (settings: SettingsStruct): void {
|
||||
save(settings);
|
||||
|
||||
// HACK This is terribe, but since the API needs to re-connect, but since
|
||||
// HACK This is terrible, but since the API needs to re-connect, but since
|
||||
// the API does not yet handle re-connections properly, it is what it is
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -10,17 +12,18 @@ import Button from './Button';
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
offset?: number;
|
||||
closeIcon?: IconName;
|
||||
offset?: number | string;
|
||||
onClose: () => void;
|
||||
position: 'left' | 'right';
|
||||
}
|
||||
|
||||
function Sidebar ({ children, className = '', onClose }: Props): React.ReactElement<Props> {
|
||||
function Sidebar ({ children, className = '', closeIcon = 'times', onClose }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={`ui--Sidebar ${className}`}>
|
||||
<Button
|
||||
className='ui--Sidebar-close'
|
||||
icon='times'
|
||||
icon={closeIcon}
|
||||
isBasic
|
||||
isCircular
|
||||
onClick={onClose}
|
||||
@ -34,9 +37,12 @@ export default React.memo(styled(Sidebar)(({ offset = 0, position }: Props) => `
|
||||
background: #f5f4f3;
|
||||
bottom: 0;
|
||||
box-shadow: ${position === 'right' ? '-6px' : '6px'} 0px 20px 0px rgba(0, 0, 0, 0.3);
|
||||
margin-left: -0.125rem;
|
||||
max-width: 24rem;
|
||||
min-width: 24rem;
|
||||
position: fixed;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
${position}: ${offset};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user