From c64f77fe9481c3a8dd9acb8aac1931c42faa0f84 Mon Sep 17 00:00:00 2001 From: goodsoft Date: Tue, 30 Jul 2019 22:01:59 +0300 Subject: [PATCH] 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 Co-Authored-By: Kirill Andreev --- .../assets/css/components/stakes/_stakes.scss | 3 +- apps/block_scout_web/assets/js/app.js | 1 + .../block_scout_web/assets/js/pages/stakes.js | 41 +++++++++++++++++++ .../channels/stakes_channel.ex | 22 ++++++++++ .../block_scout_web/channels/user_socket.ex | 1 + .../controllers/stakes_controller.ex | 18 +++++++- .../lib/block_scout_web/notifier.ex | 11 +++++ .../block_scout_web/realtime_event_handler.ex | 1 + .../templates/stakes/_rows.html.eex | 2 +- .../stakes/_stakes_stats_item.html.eex | 4 ++ .../templates/stakes/_stakes_top.html.eex | 13 ++++++ .../templates/stakes/index.html.eex | 3 ++ .../channels/stakes_channel_test.exs | 20 +++++++++ .../lib/explorer/chain/events/subscriber.ex | 2 +- .../lib/explorer/staking/contract_state.ex | 4 +- 15 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex create mode 100644 apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs diff --git a/apps/block_scout_web/assets/css/components/stakes/_stakes.scss b/apps/block_scout_web/assets/css/components/stakes/_stakes.scss index a1b8cc0c3e..97f1323116 100644 --- a/apps/block_scout_web/assets/css/components/stakes/_stakes.scss +++ b/apps/block_scout_web/assets/css/components/stakes/_stakes.scss @@ -14,6 +14,7 @@ $stakes-stats-item-border-color: #fff !default; .stakes-top-stats { display: flex; justify-content: space-between; + align-items: center; @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 { - color: $primary; + color: $secondary; cursor: pointer; margin-right: 8px; } diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index f8e6f65d77..08d09e9635 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -21,6 +21,7 @@ import './locale' import './pages/layout' import './pages/dark-mode-switcher' +import './pages/stakes' import './lib/clipboard_buttons' import './lib/currency' diff --git a/apps/block_scout_web/assets/js/pages/stakes.js b/apps/block_scout_web/assets/js/pages/stakes.js index dd379d254f..e2579c5edf 100644 --- a/apps/block_scout_web/assets/js/pages/stakes.js +++ b/apps/block_scout_web/assets/js/pages/stakes.js @@ -1 +1,42 @@ 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) +} diff --git a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex new file mode 100644 index 0000000000..29c1b6ecb0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex index 6060428a79..6fad138194 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.UserSocket do channel("rewards:*", BlockScoutWeb.RewardChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("tokens:*", BlockScoutWeb.TokenChannel) + channel("stakes:*", BlockScoutWeb.StakesChannel) def connect(%{"locale" => locale}, socket) do {:ok, assign(socket, :locale, locale)} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex index 81e598a932..3d77acfa36 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.StakesController do alias BlockScoutWeb.StakesView alias Explorer.Chain + alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Token alias Explorer.Counters.AverageBlockTime alias Explorer.Staking.ContractState @@ -14,6 +15,19 @@ defmodule BlockScoutWeb.StakesController do render_template(assigns.filter, conn, params) 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 [paging_options: options] = paging_options(params) @@ -69,8 +83,10 @@ defmodule BlockScoutWeb.StakesController do defp render_template(filter, conn, _) do render(conn, "index.html", + top: render_top(), pools_type: filter, - current_path: current_path(conn) + current_path: current_path(conn), + average_block_time: AverageBlockTime.average_block_time() ) end diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 2ce1d5f2e3..a3d4ae34f1 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.Notifier do alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion} + alias Explorer.Staking.ContractState alias Phoenix.View def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do @@ -100,6 +101,16 @@ defmodule BlockScoutWeb.Notifier do }) 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 internal_transactions |> Stream.map( diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index a02e656560..d556389209 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -27,6 +27,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do # Does not come from the indexer Subscriber.to(:exchange_rate) Subscriber.to(:transaction_stats) + Subscriber.to(:staking_update) {:ok, []} end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex index 85107a5e1d..7e656fd6b5 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex @@ -1,4 +1,4 @@ -> +>
<%= @index %>
<%= diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex new file mode 100644 index 0000000000..6ffc73265c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex @@ -0,0 +1,4 @@ +
+ <%= @title %> + <%= @value %> +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex new file mode 100644 index 0000000000..806c3694a0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex @@ -0,0 +1,13 @@ +
+
+
+ <%= 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) %> + +
+
+
+
+
+<%= render BlockScoutWeb.StakesView, "_stakes_modal_become_candidate.html" %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex index 3d0d44d7a3..4112ea6df9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex @@ -1,3 +1,6 @@ +
+ <%= raw(@top) %> +
<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %> diff --git a/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs new file mode 100644 index 0000000000..35a1046325 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs @@ -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 diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 02c7019f9c..bb4211e79b 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do @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 diff --git a/apps/explorer/lib/explorer/staking/contract_state.ex b/apps/explorer/lib/explorer/staking/contract_state.ex index 22c32dd5cc..3aebfc2564 100644 --- a/apps/explorer/lib/explorer/staking/contract_state.ex +++ b/apps/explorer/lib/explorer/staking/contract_state.ex @@ -8,7 +8,7 @@ defmodule Explorer.Staking.ContractState do use GenServer alias Explorer.Chain - alias Explorer.Chain.Events.Subscriber + alias Explorer.Chain.Events.{Publisher, Subscriber} alias Explorer.SmartContract.Reader alias Explorer.Staking.ContractReader @@ -192,6 +192,8 @@ defmodule Explorer.Staking.ContractState do staking_pools_delegators: %{params: delegator_entries}, timeout: :infinity }) + + Publisher.broadcast(:staking_update) end defp get_token(address) do