Refresh staking page top panel when new blocks arrive (#2407)

Top panel is entirely rendered on server-side using a dedicated controller
function for proper gettext support and reduced fiddling with jQuery.

Reducer is simplified and made pure as per Redux guidelines.

Co-Authored-By: Anatoly Nikiforov <anatolyniky@gmail.com>
Co-Authored-By: Kirill Andreev <hindmost.one@gmail.com>
staking
goodsoft 5 years ago committed by Victor Baranov
parent 26715dcf00
commit c64f77fe94
  1. 3
      apps/block_scout_web/assets/css/components/stakes/_stakes.scss
  2. 1
      apps/block_scout_web/assets/js/app.js
  3. 41
      apps/block_scout_web/assets/js/pages/stakes.js
  4. 22
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  5. 1
      apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex
  6. 18
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  7. 11
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  8. 1
      apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex
  9. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex
  10. 4
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex
  11. 13
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex
  12. 3
      apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex
  13. 20
      apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs
  14. 2
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  15. 4
      apps/explorer/lib/explorer/staking/contract_state.ex

@ -14,6 +14,7 @@ $stakes-stats-item-border-color: #fff !default;
.stakes-top-stats { .stakes-top-stats {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
@include stats-item($stakes-stats-item-border-color, $stakes-stats-item-color); @include stats-item($stakes-stats-item-border-color, $stakes-stats-item-color);
@ -66,7 +67,7 @@ $stakes-stats-item-border-color: #fff !default;
} }
.stakes-top-stats-login { .stakes-top-stats-login {
color: $primary; color: $secondary;
cursor: pointer; cursor: pointer;
margin-right: 8px; margin-right: 8px;
} }

@ -21,6 +21,7 @@ import './locale'
import './pages/layout' import './pages/layout'
import './pages/dark-mode-switcher' import './pages/dark-mode-switcher'
import './pages/stakes'
import './lib/clipboard_buttons' import './lib/clipboard_buttons'
import './lib/currency' import './lib/currency'

@ -1 +1,42 @@
import '../../css/stakes.scss' import '../../css/stakes.scss'
import $ from 'jquery'
import _ from 'lodash'
import { subscribeChannel } from '../socket'
import { connectElements } from '../lib/redux_helpers.js'
import { createAsyncLoadStore } from '../lib/async_listing_load'
export const initialState = {
channel: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_CONNECTED': {
return Object.assign({}, state, { channel: action.channel })
}
default:
return state
}
}
const elements = {
}
const $stakesPage = $('[data-page="stakes"]')
if ($stakesPage.length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierPool')
connectElements({ store, elements })
const channel = subscribeChannel('stakes:staking_update')
channel.on('staking_update', msg => onStakingUpdate(msg, store))
store.dispatch({ type: 'CHANNEL_CONNECTED', channel })
}
function onStakingUpdate (msg, store) {
$('[data-selector="stakes-top"]').html(msg.top_html)
}

@ -0,0 +1,22 @@
defmodule BlockScoutWeb.StakesChannel do
@moduledoc """
Establishes pub/sub channel for staking page live updates.
"""
use BlockScoutWeb, :channel
alias BlockScoutWeb.StakesController
intercept(["staking_update"])
def join("stakes:staking_update", _params, socket) do
{:ok, %{}, socket}
end
def handle_out("staking_update", _data, socket) do
push(socket, "staking_update", %{
top_html: StakesController.render_top()
})
{:noreply, socket}
end
end

@ -8,6 +8,7 @@ defmodule BlockScoutWeb.UserSocket do
channel("rewards:*", BlockScoutWeb.RewardChannel) channel("rewards:*", BlockScoutWeb.RewardChannel)
channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel)
channel("tokens:*", BlockScoutWeb.TokenChannel) channel("tokens:*", BlockScoutWeb.TokenChannel)
channel("stakes:*", BlockScoutWeb.StakesChannel)
def connect(%{"locale" => locale}, socket) do def connect(%{"locale" => locale}, socket) do
{:ok, assign(socket, :locale, locale)} {:ok, assign(socket, :locale, locale)}

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.StakesController do
alias BlockScoutWeb.StakesView alias BlockScoutWeb.StakesView
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Token alias Explorer.Chain.Token
alias Explorer.Counters.AverageBlockTime alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.ContractState alias Explorer.Staking.ContractState
@ -14,6 +15,19 @@ defmodule BlockScoutWeb.StakesController do
render_template(assigns.filter, conn, params) render_template(assigns.filter, conn, params)
end end
def render_top do
epoch_number = ContractState.get(:epoch_number, 0)
epoch_end_block = ContractState.get(:epoch_end_block, 0)
block_number = BlockNumber.get_max()
View.render_to_string(StakesView, "_stakes_top.html",
epoch_number: epoch_number,
epoch_end_in: epoch_end_block - block_number,
block_number: block_number,
logged_in: false
)
end
defp render_template(filter, conn, %{"type" => "JSON"} = params) do defp render_template(filter, conn, %{"type" => "JSON"} = params) do
[paging_options: options] = paging_options(params) [paging_options: options] = paging_options(params)
@ -69,8 +83,10 @@ defmodule BlockScoutWeb.StakesController do
defp render_template(filter, conn, _) do defp render_template(filter, conn, _) do
render(conn, "index.html", render(conn, "index.html",
top: render_top(),
pools_type: filter, pools_type: filter,
current_path: current_path(conn) current_path: current_path(conn),
average_block_time: AverageBlockTime.average_block_time()
) )
end end

@ -12,6 +12,7 @@ defmodule BlockScoutWeb.Notifier do
alias Explorer.Counters.AverageBlockTime alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion} alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion}
alias Explorer.Staking.ContractState
alias Phoenix.View alias Phoenix.View
def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do
@ -100,6 +101,16 @@ defmodule BlockScoutWeb.Notifier do
}) })
end end
def handle_event({:chain_event, :staking_update}) do
epoch_number = ContractState.get(:epoch_number, 0)
epoch_end_block = ContractState.get(:epoch_end_block, 0)
Endpoint.broadcast("stakes:staking_update", "staking_update", %{
epoch_number: epoch_number,
epoch_end_block: epoch_end_block
})
end
def handle_event({:chain_event, :internal_transactions, :realtime, internal_transactions}) do def handle_event({:chain_event, :internal_transactions, :realtime, internal_transactions}) do
internal_transactions internal_transactions
|> Stream.map( |> Stream.map(

@ -27,6 +27,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do
# Does not come from the indexer # Does not come from the indexer
Subscriber.to(:exchange_rate) Subscriber.to(:exchange_rate)
Subscriber.to(:transaction_stats) Subscriber.to(:transaction_stats)
Subscriber.to(:staking_update)
{:ok, []} {:ok, []}
end end

@ -1,4 +1,4 @@
<tr class=<%= if @pool.is_banned, do: "stakes-tr-banned" %>> <tr data-identifier-pool="<%= @index %>" class=<%= if @pool.is_banned, do: "stakes-tr-banned" %>>
<td class="stakes-td"><div class="stakes-td-order"><%= @index %></div></td> <td class="stakes-td"><div class="stakes-td-order"><%= @index %></div></td>
<td class="stakes-td"> <td class="stakes-td">
<%= <%=

@ -0,0 +1,4 @@
<div class="stakes-top-stats-item">
<span class="stakes-top-stats-label"><%= @title %></span>
<span class="stakes-top-stats-value"><%= @value %></span>
</div>

@ -0,0 +1,13 @@
<div class="stakes-top">
<div class="container">
<div class="stakes-top-stats">
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Epoch number"), value: @epoch_number %>
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Block number"), value: @block_number %>
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Next epoch in"), value: ngettext("%{blocks} block", "%{blocks} blocks", @epoch_end_in, blocks: @epoch_end_in) %>
<!-- Buttons -->
<div class="stakes-top-buttons">
</div>
</div>
</div>
</div>
<%= render BlockScoutWeb.StakesView, "_stakes_modal_become_candidate.html" %>

@ -1,3 +1,6 @@
<div data-selector="stakes-top">
<%= raw(@top) %>
</div>
<section data-page="stakes" class="container"> <section data-page="stakes" class="container">
<div class="card" data-async-load data-async-listing="<%= @current_path %>"> <div class="card" data-async-load data-async-listing="<%= @current_path %>">
<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %> <%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %>

@ -0,0 +1,20 @@
defmodule BlockScoutWeb.StakesChannelTest do
use BlockScoutWeb.ChannelCase
alias BlockScoutWeb.Notifier
test "subscribed user is notified of staking_update event" do
topic = "stakes:staking_update"
@endpoint.subscribe(topic)
Notifier.handle_event({:chain_event, :staking_update})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "staking_update", payload: %{epoch_number: _}} ->
assert true
after
:timer.seconds(5) ->
assert false, "Expected message received nothing."
end
end
end

@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a @allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a
@allowed_events ~w(exchange_rate transaction_stats)a @allowed_events ~w(exchange_rate transaction_stats staking_update)a
@type broadcast_type :: :realtime | :catchup | :on_demand @type broadcast_type :: :realtime | :catchup | :on_demand

@ -8,7 +8,7 @@ defmodule Explorer.Staking.ContractState do
use GenServer use GenServer
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.Events.{Publisher, Subscriber}
alias Explorer.SmartContract.Reader alias Explorer.SmartContract.Reader
alias Explorer.Staking.ContractReader alias Explorer.Staking.ContractReader
@ -192,6 +192,8 @@ defmodule Explorer.Staking.ContractState do
staking_pools_delegators: %{params: delegator_entries}, staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity timeout: :infinity
}) })
Publisher.broadcast(:staking_update)
end end
defp get_token(address) do defp get_token(address) do

Loading…
Cancel
Save