TxsTable wip

This commit is contained in:
mmv 2019-06-19 17:24:55 +04:00
parent c66c59c326
commit f126071669
12 changed files with 158 additions and 118 deletions

View File

@ -38,6 +38,7 @@
"axios": "0.19.0", "axios": "0.19.0",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
"connected-react-router": "^6.3.1", "connected-react-router": "^6.3.1",
"date-fns": "^1.30.1",
"final-form": "4.14.1", "final-form": "4.14.1",
"history": "^4.7.2", "history": "^4.7.2",
"immortal-db": "^1.0.2", "immortal-db": "^1.0.2",

View File

@ -100,6 +100,7 @@ class Layout extends React.Component<Props, State> {
activeTokens, activeTokens,
createTransaction, createTransaction,
fetchTransactions, fetchTransactions,
transactions,
} = this.props } = this.props
const { tabIndex } = this.state const { tabIndex } = this.state
@ -151,7 +152,9 @@ class Layout extends React.Component<Props, State> {
createTransaction={createTransaction} createTransaction={createTransaction}
/> />
)} )}
{tabIndex === 1 && <Transactions fetchTransactions={fetchTransactions} safeAddress={address} />} {tabIndex === 1 && (
<Transactions transactions={transactions} fetchTransactions={fetchTransactions} safeAddress={address} />
)}
</React.Fragment> </React.Fragment>
) )
} }

View File

@ -1,8 +1,10 @@
// @flow // @flow
import { format } from 'date-fns'
import { List } from 'immutable' import { List } from 'immutable'
import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { buildOrderFieldFrom, FIXED, type SortRow } from '~/components/Table/sorting' import { type SortRow } from '~/components/Table/sorting'
import { type Column } from '~/components/Table/TableHead' import { type Column } from '~/components/Table/TableHead'
import { getWeb3 } from '~/logic/wallets/getWeb3'
export const TX_TABLE_NONCE_ID = 'nonce' export const TX_TABLE_NONCE_ID = 'nonce'
export const TX_TABLE_TYPE_ID = 'type' export const TX_TABLE_TYPE_ID = 'type'
@ -10,51 +12,76 @@ export const TX_TABLE_DATE_ID = 'date'
export const TX_TABLE_AMOUNT_ID = 'amount' export const TX_TABLE_AMOUNT_ID = 'amount'
export const TX_TABLE_STATUS_ID = 'status' export const TX_TABLE_STATUS_ID = 'status'
type BalanceData = { const web3 = getWeb3()
asset: Object, const { toBN, fromWei } = web3.utils
balance: string,
type TxData = {
nonce: number,
type: string,
date: Date,
amount: number | string,
status: string,
} }
export type BalanceRow = SortRow<BalanceData> const formatDate = date => format(date, 'MMM D, YYYY - h:m:s')
export const getBalanceData = (activeTokens: List<Token>): List<BalanceRow> => { export type BalanceRow = SortRow<TxData>
const rows = activeTokens.map((token: Token) => ({
[BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri }, export const getTxTableData = (transactions: List<Transaction>): List<BalanceRow> => {
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name, const rows = transactions.map((tx: Transaction) => ({
[BALANCE_TABLE_BALANCE_ID]: `${token.balance} ${token.symbol}`, [TX_TABLE_NONCE_ID]: tx.nonce,
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance), [TX_TABLE_TYPE_ID]: 'Outgoing transfer',
[FIXED]: token.get('symbol') === 'ETH', [TX_TABLE_DATE_ID]: formatDate(tx.isExecuted ? tx.executionDate : tx.submissionDate),
[TX_TABLE_AMOUNT_ID]: Number(tx.value) > 0 ? fromWei(toBN(tx.value), 'ether') : 'n/a',
[TX_TABLE_STATUS_ID]: tx.isExecuted ? 'done' : 'peding',
})) }))
return rows return rows
} }
export const generateColumns = () => { export const generateColumns = () => {
const assetColumn: Column = { const nonceColumn: Column = {
id: BALANCE_TABLE_ASSET_ID, id: TX_TABLE_NONCE_ID,
order: true,
disablePadding: false, disablePadding: false,
label: 'Asset', label: 'Nonce',
custom: false, custom: false,
width: 250, order: false,
width: 50,
} }
const balanceColumn: Column = { const typeColumn: Column = {
id: BALANCE_TABLE_BALANCE_ID, id: TX_TABLE_TYPE_ID,
align: 'right', order: false,
order: true,
disablePadding: false, disablePadding: false,
label: 'Balance', label: 'Type',
custom: false,
width: 150,
}
const valueColumn: Column = {
id: TX_TABLE_AMOUNT_ID,
order: false,
disablePadding: false,
label: 'Amount',
custom: false,
width: 100,
}
const dateColumn: Column = {
id: TX_TABLE_DATE_ID,
order: false,
disablePadding: false,
label: 'Date',
custom: false, custom: false,
} }
const actions: Column = { const statusColumn: Column = {
id: 'actions', id: TX_TABLE_STATUS_ID,
order: false, order: false,
disablePadding: false, disablePadding: false,
label: '', label: '',
custom: true, custom: true,
} }
return List([assetColumn, balanceColumn, actions]) return List([nonceColumn, typeColumn, valueColumn, dateColumn, statusColumn])
} }

View File

@ -6,130 +6,64 @@ import { type Token } from '~/logic/tokens/store/model/token'
import CallMade from '@material-ui/icons/CallMade' import CallMade from '@material-ui/icons/CallMade'
import CallReceived from '@material-ui/icons/CallReceived' import CallReceived from '@material-ui/icons/CallReceived'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Checkbox from '@material-ui/core/Checkbox'
import TableRow from '@material-ui/core/TableRow' import TableRow from '@material-ui/core/TableRow'
import TableCell from '@material-ui/core/TableCell' import TableCell from '@material-ui/core/TableCell'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Block from '~/components/layout/Block'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import { type Column, cellWidth } from '~/components/Table/TableHead' import { type Column, cellWidth } from '~/components/Table/TableHead'
import Table from '~/components/Table' import Table from '~/components/Table'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { import {
getBalanceData, generateColumns, BALANCE_TABLE_ASSET_ID, type BalanceRow, filterByZero, getTxTableData, generateColumns, TX_TABLE_NONCE_ID, type BalanceRow,
} from './dataFetcher' } from './columns'
import { styles } from './style' import { styles } from './style'
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
export const BALANCE_ROW_TEST_ID = 'balance-row'
type State = { type State = {
hideZero: boolean,
showToken: boolean,
showReceive: boolean,
sendFunds: Object,
} }
type Props = { type Props = {
classes: Object, classes: Object,
granted: boolean, transactions: List<Transaction>,
tokens: List<Token>,
activeTokens: List<Token>,
safeAddress: string,
safeName: string,
etherScanLink: string,
ethBalance: string,
createTransaction: Function,
} }
type Action = 'Token' | 'Send' | 'Receive' type Action = 'Token' | 'Send' | 'Receive'
class Balances extends React.Component<Props, State> { class Balances extends React.Component<Props, State> {
state = { state = {}
hideZero: false,
showToken: false,
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showReceive: false,
}
onShow = (action: Action) => () => {
this.setState(() => ({ [`show${action}`]: true }))
}
onHide = (action: Action) => () => {
this.setState(() => ({ [`show${action}`]: false }))
}
showSendFunds = (token: Token) => {
this.setState({
sendFunds: {
isOpen: true,
selectedToken: token,
},
})
}
hideSendFunds = () => {
this.setState({
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
})
}
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
const { checked } = e.target
this.setState(() => ({ hideZero: checked }))
}
render() { render() {
const {
hideZero, showToken, showReceive, sendFunds,
} = this.state
const { const {
classes, classes,
granted, transactions,
tokens,
safeAddress,
activeTokens,
safeName,
etherScanLink,
ethBalance,
createTransaction,
} = this.props } = this.props
const columns = generateColumns() const columns = generateColumns()
const autoColumns = columns.filter(c => !c.custom) const autoColumns = columns.filter(c => !c.custom)
const checkboxClasses = { const filteredData = getTxTableData(transactions)
root: classes.root,
}
const filteredData = filterByZero(getBalanceData(activeTokens), hideZero)
return ( return (
<React.Fragment> <Block className={classes.container}>
<Table <Table
label="Balances" label="Transactions"
defaultOrderBy={BALANCE_TABLE_ASSET_ID} defaultOrderBy={TX_TABLE_NONCE_ID}
columns={columns} columns={columns}
data={filteredData} data={filteredData}
size={filteredData.size} size={filteredData.size}
defaultFixed defaultFixed
> >
{(sortedData: Array<BalanceRow>) => sortedData.map((row: any, index: number) => ( {(sortedData: Array<BalanceRow>) => sortedData.map((row: any, index: number) => (
<TableRow tabIndex={-1} key={index} className={classes.hide} data-testid={BALANCE_ROW_TEST_ID}> <TableRow tabIndex={-1} key={index} className={classes.row}>
{autoColumns.map((column: Column) => ( {autoColumns.map((column: Column) => (
<TableCell key={column.id} style={cellWidth(column.width)} align={column.align} component="td"> <TableCell key={column.id} className={classes.cell} style={cellWidth(column.width)} align={column.align} component="td">
{column.id === BALANCE_TABLE_ASSET_ID ? <AssetTableCell asset={row[column.id]} /> : row[column.id]} {row[column.id]}
</TableCell> </TableCell>
))} ))}
<TableCell component="td"> {/* <TableCell component="td">
<Row align="end" className={classes.actions}> <Row align="end" className={classes.actions}>
{granted && ( {granted && (
<Button <Button
@ -155,12 +89,12 @@ class Balances extends React.Component<Props, State> {
Receive Receive
</Button> </Button>
</Row> </Row>
</TableCell> </TableCell> */}
</TableRow> </TableRow>
)) ))
} }
</Table> </Table>
</React.Fragment> </Block>
) )
} }
} }

View File

@ -0,0 +1,55 @@
// @flow
import { xs, sm, md, lg } from '~/theme/variables'
export const styles = (theme: Object) => ({
container: {
marginTop: lg,
},
root: {
width: '20px',
marginRight: sm,
},
zero: {
letterSpacing: '-0.5px',
},
message: {
margin: `${sm} 0`,
},
actionIcon: {
marginRight: theme.spacing(1),
},
iconSmall: {
fontSize: 16,
},
cell: {
paddingTop: md,
paddingBottom: md,
},
row: {
'&:hover': {
backgroundColor: '#fff3e2',
},
},
actions: {
justifyContent: 'flex-end',
visibility: 'hidden',
},
send: {
minWidth: '0px',
marginRight: sm,
width: '70px',
},
receive: {
minWidth: '0px',
width: '95px',
},
leftIcon: {
marginRight: xs,
},
links: {
textDecoration: 'underline',
'&:hover': {
cursor: 'pointer',
},
},
})

View File

@ -1,12 +1,15 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { List } from 'immutable' import { List } from 'immutable'
import NoTransactions from '~/routes/safe/components/Transactions/NoTransactions' import NoTransactions from '~/routes/safe/components/TransactionsNew/NoTransactions'
import TxsTable from '~/routes/safe/components/TransactionsNew/TxsTable'
import { type Transaction } from '~/routes/safe/store/models/transaction'
type Props = { type Props = {
safeAddress: string, safeAddress: string,
threshold: number, threshold: number,
fetchTransactions: Function fetchTransactions: Function,
transactions: List<Transaction>,
} }
class Transactions extends React.Component<Props, {}> { class Transactions extends React.Component<Props, {}> {
@ -18,9 +21,11 @@ class Transactions extends React.Component<Props, {}> {
render() { render() {
const { transactions, safeName, threshold } = this.props const { transactions, safeName, threshold } = this.props
const hasTransactions = false const hasTransactions = transactions.size > 0
return <React.Fragment>{hasTransactions ? <div /> : <NoTransactions />}</React.Fragment> return (
<React.Fragment>{hasTransactions ? <TxsTable transactions={transactions} /> : <NoTransactions />}</React.Fragment>
)
} }
} }

View File

@ -61,6 +61,7 @@ class SafeView extends React.Component<Props> {
tokens, tokens,
createTransaction, createTransaction,
fetchTransactions, fetchTransactions,
transactions,
} = this.props } = this.props
return ( return (
@ -75,6 +76,7 @@ class SafeView extends React.Component<Props> {
granted={granted} granted={granted}
createTransaction={createTransaction} createTransaction={createTransaction}
fetchTransactions={fetchTransactions} fetchTransactions={fetchTransactions}
transactions={transactions}
/> />
</Page> </Page>
) )

View File

@ -13,8 +13,10 @@ import { type Safe } from '~/routes/safe/store/models/safe'
import { type Owner } from '~/routes/safe/store/models/owner' import { type Owner } from '~/routes/safe/store/models/owner'
import { type GlobalState } from '~/store' import { type GlobalState } from '~/store'
import { sameAddress } from '~/logic/wallets/ethAddresses' import { sameAddress } from '~/logic/wallets/ethAddresses'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors' import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
import { type Token } from '~/logic/tokens/store/model/token' import { type Token } from '~/logic/tokens/store/model/token'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance' import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
import { safeParamAddressSelector } from '../store/selectors' import { safeParamAddressSelector } from '../store/selectors'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
@ -27,6 +29,7 @@ export type SelectorProps = {
userAddress: string, userAddress: string,
network: string, network: string,
safeUrl: string, safeUrl: string,
transactions: List<Transaction>,
} }
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector( export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
@ -95,4 +98,5 @@ export default createStructuredSelector<Object, *>({
userAddress: userAccountSelector, userAddress: userAccountSelector,
network: networkSelector, network: networkSelector,
safeUrl: safeParamAddressSelector, safeUrl: safeParamAddressSelector,
transactions: safeTransactionsSelector,
}) })

View File

@ -54,6 +54,8 @@ const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel, saf
recipient: tx.to, recipient: tx.to,
data: tx.data ? tx.data : EMPTY_DATA, data: tx.data ? tx.data : EMPTY_DATA,
isExecuted: tx.isExecuted, isExecuted: tx.isExecuted,
submissionDate: tx.submissionDate,
executionDate: tx.executionDate,
}) })
} }

View File

@ -11,6 +11,8 @@ export type TransactionProps = {
recipient: string, recipient: string,
data: string, data: string,
isExecuted: boolean, isExecuted: boolean,
submissionDate: Date,
executionDate: Date,
} }
export const makeTransaction: RecordFactory<TransactionProps> = Record({ export const makeTransaction: RecordFactory<TransactionProps> = Record({
@ -21,6 +23,8 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
recipient: '', recipient: '',
data: '', data: '',
isExecuted: false, isExecuted: false,
submissionDate: '',
executionDate: '',
}) })
export type Transaction = RecordOf<TransactionProps> export type Transaction = RecordOf<TransactionProps>

View File

@ -23,17 +23,15 @@ type TransactionProps = {
transaction: Transaction, transaction: Transaction,
} }
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID] const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || '' export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
export const safeTransactionsSelector: Selector<GlobalState, SafeProps, List<Transaction>> = createSelector( export const safeTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
transactionsSelector, transactionsSelector,
safePropAddressSelector, safeParamAddressSelector,
(transactions: TransactionsState, address: string): List<Transaction> => { (transactions: TransactionsState, address: string): List<Transaction> => {
if (!transactions) { if (!transactions) {
return List([]) return List([])

View File

@ -5713,6 +5713,11 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-now@^0.1.4: date-now@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"