Merge pull request #497 from poanetwork/live-reload-transactions-on-homepage-#35
Live reload transactions on homepage #35pull/508/head
commit
4ce9115f9a
@ -1,26 +1,134 @@ |
||||
import { reducer, initialState } from '../../js/pages/chain' |
||||
|
||||
test('CHANNEL_DISCONNECTED', () => { |
||||
test('RECEIVED_NEW_BLOCK', () => { |
||||
const state = Object.assign({}, initialState, { |
||||
newBlock: 'last new block' |
||||
}) |
||||
const action = { |
||||
type: 'RECEIVED_NEW_BLOCK', |
||||
msg: { |
||||
homepageBlockHtml: 'new block' |
||||
} |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
expect(output.newBlock).toEqual('new block') |
||||
}) |
||||
|
||||
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { |
||||
test('single transaction', () => { |
||||
const state = initialState |
||||
const action = { |
||||
type: 'CHANNEL_DISCONNECTED' |
||||
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||
msgs: [{ |
||||
transactionHtml: 'test' |
||||
}] |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
expect(output.channelDisconnected).toBe(true) |
||||
expect(output.newTransactions).toEqual(['test']) |
||||
expect(output.batchCountAccumulator).toEqual(0) |
||||
expect(output.transactionCount).toEqual(1) |
||||
}) |
||||
test('large batch of transactions', () => { |
||||
const state = initialState |
||||
const action = { |
||||
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||
msgs: [{ |
||||
transactionHtml: 'test 1' |
||||
},{ |
||||
transactionHtml: 'test 2' |
||||
},{ |
||||
transactionHtml: 'test 3' |
||||
},{ |
||||
transactionHtml: 'test 4' |
||||
},{ |
||||
transactionHtml: 'test 5' |
||||
},{ |
||||
transactionHtml: 'test 6' |
||||
},{ |
||||
transactionHtml: 'test 7' |
||||
},{ |
||||
transactionHtml: 'test 8' |
||||
},{ |
||||
transactionHtml: 'test 9' |
||||
},{ |
||||
transactionHtml: 'test 10' |
||||
},{ |
||||
transactionHtml: 'test 11' |
||||
}] |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
test('RECEIVED_NEW_BLOCK', () => { |
||||
expect(output.newTransactions).toEqual([]) |
||||
expect(output.batchCountAccumulator).toEqual(11) |
||||
expect(output.transactionCount).toEqual(11) |
||||
}) |
||||
test('single transaction after single transaction', () => { |
||||
const state = Object.assign({}, initialState, { |
||||
newBlock: 'last new block' |
||||
newTransactions: ['test 1'] |
||||
}) |
||||
const action = { |
||||
type: 'RECEIVED_NEW_BLOCK', |
||||
msg: { |
||||
homepageBlockHtml: 'new block' |
||||
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||
msgs: [{ |
||||
transactionHtml: 'test 2' |
||||
}] |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
expect(output.newTransactions).toEqual(['test 1', 'test 2']) |
||||
expect(output.batchCountAccumulator).toEqual(0) |
||||
}) |
||||
test('single transaction after large batch of transactions', () => { |
||||
const state = Object.assign({}, initialState, { |
||||
newTransactions: [], |
||||
batchCountAccumulator: 11 |
||||
}) |
||||
const action = { |
||||
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||
msgs: [{ |
||||
transactionHtml: 'test 12' |
||||
}] |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
expect(output.newTransactions).toEqual([]) |
||||
expect(output.batchCountAccumulator).toEqual(12) |
||||
}) |
||||
test('large batch of transactions after large batch of transactions', () => { |
||||
const state = Object.assign({}, initialState, { |
||||
newTransactions: [], |
||||
batchCountAccumulator: 11 |
||||
}) |
||||
const action = { |
||||
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||
msgs: [{ |
||||
transactionHtml: 'test 12' |
||||
},{ |
||||
transactionHtml: 'test 13' |
||||
},{ |
||||
transactionHtml: 'test 14' |
||||
},{ |
||||
transactionHtml: 'test 15' |
||||
},{ |
||||
transactionHtml: 'test 16' |
||||
},{ |
||||
transactionHtml: 'test 17' |
||||
},{ |
||||
transactionHtml: 'test 18' |
||||
},{ |
||||
transactionHtml: 'test 19' |
||||
},{ |
||||
transactionHtml: 'test 20' |
||||
},{ |
||||
transactionHtml: 'test 21' |
||||
},{ |
||||
transactionHtml: 'test 22' |
||||
}] |
||||
} |
||||
const output = reducer(state, action) |
||||
|
||||
expect(output.newBlock).toEqual('new block') |
||||
expect(output.newTransactions).toEqual([]) |
||||
expect(output.batchCountAccumulator).toEqual(22) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1,60 @@ |
||||
import $ from 'jquery' |
||||
import humps from 'humps' |
||||
import socket from '../socket' |
||||
import router from '../router' |
||||
import { updateAllAges } from '../lib/from_now' |
||||
import { initRedux } from '../utils' |
||||
|
||||
export const initialState = { |
||||
beyondPageOne: null, |
||||
channelDisconnected: false, |
||||
newBlock: null |
||||
} |
||||
|
||||
export function reducer (state = initialState, action) { |
||||
switch (action.type) { |
||||
case 'PAGE_LOAD': { |
||||
return Object.assign({}, state, { |
||||
beyondPageOne: !!action.blockNumber |
||||
}) |
||||
} |
||||
case 'CHANNEL_DISCONNECTED': { |
||||
if (state.beyondPageOne) return state |
||||
|
||||
return Object.assign({}, state, { |
||||
channelDisconnected: true |
||||
}) |
||||
} |
||||
case 'RECEIVED_NEW_BLOCK': { |
||||
if (state.channelDisconnected || state.beyondPageOne) return state |
||||
|
||||
return Object.assign({}, state, { |
||||
newBlock: action.msg.blockHtml |
||||
}) |
||||
} |
||||
default: |
||||
return state |
||||
} |
||||
} |
||||
|
||||
router.when('/blocks', { exactPathMatch: true }).then(({ blockNumber }) => initRedux(reducer, { |
||||
main (store) { |
||||
const blocksChannel = socket.channel(`blocks:new_block`, {}) |
||||
store.dispatch({ type: 'PAGE_LOAD', blockNumber }) |
||||
blocksChannel.join() |
||||
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) |
||||
blocksChannel.on('new_block', (msg) => |
||||
store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }) |
||||
) |
||||
}, |
||||
render (state, oldState) { |
||||
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') |
||||
const $blocksList = $('[data-selector="blocks-list"]') |
||||
|
||||
if (state.channelDisconnected) $channelDisconnected.show() |
||||
if (oldState.newBlock !== state.newBlock) { |
||||
$blocksList.prepend(state.newBlock) |
||||
updateAllAges() |
||||
} |
||||
} |
||||
})) |
@ -1,46 +1,99 @@ |
||||
import $ from 'jquery' |
||||
import humps from 'humps' |
||||
import numeral from 'numeral' |
||||
import 'numeral/locales' |
||||
import router from '../router' |
||||
import socket from '../socket' |
||||
import { updateAllAges } from '../lib/from_now' |
||||
import { initRedux } from '../utils' |
||||
import { batchChannel, initRedux } from '../utils' |
||||
|
||||
const BATCH_THRESHOLD = 10 |
||||
|
||||
export const initialState = { |
||||
batchCountAccumulator: 0, |
||||
newBlock: null, |
||||
channelDisconnected: false |
||||
newTransactions: [], |
||||
transactionCount: null |
||||
} |
||||
|
||||
export function reducer (state = initialState, action) { |
||||
switch (action.type) { |
||||
case 'CHANNEL_DISCONNECTED': { |
||||
case 'PAGE_LOAD': { |
||||
return Object.assign({}, state, { |
||||
channelDisconnected: true |
||||
transactionCount: numeral(action.transactionCount).value() |
||||
}) |
||||
} |
||||
case 'RECEIVED_NEW_BLOCK': { |
||||
return Object.assign({}, state, { |
||||
newBlock: humps.camelizeKeys(action.msg).homepageBlockHtml |
||||
newBlock: action.msg.homepageBlockHtml |
||||
}) |
||||
} |
||||
case 'RECEIVED_NEW_TRANSACTION_BATCH': { |
||||
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { |
||||
return Object.assign({}, state, { |
||||
newTransactions: [ |
||||
...state.newTransactions, |
||||
...action.msgs.map(({transactionHtml}) => transactionHtml) |
||||
], |
||||
transactionCount: state.transactionCount + action.msgs.length |
||||
}) |
||||
} else { |
||||
return Object.assign({}, state, { |
||||
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, |
||||
transactionCount: state.transactionCount + action.msgs.length |
||||
}) |
||||
} |
||||
} |
||||
default: |
||||
return state |
||||
} |
||||
} |
||||
|
||||
router.when('', { exactPathMatch: true }).then(() => initRedux(reducer, { |
||||
router.when('', { exactPathMatch: true }).then(({ locale }) => initRedux(reducer, { |
||||
main (store) { |
||||
const blocksChannel = socket.channel(`blocks:new_block`) |
||||
numeral.locale(locale) |
||||
store.dispatch({ |
||||
type: 'PAGE_LOAD', |
||||
transactionCount: $('[data-selector="transaction-count"]').text() |
||||
}) |
||||
blocksChannel.join() |
||||
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) |
||||
blocksChannel.on('new_block', msg => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg })) |
||||
blocksChannel.on('new_block', msg => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) |
||||
|
||||
const transactionsChannel = socket.channel(`transactions:new_transaction`) |
||||
transactionsChannel.join() |
||||
transactionsChannel.on('new_transaction', batchChannel((msgs) => |
||||
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) |
||||
) |
||||
}, |
||||
render (state, oldState) { |
||||
const $blockList = $('[data-selector="chain-block-list"]') |
||||
const $channelBatching = $('[data-selector="channel-batching-message"]') |
||||
const $channelBatchingCount = $('[data-selector="channel-batching-count"]') |
||||
const $transactionsList = $('[data-selector="transactions-list"]') |
||||
const $transactionCount = $('[data-selector="transaction-count"]') |
||||
|
||||
if (oldState.newBlock !== state.newBlock) { |
||||
$blockList.children().last().remove() |
||||
$blockList.prepend(state.newBlock) |
||||
updateAllAges() |
||||
} |
||||
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) |
||||
if (state.batchCountAccumulator) { |
||||
$channelBatching.show() |
||||
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() |
||||
} else { |
||||
$channelBatching.hide() |
||||
} |
||||
if (oldState.newTransactions !== state.newTransactions) { |
||||
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) |
||||
$transactionsList |
||||
.children() |
||||
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length) |
||||
.remove() |
||||
$transactionsList.prepend(newTransactionsToInsert.reverse().join('')) |
||||
|
||||
updateAllAges() |
||||
} |
||||
} |
||||
})) |
||||
|
@ -1,10 +1,33 @@ |
||||
defmodule ExplorerWeb.TransactionChannel do |
||||
@moduledoc """ |
||||
Establishes pub/sub channel for transaction page live updates. |
||||
Establishes pub/sub channel for live updates of transaction events. |
||||
""" |
||||
use ExplorerWeb, :channel |
||||
|
||||
def join("transactions:confirmations", _params, socket) do |
||||
alias ExplorerWeb.TransactionView |
||||
alias Phoenix.View |
||||
|
||||
intercept(["new_transaction"]) |
||||
|
||||
def join("transactions:new_transaction", _params, socket) do |
||||
{:ok, %{}, socket} |
||||
end |
||||
|
||||
def handle_out("new_transaction", %{transaction: transaction}, socket) do |
||||
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale) |
||||
|
||||
rendered_transaction = |
||||
View.render_to_string( |
||||
TransactionView, |
||||
"_tile.html", |
||||
locale: socket.assigns.locale, |
||||
transaction: transaction |
||||
) |
||||
|
||||
push(socket, "new_transaction", %{ |
||||
transaction_html: rendered_transaction |
||||
}) |
||||
|
||||
{:noreply, socket} |
||||
end |
||||
end |
||||
|
@ -0,0 +1,48 @@ |
||||
<div class="tile fade-up"> |
||||
<div class="row"> |
||||
<div class="col-md-6"> |
||||
<!-- block height --> |
||||
<%= link( |
||||
@block, |
||||
class: "tile-title", |
||||
to: block_path(ExplorerWeb.Endpoint, :show, @locale, @block), |
||||
"data-test": "block_number", |
||||
"data-block-number": to_string(@block.number) |
||||
) %> |
||||
<div> |
||||
<!-- transactions --> |
||||
<span class="mr-2"> |
||||
<%= ngettext("%{count} transaction", "%{count} transactions", Enum.count(@block.transactions)) %> |
||||
</span> |
||||
<!-- size --> |
||||
<span class="mr-2"> <%= Cldr.Unit.new(:byte, @block.size) |> Cldr.Unit.to_string! %> </span> |
||||
<!-- age --> |
||||
<span data-from-now="<%= @block.timestamp %>"></span> |
||||
</div> |
||||
<div class=""> |
||||
<!-- validator --> |
||||
<%= gettext "Miner" %> |
||||
<span class="ml-2"> |
||||
<%= link to: address_path(ExplorerWeb.Endpoint, :show, @locale, @block.miner_hash) do %> |
||||
<%= @block.miner_hash %> |
||||
<% end %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
<div class="col-md-6 text-right d-flex flex-column align-items-end justify-content-end"> |
||||
<!-- Gas Used --> |
||||
<div class=""> |
||||
<%= formatted_gas(@block.gas_used) %> |
||||
(<%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>) |
||||
<%= gettext "Gas Used" %> |
||||
</div> |
||||
<div class="progress w-25"> |
||||
<div class="progress-bar" role="progressbar" style="width: <%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"> |
||||
|
||||
</div> |
||||
</div> |
||||
<!-- Gas Limit --> |
||||
<span> <%= formatted_gas(@block.gas_limit) %> <%= gettext "Gas Limit" %> </span> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,42 +0,0 @@ |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<%= link(gettext("View All Transactions →"), to: transaction_path(@conn, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %> |
||||
<h2 class="card-title"><%= gettext "Transactions" %></h2> |
||||
<%= for transaction <- @chain.transactions do %> |
||||
<div class="tile tile-type-<%= ExplorerWeb.TransactionView.type_suffix(transaction) %> tile-status--<%= ExplorerWeb.TransactionView.status(transaction) %>" data-test="<%= ExplorerWeb.TransactionView.type_suffix(transaction) %>" data-transaction-hash="<%= transaction.hash %>"> |
||||
<div class="row" data-test="chain_transaction"> |
||||
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center"> |
||||
<div class="ml-4"> |
||||
<span class="tile-label" data-test="transaction_type"> <%= ExplorerWeb.TransactionView.transaction_display_type(transaction) %></span> |
||||
<div class="tile-status-label" data-test="transaction_status"><%= ExplorerWeb.TransactionView.formatted_status(transaction) %></div> |
||||
</div> |
||||
</div> |
||||
<div class="col-md-7 col-lg-8 d-flex flex-column"> |
||||
<%= render ExplorerWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: transaction.hash %> |
||||
<span class="text-nowrap"> |
||||
<%= render ExplorerWeb.AddressView, "_link.html", address_hash: transaction.from_address_hash, contract: ExplorerWeb.AddressView.contract?(transaction.from_address), locale: @locale %> |
||||
→ |
||||
<%= render ExplorerWeb.AddressView, "_link.html", address_hash: ExplorerWeb.TransactionView.to_address_hash(transaction), contract: ExplorerWeb.AddressView.contract?(transaction.to_address), locale: @locale %> |
||||
</span> |
||||
<span> |
||||
<span class="ml-1" data-from-now="<%= transaction.block.timestamp %>"></span> |
||||
<span class="ml-1"> |
||||
<%= link( |
||||
gettext("Block #%{number}", number: to_string(transaction.block.number)), |
||||
to: block_path(@conn, :show, @conn.assigns.locale, transaction.block) |
||||
) %> |
||||
</span> |
||||
</span> |
||||
</div> |
||||
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column justify-content-start text-md-right"> |
||||
<span class="tile-title"> |
||||
<%= ExplorerWeb.TransactionView.value(transaction, include_label: false) %> <%= gettext "Ether" %> |
||||
</span> |
||||
<span><%= ExplorerWeb.TransactionView.formatted_fee(transaction, denomination: :ether) %> <%= gettext "Fee" %></span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<% end %> |
||||
|
||||
</div> |
||||
</div> |
Loading…
Reference in new issue