Refactor transactions page

As we did in other pages, we removed the infinite scroll and make the
first load async.
pull/1180/head
Felipe Renan 6 years ago
parent dd23bcbe3f
commit f9ea258ad0
  1. 115
      apps/block_scout_web/assets/__tests__/pages/transactions.js
  2. 48
      apps/block_scout_web/assets/js/pages/transactions.js
  3. 57
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  4. 49
      apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex
  5. 71
      apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs

@ -13,76 +13,63 @@ test('CHANNEL_DISCONNECTED', () => {
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => { test('single transaction', () => {
const state = initialState const state = Object.assign({}, initialState, { items: [] })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{ msgs: [{
transactionHtml: 'test' transactionHtml: 'transaction_html'
}] }]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([{ transactionHtml: 'test' }]) expect(output.items).toEqual(['transaction_html'])
expect(output.transactionsBatch.length).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
expect(output.transactionCount).toEqual(1) expect(output.transactionCount).toEqual(1)
}) })
test('large batch of transactions', () => { test('large batch of transactions', () => {
const state = initialState const state = Object.assign({}, initialState, { items: [] })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{ msgs: [
transactionHtml: 'test 1' { transactionHtml: 'transaction_html_1' },
},{ { transactionHtml: 'transaction_html_2' },
transactionHtml: 'test 2' { transactionHtml: 'transaction_html_3' },
},{ { transactionHtml: 'transaction_html_4' },
transactionHtml: 'test 3' { transactionHtml: 'transaction_html_5' },
},{ { transactionHtml: 'transaction_html_6' },
transactionHtml: 'test 4' { transactionHtml: 'transaction_html_7' },
},{ { transactionHtml: 'transaction_html_8' },
transactionHtml: 'test 5' { transactionHtml: 'transaction_html_9' },
},{ { transactionHtml: 'transaction_html_10' },
transactionHtml: 'test 6' { transactionHtml: 'transaction_html_11' },
},{ ]
transactionHtml: 'test 7'
},{
transactionHtml: 'test 8'
},{
transactionHtml: 'test 9'
},{
transactionHtml: 'test 10'
},{
transactionHtml: 'test 11'
}]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([]) expect(output.items).toEqual([])
expect(output.transactionsBatch.length).toEqual(11) expect(output.transactionsBatch.length).toEqual(11)
expect(output.transactionCount).toEqual(11) expect(output.transactionCount).toEqual(11)
}) })
test('single transaction after single transaction', () => { test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, { items: [ 'transaction_html' ] })
transactions: [{
transactionHtml: 'test 1'
}]
})
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{ msgs: [{
transactionHtml: 'test 2' transactionHtml: 'another_transaction_html'
}] }]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([ expect(output.items).toEqual([ 'another_transaction_html', 'transaction_html' ])
{ transactionHtml: 'test 2' },
{ transactionHtml: 'test 1' }
])
expect(output.transactionsBatch.length).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
}) })
test('single transaction after large batch of transactions', () => { test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11] items: [],
transactionsBatch: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -92,57 +79,51 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([]) expect(output.items).toEqual([])
expect(output.transactionsBatch.length).toEqual(12) expect(output.transactionsBatch.length).toEqual(12)
}) })
test('large batch of transactions after large batch of transactions', () => { test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11] items: [],
transactionsBatch: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{ msgs: [
transactionHtml: 'test 12' { transactionHtml: 'transaction_html_12' },
},{ { transactionHtml: 'transaction_html_13' },
transactionHtml: 'test 13' { transactionHtml: 'transaction_html_14' },
},{ { transactionHtml: 'transaction_html_15' },
transactionHtml: 'test 14' { transactionHtml: 'transaction_html_16' },
},{ { transactionHtml: 'transaction_html_17' },
transactionHtml: 'test 15' { transactionHtml: 'transaction_html_18' },
},{ { transactionHtml: 'transaction_html_19' },
transactionHtml: 'test 16' { transactionHtml: 'transaction_html_20' },
},{ { transactionHtml: 'transaction_html_21' },
transactionHtml: 'test 17' { transactionHtml: 'transaction_html_22' }
},{ ]
transactionHtml: 'test 18'
},{
transactionHtml: 'test 19'
},{
transactionHtml: 'test 20'
},{
transactionHtml: 'test 21'
},{
transactionHtml: 'test 22'
}]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([]) expect(output.items).toEqual([])
expect(output.transactionsBatch.length).toEqual(22) expect(output.transactionsBatch.length).toEqual(22)
}) })
test('after disconnection', () => { test('after disconnection', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
items: [],
channelDisconnected: true channelDisconnected: true
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{ msgs: [{
transactionHtml: 'test' transactionHtml: 'transaction_html'
}] }]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([]) expect(output.items).toEqual([])
expect(output.transactionsBatch.length).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
}) })
}) })

@ -3,25 +3,19 @@ import _ from 'lodash'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { createStore, connectElements } from '../lib/redux_helpers.js' import { connectElements } from '../lib/redux_helpers'
import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' import { createAsyncLoadStore } from '../lib/async_listing_load'
import { batchChannel } from '../lib/utils' import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
const BATCH_THRESHOLD = 10 const BATCH_THRESHOLD = 10
export const initialState = { export const initialState = {
channelDisconnected: false, channelDisconnected: false,
transactionCount: null, transactionCount: null,
transactions: [],
transactionsBatch: [] transactionsBatch: []
} }
export const reducer = withInfiniteScroll(baseReducer) export function reducer (state = initialState, action) {
function baseReducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'ELEMENTS_LOAD': { case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type')) return Object.assign({}, state, _.omit(action, 'type'))
@ -33,15 +27,15 @@ function baseReducer (state = initialState, action) {
}) })
} }
case 'RECEIVED_NEW_TRANSACTION_BATCH': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state if (state.channelDisconnected || state.beyondPageOne) return state
const transactionCount = state.transactionCount + action.msgs.length const transactionCount = state.transactionCount + action.msgs.length
if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) { if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
transactions: [ items: [
...action.msgs.reverse(), ...action.msgs.map(msg => msg.transactionHtml).reverse(),
...state.transactions ...state.items
], ],
transactionCount transactionCount
}) })
@ -55,14 +49,6 @@ function baseReducer (state = initialState, action) {
}) })
} }
} }
case 'RECEIVED_NEXT_PAGE': {
return Object.assign({}, state, {
transactions: [
...state.transactions,
...action.msg.transactions
]
})
}
default: default:
return state return state
} }
@ -90,30 +76,14 @@ const elements = {
if (oldState.transactionCount === state.transactionCount) return if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format()) $el.empty().append(numeral(state.transactionCount).format())
} }
},
'[data-selector="transactions-list"]': {
load ($el, store) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
} }
} }
const $transactionListPage = $('[data-page="transaction-list"]') const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) { if ($transactionListPage.length) {
const store = createStore(reducer) const store = createAsyncLoadStore(reducer, initialState, 'dataset.transactionHash')
connectElements({ store, elements }) connectElements({ store, elements })
connectInfiniteScroll(store)
const transactionsChannel = socket.channel(`transactions:new_transaction`) const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join() transactionsChannel.join()

@ -5,7 +5,6 @@ defmodule BlockScoutWeb.TransactionController do
alias BlockScoutWeb.TransactionView alias BlockScoutWeb.TransactionView
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Hash
alias Phoenix.View alias Phoenix.View
def index(conn, %{"type" => "JSON"} = params) do def index(conn, %{"type" => "JSON"} = params) do
@ -22,65 +21,42 @@ defmodule BlockScoutWeb.TransactionController do
paging_options(params) paging_options(params)
) )
{transactions, next_page} = get_transactions_and_next_page(full_options) transactions_plus_one = Chain.recent_collated_transactions(full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_url = next_page_path =
case next_page_params(next_page, transactions, params) do case next_page_params(next_page, transactions, params) do
nil -> nil ->
nil nil
next_page_params -> next_page_params ->
transaction_path( transaction_path(conn, :index, Map.delete(next_page_params, "type"))
conn,
:index,
next_page_params
)
end end
json( json(
conn, conn,
%{ %{
transactions: items:
Enum.map(transactions, fn transaction -> Enum.map(transactions, fn transaction ->
%{ View.render_to_string(
transaction_hash: Hash.to_string(transaction.hash), TransactionView,
transaction_html: "_tile.html",
View.render_to_string( transaction: transaction
TransactionView, )
"_tile.html",
transaction: transaction
)
}
end), end),
next_page_url: next_page_url next_page_path: next_page_path
} }
) )
end end
def index(conn, params) do def index(conn, _params) do
full_options =
Keyword.merge(
[
necessity_by_association: %{
:block => :required,
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional
}
],
paging_options(%{})
)
{transactions, next_page} = get_transactions_and_next_page(full_options)
transaction_estimated_count = Chain.transaction_estimated_count() transaction_estimated_count = Chain.transaction_estimated_count()
render( render(
conn, conn,
"index.html", "index.html",
next_page_params: next_page_params(next_page, transactions, params), current_path: current_path(conn),
transaction_estimated_count: transaction_estimated_count, transaction_estimated_count: transaction_estimated_count
transactions: transactions
) )
end end
@ -102,9 +78,4 @@ defmodule BlockScoutWeb.TransactionController do
redirect(conn, to: transaction_internal_transaction_path(conn, :index, id)) redirect(conn, to: transaction_internal_transaction_path(conn, :index, id))
end end
end end
defp get_transactions_and_next_page(options) do
transactions_plus_one = Chain.recent_collated_transactions(options)
split_list_by_page(transactions_plus_one)
end
end end

@ -1,6 +1,6 @@
<section class="container" data-page="transaction-list"> <section class="container" data-page="transaction-list">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body" data-async-listing="<%= @current_path %>">
<h1><%= gettext "Validated Transactions" %></h1> <h1><%= gettext "Validated Transactions" %></h1>
<p> <p>
@ -20,35 +20,38 @@
</div> </div>
</div> </div>
<span data-selector="transactions-list"> <button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<%= for transaction <- @transactions do %> <span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
<%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> </button>
<% end %>
</span> <div data-empty-response-message style="display: none;">
<div data-selector="loading-next-page" class="tile tile-muted text-center mt-3" style="display: none;"> <div class="tile tile-muted text-center">
<span data-selector="empty-internal-transactions-list">
<%= gettext "There are no transactions." %>
</span>
</div>
</div>
<div data-loading-message class="tile tile-muted text-center mt-3">
<span class="loading-spinner-small mr-2"> <span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span> <span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span> <span class="loading-spinner-block-2"></span>
</span> </span>
<%= gettext("Loading") %>... <%= gettext("Loading") %>...
</div> </div>
<div data-selector="paging-error-message" class="alert alert-danger text-center mt-3" style="display: none;">
<%= gettext("Error trying to fetch next page.") %>
</div>
<%= if @next_page_params do %> <div data-selector="transactions-list" data-items></div >
<%= link(
gettext("Older"), <a href="#" data-next-page-button class="button button-secondary button-small float-right mt-4" style="display: none;">
class: "button button-secondary button-sm float-right mt-3", <%= gettext("Older") %>
"data-selector": "next-page-button", </a>
to: transaction_path(
@conn,
:index,
@next_page_params
)
) %>
<% end %>
</div>
<div class="button button-secondary button-small float-right mt-4" data-loading-button style="display: none;">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<%= gettext("Loading") %>...
</div>
</div> </div>
</section> </section>

@ -7,14 +7,18 @@ defmodule BlockScoutWeb.TransactionControllerTest do
describe "GET index/2" do describe "GET index/2" do
test "returns a collated transactions", %{conn: conn} do test "returns a collated transactions", %{conn: conn} do
transaction = :transaction
:transaction |> insert()
|> insert() |> with_block()
|> with_block()
conn = get(conn, "/txs") conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"}))
transactions_html =
conn
|> json_response(200)
|> Map.get("items")
assert List.first(conn.assigns.transactions).hash == transaction.hash assert length(transactions_html) == 1
end end
test "returns a count of transactions", %{conn: conn} do test "returns a count of transactions", %{conn: conn} do
@ -28,16 +32,22 @@ defmodule BlockScoutWeb.TransactionControllerTest do
end end
test "excludes pending transactions", %{conn: conn} do test "excludes pending transactions", %{conn: conn} do
%Transaction{hash: hash} = %Transaction{hash: transaction_hash} =
:transaction :transaction
|> insert() |> insert()
|> with_block() |> with_block()
insert(:transaction) %Transaction{hash: pending_transaction_hash} = insert(:transaction)
conn = get(conn, "/txs") conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"}))
transactions_html =
conn
|> json_response(200)
|> Map.get("items")
assert [%Transaction{hash: ^hash}] = conn.assigns.transactions assert Enum.any?(transactions_html, &String.contains?(&1, to_string(transaction_hash)))
refute Enum.any?(transactions_html, &String.contains?(&1, to_string(pending_transaction_hash)))
end end
test "returns next page of results based on last seen transaction", %{conn: conn} do test "returns next page of results based on last seen transaction", %{conn: conn} do
@ -53,33 +63,34 @@ defmodule BlockScoutWeb.TransactionControllerTest do
|> with_block() |> with_block()
conn = conn =
get(conn, "/txs", %{ get(
"type" => "JSON", conn,
"block_number" => Integer.to_string(block_number), transaction_path(conn, :index, %{
"index" => Integer.to_string(index) "type" => "JSON",
}) "block_number" => Integer.to_string(block_number),
"index" => Integer.to_string(index)
{:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode() })
)
actual_hashes =
transactions transactions_html =
|> Enum.map(& &1["transaction_hash"]) conn
|> Enum.reverse() |> json_response(200)
|> Map.get("items")
assert second_page_hashes == actual_hashes
assert length(second_page_hashes) == length(transactions_html)
end end
test "next_page_params exist if not on last page", %{conn: conn} do test "next_page_params exist if not on last page", %{conn: conn} do
address = insert(:address) address = insert(:address)
block = %Block{number: number} = insert(:block) block = insert(:block)
60 60
|> insert_list(:transaction, from_address: address) |> insert_list(:transaction, from_address: address)
|> with_block(block) |> with_block(block)
conn = get(conn, "/txs") conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"}))
assert %{"block_number" => ^number, "index" => 10} = conn.assigns.next_page_params assert conn |> json_response(200) |> Map.get("next_page_path")
end end
test "next_page_params are empty if on last page", %{conn: conn} do test "next_page_params are empty if on last page", %{conn: conn} do
@ -89,15 +100,15 @@ defmodule BlockScoutWeb.TransactionControllerTest do
|> insert(from_address: address) |> insert(from_address: address)
|> with_block() |> with_block()
conn = get(conn, "/txs") conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"}))
refute conn.assigns.next_page_params refute conn |> json_response(200) |> Map.get("next_page_path")
end end
test "works when there are no transactions", %{conn: conn} do test "works when there are no transactions", %{conn: conn} do
conn = get(conn, "/txs") conn = get(conn, "/txs")
assert conn.assigns.transactions == [] assert html_response(conn, 200)
end end
end end

Loading…
Cancel
Save