Combine AddressSummary & Row (#1260)

* Combine AddressSummary & Row

* Cleanup address rendering

* Tiny tags cleanups

* Remove unused ui--AddressSummary overrides

* Column component

* Columar

* Cleanups

* Remove hot client

* Backport data padding
This commit is contained in:
Jaco Greeff 2019-06-02 10:13:38 +02:00 committed by GitHub
parent ba320b4d01
commit f887f68cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 745 additions and 870 deletions

View File

@ -2,11 +2,6 @@
/* This software may be modified and distributed under the terms
/* of the Apache-2.0 license. See the LICENSE file for details. */
.app--accounts-Modal .ui--AddressSummary {
display: inline-block;
margin-bottom: 1rem;
}
.accounts--Creator-advanced {
margin-top: 1rem;
}
}

View File

@ -149,7 +149,7 @@ class Address extends React.PureComponent<Props, State> {
icon='trash'
key='forget'
size='small'
tooltip={t('Forget this account')}
tooltip={t('Forget this address')}
/>
</>
)}

View File

@ -2,19 +2,12 @@
/* This software may be modified and distributed under the terms
/* of the Apache-2.0 license. See the LICENSE file for details. */
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
header > .explorer--BlockHeader {
border: none;
box-shadow: none;
}
.explorer--BlockHeader {
animation: fadein 1s;
.author {
font-size: 1rem;
text-align: right;
@ -25,15 +18,6 @@ header > .explorer--BlockHeader {
}
}
.header-outer {
padding: 0.5rem;
overflow: hidden;
}
.header-outer + details {
margin-top: -0.5rem;
}
.header {
color: rgba(0, 0, 0, 0.6);
display: flex;
@ -74,6 +58,7 @@ header > .explorer--BlockHeader {
.contains {
border: 0;
margin-top: 0.5rem;
text-align: center;
> .info {
@ -89,7 +74,7 @@ header > .explorer--BlockHeader {
> label {
display: inline-block;
}
> span,
> label {
vertical-align: middle;

View File

@ -43,18 +43,14 @@ class BlockHeaders extends React.PureComponent<Props> {
render () {
const { headers = [] } = this.props;
return (
<div className='explorer--BlockHeaders'>
{headers.map((header, index) => (
<BlockHeader
isSummary={!!index}
key={header.blockNumber.toString()}
value={header}
withLink={!header.blockNumber.isZero()}
/>
))}
</div>
);
return headers.map((header, index) => (
<BlockHeader
isSummary={!!index}
key={header.blockNumber.toString()}
value={header}
withLink={!header.blockNumber.isZero()}
/>
));
}
}

View File

@ -9,6 +9,7 @@ import React from 'react';
import { HeaderExtended } from '@polkadot/api-derive';
import { EventRecord, SignedBlock } from '@polkadot/types';
import { withCalls } from '@polkadot/ui-api';
import { Columar } from '@polkadot/ui-app';
import BlockHeader from '../BlockHeader';
import translate from '../translate';
@ -39,9 +40,11 @@ class BlockByHash extends React.PureComponent<Props> {
withExplorer
/>
</header>
<Extrinsics value={chain_getBlock.block.extrinsics} />
<Events value={system_events} />
<Logs value={chain_getHeader.digest.logs} />
<Columar>
<Extrinsics value={chain_getBlock.block.extrinsics} />
<Events value={system_events} />
<Logs value={chain_getHeader.digest.logs} />
</Columar>
</>
);
}

View File

@ -6,6 +6,7 @@ import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import { EventRecord } from '@polkadot/types';
import { Column } from '@polkadot/ui-app';
import EventsDisplay from '../Events';
import translate from '../translate';
@ -27,15 +28,12 @@ class Events extends React.PureComponent<Props> {
}));
return (
<section>
<h1>{t('events')}</h1>
<div className='explorer--BlockByHash-flexable ui--flex-medium'>
<EventsDisplay
eventClassName='explorer--BlockByHash-block'
events={events}
/>
</div>
</section>
<Column header={t('events')}>
<EventsDisplay
eventClassName='explorer--BlockByHash-block'
events={events}
/>
</Column>
);
}
}

View File

@ -5,7 +5,7 @@
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import { AddressMini, Call, LinkPolkascan } from '@polkadot/ui-app';
import { AddressMini, Call, Column, LinkPolkascan } from '@polkadot/ui-app';
import { formatNumber } from '@polkadot/util';
import { Extrinsic, Method } from '@polkadot/types';
@ -21,10 +21,9 @@ class Extrinsics extends React.PureComponent<Props> {
const { label, t } = this.props;
return (
<section key='extrinsics'>
<h1>{label || t('extrinsics')}</h1>
<Column header={label || t('extrinsics')}>
{this.renderContent()}
</section>
</Column>
);
}
@ -33,17 +32,13 @@ class Extrinsics extends React.PureComponent<Props> {
if (!value || !value.length) {
return (
<div className='ui disabled'>
<article>
{t('no pending extrinsics are in the queue')}
</div>
</article>
);
}
return (
<div className='explorer--BlockByHash-flexable ui--flex-medium'>
{value.map(this.renderExtrinsic)}
</div>
);
return value.map(this.renderExtrinsic);
}
// FIXME This is _very_ similar to what we have in democracy/Item
@ -51,36 +46,31 @@ class Extrinsics extends React.PureComponent<Props> {
const { meta, method, section } = Method.findFunction(extrinsic.callIndex);
return (
<div
className='explorer--BlockByHash-block'
key={`extrinsic:${index}`}
>
<article className='explorer--Container'>
<div className='header'>
<h3>
{section}.{method}&nbsp;(#{formatNumber(index)})
</h3>
{this.renderSigner(extrinsic)}
</div>
<details>
<summary>{
meta && meta.documentation
? meta.documentation.join(' ')
: 'Details'
}</summary>
<Call
className='details'
value={extrinsic}
withHash
/>
</details>
{
extrinsic.isSigned
? <LinkPolkascan data={extrinsic.hash.toHex()} type='extrinsic' />
: null
}
</article>
</div>
<article key={`extrinsic:${index}`}>
<div className='header'>
<h3>
{section}.{method}&nbsp;(#{formatNumber(index)})
</h3>
{this.renderSigner(extrinsic)}
</div>
<details>
<summary>{
meta && meta.documentation
? meta.documentation.join(' ')
: 'Details'
}</summary>
<Call
className='details'
value={extrinsic}
withHash
/>
</details>
{
extrinsic.isSigned
? <LinkPolkascan data={extrinsic.hash.toHex()} type='extrinsic' />
: null
}
</article>
);
}

View File

@ -6,6 +6,7 @@ import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import { DigestItem, Struct, Tuple, U8a, Vector, getTypeDef } from '@polkadot/types';
import { Column } from '@polkadot/ui-app';
import Params from '@polkadot/ui-params';
import translate from '../translate';
@ -23,12 +24,9 @@ class Logs extends React.PureComponent<Props> {
}
return (
<section>
<h1>{t('logs')}</h1>
<div className='explorer--BlockByHash-flexable ui--flex-medium'>
{value.map(this.renderItem)}
</div>
</section>
<Column header={t('logs')}>
{value.map(this.renderItem)}
</Column>
);
}

View File

@ -23,7 +23,7 @@ class Events extends React.PureComponent<Props> {
const { emptyLabel, events, t } = this.props;
if (!events || events.length === 0) {
return emptyLabel || t('no events available');
return <article>{emptyLabel || t('no events available')}</article>;
}
return events.map(this.renderEvent);
@ -40,35 +40,33 @@ class Events extends React.PureComponent<Props> {
}
return (
<div
className={eventClassName}
<article
className={`explorer--Container ${eventClassName}`}
key={key}
>
<article className='explorer--Container'>
<div className='header'>
<h3>
{event.section}.{event.method}&nbsp;{
extIndex !== -1
? `(#${formatNumber(extIndex)})`
: ''
}
</h3>
</div>
<details>
<summary>
{
event.meta && event.meta.documentation
? event.meta.documentation.join(' ')
: 'Details'
}
</summary>
<EventDisplay
className='details'
value={event}
/>
</details>
</article>
</div>
<div className='header'>
<h3>
{event.section}.{event.method}&nbsp;{
extIndex !== -1
? `(#${formatNumber(extIndex)})`
: ''
}
</h3>
</div>
<details>
<summary>
{
event.meta && event.meta.documentation
? event.meta.documentation.join(' ')
: 'Details'
}
</summary>
<EventDisplay
className='details'
value={event}
/>
</details>
</article>
);
}
}

View File

@ -6,7 +6,7 @@ import { I18nProps } from '@polkadot/ui-app/types';
import { KeyedEvent } from './types';
import React from 'react';
import styled from 'styled-components';
import { Columar, Column } from '@polkadot/ui-app';
import BlockHeaders from './BlockHeaders';
import Events from './Events';
@ -18,14 +18,6 @@ type Props = I18nProps & {
events: Array<KeyedEvent>
};
const Wrapper = styled.div`
.column {
flex: 0 0 50%;
min-width: 0;
padding: 0 1em 1em;
}
`;
class Main extends React.PureComponent<Props> {
render () {
const { events, t } = this.props;
@ -34,16 +26,14 @@ class Main extends React.PureComponent<Props> {
<>
<Query />
<Summary />
<Wrapper className='ui--flex-medium'>
<div className='column'>
<h1>{t('recent blocks')}</h1>
<Columar>
<Column header={t('recent blocks')}>
<BlockHeaders />
</div>
<div className='column'>
<h1>{t('recent events')}</h1>
</Column>
<Column header={t('recent events')}>
<Events events={events} />
</div>
</Wrapper>
</Column>
</Columar>
</>
);
}

View File

@ -54,8 +54,8 @@
.explorer--BlockByHash-header-right {
position: absolute;
top: -0.5rem;
right: 0rem;
top: 0.25rem;
right: 0.5rem;
}
.explorer--query {

View File

@ -7,7 +7,7 @@ import { I18nProps } from '@polkadot/ui-app/types';
import { ValidatorFilter, RecentlyOfflineMap } from '../types';
import React from 'react';
import { Dropdown, FilterOverlay } from '@polkadot/ui-app';
import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/ui-app';
import translate from '../translate';
import Address from './Address';
@ -48,7 +48,7 @@ class CurrentList extends React.PureComponent<Props, State> {
}
render () {
const { t } = this.props;
const { current, next, t } = this.props;
const { filter, filterOptions } = this.state;
return (
<div>
@ -61,73 +61,42 @@ class CurrentList extends React.PureComponent<Props, State> {
value={filter}
/>
</FilterOverlay>
<div className='validator--ValidatorsList ui--flex-medium'>
<div className='validator--current'>
{this.renderCurrent()}
</div>
<div className='validator--next'>
{this.renderNext()}
</div>
</div>
<Columar className='validator--ValidatorsList'>
<Column header={t('validators')}>
{this.renderColumn(current, t('validator (stash)'))}
</Column>
<Column header={t('next up')}>
{this.renderColumn(next, t('intention (stash)'))}
</Column>
</Columar>
</div>
);
}
private renderCurrent () {
const { current, t } = this.props;
return (
<>
<h1>
{t('validators', {
replace: {
count: current.length
}
})}
</h1>
{this.renderColumn(current, t('validator (stash)'))}
</>
);
}
private renderNext () {
const { next, t } = this.props;
return (
<>
<h1>{t('next up')}</h1>
{this.renderColumn(next, t('intention (stash)'))}
</>
);
}
private renderColumn (addresses: Array<string>, defaultName: string) {
const { balances, lastAuthor, lastBlock, recentlyOffline, t } = this.props;
const { filter } = this.state;
if (addresses.length === 0) {
return (
<div>{t('no addresses found')}</div>
<article>{t('no addresses found')}</article>
);
}
return (
<div>
{addresses.map((address) => (
<Address
address={address}
balances={balances}
defaultName={defaultName}
key={address}
filter={filter}
lastAuthor={lastAuthor}
lastBlock={lastBlock}
recentlyOffline={recentlyOffline}
/>
))}
</div>
);
return addresses.map((address) => (
<Address
address={address}
balances={balances}
defaultName={defaultName}
key={address}
filter={filter}
lastAuthor={lastAuthor}
lastBlock={lastBlock}
recentlyOffline={recentlyOffline}
/>
));
}
private onChangeFilter = (filter: ValidatorFilter): void => {
this.setState({ filter });
}

View File

@ -114,14 +114,3 @@
.staking--accounts-info .staking--label {
margin: 0 2.25rem -0.75rem 0;
}
.ui--AddressSummary {
display: flex;
justify-content: space-between;
padding: 0;
.ui--AddressSummary-base,
.ui--AddressSummary-children {
min-width: 0;
}
}

View File

@ -33,7 +33,7 @@ function createWebpack ({ alias = {}, context, name = 'index' }) {
`./src/${name}.tsx`,
isProd
? null
: 'webpack-plugin-serve/client'
: null // 'webpack-plugin-serve/client'
].filter((entry) => entry),
mode: ENV,
output: {

View File

@ -90,7 +90,6 @@ export default class AddressMini extends React.PureComponent<Props> {
return (
<BalanceDisplay
balance={balance}
className='ui--AddressSummary-balance'
params={value}
/>
);
@ -106,7 +105,6 @@ export default class AddressMini extends React.PureComponent<Props> {
return (
<BondedDisplay
bonded={bonded}
className='ui--AddressSummary-balance'
label=''
params={value}
/>

View File

@ -2,32 +2,110 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, AccountIndex, Address } from '@polkadot/types';
import { I18nProps } from './types';
import BN from 'bn.js';
import { Label } from 'semantic-ui-react';
import React from 'react';
import { withCalls } from '@polkadot/ui-api';
import styled from 'styled-components';
import { withCalls, withMulti } from '@polkadot/ui-api';
import { Button, Input, InputTags } from '@polkadot/ui-app';
import BaseIdentityIcon from '@polkadot/ui-identicon';
import keyring from '@polkadot/ui-keyring';
import { classes } from './util';
import { AddressSummary, Props } from './AddressSummary';
import AddressInfo, { BalanceActiveType } from './AddressInfo';
import CopyButton from './CopyButton';
import IdentityIcon from './IdentityIcon';
import LinkPolkascan from './LinkPolkascan';
import translate from './translate';
import { classes, getAddrName, getAddrTags, toShortAddress } from './util';
const DEFAULT_ICON_SIZE = 64;
export type Props = I18nProps & {
accounts_idAndIndex?: [AccountId?, AccountIndex?],
bonded?: BN | Array<BN>,
buttons?: React.ReactNode,
children?: React.ReactNode,
defaultName?: string,
extraInfo?: React.ReactNode,
isEditable?: boolean,
isInline?: boolean,
value: AccountId | AccountIndex | Address | string | null,
withBalance?: boolean | BalanceActiveType,
withExplorer?: boolean,
withIcon?: boolean,
withIndex?: boolean,
withTags?: boolean
};
type State = {
address: string,
isEditingName: boolean,
isEditingTags: boolean,
name: string,
tags: string[]
};
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
const ICON_SIZE = 48;
class AddressRow extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = this.createState();
}
static defaultProps = {
defaultName: '<unknown>'
};
static getDerivedStateFromProps ({ accounts_idAndIndex = [], defaultName, value }: Props, prevState: State) {
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
const address = accountId
? accountId.toString()
: DEFAULT_ADDR;
const name = getAddrName(address, false, defaultName) || '';
const tags = getAddrTags(address);
const state = { tags } as State;
let hasChanged = false;
if (address !== prevState.address) {
state.address = address;
hasChanged = true;
}
if (!prevState.isEditingName && name !== prevState.name) {
state.name = name;
hasChanged = true;
}
return hasChanged
? state
: null;
}
class AddressRow extends AddressSummary {
render () {
const { className, style, identIconSize = DEFAULT_ICON_SIZE, isInline, value, withIndex } = this.props;
const { accounts_idAndIndex = [], className, isInline, style } = this.props;
const [accountId, accountIndex] = accounts_idAndIndex;
const isValid = accountId || accountIndex;
return (
<div
className={classes('ui--AddressRow', !value && 'invalid', isInline && 'inline', className)}
className={classes('ui--AddressRow', !isValid && 'invalid', isInline && 'inline', className)}
style={style}
>
<div className='ui--AddressRow-base'>
{this.renderIcon('ui--AddressRow-icon', identIconSize)}
{this.renderIcon()}
{this.renderButtons()}
<div className='ui--AddressRow-details'>
<div className='ui--AddressSummary-data'>
<div className='ui--AddressRow-data'>
{this.renderName()}
{this.renderAddress()}
{this.renderAccountIndex(withIndex)}
{this.renderAccountIndex()}
</div>
{this.renderBalances()}
{this.renderTags()}
@ -38,11 +116,450 @@ class AddressRow extends AddressSummary {
</div>
);
}
private createState () {
const { accounts_idAndIndex = [], defaultName, value } = this.props;
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
const address = accountId
? accountId.toString()
: DEFAULT_ADDR;
const name = getAddrName(address, false, defaultName) || '';
const tags = getAddrTags(address);
return {
address,
isEditingName: false,
isEditingTags: false,
name,
tags
};
}
protected renderAddress () {
const { address } = this.state;
return (
<div className='ui--AddressRow-accountId'>
<CopyButton
isAddress
value={address}
>
<span>{toShortAddress(address)}</span>
</CopyButton>
</div>
);
}
protected renderButtons () {
const { buttons } = this.props;
return buttons
? <div className='ui--AddressRow-buttons'>{buttons}</div>
: null;
}
protected renderExplorer () {
const { value, withExplorer } = this.props;
if (!withExplorer) {
return null;
}
return (
<div className='ui--AddressRow-explorer'>
<LinkPolkascan
className='polkascan'
data={value}
type='address'
/>
</div>
);
}
protected renderName () {
const { isEditable } = this.props;
const { isEditingName, name } = this.state;
return isEditingName
? (
<Input
autoFocus
className='ui--AddressRow-name-input'
defaultValue={name}
onBlur={this.saveName}
onChange={this.onChangeName}
onEnter={this.saveName}
withLabel={false}
/>
)
: (
<div
className={classes('ui--AddressRow-name', isEditable && 'editable')}
onClick={isEditable ? this.toggleNameEditor : undefined}
>
{name}
{isEditable && this.renderEditIcon()}
</div>
);
}
protected onChangeName = (name: string) => {
this.setState({ name });
}
protected onChangeTags = (tags: string[]) => {
this.setState({ tags });
}
protected renderAccountIndex () {
const { accounts_idAndIndex = [], withIndex } = this.props;
const [, accountIndex] = accounts_idAndIndex;
if (!accountIndex || !withIndex) {
return null;
}
return (
<div className='ui--AddressRow-accountIndex'>
{accountIndex.toString()}
</div>
);
}
protected renderBalances () {
const { accounts_idAndIndex = [], withBalance } = this.props;
const [accountId] = accounts_idAndIndex;
if (!withBalance || !accountId) {
return null;
}
return (
<div className='ui--AddressRow-balances'>
<AddressInfo
value={accountId}
withBalance={withBalance}
/>
</div>
);
}
protected renderChildren () {
const { children } = this.props;
// we need children, or when an array, at least 1 non-empty value
const hasChildren = !children
? false
: Array.isArray(children)
? children.filter((child) => child).length !== 0
: true;
if (!hasChildren) {
return null;
}
return (
<div className='ui--AddressRow-children'>
{children}
</div>
);
}
protected renderEditIcon () {
return (
<Button
className='iconButton'
icon='edit'
size='mini'
isPrimary
key='unlock'
/>
);
}
protected renderIcon () {
const { accounts_idAndIndex = [], withIcon = true } = this.props;
const { address } = this.state;
const [accountId] = accounts_idAndIndex;
if (!withIcon) {
return null;
}
// Since we do queries to storage in the wrapped example, we don't want
// to follow that route if we don't have a valid address.
const Component = accountId
? IdentityIcon
: BaseIdentityIcon;
return (
<Component
className='ui--AddressRow-icon'
size={ICON_SIZE}
value={address}
/>
);
}
protected renderSaveIcon (callback: () => void) {
return (
<Button
className='saveButton'
onClick={callback}
icon='save'
size='small'
isPrimary
key='save'
/>
);
}
protected renderTags () {
const { isEditingTags, tags } = this.state;
const { isEditable, withTags = false } = this.props;
if (!withTags) {
return null;
}
return isEditingTags
? (
<InputTags
className='ui--AddressRow-tags-input'
onBlur={this.saveTags}
onChange={this.onChangeTags}
onClose={this.saveTags}
openOnFocus
defaultValue = {tags}
searchInput={{ autoFocus: true }}
value={tags}
withLabel={false}
/>
)
: (
<div
className={classes('ui--AddressRow-tags', isEditable && 'editable')}
onClick={isEditable ? this.toggleTagsEditor : undefined}
>
{
!tags.length
? (isEditable ? <span className='addTags'>add tags</span> : undefined)
: tags.map((tag) => (
<Label key={tag} size='tiny' color='grey'>{tag}</Label>
))
}
{isEditable && this.renderEditIcon()}
</div>
);
}
protected saveName = () => {
const { address, name } = this.state;
const trimmedName = name.trim();
const meta = {
name: trimmedName,
whenEdited: Date.now()
};
// Save only if the name was changed or if it's no empty.
if (trimmedName && address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingName: false });
}
}
protected saveTags = () => {
const { address, tags } = this.state;
const meta = {
tags,
whenEdited: Date.now()
};
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingTags: false });
}
}
protected toggleNameEditor = () => {
this.setState(({ isEditingName }) => ({
isEditingName: !isEditingName
}));
}
protected toggleTagsEditor = () => {
this.setState(({ isEditingTags }) => ({
isEditingTags: !isEditingTags
}));
}
}
export default translate(
export {
DEFAULT_ADDR,
AddressRow
};
export default withMulti(
styled(AddressRow as any)`
text-align: left;
&.inline {
display: flex;
.ui--AddressRow-children {
padding: 0 0 0 3rem;
}
}
&.invalid {
filter: grayscale(100);
opacity: 0.5;
}
button.ui.icon.editButton {
padding: 0em .3em .3em .3em;
color: #2e86ab;
background: none;
/*trick to let the button in the flow but keep the content centered regardless*/
margin-left: -2em;
position: relative;
right: -2.3em;
z-index: 1;
}
.ui--AddressRow-accountId,
.ui--AddressRow-accountIndex {
font-family: monospace;
font-size: 1.25em;
padding: 0;
margin-bottom: 0.25rem;
}
.ui--AddressRow-accountIndex {
font-style: italic;
}
.ui--AddressRow-balances {
.column {
display: block;
label,
.result {
display: inline-block;
}
}
> span {
text-align: left;
}
}
.ui--AddressRow-base {
display: flex;
}
.ui--AddressRow-buttons {
position: absolute;
right: 0.75rem;
top: 0.75rem;
}
.ui--AddressRow-children {
display: block;
padding-top: 1rem;
}
.ui--AddressRow-data {
margin: 0;
padding: 0.25rem 0 0;
white-space: nowrap;
* {
vertical-align: middle;
}
}
.ui--AddressRow-details {
white-space: nowrap;
}
.ui--AddressRow-explorer {
content: ' ';
margin-top: 1rem;
.polkascan {
position: absolute;
bottom: 0.75rem;
right: 1rem;
}
}
.ui--AddressRow-icon {
margin-right: 1em;
}
.ui--AddressRow-name {
box-sizing: border-box;
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: normal;
width: 17rem;
}
.ui--AddressRow-name-input {
input {
height: 1em;
text-transform: uppercase;
margin-top: -0.3em;
margin-bottom: -0.35em;
}
}
.ui--AddressRow-tags {
&.editable {
display: flex;
flex-wrap: wrap;
justify-content: left;
> span {
border: 1px #00000052 solid;
border-radius: .5em;
border-style: dashed;
color: grey;
font-size: x-small;
padding: .1em 0.3em 0.1em 0.3em;
margin-top: .2em;
}
> div.label {
margin-top:.3em
}
}
}
.ui--AddressRow-tags-input {
margin-bottom: -1.4em;
}
`,
translate,
withCalls<Props>(
['derive.accounts.idAndIndex', { paramName: 'value' }],
'query.session.validators'
)(AddressRow)
['derive.accounts.idAndIndex', { paramName: 'value' }]
)
);

View File

@ -1,435 +0,0 @@
// Copyright 2017-2019 @polkadot/ui-app 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 { AccountId, AccountIndex, Address } from '@polkadot/types';
import { I18nProps } from './types';
import BN from 'bn.js';
import { Label } from 'semantic-ui-react';
import React from 'react';
import { withCalls } from '@polkadot/ui-api';
import { Button, Input, InputTags } from '@polkadot/ui-app';
import BaseIdentityIcon from '@polkadot/ui-identicon';
import keyring from '@polkadot/ui-keyring';
import AddressInfo, { BalanceActiveType } from './AddressInfo';
import CopyButton from './CopyButton';
import IdentityIcon from './IdentityIcon';
import LinkPolkascan from './LinkPolkascan';
import translate from './translate';
import { classes, getAddrName, getAddrTags, toShortAddress } from './util';
export type Props = I18nProps & {
accounts_idAndIndex?: [AccountId?, AccountIndex?],
bonded?: BN | Array<BN>,
buttons?: React.ReactNode,
children?: React.ReactNode,
defaultName?: string,
extraInfo?: React.ReactNode,
identIconSize?: number,
isEditable?: boolean,
isInline?: boolean,
isShort?: boolean,
session_validators?: Array<AccountId>,
value: AccountId | AccountIndex | Address | string | null,
withBalance?: boolean | BalanceActiveType,
withExplorer?: boolean,
withIcon?: boolean,
withIndex?: boolean,
withTags?: boolean
};
type State = {
address: string,
isEditingName: boolean,
isEditingTags: boolean,
name: string,
tags: string[]
};
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
const ICON_SIZE = 64;
class AddressSummary extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = this.createState();
}
static defaultProps = {
defaultName: '<unknown>'
};
static getDerivedStateFromProps ({ accounts_idAndIndex = [], defaultName, value }: Props, prevState: State): State | null {
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
const address = accountId
? accountId.toString()
: DEFAULT_ADDR;
const name = getAddrName(address, false, defaultName) || '';
const tags = getAddrTags(address);
const state = { tags } as State;
let hasChanged = false;
if (address !== prevState.address) {
state.address = address;
hasChanged = true;
}
if (!prevState.isEditingName && name !== prevState.name) {
state.name = name;
hasChanged = true;
}
return hasChanged
? state
: null;
}
render () {
const { accounts_idAndIndex = [], className, isInline, style, withIndex } = this.props;
const [accountId, accountIndex] = accounts_idAndIndex;
const isValid = accountId || accountIndex;
return (
<div
className={classes('ui--AddressSummary', !isValid && 'invalid', isInline && 'inline', className)}
style={style}
>
<div className='ui--AddressSummary-base'>
{this.renderIcon()}
{this.renderButtons()}
<div className='ui--AddressSummary-data'>
{this.renderName()}
{this.renderAddress()}
{this.renderAccountIndex(withIndex)}
</div>
{this.renderBalances()}
{this.renderTags()}
</div>
{this.renderChildren()}
{this.renderExplorer()}
</div>
);
}
private createState () {
const { accounts_idAndIndex = [], defaultName, value } = this.props;
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
const address = accountId
? accountId.toString()
: DEFAULT_ADDR;
const name = getAddrName(address, false, defaultName) || '';
const tags = getAddrTags(address);
return {
address,
isEditingName: false,
isEditingTags: false,
name,
tags
};
}
protected renderAddress () {
const { isShort = true } = this.props;
const { address } = this.state;
const addrElem = isShort
? (
<CopyButton
isAddress
value={address}
>
<span>{toShortAddress(address)}</span>
</CopyButton>
)
: address;
return (
<>
<div className='ui--AddressSummary-name'>
{name}
</div>
<div className='ui--AddressSummary-accountId'>
{addrElem}
</div>
</>
);
}
protected renderButtons () {
const { buttons } = this.props;
return buttons
? <div className='ui--AddressSummary-buttons'>{buttons}</div>
: null;
}
protected renderExplorer () {
const { value, withExplorer } = this.props;
if (!withExplorer) {
return null;
}
return (
<div className='ui--AddressSummary-explorer'>
<LinkPolkascan
className='polkascan'
data={value}
type='address'
/>
</div>
);
}
protected renderName () {
const { isEditable } = this.props;
const { isEditingName, name } = this.state;
return isEditingName
? (
<Input
autoFocus
defaultValue={name}
onBlur={this.saveName}
onChange={this.onChangeName}
onEnter={this.saveName}
withLabel={false}
/>
)
: (
<div
className={classes('ui--AddressSummary-name', isEditable && 'editable')}
onClick={isEditable ? this.toggleNameEditor : undefined}
>
{name}
{isEditable && this.renderEditIcon()}
</div>
);
}
protected onChangeName = (name: string) => {
this.setState({ name });
}
protected onChangeTags = (tags: string[]) => {
this.setState({ tags });
}
protected renderAccountIndex (withIndex?: boolean) {
const { accounts_idAndIndex = [] } = this.props;
const [, accountIndex] = accounts_idAndIndex;
if (!accountIndex || !withIndex) {
return null;
}
return (
<div className='ui--AddressSummary-accountIndex'>
{accountIndex.toString()}
</div>
);
}
protected renderBalances () {
const { accounts_idAndIndex = [], withBalance } = this.props;
const [accountId] = accounts_idAndIndex;
if (!withBalance || !accountId) {
return null;
}
return (
<div className='ui--AddressSummary-balances'>
<AddressInfo
value={accountId}
withBalance={withBalance}
/>
</div>
);
}
protected renderChildren () {
const { children } = this.props;
// we need children, or when an array, at least 1 non-empty value
const hasChildren = !children
? false
: Array.isArray(children)
? children.filter((child) => child).length !== 0
: true;
if (!hasChildren) {
return null;
}
return (
<div className='ui--AddressSummary-children'>
{children}
</div>
);
}
protected renderEditIcon () {
return (
<Button
className='iconButton'
icon='edit'
size='mini'
isPrimary
key='unlock'
/>
);
}
protected renderIcon (className: string = 'ui--AddressSummary-icon', size?: number) {
const { accounts_idAndIndex = [], identIconSize = ICON_SIZE, withIcon = true } = this.props;
const { address } = this.state;
const [accountId] = accounts_idAndIndex;
if (!withIcon) {
return null;
}
// Since we do queries to storage in the wrapped example, we don't want
// to follow that route if we don't have a valid address.
const Component = accountId
? IdentityIcon
: BaseIdentityIcon;
return (
<Component
className={className}
size={size || identIconSize}
value={address}
/>
);
}
protected renderSaveIcon (callback: () => void) {
return (
<Button
className='saveButton'
onClick={callback}
icon='save'
size='small'
isPrimary
key='save'
/>
);
}
protected renderTags () {
const { isEditingTags, tags } = this.state;
const { isEditable, withTags = false } = this.props;
if (!withTags) {
return null;
}
const resultingDom = isEditingTags ?
<>
<InputTags
className='ui--AddressSummary-tags-input'
onBlur={this.saveTags}
onChange={this.onChangeTags}
onClose={this.saveTags}
openOnFocus
defaultValue = {tags}
searchInput={{ autoFocus: true }}
value={tags}
withLabel={false}
/>
</>
:
<div
className={classes('ui--AddressSummary-tags', isEditable && 'editable')}
onClick={isEditable ? this.toggleTagsEditor : undefined}
>
{
!tags.length
? (isEditable ? <span className='addTags'>add tags</span> : undefined)
: tags.map((tag) => {
return (
<Label key={tag} size='tiny' color='grey'>
{tag}
</Label>
);
})
}
{isEditable && this.renderEditIcon()}
</div>;
return resultingDom;
}
protected saveName = () => {
const { address, name } = this.state;
const trimmedName = name.trim();
const meta = {
name: trimmedName,
whenEdited: Date.now()
};
// Save only if the name was changed or if it's no empty.
if (trimmedName && address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingName: false });
}
}
protected saveTags = () => {
const { address, tags } = this.state;
const meta = {
tags,
whenEdited: Date.now()
};
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingTags: false });
}
}
protected toggleNameEditor = () => {
this.setState(({ isEditingName }) => ({
isEditingName: !isEditingName
}));
}
protected toggleTagsEditor = () => {
this.setState(({ isEditingTags }) => ({
isEditingTags: !isEditingTags
}));
}
}
export {
DEFAULT_ADDR,
AddressSummary
};
export default translate(
withCalls<Props>(
['derive.accounts.idAndIndex', { paramName: 'value' }]
)(AddressSummary)
);

View File

@ -43,15 +43,6 @@ export default styled(Card)`
}
}
.ui--AddressSummary-buttons {
text-align: right;
margin-bottom: 2em;
button {
margin-left: 0.2em;
}
}
@media (max-width: 2060px) {
min-width: 32%;
max-width: 32%;

View File

@ -0,0 +1,35 @@
// Copyright 2017-2019 @polkadot/ui-app 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 styled from 'styled-components';
type Props = {
children: React.ReactNode,
className?: string
};
class Columar extends React.PureComponent<Props> {
render () {
const { children, className } = this.props;
return (
<div className={`ui--Columnar ${className}`}>
{children}
</div>
);
}
}
export default styled(Columar)`
display: flex;
flex-wrap: wrap;
.ui--Column {
@media (min-width: 1025px) {
max-width: 50%;
min-width: 50%;
}
}
`;

View File

@ -0,0 +1,53 @@
// Copyright 2017-2019 @polkadot/ui-app 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 styled from 'styled-components';
type Props = {
children: React.ReactNode,
className?: string,
header: React.ReactNode
};
class Column extends React.PureComponent<Props> {
render () {
const { children, className, header } = this.props;
return (
<div className={`ui--Column ${className}`}>
<h1>{header}</h1>
<article className='container'>
{children}
</article>
</div>
);
}
}
export default styled(Column)`
box-sizing: border-box;
flex: 1 1;
margin: 0;
padding: 0.5rem;
.container {
margin: 0;
padding: 0;
article {
border: none;
margin: 0;
}
article+article {
border-top: 1px solid #f2f2f2;
}
}
@media (min-width: 1025px) {
max-width: 50%;
min-width: 50%;
}
`;

View File

@ -5,7 +5,6 @@
export { default as AddressInfo } from './AddressInfo';
export { default as AddressMini } from './AddressMini';
export { default as AddressRow } from './AddressRow';
export { default as AddressSummary } from './AddressSummary';
export { default as Available } from './Available';
export { default as Balance } from './Balance';
export { default as Bonded } from './Bonded';
@ -16,6 +15,8 @@ export { default as Card } from './Card';
export { default as CardGrid } from './CardGrid';
export { default as CardSummary } from './CardSummary';
export { default as Chart } from './Chart';
export { default as Columar } from './Columar';
export { default as Column } from './Column';
export { default as CopyButton } from './CopyButton';
export { default as CryptoType } from './CryptoType';
export { default as Dropdown } from './Dropdown';

View File

@ -74,6 +74,7 @@ article {
box-sizing: border-box;
margin: 0.25rem;
padding: 1.25rem 1.5rem;
position: relative;
text-align: left;
&:hover {

View File

@ -83,20 +83,6 @@ header .ui--Button-Group {
}
}
.ui--AddressRow-base {
display: flex;
}
.ui--AddressRow-icon {
margin-right: 1em;
}
.ui--AddressSummary {
position: relative;
padding: 0 1em;
white-space: nowrap
}
button.ui.icon.iconButton {
padding: 0em .3em .3em .3em !important;
color: #2e86ab !important;
@ -111,138 +97,6 @@ button.ui.icon.iconButton {
cursor: pointer;
}
.ui--AddressSummary-tags.editable {
display: flex;
flex-wrap: wrap;
justify-content: center;
> span {
border: 1px #00000052 solid;
border-radius: .5em;
border-style: dashed;
color: grey;
font-size: x-small;
padding: .1em 0.3em 0.1em 0.3em;
margin-top: .2em;
}
> div.label {
margin-top:.3em
}
}
.ui--AddressSummary-name.editable > button.ui.icon.editButton,
.ui--AddressSummary-tags.editable > button.ui.icon.editButton {
padding: 0em .3em .3em .3em;
color: #2e86ab;
background: none;
/*trick to let the button in the flow but keep the content centered regardless*/
margin-left: -2em;
position: relative;
right: -2.3em;
z-index: 1;
}
.ui--AddressSummary-base {
padding: 0 1em;
text-align: center;
}
.ui--AddressSummary-base,
.ui--AddressSummary-children {
display: inline-block;
vertical-align: top;
}
.grow+.ui--AddressRow {
padding: 1rem 0 0 1rem;
}
.ui--AddressRow {
&.inline {
display: flex;
.ui--AddressSummary-children {
padding: 0 0 0 3rem;
}
}
.ui--AddressSummary-children {
display: block;
padding-top: 1rem;
}
.ui--AddressRow-details {
text-align: left;
white-space: nowrap;
.ui--AddressSummary-data,
.ui--AddressSummary-name {
margin: 0;
text-align: left;
}
.ui--AddressSummary-name {
padding: 0;
}
}
.ui--AddressSummary-balances {
.column {
display: block;
label,
.result {
display: inline-block;
}
}
> span {
text-align: left;
}
}
.ui--AddressSummary-tags.editable {
justify-content: left;
}
}
.ui--AddressSummary-explorer {
content: ' ';
margin-top: 1rem;
.polkascan {
position: absolute;
bottom: 0.75rem;
right: 1rem;
}
}
.ui--AddressSummary-buttons {
position: absolute;
right: 0.75rem;
top: 0.75rem;
}
.ui--AddressSummary .rx--updated {
background: transparent !important;
}
.ui--AddressSummary.invalid {
filter: grayscale(100);
opacity: 0.5;
}
.ui--AddressSummary-icon {
display: block
}
.ui--AddressSummary-data+.ui--AddressSummary-balance,
.ui--AddressSummary-data+.ui--AddressSummary-bonded,
.ui--AddressSummary-data+.ui--AddressSummary-nonce {
padding-top: 0.5em;
}
.ui--AddressMini-balances {
display: grid;
}
@ -255,60 +109,6 @@ button.ui.icon.iconButton {
text-align: right;
}
.ui--AddressSummary-balance.rx--updated,
.ui--AddressSummary-nonce.rx--updated {
opacity: 1;
}
.ui--AddressSummary .ui--AddressSummary-data {
margin-top: 0.5rem;
}
.ui--AddressSummary-data {
padding: 0;
text-align: center;
white-space: nowrap;
.ui--AddressSummary-name {
box-sizing: border-box;
margin: 0 auto;
padding: 0 1rem;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: normal;
width: 17rem;
}
}
.ui--AddressSummary-data * {
vertical-align: middle;
}
.ui--AddressSummary-accountId,
.ui--AddressSummary-accountIndex {
font-family: monospace;
font-size: 1.25em;
padding: 0;
margin-bottom: 0.25rem;
}
.ui--AddressSummary-accountIndex {
font-style: italic;
}
.ui--AddressSummary-data .ui--Input input {
height: 1em;
text-align: left;
text-transform: uppercase;
margin-top: -0.3em;
margin-bottom: -0.35em;
}
.ui--AddressSummary-tags-input {
margin-bottom: -1.4em;
}
.ui--Bubble.ui.label {
font-weight: normal;
margin: 0.25rem;

View File

@ -30,6 +30,7 @@ th.ui--media-small {
@media (min-width: 1281px) {
.ui--flex-large {
display: flex;
flex-wrap: wrap;
}
.ui--media-large {
@ -47,6 +48,7 @@ th.ui--media-small {
@media (min-width: 1025px) {
.ui--flex-medium {
display: flex;
flex-wrap: wrap;
}
.ui--media-medium {
@ -64,6 +66,7 @@ th.ui--media-small {
@media (min-width: 768px) {
.ui--flex-small {
display: flex;
flex-wrap: wrap;
}
.ui--media-small {