Unverified Commit 692e7ede authored by zeroleak's avatar zeroleak Committed by GitHub
Browse files

Merge pull request #24 from pajasevi/sortable-table

Sortable table
parents 35f71bec 366ea08f
......@@ -55,6 +55,10 @@ td.utxoMessage {
width: 7.5em;
}
.table-utxos th a {
cursor: pointer;
}
.table-utxos tr.utxo-disabled {
opacity: 0.5;
}
......
......@@ -4,7 +4,8 @@
*
*/
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import _ from 'lodash';
import mixService from '../../services/mixService';
import * as Icon from 'react-feather';
import utils, { MIXABLE_STATUS, UTXO_STATUS, WHIRLPOOL_ACCOUNTS } from '../../services/utils';
......@@ -13,16 +14,27 @@ import UtxoMixsTargetSelector from './UtxoMixsTargetSelector';
import UtxoPoolSelector from './UtxoPoolSelector';
import modalService from '../../services/modalService';
const UtxoControls = React.memo(({ utxo }) => {
return (
<div>
{utxo.account === WHIRLPOOL_ACCOUNTS.DEPOSIT && mixService.isTx0Possible(utxo) && <button className='btn btn-sm btn-primary' title='Send to Premix' onClick={() => modalService.openTx0(utxo)}>Premix <Icon.ArrowRight size={12}/></button>}
{mixService.isStartMixPossible(utxo) && utxo.mixableStatus === MIXABLE_STATUS.MIXABLE && <button className='btn btn-sm btn-primary' title='Start mixing' onClick={() => mixService.startMixUtxo(utxo)}>Mix <Icon.Play size={12} /></button>}
{mixService.isStartMixPossible(utxo) && utxo.mixableStatus !== MIXABLE_STATUS.MIXABLE && <button className='btn btn-sm btn-border' title='Add to queue' onClick={() => mixService.startMixUtxo(utxo)}><Icon.Plus size={12} />queue</button>}
{mixService.isStopMixPossible(utxo) && utxo.status === UTXO_STATUS.MIX_QUEUE && <button className='btn btn-sm btn-border' title='Remove from queue' onClick={() => mixService.stopMixUtxo(utxo)}><Icon.Minus size={12} />queue</button>}
{mixService.isStopMixPossible(utxo) && utxo.status !== UTXO_STATUS.MIX_QUEUE && <button className='btn btn-sm btn-primary' title='Stop mixing' onClick={() => mixService.stopMixUtxo(utxo)}>Stop <Icon.Square size={12} /></button>}
</div>
)
});
/* eslint-disable react/prefer-stateless-function */
class UtxosTable extends React.PureComponent {
copyToClipboard = (text: string) => {
const UtxosTable = ({ controls, account, utxos }) => {
const copyToClipboard = useCallback((text) => {
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', '');
el.style = { position: 'absolute', left: '-9999px' };
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
......@@ -30,70 +42,134 @@ class UtxosTable extends React.PureComponent {
document.execCommand('copy');
document.body.removeChild(el);
}
render () {
const controls = this.props.controls
return (
<div className='table-utxos'>
<table className="table table-sm table-hover">
<thead>
}, []);
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);
}
return sortedUtxos;
}, [utxos, sortBy, ascending]);
return (
<div className='table-utxos'>
<table className="table table-sm table-hover">
<thead>
<tr>
{this.props.account && <th scope="col">Account</th>}
{account && <th scope="col">
<a onClick={() => handleSetSort('account')}>
Account {sortBy === 'account' && (ascending ? "" : "")}
</a>
</th>}
<th scope="col"/>
<th scope="col" className='utxo'>
<a onClick={() => handleSetSort('path')}>
UTXO {sortBy === 'path' && (ascending ? "" : "")}
</a>
</th>
<th scope="col">
<a onClick={() => handleSetSort('value')}>
Amount {sortBy === 'value' && (ascending ? "" : "")}
</a>
</th>
<th scope="col">
<a onClick={() => handleSetSort('poolId')}>
Pool {sortBy === 'poolId' && (ascending ? "" : "")}
</a>
</th>
<th scope="col" className='utxoStatus'>
<a onClick={() => handleSetSort('status')}>
Status {sortBy === 'status' && (ascending ? "" : "")}
</a>
</th>
<th scope="col" />
<th scope="col" className='utxo'>UTXO</th>
<th scope="col">Amount</th>
<th scope="col">Pool</th>
<th scope="col" className='utxoStatus'>Status</th>
<th scope="col"></th>
<th scope="col">Mixs</th>
<th scope="col" colSpan={2}>Last activity</th>
{controls && <th scope="col" className='utxoControls'></th>}
<th scope="col">
<a onClick={() => handleSetSort('mixsDone')}>
Mixs {sortBy === 'mixsDone' && (ascending ? "" : "")}
</a>
</th>
<th scope="col" colSpan={2}>
<a onClick={() => handleSetSort('lastActivityElapsed')}>
Last activity {sortBy === 'lastActivityElapsed' && (ascending ? "" : "")}
</a>
</th>
{controls && <th scope="col" className='utxoControls' />}
</tr>
</thead>
<tbody>
{this.props.utxos.map((utxo,i) => {
const lastActivity = mixService.computeLastActivity(utxo)
const utxoReadOnly = utils.isUtxoReadOnly(utxo)
const allowNoPool = utxo.account === WHIRLPOOL_ACCOUNTS.DEPOSIT
return <tr key={i} className={utxoReadOnly?'utxo-disabled':''}>
{this.props.account && <td><small>{utxo.account}</small></td>}
</thead>
<tbody>
{sortedUtxos.map((utxo, i) => {
const lastActivity = mixService.computeLastActivity(utxo);
const utxoReadOnly = utils.isUtxoReadOnly(utxo);
const allowNoPool = utxo.account === WHIRLPOOL_ACCOUNTS.DEPOSIT;
return (
<tr key={i} className={utxoReadOnly ? 'utxo-disabled' : ''}>
{account && <td><small>{utxo.account}</small></td>}
<td>
<span title='Copy TX ID'><Icon.Clipboard className='clipboard-icon' tabIndex={0} size={18} onClick={() => this.copyToClipboard(utxo.hash)} /></span>
<span title='Copy TX ID'>
<Icon.Clipboard
className='clipboard-icon'
tabIndex={0}
size={18}
onClick={() => copyToClipboard(utxo.hash)}
/>
</span>
</td>
<td>
<small><span title={utxo.hash+':'+utxo.index}><LinkExternal href={utils.linkExplorer(utxo)}>{utxo.hash.substring(0,20)}...{utxo.hash.substring(utxo.hash.length-5)}:{utxo.index}</LinkExternal></span> · {utxo.path} · {utxo.confirmations>0?<span>{utxo.confirmations} confirms</span>:<strong>unconfirmed</strong>}</small>
<small>
<span title={utxo.hash + ':' + utxo.index}>
<LinkExternal href={utils.linkExplorer(utxo)}>
{utxo.hash.substring(0, 20)}...{utxo.hash.substring(utxo.hash.length - 5)}:{utxo.index}
</LinkExternal>
</span> · {utxo.path} · {utxo.confirmations > 0 ? (
<span>{utxo.confirmations} confirms</span>
) : (
<strong>unconfirmed</strong>
)}
</small>
</td>
<td>{utils.toBtc(utxo.value)}</td>
<td>{!utxoReadOnly && <UtxoPoolSelector utxo={utxo} noPool={allowNoPool}/>}
</td>
<td>{!utxoReadOnly && <span className='text-primary'>{utils.statusLabel(utxo)}</span>}</td>
<td></td>
<td>
{!utxoReadOnly && <span className='text-primary'>{utils.statusLabel(utxo)}</span>}
</td>
<td/>
<td>
{!utxoReadOnly && <UtxoMixsTargetSelector utxo={utxo}/>}
</td>
<td className='utxoMessage'>{!utxoReadOnly && <small>{utils.utxoMessage(utxo)}</small>}</td>
<td>{!utxoReadOnly && <small>{lastActivity ? lastActivity : '-'}</small>}</td>
<td>{!utxoReadOnly && controls && this.renderUtxoControls(utxo)}</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>
)
}
renderUtxoControls (utxo) {
return (
<div>
{utxo.account == WHIRLPOOL_ACCOUNTS.DEPOSIT && mixService.isTx0Possible(utxo) && <button className='btn btn-sm btn-primary' title='Send to Premix' onClick={() => modalService.openTx0(utxo)}>Premix <Icon.ArrowRight size={12}/></button>}
{mixService.isStartMixPossible(utxo) && utxo.mixableStatus === MIXABLE_STATUS.MIXABLE && <button className='btn btn-sm btn-primary' title='Start mixing' onClick={() => mixService.startMixUtxo(utxo)}>Mix <Icon.Play size={12} /></button>}
{mixService.isStartMixPossible(utxo) && utxo.mixableStatus !== MIXABLE_STATUS.MIXABLE && <button className='btn btn-sm btn-border' title='Add to queue' onClick={() => mixService.startMixUtxo(utxo)}><Icon.Plus size={12} />queue</button>}
{mixService.isStopMixPossible(utxo) && utxo.status === UTXO_STATUS.MIX_QUEUE && <button className='btn btn-sm btn-border' title='Remove from queue' onClick={() => mixService.stopMixUtxo(utxo)}><Icon.Minus size={12} />queue</button>}
{mixService.isStopMixPossible(utxo) && utxo.status !== UTXO_STATUS.MIX_QUEUE && <button className='btn btn-sm btn-primary' title='Stop mixing' onClick={() => mixService.stopMixUtxo(utxo)}>Stop <Icon.Square size={12} /></button>}
</div>
)
}
}
);
})}
</tbody>
</table>
</div>
);
};
export default UtxosTable
......@@ -119,7 +119,7 @@
"author": {
"name": "zeroleak",
"url": "https://github.com/zeroleak",
"email" : "zeroleak@samourai.io"
"email": "zeroleak@samourai.io"
},
"license": "Unlicense",
"bugs": {
......@@ -152,29 +152,29 @@
]
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-do-expressions": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.1.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-transform-react-constant-elements": "^7.0.0",
"@babel/plugin-transform-react-inline-elements": "^7.0.0",
"@babel/preset-env": "^7.1.6",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-do-expressions": "^7.8.3",
"@babel/plugin-proposal-export-default-from": "^7.8.3",
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
"@babel/plugin-proposal-function-bind": "^7.8.3",
"@babel/plugin-proposal-function-sent": "^7.8.3",
"@babel/plugin-proposal-json-strings": "^7.8.3",
"@babel/plugin-proposal-logical-assignment-operators": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-numeric-separator": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-proposal-pipeline-operator": "^7.8.3",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.8.3",
"@babel/plugin-transform-react-constant-elements": "^7.8.3",
"@babel/plugin-transform-react-inline-elements": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-flow": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@babel/register": "^7.8.3",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.9.0",
......@@ -182,72 +182,72 @@
"babel-plugin-dev-expression": "^0.2.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.20",
"chalk": "^3.0.0",
"concurrently": "^5.0.0",
"connected-react-router": "^6.6.1",
"concurrently": "^5.1.0",
"connected-react-router": "^6.7.0",
"cross-env": "^6.0.3",
"cross-spawn": "^7.0.1",
"css-loader": "^3.2.1",
"css-loader": "^3.4.2",
"detect-port": "^1.3.0",
"electron": "^7.1.7",
"electron-builder": "^21.2.0",
"electron-devtools-installer": "^2.2.4",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"enzyme-to-json": "^3.3.4",
"eslint": "^6.7.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.7.0",
"eslint-config-prettier": "^6.10.0",
"eslint-formatter-pretty": "^3.0.1",
"eslint-import-resolver-webpack": "^0.11.1",
"eslint-plugin-compat": "^3.3.0",
"eslint-plugin-flowtype": "^4.5.2",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jest": "^23.1.1",
"eslint-plugin-compat": "^3.5.1",
"eslint-plugin-flowtype": "^4.6.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jest": "^23.7.0",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-testcafe": "^0.2.1",
"fbjs-scripts": "^1.0.1",
"file-loader": "^5.0.2",
"file-loader": "^5.1.0",
"flow-bin": "^0.113.0",
"flow-runtime": "^0.17.0",
"flow-typed": "^2.5.1",
"husky": "^3.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.9.0",
"jest": "^25.1.0",
"lint-staged": "^9.5.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"node-sass": "^4.13.1",
"opencollective-postinstall": "^2.0.1",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"prettier": "^1.15.2",
"raw-loader": "^4.0.0",
"react-test-renderer": "^16.6.3",
"redux-logger": "^3.0.6",
"rimraf": "^3.0.0",
"sass-loader": "^8.0.0",
"rimraf": "^3.0.2",
"sass-loader": "^8.0.2",
"sinon": "^7.1.1",
"spectron": "^9.0.0",
"style-loader": "^1.0.1",
"style-loader": "^1.1.3",
"stylelint": "^12.0.0",
"stylelint-config-prettier": "^8.0.0",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-standard": "^19.0.0",
"terser-webpack-plugin": "^2.2.1",
"testcafe": "^1.7.0",
"terser-webpack-plugin": "^2.3.5",
"testcafe": "^1.8.2",
"testcafe-browser-provider-electron": "^0.0.13",
"testcafe-live": "^0.1.4",
"testcafe-react-selectors": "^3.0.0",
"url-loader": "^3.0.0",
"webpack": "^4.26.0",
"webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-merge": "^4.1.4"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.12",
"@fortawesome/free-solid-svg-icons": "^5.6.3",
"@fortawesome/react-fontawesome": "^0.1.4",
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"await-lock": "^2.0.1",
"bootstrap": "^4.3.1",
"crypto": "^1.0.1",
......@@ -256,8 +256,8 @@
"electron-dl": "^1.13.0",
"electron-log": "^3.0.1",
"electron-store": "^5.1.0",
"electron-updater": "^4.0.0",
"feather-icons": "^4.10.0",
"electron-updater": "^4.2.2",
"feather-icons": "^4.26.0",
"history": "^4.7.2",
"if-not-running": "^0.0.15",
"immer": "3.3.0",
......@@ -266,7 +266,7 @@
"md5ify": "^1.0.0",
"moment": "^2.24.0",
"node-fetch": "^2.3.0",
"popper.js": "^1.14.6",
"popper.js": "^1.16.1",
"ps-node": "^0.1.6",
"qrcode-decoder": "^0.1.2",
"qrcode.react": "^1.0.0",
......@@ -275,12 +275,12 @@
"react-dom": "^16.6.3",
"react-feather": "^2.0.3",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.12",
"react-redux": "^7.1.3",
"react-hot-loader": "^4.12.19",
"react-redux": "^7.2.0",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-webcam": "^4.0.0",
"redux": "^4.0.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"sjcl": "^1.0.8",
"source-map-support": "^0.5.9",
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment