Commit aac5f20c authored by zeroleak's avatar zeroleak
Browse files

hide non-mixable utxos + remove mixs-target + use react-table

parent 0aa65759
......@@ -2,7 +2,7 @@
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Icon from 'react-feather';
import utils, { MIXSTARGET_VALUES } from '../../services/utils';
import utils from '../../services/utils';
import mixService from '../../services/mixService';
import AbstractModal from './AbstractModal';
import poolsService from '../../services/poolsService';
......@@ -15,7 +15,6 @@ export default class Tx0Modal extends AbstractModal {
pools: undefined,
feeTarget: TX0_FEE_TARGET.BLOCKS_2.value,
poolId: props.utxo.poolId,
mixsTarget: props.utxo.mixsTargetOrDefault
}
super(props, 'modal-tx0', initialState)
......@@ -23,7 +22,6 @@ export default class Tx0Modal extends AbstractModal {
this.handleChangeFeeTarget = this.handleChangeFeeTarget.bind(this);
this.handleChangePoolTx0 = this.handleChangePoolTx0.bind(this);
this.handleChangeMixsTargetTx0 = this.handleChangeMixsTargetTx0.bind(this);
this.handleSubmitTx0 = this.handleSubmitTx0.bind(this)
this.fetchPoolsForTx0FeeTarget = this.fetchPoolsForTx0FeeTarget.bind(this)
this.setStateWithTx0Preview = this.setStateWithTx0Preview.bind(this)
......@@ -85,16 +83,8 @@ export default class Tx0Modal extends AbstractModal {
})
}
handleChangeMixsTargetTx0(e) {
const mixsTarget = parseInt(e.target.value)
this.setState({
mixsTarget: mixsTarget
})
}
handleSubmitTx0() {
mixService.tx0(this.props.utxo, this.state.feeTarget, this.state.poolId, this.state.mixsTarget)
mixService.tx0(this.props.utxo, this.state.feeTarget, this.state.poolId)
this.props.onClose();
}
......@@ -132,15 +122,6 @@ export default class Tx0Modal extends AbstractModal {
</select><br/>
{!this.isError() && <div>
Mixs target: (editable later)
<select className="form-control col-sm-2" onChange={this.handleChangeMixsTargetTx0} defaultValue={this.state.mixsTarget}>
{MIXSTARGET_VALUES.map(value => {
value = parseInt(value)
const label = utils.mixsTargetLabel(value)
return <option value={value}>{label}</option>
})}
</select><br/>
{this.state.tx0Preview && <div>
This will generate <strong>{this.state.tx0Preview.nbPremix} premixs</strong> of <strong>{utils.toBtc(this.state.tx0Preview.premixValue)} btc</strong> + <strong>{utils.toBtc(this.state.tx0Preview.changeValue)} btc</strong> change
</div>}
......
import React from 'react';
import BTable from 'react-bootstrap/Table';
import { usePagination, useSortBy, useTable } from 'react-table';
export default function TableGeneric({ columns, data, size='sm', /*onFetchData, pageIndex, pageSize, filters, */sortBy, getRowStyle=()=>{}, getRowClassName=()=>{} }) {
if (!data) {
return
}
const initialState = {};
if (sortBy) {
initialState.sortBy = sortBy;
}
// Use the state and functions returned from useTable to build your UI
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
initialState,
},
useSortBy,
);
// When these table states change, fetch new data!
/*React.useEffect(() => {
onFetchData({ pageIndex, pageSize, sortBy, filters })
}, [onFetchData, pageIndex, pageSize, sortBy, filters])*/
// Render the UI for your table
return (
<BTable hover size={size} {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
<span>
{column.isSorted ? (column.isSortedDesc ? '' : '') : ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps({
style: getRowStyle(row),
className:getRowClassName(row)
})}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
);
})}
</tbody>
</BTable>
);
}
/**
*
* Status
*
*/
import React from 'react';
import { Dropdown, DropdownButton } from 'react-bootstrap';
import mixService from '../../services/mixService';
import utils, { MIXSTARGET_VALUES } from '../../services/utils';
/* eslint-disable react/prefer-stateless-function */
class UtxoMixsTargetSelector extends React.PureComponent {
render () {
const utxo = this.props.utxo
return (
<DropdownButton size='sm' variant="default" title={utxo.mixsDone+' / '+utils.mixsTargetLabel(utxo.mixsTargetOrDefault)} className='utxoMixsTargetSelector'>
{MIXSTARGET_VALUES.map(value => {
value = parseInt(value)
const label = utils.mixsTargetLabel(value)
return <Dropdown.Item key={value} active={value === utxo.mixsTarget} onClick={() => mixService.setMixsTarget(utxo, value)}>{label}</Dropdown.Item>
})}
</DropdownButton>
)
}
}
export default UtxoMixsTargetSelector
......@@ -4,17 +4,17 @@
*
*/
import React, { useCallback, useMemo, useState } from 'react';
import _ from 'lodash';
import React, { useState } from 'react';
import mixService from '../../services/mixService';
import * as Icon from 'react-feather';
import utils, { MIXABLE_STATUS, UTXO_STATUS, WHIRLPOOL_ACCOUNTS } from '../../services/utils';
import LinkExternal from '../Utils/LinkExternal';
import UtxoMixsTargetSelector from './UtxoMixsTargetSelector';
import UtxoPoolSelector from './UtxoPoolSelector';
import modalService from '../../services/modalService';
import * as Icons from '@fortawesome/free-solid-svg-icons';
import {FormCheck} from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import TableGeneric from '../TableGeneric/TableGeneric';
const UtxoControls = React.memo(({ utxo }) => {
return (
......@@ -30,172 +30,137 @@ const UtxoControls = React.memo(({ utxo }) => {
/* eslint-disable react/prefer-stateless-function */
const UtxosTable = ({ controls, account, utxos }) => {
const copyToClipboard = useCallback((text) => {
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}, []);
const [sortBy, setSortBy] = useState('lastActivityElapsed');
const [ascending, setAscending] = useState(true);
const handleSetSort = useCallback((key) => {
if (sortBy === key) {
setAscending(!ascending);
} else {
setAscending(true);
}
setSortBy(key);
}, [sortBy, ascending]);
const sortedUtxos = useMemo(() => {
const sortedUtxos = _.sortBy(utxos, sortBy);
if (!ascending) {
return _.reverse(sortedUtxos);
const [showReadOnly, setShowReadOnly] = useState(false)
const isReadOnly = utxo => utils.isUtxoReadOnly(utxo) || mixService.getPoolsForUtxo(utxo).length == 0;
const columns = []
if (account) {
columns.push({
Header: 'Account',
accessor: o => o.account,
Cell: o => <small>{o.cell.value}</small>
})
}
columns.push(
{
Header: 'UTXO',
accessor: o => o.hash+':'+o.index,
Cell: o => {
const utxo = o.row.original
return <small>
<span title={utxo.hash + ':' + utxo.index}>
<LinkExternal href={utils.linkExplorer(utxo)}>
{utils.shorten(utxo.hash)}:{utxo.index}
</LinkExternal>
</span>{' '}
<span title='Copy TXID'>
<Icon.Clipboard
className='clipboard-icon'
size={18}
onClick={() => utils.copyToClipboard(utxo.hash)}
/>
</span>
</small>
}
},
{
Header: 'Address',
accessor: o => o.address,
Cell: o => {
const utxo = o.row.original
return <small>
<span title={utxo.address+'\n('+utxo.path+')'}>
<LinkExternal href={utils.linkExplorerAddress(utxo)}>
{utils.shorten(utxo.address)}
</LinkExternal>
</span>{' '}
<span title='Copy address'>
<Icon.Clipboard
className='clipboard-icon'
size={18}
onClick={() => copyToClipboard(utxo.address)}
/>
</span>
</small>
}
},
{
Header: 'Confs',
accessor: o => o.confirmations,
className: 'text-muted',
Cell: o => o.cell.value > 0 ? (
<small title="confirmations">{o.cell.value}</small>
) : (
<FontAwesomeIcon icon={Icons.faClock} size='xs' title='Unconfirmed'/>
)
},
{
Header: 'Amount',
accessor: o => o.value,
Cell: o => utils.toBtc(o.cell.value)
},
{
Header: 'Pool',
accessor: o => o.poolId,
Cell: o => {
const utxo = o.row.original
const allowNoPool = utxo.account === WHIRLPOOL_ACCOUNTS.DEPOSIT;
return !isReadOnly(utxo) && <UtxoPoolSelector utxo={utxo} noPool={allowNoPool}/>
}
},
{
Header: 'Mixs',
accessor: o => o.mixsDone,
Cell: o => !isReadOnly(o.row.original) && <span>{o.cell.value}</span>
},
{
Header: 'Status',
accessor: o => o.status,
Cell: o => !isReadOnly(o.row.original) && <span className='text-primary'>{utils.statusLabel(o.row.original)}</span>
},
{
Header: 'Last activity',
accessor: o => o.lastActivityElapsed,
Cell: o => {
const lastActivity = mixService.computeLastActivity(o.row.original);
return lastActivity ? lastActivity : '-'
}
},
{
Header: 'Info',
accessor: o => o.error,
className: 'utxoMessage',
Cell: o => !isReadOnly(o.row.original) && <small>{utils.utxoMessage(o.row.original)}</small>
}
return sortedUtxos;
}, [utxos, sortBy, ascending]);
const renderSort = sort => sortBy === sort && (ascending ? "" : "")
);
if (controls) {
columns.push({
id: 'utxoControls',
Header: '',
Cell: o => !isReadOnly(o.row.original) && <UtxoControls utxo={o.row.original}/>
})
}
const visibleUtxos = showReadOnly ? utxos : utxos.filter(utxo => !isReadOnly(utxo))
const utxosReadOnly = utxos.filter(utxo => isReadOnly(utxo))
const amountUtxosReadOnly = utxosReadOnly.map(utxo => utxo.value).reduce((total,current) => total+current, 0)
return (
<div className='table-utxos'>
<table className="table table-sm table-hover">
<thead>
<tr>
{account && <th scope="col" className='account'>
<a onClick={() => handleSetSort('account')}>
Account {renderSort('account')}
</a>
</th>}
<th scope="col" className='hash'>
<a onClick={() => handleSetSort('hash')}>
UTXO {renderSort('hash')}
</a>
</th>
<th scope="col" className='address'>
<a onClick={() => handleSetSort('address')}>
Address {renderSort('address')}
</a>
</th>
<th scope="col" className='confirmations'>
<a onClick={() => handleSetSort('confirmations')}>
Confs {renderSort('confirmations')}
</a>
</th>
<th scope="col" className='value'>
<a onClick={() => handleSetSort('value')}>
Amount {renderSort('value')}
</a>
</th>
<th scope="col" className='poolId'>
<a onClick={() => handleSetSort('poolId')}>
Pool {renderSort('poolId')}
</a>
</th>
<th scope="col" className='mixsDone'>
<a onClick={() => handleSetSort('mixsDone')}>
Mixs {renderSort('mixsDone')}
</a>
</th>
<th scope="col" className='utxoStatus'>
<a onClick={() => handleSetSort('status')}>
Status {renderSort('status')}
</a>
</th>
<th scope="col" colSpan={2}>
<a onClick={() => handleSetSort('lastActivityElapsed')}>
Last activity {renderSort('lastActivityElapsed')}
</a>
</th>
{controls && <th scope="col" className='utxoControls' />}
</tr>
</thead>
<tbody>
{sortedUtxos.map((utxo, i) => {
const lastActivity = mixService.computeLastActivity(utxo);
const utxoReadOnly = utils.isUtxoReadOnly(utxo) || mixService.getPoolsForUtxo(utxo).length == 0;
const allowNoPool = utxo.account === WHIRLPOOL_ACCOUNTS.DEPOSIT;
return (
<tr key={i} className={utxoReadOnly ? 'utxo-disabled' : ''}>
{account && <td><small>{utxo.account}</small></td>}
<td>
<small>
<span title={utxo.hash + ':' + utxo.index}>
<LinkExternal href={utils.linkExplorer(utxo)}>
{utils.shorten(utxo.hash)}:{utxo.index}
</LinkExternal>
</span>{' '}
<span title='Copy TXID'>
<Icon.Clipboard
className='clipboard-icon'
size={18}
onClick={() => copyToClipboard(utxo.hash)}
/>
</span>
</small>
</td>
<td>
<small>
<span title={utxo.address+'\n('+utxo.path+')'}>
<LinkExternal href={utils.linkExplorerAddress(utxo)}>
{utils.shorten(utxo.address)}
</LinkExternal>
</span>{' '}
<span title='Copy address'>
<Icon.Clipboard
className='clipboard-icon'
size={18}
onClick={() => copyToClipboard(utxo.address)}
/>
</span>
</small>
</td>
<td className='text-muted'>
{utxo.confirmations > 0 ? (
<small title="confirmations">{utxo.confirmations}</small>
) : (
<FontAwesomeIcon icon={Icons.faClock} size='xs' title='Unconfirmed'/>
)}
</td>
<td>{utils.toBtc(utxo.value)}</td>
<td>
{!utxoReadOnly && <UtxoPoolSelector utxo={utxo} noPool={allowNoPool}/>}
</td>
<td>
{!utxoReadOnly && <UtxoMixsTargetSelector utxo={utxo}/>}
</td>
<td>
{!utxoReadOnly && <span className='text-primary'>{utils.statusLabel(utxo)}</span>}
</td>
<td className='utxoMessage'>
{!utxoReadOnly && <small>{utils.utxoMessage(utxo)}</small>}
</td>
<td>
{!utxoReadOnly && <small>{lastActivity ? lastActivity : '-'}</small>}
</td>
<td>
{!utxoReadOnly && controls && <UtxoControls utxo={utxo}/>}
</td>
</tr>
);
})}
</tbody>
</table>
<div>
{utxosReadOnly.length>0 && <div className='text-center text-muted'>
<FormCheck id="showReadOnly" type="checkbox" label={<span>{utxosReadOnly.length} non-mixable utxos ({utils.toBtc(amountUtxosReadOnly)}btc)</span>} onClick={() => setShowReadOnly(!showReadOnly)} checked={showReadOnly}/>
</div>}
<div className='table-utxos'>
<TableGeneric
columns={columns}
data={visibleUtxos}
sortBy={[{ id: 'lastActivityElapsed', desc: true }]}
getRowClassName={row => isReadOnly(row.original) ? 'utxo-disabled' : ''}
/>
{visibleUtxos.length == 0 && <div className='text-center text-muted'><small>No utxo yet</small></div>}
</div>
</div>
);
};
......
......@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { Alert, Card } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as Icons from '@fortawesome/free-solid-svg-icons';
import { WHIRLPOOL_SERVER } from '../const';
import { logger } from '../utils/logger';
import { CliConfigService } from '../services/cliConfigService';
import cliService from '../services/cliService';
......@@ -98,15 +97,6 @@ export default class ConfigPage extends Component<Props> {
<Card>
<Card.Header>CLI General configuration</Card.Header>
<Card.Body>
<div className="form-group row">
<label htmlFor="mixsTarget" className="col-sm-2 col-form-label">Mixs target min</label>
<input type="number" className='form-control col-sm-3' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.mixsTarget = myValue)
}} defaultValue={cliConfig.mix.mixsTarget} id="mixsTarget"/>
<label className='col-form-label col-sm-5 text-muted'>Minimum number of mixs to achieve per UTXO</label>
</div>
<div className="form-group row">
<label htmlFor="autoMix" className="col-sm-2 col-form-label">Auto-MIX</label>
<div className="col-sm-10 custom-control custom-switch">
......@@ -172,7 +162,7 @@ export default class ConfigPage extends Component<Props> {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.tx0MaxOutputs = myValue)
}} defaultValue={cliConfig.mix.tx0MaxOutputs} id="tx0MaxOutputs"/>
<label className='col-form-label col-sm-5 text-muted'>Max premixes per TX0 (0 = no limit)</label>
<label className='col-form-label col-sm-5 text-muted'>Max premixes per TX0 (0 = max limit)</label>
</div>
{clientsPerPoolEditable && <div className="form-group row">
......@@ -196,16 +186,6 @@ export default class ConfigPage extends Component<Props> {
</label>
</div>
<div className="form-group row">
<label htmlFor="server" className="col-sm-2 col-form-label">Server</label>
<select className="col-sm-3 form-control" id="server" onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.server = myValue)
}} defaultValue={cliConfig.server}>
{Object.keys(WHIRLPOOL_SERVER).map((value) => <option value={value} key={value}>{WHIRLPOOL_SERVER[value]}</option>)}
</select>
</div>
</Card.Body>
</Card>}
<br/>
......
......@@ -39,7 +39,7 @@ class LastActivityPage extends Component {
<div className='lastActivityPage'>
<div className='row'>
<div className='col-sm-12'>
<h2>Last activity <FontAwesomeIcon icon={Icons.faCircleNotch} spin size='xs' /></h2>
<h2>Last activity</h2>
</div>
</div>
<div className='row h-100 d-flex flex-column'>
......
......@@ -153,8 +153,7 @@ code {
width: 100%;
}
.utxoPoolSelector .dropdown-item,
.utxoMixsTargetSelector .dropdown-item {
.utxoPoolSelector .dropdown-item {
padding: 0 0.4em;
}
......
......@@ -160,20 +160,18 @@ class BackendService {
})
)
},
tx0: (hash, index, feeTarget, poolId, mixsTarget) => {
tx0: (hash, index, feeTarget, poolId) => {
return this.withStatus('Utxo', 'New tx0', () =>
this.fetchBackendAsJson('/rest/utxos/'+hash+':'+index+'/tx0', 'POST', {
feeTarget: feeTarget,
poolId: poolId,
mixsTarget: mixsTarget
poolId: poolId
})
)
},
configure: (hash, index, poolId, mixsTarget) => {
configure: (hash, index, poolId) => {
return this.withStatus('Utxo', 'Configure utxo', () =>
this.fetchBackendAsJson('/rest/utxos/'+hash+':'+index, 'POST', {
poolId: poolId,
mixsTarget: mixsTarget
poolId: poolId
})
)
},
......
import ifNot from 'if-not-running';
import backendService from './backendService';
import utils, { TX0_MIN_CONFIRMATIONS, WHIRLPOOL_ACCOUNTS } from './utils';
import utils, { TX0_MIN_CONFIRMATIONS, UTXO_STATUS, WHIRLPOOL_ACCOUNTS } from './utils';
import poolsService from './poolsService';
import walletService from './walletService';
......@@ -75,17 +75,12 @@ class MixService {
return this.configure(utxo)
}