Make setting URL component re-usable (#1556)

* Make setting URL component re-usable

* Adjust toggle

* Center icons

* Cleanups

* Fix linting

* extract label

* Simplify

* More cleanups

* Readability tweaks
This commit is contained in:
Jaco Greeff 2019-08-29 11:47:11 +02:00 committed by GitHub
parent 10896e04bf
commit c3f0151746
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 262 additions and 259 deletions

View File

@ -3,259 +3,87 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import { SettingsStruct } from '@polkadot/ui-settings/types';
import { Option } from './types';
import React from 'react';
import styled from 'styled-components';
import { Button, ChainImg, Dropdown, Input, Toggle } from '@polkadot/react-components';
import React, { useState } from 'react';
import { Button, Dropdown } from '@polkadot/react-components';
import { ActionStatus } from '@polkadot/react-components/Status/types';
import uiSettings from '@polkadot/ui-settings';
import translate from './translate';
interface Option {
text: React.ReactNode;
value: string | number;
}
interface SetOption {
info: string;
text: string;
value: string | number;
}
import { createOption, saveAndReload } from './util';
import SelectUrl from './SelectUrl';
interface Props extends AppProps, I18nProps {
onStatusChange: (status: ActionStatus) => void;
}
interface State {
isCustomNode: boolean;
isUrlValid: boolean;
settings: SettingsStruct;
}
const prefixOptions = uiSettings.availablePrefixes.map((o): Option => createOption(o, ['default']));
const themeOptions = uiSettings.availableUIThemes; // .map((o): Option => createOption(o));
const createOption = ({ info, text, value }: SetOption, overrides: string[] = [], override: string = 'empty'): Option => ({
text: (
<div className='ui--Dropdown-item'>
<ChainImg
className='ui--Dropdown-icon'
logo={
overrides.includes(info)
? override
: info
}
/>
<div className='ui--Dropdown-name'>{text}</div>
</div>
),
value
});
function General ({ className, t }: Props): React.ReactElement<Props> {
const [settings, setSettings] = useState(uiSettings.get());
const { i18nLang, prefix, uiMode, uiTheme } = settings;
const endpointOptions: Option[] = uiSettings.availableNodes.map((o): Option => createOption(o, ['local']));
const prefixOptions: Option[] = uiSettings.availablePrefixes.map((o): Option => createOption(o, ['default']));
const themeOptions: Option[] = uiSettings.availableUIThemes.map((o): Option => createOption(o));
const _onChangeApiUrl = (apiUrl: string): void => setSettings({ ...settings, apiUrl });
const _onChangePrefix = (prefix: number): void => setSettings({ ...settings, prefix });
const _onChangeUiMode = (uiMode: string): void => setSettings({ ...settings, uiMode });
const _onChangeUiTheme = (uiTheme: string): void => setSettings({ ...settings, uiTheme });
const _saveAndReload = (): void => saveAndReload(settings);
class General extends React.PureComponent<Props, State> {
public constructor (props: Props) {
super(props);
const settings = uiSettings.get();
const isCustomNode = uiSettings.availableNodes.reduce((isCustomNode, { value }): boolean => {
return isCustomNode && value !== settings.apiUrl;
}, true);
this.state = {
isCustomNode,
isUrlValid: this.isValidUrl(settings.apiUrl),
settings
};
}
public render (): React.ReactNode {
const { className, t } = this.props;
const { isUrlValid, settings: { i18nLang, prefix, uiMode, uiTheme } } = this.state;
return (
<div className={className}>
{this.renderEndpoint()}
<div className='ui--row'>
<div className='ui--medium'>
<Dropdown
defaultValue={prefix}
help={t('Override the default ss58 prefix for address generation')}
label={t('address prefix')}
onChange={this.onChangePrefix}
options={prefixOptions}
/>
</div>
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
help={t('The logo and colors for the app along with the identity icon theme.')}
label={t('default interface theme')}
onChange={this.onChangeUiTheme}
options={themeOptions}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
help={t('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={this.onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
options={uiSettings.availableLanguages}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!isUrlValid}
isPrimary
onClick={this.saveAndReload}
label={t('Save & Reload')}
return (
<div className={className}>
<SelectUrl onChange={_onChangeApiUrl} />
<div className='ui--row'>
<div className='ui--medium'>
<Dropdown
defaultValue={prefix}
help={t('Override the default ss58 prefix for address generation')}
label={t('address prefix')}
onChange={_onChangePrefix}
options={prefixOptions}
/>
</Button.Group>
</div>
);
}
private renderEndpoint = (): React.ReactNode => {
const { t } = this.props;
const { isCustomNode, isUrlValid, settings: { apiUrl } } = this.state;
return (
<>
<Toggle
asSwitch
className='settings--cutomToggle'
defaultValue={isCustomNode}
label={t('custom endpoint')}
onChange={this.onChangeCustom}
/>
<div className='ui--row'>
{
isCustomNode
? (
<Input
defaultValue={apiUrl}
isError={!isUrlValid}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
/>
)
: (
<Dropdown
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
options={endpointOptions}
/>
)
}
</div>
</>
);
}
private onChangeApiUrl = (apiUrl: string): void => {
this.setState(({ settings }: State): Pick<State, never> => ({
isUrlValid: this.isValidUrl(apiUrl),
settings: {
...settings,
apiUrl
}
}));
}
private onChangePrefix = (prefix: number): void => {
this.setState(({ settings }: State): Pick<State, never> => ({
settings: {
...settings,
prefix
}
}));
}
private onChangeUiMode = (uiMode: string): void => {
this.setState(({ settings }: State): Pick<State, never> => ({
settings: {
...settings,
uiMode
}
}));
}
private onChangeUiTheme = (uiTheme: string): void => {
this.setState(({ settings }: State): Pick<State, never> => ({
settings: {
...settings,
uiTheme
}
}));
}
private onChangeCustom = (isCustomNode: boolean): void => {
this.setState(({ settings }: State): Pick<State, never> => ({
isCustomNode,
isUrlValid: true,
settings: {
...settings,
apiUrl: isCustomNode
? settings.apiUrl
: uiSettings.availableNodes[0].value
}
}));
}
private isValidUrl (apiUrl: string): boolean {
return (
// some random length... we probably want to parse via some lib
(apiUrl.length >= 7) &&
// check that it starts with a valid ws identifier
(apiUrl.startsWith('ws://') || apiUrl.startsWith('wss://'))
);
}
private saveAndReload = (): void => {
const { settings } = this.state;
uiSettings.set(settings);
// HACK This is terribe, 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();
}
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
help={t('The logo and colors for the app along with the identity icon theme.')}
label={t('default interface theme')}
onChange={_onChangeUiTheme}
options={themeOptions}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
help={t('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={_onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
options={uiSettings.availableLanguages}
/>
</div>
</div>
<Button.Group>
<Button
isPrimary
onClick={_saveAndReload}
label={t('Save & Reload')}
/>
</Button.Group>
</div>
);
}
export default translate(styled(General)`
.settings--cutomToggle {
text-align: right;
}
.ui.menu {
justify-content: flex-end;
margin-bottom: 0;
.active.item {
font-weight: bold;
}
}
.sub-label {
cursor: pointer;
padding: 0rem .5833rem;
text-align: right;
}
`);
export default translate(General);

View File

@ -0,0 +1,123 @@
// Copyright 2017-2019 @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 { I18nProps } from '@polkadot/react-components/types';
import { Option } from './types';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Dropdown, Input, Toggle } from '@polkadot/react-components';
import uiSettings from '@polkadot/ui-settings';
import translate from './translate';
import { createOption } from './util';
interface Props extends I18nProps {
className?: string;
onChange: (url: string) => void;
}
interface StateUrl {
isValid: boolean;
url: string;
}
interface State extends StateUrl {
isCustom: boolean;
}
const endpointOptions = uiSettings.availableNodes.map((o): Option => createOption(o, ['local']));
// 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 (): State {
const url = uiSettings.get().apiUrl;
const isCustom = uiSettings.availableNodes.reduce((isCustom: boolean, { value }): boolean => {
return isCustom && value !== url;
}, true);
const isValid = isValidUrl(url);
return { isCustom, isValid, url };
}
function SelectUrl ({ className, onChange, t }: Props): React.ReactElement<Props> {
const [info, setInfo] = useState(getInitialState());
const { isCustom, isValid, url } = info;
const help = t('Select the remote endpoint, either from the dropdown on manual entered via the custom toggle');
const label = t('remote node/endpoint to connect to');
const _onChangeUrl = (url: string): void => setInfo({ ...info, ...makeUrl(url) });
const _onChangeCustom = (isCustom: boolean): void =>
setInfo({
...makeUrl(
isCustom
? info.url
: uiSettings.availableNodes[0].value as string
),
isCustom
});
useEffect((): void => {
if (onChange && info.isValid) {
onChange(info.url);
}
}, [info]);
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={endpointOptions}
/>
}</div>
<Toggle
className='settings--customToggle'
defaultValue={isCustom}
label={t('custom endpoint')}
onChange={_onChangeCustom}
/>
</div>
);
}
export default translate(
styled(SelectUrl)`
position: relative;
.settings--customToggle {
position: absolute;
top: .5rem;
right: 3.5rem;
}
`
);

View File

@ -0,0 +1,14 @@
// Copyright 2017-2019 @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.
export interface Option {
text: React.ReactNode;
value: string | number;
}
export interface SetOption {
info: string;
text: string;
value: string | number;
}

View File

@ -0,0 +1,41 @@
// Copyright 2017-2019 @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 { SettingsStruct } from '@polkadot/ui-settings/types';
import { Option, SetOption } from './types';
import React from 'react';
import { ChainImg } from '@polkadot/react-components';
import uiSettings from '@polkadot/ui-settings';
export function createOption ({ info, text, value }: SetOption, overrides: string[] = [], override: string = 'empty'): Option {
return {
text: (
<div className='ui--Dropdown-item'>
<ChainImg
className='ui--Dropdown-icon'
logo={
overrides.includes(info)
? override
: info
}
/>
<div className='ui--Dropdown-name'>{text}</div>
</div>
),
value
};
}
export function saveAndReload (settings: SettingsStruct | null): void {
if (!settings) {
return;
}
uiSettings.set(settings);
// HACK This is terribe, 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();
}

View File

@ -172,7 +172,7 @@ export default styled(Dropdown)`
> .text > .ui--Dropdown-item {
.ui--Dropdown-icon {
left: -2.6rem;
top: -1rem;
top: -1.15rem;
opacity: 1;
}

View File

@ -23,6 +23,7 @@ class Toggle extends React.PureComponent<Props> {
return (
<div className={className}>
<label>{label}</label>
<SUICheckbox
checked={value}
disabled={isDisabled}
@ -30,7 +31,6 @@ class Toggle extends React.PureComponent<Props> {
onChange={this.onChange}
toggle={asSwitch}
/>
<label>{label}</label>
</div>
);
}
@ -45,7 +45,7 @@ class Toggle extends React.PureComponent<Props> {
export default styled(Toggle)`
> label {
display: inline-block;
margin-left: 0.5rem;
margin: 0 0.5rem;
}
> label,

View File

@ -13,28 +13,12 @@
.ui--row {
flex-wrap: wrap;
}
.ui--Param-overlay {
margin-bottom: -100%;
z-index: 1;
text-align: right;
}
}
.ui--Param-Address {
font-family: monospace;
}
.ui--Param-composite {
position: relative;
.ui--Param-overlay {
position: absolute;
top: 0.5rem;
right: 4.5rem;
}
}
.ui--Params-Content {
box-sizing: border-box;
padding: 0 0 0 1.75rem;

View File

@ -8,6 +8,7 @@ import { ComponentMap, ParamDef, RawParam, RawParams, RawParamOnChange, RawParam
import './Params.css';
import React from 'react';
import styled from 'styled-components';
import { Toggle } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import translate from '@polkadot/react-components/translate';
@ -235,4 +236,16 @@ class Params extends React.PureComponent<Props, State> {
}
}
export default translate(Params);
export default translate(
styled(Params as React.ComponentClass<Props>)`
.ui--Param-composite {
position: relative;
.ui--Param-overlay {
position: absolute;
top: 0.5rem;
right: 3.5rem;
}
}
`
);