Merge pull request #4690 from blockscout/np-improve-pagination

Improve pagination
pull/5118/head
Victor Baranov 3 years ago committed by GitHub
commit e048147248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 70
      apps/block_scout_web/assets/css/components/_pagination_container.scss
  3. 369
      apps/block_scout_web/assets/js/lib/random_access_pagination.js
  4. 14
      apps/block_scout_web/assets/js/pages/transactions.js
  5. 16
      apps/block_scout_web/lib/block_scout_web/chain.ex
  6. 76
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  7. 15
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex
  8. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex
  9. 15
      apps/block_scout_web/priv/gettext/default.pot
  10. 15
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  11. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
  12. 99
      apps/explorer/lib/explorer/chain.ex

@ -1,6 +1,7 @@
## Current ## Current
### Features ### Features
- [#4690](https://github.com/blockscout/blockscout/pull/4690) - Improve pagination: introduce pagination with random access to pages; Integrate it to the Transactions List page
### Fixes ### Fixes

@ -91,6 +91,7 @@ $pagination-page-link-color-active: #fff !default;
user-select: none; user-select: none;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
&:not(.no-hover):hover { &:not(.no-hover):hover {
@include pagination-container-base($pagination-page-link-background-active, $pagination-page-link-color-active); @include pagination-container-base($pagination-page-link-background-active, $pagination-page-link-color-active);
@ -112,9 +113,78 @@ $pagination-page-link-color-active: #fff !default;
pointer-events: none; pointer-events: none;
} }
} }
.page-link-light-hover {
&:not(.no-hover):hover {
@include pagination-container-base($pagination-page-link-background-active, $pagination-page-link-color-active);
background-color: rgba($pagination-page-link-background-active, 0.5);
border-color: rgba($pagination-page-link-background-active, 0.5);
}
}
}
}
.tb {
background-color: #00000000 !important;
border: none !important;
}
.ml10 {
margin-left: 10px;
}
.mlm17 {
@include media-breakpoint-down(sm) {
margin-left: -17px;
}
}
.mrm18 {
@include media-breakpoint-down(sm) {
margin-right: -18px;
} }
} }
.go-to {
@include media-breakpoint-down(sm) {
margin-top: 10px !important;
justify-content: end !important;
}
}
.limit {
color: $pagination-page-link-color;
font-size: 12px;
font-weight: 600;
height: 24px;
margin-top: -25px;
text-align: right;
@include media-breakpoint-down(sm) {
display: none;
}
}
.align-end {
align-self: end;
}
.page-number {
background-color: $pagination-page-link-background;
border-width: 1px;
border-style: solid;
border-radius: 2px;
border-color: $pagination-page-link-color;
color: $pagination-page-link-color;
}
.page-number:focus {
border: 1px solid $pagination-page-link-color;
}
.fml5 {
margin-left: 5px !important;
}
.top-pagination-outer-container { .top-pagination-outer-container {
display: flex; display: flex;
} }

@ -0,0 +1,369 @@
import $ from 'jquery'
import map from 'lodash/map'
import merge from 'lodash/merge'
import humps from 'humps'
import URI from 'urijs'
import listMorph from '../lib/list_morph'
import reduceReducers from 'reduce-reducers'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import '../app'
const maxPageNumberInOneLine = 7
const groupedPagesNumber = 3
/**
*
* This module is a clone of async_listing_load.js adapted for pagination with random access
*
*/
let enableFirstLoading = true
export const asyncInitialState = {
/* it will consider any query param in the current URI as paging */
beyondPageOne: false,
/* will be sent along with { type: 'JSON' } to controller, useful for dynamically changing parameters */
additionalParams: {},
/* an array with every html element of the list being shown */
items: [],
/* the key for diffing the elements in the items array */
itemKey: null,
/* represents whether a request is happening or not */
loading: false,
/* if there was an error fetching items */
requestError: false,
/* if response has no items */
emptyResponse: false,
/* current's page number */
currentPageNumber: 0
}
export function asyncReducer (state = asyncInitialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, {
nextPagePath: action.nextPagePath,
currentPagePath: action.nextPagePath
})
}
case 'ADD_ITEM_KEY': {
return Object.assign({}, state, { itemKey: action.itemKey })
}
case 'START_REQUEST': {
let pageNumber = state.currentPageNumber
if (action.pageNumber) { pageNumber = parseInt(action.pageNumber) }
return Object.assign({}, state, {
loading: true,
requestError: false,
currentPagePath: action.path,
currentPageNumber: pageNumber,
items: generateStub(state.items.length)
})
}
case 'REQUEST_ERROR': {
return Object.assign({}, state, { requestError: true })
}
case 'FINISH_REQUEST': {
return Object.assign({}, state, {
loading: false
})
}
case 'ITEMS_FETCHED': {
if (action.nextPageParams !== null) {
const pageNumber = parseInt(action.nextPageParams.pageNumber)
if (typeof action.path !== 'undefined') {
history.replaceState({}, null, URI(action.path).query(humps.decamelizeKeys(action.nextPageParams)))
}
delete action.nextPageParams.pageNumber
return Object.assign({}, state, {
requestError: false,
emptyResponse: action.items.length === 0,
items: action.items,
nextPageParams: humps.decamelizeKeys(action.nextPageParams),
pagesLimit: parseInt(action.nextPageParams.pagesLimit),
currentPageNumber: pageNumber,
beyondPageOne: pageNumber !== 1
})
}
return Object.assign({}, state, {
requestError: false,
emptyResponse: action.items.length === 0,
items: action.items,
nextPageParams: humps.decamelizeKeys(action.nextPageParams),
pagesLimit: 1,
currentPageNumber: 1,
beyondPageOne: false
})
}
default:
return state
}
}
export const elements = {
'[data-async-listing]': {
load ($el) {
const nextPagePath = $el.data('async-listing')
return { nextPagePath }
}
},
'[data-async-listing] [data-loading-message]': {
render ($el, state) {
if (state.loading) return $el.show()
$el.hide()
}
},
'[data-async-listing] [data-empty-response-message]': {
render ($el, state) {
if (
!state.requestError &&
(!state.loading) &&
state.items.length === 0
) {
return $el.show()
}
$el.hide()
}
},
'[data-async-listing] [data-error-message]': {
render ($el, state) {
if (state.requestError) return $el.show()
$el.hide()
}
},
'[data-async-listing] [data-items]': {
render ($el, state, oldState) {
if (state.items === oldState.items) return
if (state.itemKey) {
const container = $el[0]
const newElements = map(state.items, (item) => $(item)[0])
listMorph(container, newElements, { key: state.itemKey })
return
}
$el.html(state.items)
}
},
'[data-async-listing] [data-next-page-button]': {
render ($el, state) {
if (state.emptyResponse) {
return $el.hide()
}
$el.show()
if (state.requestError || state.currentPageNumber >= state.pagesLimit || state.loading) {
return $el.attr('disabled', 'disabled')
}
$el.attr('disabled', false)
$el.attr('href', state.nextPagePath)
}
},
'[data-async-listing] [data-prev-page-button]': {
render ($el, state) {
if (state.emptyResponse) {
return $el.hide()
}
$el.show()
if (state.requestError || state.currentPageNumber <= 1 || state.loading) {
return $el.attr('disabled', 'disabled')
}
$el.attr('disabled', false)
$el.attr('href', state.prevPagePath)
}
},
'[data-async-listing] [pages-numbers-container]': {
render ($el, state) {
if (typeof state.pagesLimit !== 'undefined') { pagesNumbersGenerate(state.pagesLimit, $el, state.currentPageNumber, state.loading) }
}
},
'[data-async-listing] [data-loading-button]': {
render ($el, state) {
if (state.loading) return $el.show()
$el.hide()
}
},
'[data-async-listing] [data-pagination-container]': {
render ($el, state) {
if (state.emptyResponse) {
return $el.hide()
}
$el.show()
}
},
'[csv-download]': {
render ($el, state) {
if (state.emptyResponse) {
return $el.hide()
}
return $el.show()
}
}
}
/**
* Create a store combining the given reducer and initial state with the async reducer.
*
* reducer: The reducer that will be merged with the asyncReducer to add async
* loading capabilities to a page. Any state changes in the reducer passed will be
* applied AFTER the asyncReducer.
*
* initialState: The initial state to be merged with the async state. Any state
* values passed here will overwrite the values on asyncInitialState.
*
* itemKey: it will be added to the state as the key for diffing the elements and
* adding or removing with the correct animation. Check list_morph.js for more informantion.
*/
export function createAsyncLoadStore (reducer, initialState, itemKey) {
const state = merge(asyncInitialState, initialState)
const store = createStore(reduceReducers(asyncReducer, reducer, state))
if (typeof itemKey !== 'undefined') {
store.dispatch({
type: 'ADD_ITEM_KEY',
itemKey
})
}
connectElements({ store, elements })
firstPageLoad(store)
return store
}
export function refreshPage (store) {
loadPageByNumber(store, store.getState().currentPageNumber)
}
export function loadPageByNumber (store, pageNumber) {
const path = $('[data-async-listing]').data('async-listing')
store.dispatch({ type: 'START_REQUEST', path, pageNumber })
if (URI(path).query() !== '' && typeof store.getState().nextPageParams === 'undefined') {
$.getJSON(path, { type: 'JSON' })
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED', path }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
} else {
$.getJSON(URI(path).path(), merge({ type: 'JSON', page_number: pageNumber }, store.getState().nextPageParams))
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED', path }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
}
}
function firstPageLoad (store) {
const $element = $('[data-async-listing]')
function loadItemsNext () {
loadPageByNumber(store, store.getState().currentPageNumber + 1)
}
function loadItemsPrev () {
loadPageByNumber(store, store.getState().currentPageNumber - 1)
}
if (enableFirstLoading) {
loadItemsNext()
}
$element.on('click', '[data-error-message]', (event) => {
event.preventDefault()
loadItemsNext()
})
$element.on('click', '[data-next-page-button]', (event) => {
event.preventDefault()
loadItemsNext()
})
$element.on('click', '[data-prev-page-button]', (event) => {
event.preventDefault()
loadItemsPrev()
})
$element.on('click', '[data-page-number]', (event) => {
event.preventDefault()
loadPageByNumber(store, event.target.dataset.pageNumber)
})
$element.on('submit', '[input-page-number-form]', (event) => {
event.preventDefault()
const $input = event.target.querySelector('#page-number')
const input = parseInt($input.value)
const loading = store.getState().loading
const pagesLimit = store.getState().pagesLimit
if (!isNaN(input) && input <= pagesLimit && !loading) { loadPageByNumber(store, input) }
if (!loading || isNaN(input) || input > pagesLimit) { $input.value = '' }
return false
})
}
const $element = $('[data-async-load]')
if ($element.length) {
if (Object.prototype.hasOwnProperty.call($element.data(), 'noFirstLoading')) {
enableFirstLoading = false
}
if (enableFirstLoading) {
const store = createStore(asyncReducer)
connectElements({ store, elements })
firstPageLoad(store)
}
}
function pagesNumbersGenerate (pagesLimit, $container, currentPageNumber, loading) {
let resultHTML = ''
if (pagesLimit < 1) { return }
if (pagesLimit <= maxPageNumberInOneLine) {
resultHTML = renderPaginationElements(1, pagesLimit, currentPageNumber, loading)
} else if (currentPageNumber < groupedPagesNumber) {
resultHTML += renderPaginationElements(1, groupedPagesNumber, currentPageNumber, loading)
resultHTML += renderPaginationElement('...', false, loading)
resultHTML += renderPaginationElement(pagesLimit, currentPageNumber === pagesLimit, loading)
} else if (currentPageNumber > pagesLimit - groupedPagesNumber) {
resultHTML += renderPaginationElement(1, currentPageNumber === 1, loading)
resultHTML += renderPaginationElement('...', false, loading)
resultHTML += renderPaginationElements(pagesLimit - groupedPagesNumber, pagesLimit, currentPageNumber, loading)
} else {
resultHTML += renderPaginationElement(1, currentPageNumber === 1, loading)
const step = parseInt(groupedPagesNumber / 2)
if (currentPageNumber - step - 1 === 2) {
resultHTML += renderPaginationElement(2, currentPageNumber === 2, loading)
} else if (currentPageNumber - step > 2) {
resultHTML += renderPaginationElement('...', false, loading)
}
resultHTML += renderPaginationElements(currentPageNumber - step, currentPageNumber + step, currentPageNumber, loading)
if (currentPageNumber + step + 1 === pagesLimit - 1) {
resultHTML += renderPaginationElement(pagesLimit - 1, pagesLimit - 1 === currentPageNumber, loading)
} else if (currentPageNumber + step < pagesLimit - 1) {
resultHTML += renderPaginationElement('...', false, loading)
}
resultHTML += renderPaginationElement(pagesLimit, currentPageNumber === pagesLimit, loading)
}
$container.html(resultHTML)
}
function renderPaginationElements (start, end, currentPageNumber, loading) {
let resultHTML = ''
for (let i = start; i <= end; i++) {
resultHTML += renderPaginationElement(i, i === currentPageNumber, loading)
}
return resultHTML
}
function renderPaginationElement (text, active, loading) {
return '<li class="page-item' + (active ? ' active' : '') + (text === '...' || loading ? ' disabled' : '') + '"><a class="page-link page-link-light-hover" data-page-number=' + text + '>' + text + '</a></li>'
}
function generateStub (size) {
const stub = '<div data-loading-message data-selector="loading-message" class="tile tile-type-loading"> <div class="row tile-body"> <div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column"> <span class="tile-label"> <span class="tile-loader tile-label-loader"></span> </span> <span class="tile-status-label ml-2 ml-md-0"> <span class="tile-loader tile-label-loader"></span> </span> </div> <div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0"> <span class="tile-loader tile-address-loader"></span> <span class="tile-loader tile-address-loader"></span> </div> <div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0 tile-bottom"> <span class="mr-2 mr-md-0 order-1"> <span class="tile-loader tile-label-loader"></span> </span> <span class="mr-2 mr-md-0 order-2"> <span class="tile-loader tile-label-loader"></span> </span> </div> </div> </div>'
return Array.from(Array(size > 10 ? 10 : 10), () => stub) // I decided to always put 10 lines in order to make page lighter
}

@ -4,7 +4,7 @@ import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { connectElements } from '../lib/redux_helpers' import { connectElements } from '../lib/redux_helpers'
import { createAsyncLoadStore } from '../lib/async_listing_load' import { createAsyncLoadStore } from '../lib/random_access_pagination'
import { batchChannel } from '../lib/utils' import { batchChannel } from '../lib/utils'
import '../app' import '../app'
@ -95,8 +95,12 @@ if ($transactionListPage.length) {
transactionsChannel.onError(() => store.dispatch({ transactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED' type: 'CHANNEL_DISCONNECTED'
})) }))
transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({ transactionsChannel.on('transaction', batchChannel((msgs) => {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', if (!store.getState().beyondPageOne && !store.getState().loading) {
msgs: humps.camelizeKeys(msgs) store.dispatch({
}))) type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})
}
}))
} }

@ -223,6 +223,22 @@ defmodule BlockScoutWeb.Chain do
[paging_options: Map.put(paging_options, key, value)] [paging_options: Map.put(paging_options, key, value)]
end end
def fetch_page_number(%{"page_number" => page_number_string}) do
case Integer.parse(page_number_string) do
{number, ""} ->
number
_ ->
1
end
end
def fetch_page_number(_), do: 1
def update_page_parameters(new_page_number, new_page_size, %PagingOptions{} = options) do
%PagingOptions{options | page_number: new_page_number, page_size: new_page_size}
end
def param_to_block_number(formatted_number) when is_binary(formatted_number) do def param_to_block_number(formatted_number) when is_binary(formatted_number) do
case Integer.parse(formatted_number) do case Integer.parse(formatted_number) do
{number, ""} -> {:ok, number} {number, ""} -> {:ok, number}

@ -1,7 +1,14 @@
defmodule BlockScoutWeb.TransactionController do defmodule BlockScoutWeb.TransactionController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] import BlockScoutWeb.Chain,
only: [
fetch_page_number: 1,
paging_options: 1,
next_page_params: 3,
update_page_parameters: 3,
split_list_by_page: 1
]
alias BlockScoutWeb.{AccessHelpers, Controller, TransactionView} alias BlockScoutWeb.{AccessHelpers, Controller, TransactionView}
alias Explorer.Chain alias Explorer.Chain
@ -10,30 +17,59 @@ defmodule BlockScoutWeb.TransactionController do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash @burn_address_hash burn_address_hash
@default_options [
necessity_by_association: %{
:block => :required,
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional
}
]
def index(conn, %{"type" => "JSON"} = params) do def index(conn, %{"type" => "JSON"} = params) do
options =
@default_options
|> Keyword.merge(paging_options(params))
full_options = full_options =
Keyword.merge( options
[ |> Keyword.put(
necessity_by_association: %{ :paging_options,
:block => :required, params
[created_contract_address: :names] => :optional, |> fetch_page_number()
[from_address: :names] => :optional, |> update_page_parameters(Chain.default_page_size(), Keyword.get(options, :paging_options))
[to_address: :names] => :optional
}
],
paging_options(params)
) )
transactions_plus_one = Chain.recent_collated_transactions(full_options) %{total_transactions_count: transactions_count, transactions: transactions_plus_one} =
{transactions, next_page} = split_list_by_page(transactions_plus_one) Chain.recent_collated_transactions_for_rap(full_options)
{transactions, next_page} =
if fetch_page_number(params) == 1 do
split_list_by_page(transactions_plus_one)
else
{transactions_plus_one, nil}
end
next_page_params =
if fetch_page_number(params) == 1 do
page_size = Chain.default_page_size()
next_page_path = pages_limit = transactions_count |> Kernel./(page_size) |> Float.ceil() |> trunc()
case next_page_params(next_page, transactions, params) do
nil ->
nil
next_page_params -> case next_page_params(next_page, transactions, params) do
transaction_path(conn, :index, Map.delete(next_page_params, "type")) nil ->
nil
next_page_params ->
next_page_params
|> Map.delete("type")
|> Map.delete("items_count")
|> Map.put("pages_limit", pages_limit)
|> Map.put("page_size", page_size)
|> Map.put("page_number", 1)
end
else
Map.delete(params, "type")
end end
json( json(
@ -49,7 +85,7 @@ defmodule BlockScoutWeb.TransactionController do
conn: conn conn: conn
) )
end), end),
next_page_path: next_page_path next_page_params: next_page_params
} }
) )
end end

@ -0,0 +1,15 @@
<div
class='pagination-container mlm17 mrm18 <%= if assigns[:position] == "top" do %>position-top<% end %> <%= if assigns[:position] == "bottom" do %>position-bottom<% end %>'
data-pagination-container>
<ul class="pagination align-end" pages-numbers-container>
</ul>
<ul class="pagination fml5 go-to">
<!-- Go to -->
<li class="page-link no-hover tb ml10" ><%= gettext("Go to")%></li>
<li class="page-item"><form input-page-number-form><input class="page-number" id="page-number" type=text size=3><input class="d-none" type=submit input-page-number></form></li>
</ul>
</div>
<%= if assigns[:showing_limit] do %>
<div class="limit mrm18" >(<%= gettext("Only the first")%> <%= assigns[:showing_limit] |> BlockScoutWeb.Cldr.Number.to_string! %> <%= gettext("elements are displayed")%>)
</div>
<% end %>

@ -11,7 +11,7 @@
<div class="card-body" data-async-listing="<%= @current_path %>"> <div class="card-body" data-async-listing="<%= @current_path %>">
<h1 class="card-title list-title-description"><%= gettext "Validated Transactions" %></h1> <h1 class="card-title list-title-description"><%= gettext "Validated Transactions" %></h1>
<div class="list-top-pagination-container-wrapper"> <div class="list-top-pagination-container-wrapper">
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "top", showing_limit: if Chain.transactions_available_count() == Chain.limit_shownig_transactions(), do: Chain.limit_shownig_transactions(), else: nil %>
</div> </div>
<div data-selector="channel-batching-message" class="d-none"> <div data-selector="channel-batching-message" class="d-none">
@ -37,7 +37,7 @@
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div> </div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "bottom" %>
</div> </div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/validated-transactions.js") %>"></script> <script defer data-cfasync="false" src="<%= static_path(@conn, "/js/validated-transactions.js") %>"></script>

@ -1242,6 +1242,11 @@ msgstr ""
msgid "Github" msgid "Github"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8
msgid "Go to"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:128 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128
msgid "GraphQL" msgid "GraphQL"
@ -1705,6 +1710,11 @@ msgstr ""
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13
msgid "Only the first"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:61 #: lib/block_scout_web/templates/address_contract/index.html.eex:61
msgid "Optimization enabled" msgid "Optimization enabled"
@ -3190,6 +3200,11 @@ msgstr ""
msgid "custom RPC" msgid "custom RPC"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13
msgid "elements are displayed"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
msgid "fallback" msgid "fallback"

@ -1242,6 +1242,11 @@ msgstr ""
msgid "Github" msgid "Github"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8
msgid "Go to"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:128 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128
msgid "GraphQL" msgid "GraphQL"
@ -1705,6 +1710,11 @@ msgstr ""
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13
msgid "Only the first"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:61 #: lib/block_scout_web/templates/address_contract/index.html.eex:61
msgid "Optimization enabled" msgid "Optimization enabled"
@ -3190,6 +3200,11 @@ msgstr ""
msgid "custom RPC" msgid "custom RPC"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13
msgid "elements are displayed"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
msgid "fallback" msgid "fallback"

@ -91,7 +91,7 @@ defmodule BlockScoutWeb.TransactionControllerTest do
conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"}))
assert conn |> json_response(200) |> Map.get("next_page_path") assert conn |> json_response(200) |> Map.get("next_page_params")
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

@ -104,6 +104,9 @@ defmodule Explorer.Chain do
# seconds # seconds
@check_bytecode_interval 86_400 @check_bytecode_interval 86_400
@limit_showing_transaсtions 10_000
@default_page_size 50
@typedoc """ @typedoc """
The name of an association on the `t:Ecto.Schema.t/0` The name of an association on the `t:Ecto.Schema.t/0`
""" """
@ -3245,6 +3248,61 @@ defmodule Explorer.Chain do
end end
end end
# RAP - random access pagination
@spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{
:total_transactions_count => non_neg_integer(),
:transactions => [Transaction.t()]
}
def recent_collated_transactions_for_rap(options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
total_transactions_count = transactions_available_count()
fetched_transactions =
if is_nil(paging_options.key) or paging_options.page_number == 1 do
paging_options.page_size
|> Kernel.+(1)
|> Transactions.take_enough()
|> case do
nil ->
transactions = fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association)
Transactions.update(transactions)
transactions
transactions ->
transactions
end
else
fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association)
end
%{total_transactions_count: total_transactions_count, transactions: fetched_transactions}
end
def default_page_size, do: @default_page_size
def fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) do
fetch_transactions_for_rap()
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|> handle_random_access_paging_options(paging_options)
|> join_associations(necessity_by_association)
|> preload([{:token_transfers, [:token, :from_address, :to_address]}])
|> Repo.all()
end
defp fetch_transactions_for_rap do
Transaction
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
end
def transactions_available_count do
Transaction
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|> limit(^@limit_showing_transaсtions)
|> Repo.aggregate(:count, :hash)
end
def fetch_recent_collated_transactions(paging_options, necessity_by_association) do def fetch_recent_collated_transactions(paging_options, necessity_by_association) do
paging_options paging_options
|> fetch_transactions() |> fetch_transactions()
@ -4311,6 +4369,47 @@ defmodule Explorer.Chain do
|> limit(^paging_options.page_size) |> limit(^paging_options.page_size)
end end
defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}],
do: limit(query, ^(@default_page_size + 1))
defp handle_random_access_paging_options(query, paging_options) do
query
|> (&if(paging_options |> Map.get(:page_number, 1) |> proccess_page_number() == 1,
do: &1,
else: page_transaction(&1, paging_options)
)).()
|> handle_page(paging_options)
end
defp handle_page(query, paging_options) do
page_number = paging_options |> Map.get(:page_number, 1) |> proccess_page_number()
page_size = Map.get(paging_options, :page_size, @default_page_size)
cond do
page_in_bounds?(page_number, page_size) && page_number == 1 ->
query
|> limit(^(page_size + 1))
page_in_bounds?(page_number, page_size) ->
query
|> limit(^page_size)
|> offset(^((page_number - 2) * page_size))
true ->
query
|> limit(^(@default_page_size + 1))
end
end
defp proccess_page_number(number) when number < 1, do: 1
defp proccess_page_number(number), do: number
defp page_in_bounds?(page_number, page_size),
do: page_size <= @limit_showing_transaсtions && @limit_showing_transaсtions - page_number * page_size >= 0
def limit_shownig_transactions, do: @limit_showing_transaсtions
defp join_association(query, [{association, nested_preload}], necessity) defp join_association(query, [{association, nested_preload}], necessity)
when is_atom(association) and is_atom(nested_preload) do when is_atom(association) and is_atom(nested_preload) do
case necessity do case necessity do

Loading…
Cancel
Save