diff --git a/apps/block_scout_web/assets/__tests__/pages/block.js b/apps/block_scout_web/assets/__tests__/pages/block.js index 0f50e9cf7f..f6f84976fb 100644 --- a/apps/block_scout_web/assets/__tests__/pages/block.js +++ b/apps/block_scout_web/assets/__tests__/pages/block.js @@ -1,13 +1,111 @@ import { reducer, initialState } from '../../js/pages/block' -test('RECEIVED_NEW_BLOCK', () => { + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState const action = { - type: 'RECEIVED_NEW_BLOCK', - msg: { - blockHtml: "test" - } + type: 'CHANNEL_DISCONNECTED' } - const output = reducer(initialState, action) + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) +}) + +describe('RECEIVED_NEW_BLOCK', () => { + test('receives new block', () => { + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test', + blockNumber: 1 + } + } + const output = reducer(initialState, action) + + expect(output.newBlock).toBe('test') + expect(output.blockNumbers).toEqual([1]) + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msgs: [{ + blockHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newBlock).toBe(null) + expect(output.blockNumbers).toEqual([]) + expect(output.skippedBlockNumbers).toEqual([]) + }) + test('inserts place holders if block received out of order', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [2] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test5', + blockNumber: 5 + } + } + const output = reducer(state, action) + + expect(output.newBlock).toBe('test5') + expect(output.blockNumbers).toEqual([5, 4, 3, 2]) + expect(output.skippedBlockNumbers).toEqual([3, 4]) + }) + test('replaces skipped block', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4, 3, 2, 1], + skippedBlockNumbers: [1, 3, 4] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test3', + blockNumber: 3 + } + } + const output = reducer(state, action) + + expect(output.newBlock).toBe('test3') + expect(output.blockNumbers).toEqual([5, 4, 3, 2, 1]) + expect(output.skippedBlockNumbers).toEqual([1, 4]) + }) + test('replaces duplicated block', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test5', + blockNumber: 5 + } + } + const output = reducer(state, action) + + expect(output.newBlock).toBe('test5') + expect(output.blockNumbers).toEqual([5, 4]) + }) + test('skips if new block height is lower than lowest on page', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4, 3, 2] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test1', + blockNumber: 1 + } + } + const output = reducer(state, action) - expect(output.newBlock).toBe("test") + expect(output.newBlock).toBe(null) + expect(output.blockNumbers).toEqual([5, 4, 3, 2]) + }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/chain.js b/apps/block_scout_web/assets/__tests__/pages/chain.js index dca639315b..d3bc264f05 100644 --- a/apps/block_scout_web/assets/__tests__/pages/chain.js +++ b/apps/block_scout_web/assets/__tests__/pages/chain.js @@ -15,22 +15,140 @@ test('RECEIVED_NEW_ADDRESS_COUNT', () => { expect(output.addressCount).toEqual('1,000,000') }) -test('RECEIVED_NEW_BLOCK', () => { - const state = Object.assign({}, initialState, { - averageBlockTime: '6 seconds', - newBlock: 'last new block' +describe('RECEIVED_NEW_BLOCK', () => { + test('receives new block', () => { + const state = Object.assign({}, initialState, { + averageBlockTime: '6 seconds', + blockNumbers: [1], + newBlock: 'last new block' + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + averageBlockTime: '5 seconds', + blockNumber: 2, + chainBlockHtml: 'new block' + } + } + const output = reducer(state, action) + + expect(output.averageBlockTime).toEqual('5 seconds') + expect(output.newBlock).toEqual('new block') + expect(output.blockNumbers).toEqual([2, 1]) }) - const action = { - type: 'RECEIVED_NEW_BLOCK', - msg: { + + test('inserts place holders if block received out of order', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [2] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + averageBlockTime: '5 seconds', + chainBlockHtml: 'test5', + blockNumber: 5 + } + } + const output = reducer(state, action) + + expect(output.averageBlockTime).toEqual('5 seconds') + expect(output.newBlock).toBe('test5') + expect(output.blockNumbers).toEqual([5, 4, 3, 2]) + expect(output.skippedBlockNumbers).toEqual([3, 4]) + }) + test('replaces skipped block', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [4, 3, 2, 1], + skippedBlockNumbers: [1, 2, 3] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + averageBlockTime: '5 seconds', + chainBlockHtml: 'test2', + blockNumber: 2 + } + } + const output = reducer(state, action) + + expect(output.averageBlockTime).toEqual('5 seconds') + expect(output.newBlock).toBe('test2') + expect(output.blockNumbers).toEqual([4, 3, 2, 1]) + expect(output.skippedBlockNumbers).toEqual([1, 3]) + }) + test('replaces duplicated block', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + averageBlockTime: '5 seconds', + chainBlockHtml: 'test5', + blockNumber: 5 + } + } + const output = reducer(state, action) + + expect(output.averageBlockTime).toEqual('5 seconds') + expect(output.newBlock).toBe('test5') + expect(output.blockNumbers).toEqual([5, 4]) + }) + test('skips if new block height is lower than lowest on page', () => { + const state = Object.assign({}, initialState, { averageBlockTime: '5 seconds', - chainBlockHtml: 'new block' + blockNumbers: [5, 4, 3, 2] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + averageBlockTime: '9 seconds', + chainBlockHtml: 'test1', + blockNumber: 1 + } } - } - const output = reducer(state, action) + const output = reducer(state, action) + + expect(output.averageBlockTime).toEqual('5 seconds') + expect(output.newBlock).toBe(null) + expect(output.blockNumbers).toEqual([5, 4, 3, 2]) + }) + test('only tracks 4 blocks based on page display limit', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4, 3, 2], + skippedBlockNumbers: [2, 3, 4] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + chainBlockHtml: 'test6', + blockNumber: 6 + } + } + const output = reducer(state, action) - expect(output.averageBlockTime).toEqual('5 seconds') - expect(output.newBlock).toEqual('new block') + expect(output.newBlock).toBe('test6') + expect(output.blockNumbers).toEqual([6, 5, 4, 3]) + expect(output.skippedBlockNumbers).toEqual([3, 4]) + }) + test('skipped blocks list replaced when another block comes in with +3 blockheight', () => { + const state = Object.assign({}, initialState, { + blockNumbers: [5, 4, 3, 2], + skippedBlockNumbers: [2, 3, 4] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + chainBlockHtml: 'test10', + blockNumber: 10 + } + } + const output = reducer(state, action) + + expect(output.newBlock).toBe('test10') + expect(output.blockNumbers).toEqual([10, 9, 8, 7]) + expect(output.skippedBlockNumbers).toEqual([7, 8, 9]) + }) }) test('RECEIVED_NEW_EXCHANGE_RATE', () => { diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js index b4ba0f62b7..ea4eb3595f 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transaction.js +++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js @@ -1,5 +1,16 @@ import { reducer, initialState } from '../../js/pages/transaction' +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) + expect(output.batchCountAccumulator).toBe(0) +}) + test('RECEIVED_NEW_BLOCK', () => { const state = { ...initialState, blockNumber: 1 } const action = { diff --git a/apps/block_scout_web/assets/css/components/_animations.scss b/apps/block_scout_web/assets/css/components/_animations.scss index 8318de83dc..b8fb3d1db4 100644 --- a/apps/block_scout_web/assets/css/components/_animations.scss +++ b/apps/block_scout_web/assets/css/components/_animations.scss @@ -8,20 +8,11 @@ 0% { flex-basis: 0%; width: 0%; - opacity: 0; - } - 25% { - opacity: 0; - transform: translateY(10px) scale(0.97); } 50% { flex-basis: 25%; width: 25%; } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - } } @keyframes fade-up { @@ -34,25 +25,7 @@ transform: translateY(10px) scale(0.97); } 50% { - height: 98px; - } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes fade-up--mobile { - 0% { - height: 0; - opacity: 0; - } - 25% { - opacity: 0; - transform: translateY(10px) scale(0.97); - } - 50% { - height: 202px; + height: 100px; } 100% { opacity: 1; @@ -67,7 +40,7 @@ } 100% { opacity: 0; - transform: scale(0.5); + transform: scale(0.75); } } @@ -80,22 +53,17 @@ max-height: 98px; animation: fade-up-blocks-chain 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955); - @media (max-width: 767px) { - animation: fade-up--mobile 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955); + @include media-breakpoint-down(md) { + animation: none; } } .fade-up { will-change: transform, opacity, height; - max-height: 98px; animation: fade-up 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955); - - @media (max-width: 767px) { - max-height: 202px; - animation: fade-up--mobile 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955); - } } .shrink-out { - animation: shrink-out 0.45s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards; + transform-origin: bottom center; + animation: shrink-out 0.3s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards; } diff --git a/apps/block_scout_web/assets/css/components/_card.scss b/apps/block_scout_web/assets/css/components/_card.scss index 13f7cf925c..d34f461fa7 100644 --- a/apps/block_scout_web/assets/css/components/_card.scss +++ b/apps/block_scout_web/assets/css/components/_card.scss @@ -34,5 +34,5 @@ .card-chain-blocks { height: auto; - @media (max-width: 767px) { height: 595px; } + @include media-breakpoint-down(md) { height: 595px; } } diff --git a/apps/block_scout_web/assets/css/components/_loading-spinner.scss b/apps/block_scout_web/assets/css/components/_loading-spinner.scss index 9ea746441a..a4b02051e6 100644 --- a/apps/block_scout_web/assets/css/components/_loading-spinner.scss +++ b/apps/block_scout_web/assets/css/components/_loading-spinner.scss @@ -1,3 +1,10 @@ +// ATTRIBUTION +// +// Author: Tobias Ahlin (tobiasahlin) +// +// SpinKit - Simple loading spinners animated with CSS. +// https://github.com/tobiasahlin/SpinKit + .loading-spinner { margin: auto 1rem; width: 40px; @@ -38,7 +45,7 @@ .loading-spinner-small { display: inline-block; position: relative; - top: -0.05em; + top: -0.125em; margin: auto 0.5em auto 0; width: 1em; height: 1em; diff --git a/apps/block_scout_web/assets/js/pages/block.js b/apps/block_scout_web/assets/js/pages/block.js index 17cf0eec5a..1ff04f7225 100644 --- a/apps/block_scout_web/assets/js/pages/block.js +++ b/apps/block_scout_web/assets/js/pages/block.js @@ -1,4 +1,5 @@ import $ from 'jquery' +import _ from 'lodash' import URI from 'urijs' import humps from 'humps' import socket from '../socket' @@ -6,16 +7,20 @@ import { updateAllAges } from '../lib/from_now' import { initRedux, prependWithClingBottom } from '../utils' export const initialState = { + blockNumbers: [], beyondPageOne: null, channelDisconnected: false, - newBlock: null + newBlock: null, + replaceBlock: null, + skippedBlockNumbers: [] } export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': { return Object.assign({}, state, { - beyondPageOne: action.beyondPageOne + beyondPageOne: action.beyondPageOne, + blockNumbers: action.blockNumbers }) } case 'CHANNEL_DISCONNECTED': { @@ -24,11 +29,36 @@ export function reducer (state = initialState, action) { }) } case 'RECEIVED_NEW_BLOCK': { - if (state.channelDisconnected) return state + if (state.channelDisconnected || state.beyondPageOne) return state - return Object.assign({}, state, { - newBlock: action.msg.blockHtml - }) + const blockNumber = parseInt(action.msg.blockNumber) + if (_.includes(state.blockNumbers, blockNumber)) { + return Object.assign({}, state, { + newBlock: action.msg.blockHtml, + replaceBlock: blockNumber, + skippedBlockNumbers: _.without(state.skippedBlockNumbers, blockNumber) + }) + } else if (blockNumber < _.last(state.blockNumbers)) { + return state + } else { + let skippedBlockNumbers = state.skippedBlockNumbers.slice(0) + if (blockNumber > state.blockNumbers[0] + 1) { + for (let i = state.blockNumbers[0] + 1; i < blockNumber; i++) { + skippedBlockNumbers.push(i) + } + } + const newBlockNumbers = _.chain([blockNumber]) + .union(skippedBlockNumbers, state.blockNumbers) + .orderBy([], ['desc']) + .value() + + return Object.assign({}, state, { + blockNumbers: newBlockNumbers, + newBlock: action.msg.blockHtml, + replaceBlock: null, + skippedBlockNumbers + }) + } } default: return state @@ -41,7 +71,8 @@ if ($blockListPage.length) { main (store) { const state = store.dispatch({ type: 'PAGE_LOAD', - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber + beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber, + blockNumbers: $('[data-selector="block-number"]').map((index, el) => parseInt(el.innerText)).toArray() }) if (!state.beyondPageOne) { const blocksChannel = socket.channel(`blocks:new_block`, {}) @@ -57,10 +88,44 @@ if ($blockListPage.length) { const $blocksList = $('[data-selector="blocks-list"]') if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.newBlock !== state.newBlock) { - prependWithClingBottom($blocksList, state.newBlock) + if (oldState.newBlock !== state.newBlock || (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock)) { + if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) { + const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`) + $replaceBlock.addClass('shrink-out') + setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400) + } else { + if (oldState.skippedBlockNumbers !== state.skippedBlockNumbers) { + const newSkippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers) + _.map(newSkippedBlockNumbers, (skippedBlockNumber) => { + prependWithClingBottom($blocksList, placeHolderBlock(skippedBlockNumber)) + }) + } + prependWithClingBottom($blocksList, state.newBlock) + } updateAllAges() } } }) } + +function placeHolderBlock (blockNumber) { + return ` +
+
+ + + + +
+
${blockNumber}
+
${window.localized['Block Processing']}
+
+
+
+ ` +} diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js index 08ed8fe023..90ff02ae38 100644 --- a/apps/block_scout_web/assets/js/pages/chain.js +++ b/apps/block_scout_web/assets/js/pages/chain.js @@ -1,4 +1,5 @@ import $ from 'jquery' +import _ from 'lodash' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' @@ -14,9 +15,12 @@ export const initialState = { availableSupply: null, averageBlockTime: null, batchCountAccumulator: 0, + blockNumbers: [], marketHistoryData: null, newBlock: null, newTransactions: [], + replaceBlock: null, + skippedBlockNumbers: [], transactionCount: null, usdMarketCap: null } @@ -25,6 +29,7 @@ export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': { return Object.assign({}, state, { + blockNumbers: action.blockNumbers, transactionCount: numeral(action.transactionCount).value() }) } @@ -34,10 +39,43 @@ export function reducer (state = initialState, action) { }) } case 'RECEIVED_NEW_BLOCK': { - return Object.assign({}, state, { - averageBlockTime: action.msg.averageBlockTime, - newBlock: action.msg.chainBlockHtml - }) + const blockNumber = parseInt(action.msg.blockNumber) + if (_.includes(state.blockNumbers, blockNumber)) { + return Object.assign({}, state, { + averageBlockTime: action.msg.averageBlockTime, + newBlock: action.msg.chainBlockHtml, + replaceBlock: blockNumber, + skippedBlockNumbers: _.without(state.skippedBlockNumbers, blockNumber) + }) + } else if (blockNumber < _.last(state.blockNumbers)) { + return Object.assign({}, state, { newBlock: null }) + } else { + let skippedBlockNumbers = state.skippedBlockNumbers.slice(0) + if (blockNumber > state.blockNumbers[0] + 1) { + let lastPlaceholder = state.blockNumbers[0] + 1 + if (blockNumber - lastPlaceholder > 3) { + lastPlaceholder = blockNumber - 3 + skippedBlockNumbers = [] + } + for (let i = lastPlaceholder; i < blockNumber; i++) { + skippedBlockNumbers.push(i) + } + } + const newBlockNumbers = _.chain([blockNumber]) + .union(skippedBlockNumbers, state.blockNumbers) + .orderBy([], ['desc']) + .slice(0, 4) + .value() + + const newSkippedBlockNumbers = _.intersection(skippedBlockNumbers, newBlockNumbers) + return Object.assign({}, state, { + averageBlockTime: action.msg.averageBlockTime, + blockNumbers: newBlockNumbers, + newBlock: action.msg.chainBlockHtml, + replaceBlock: null, + skippedBlockNumbers: newSkippedBlockNumbers + }) + } } case 'RECEIVED_NEW_EXCHANGE_RATE': { return Object.assign({}, state, { @@ -79,6 +117,7 @@ if ($chainDetailsPage.length) { const blocksChannel = socket.channel(`blocks:new_block`) store.dispatch({ type: 'PAGE_LOAD', + blockNumbers: $('[data-selector="block-number"]').map((index, el) => parseInt(el.innerText)).toArray(), transactionCount: $('[data-selector="transaction-count"]').text() }) blocksChannel.join() @@ -113,9 +152,25 @@ if ($chainDetailsPage.length) { if (oldState.usdMarketCap !== state.usdMarketCap) { $marketCap.empty().append(formatUsdValue(state.usdMarketCap)) } - if (oldState.newBlock !== state.newBlock) { - $blockList.children().last().remove() - $blockList.prepend(state.newBlock) + if (state.newBlock && oldState.newBlock !== state.newBlock) { + if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) { + const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`) + $replaceBlock.addClass('shrink-out') + setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400) + } else { + if (oldState.skippedBlockNumbers !== state.skippedBlockNumbers) { + const newSkippedBlockNumbers = _.chain(state.skippedBlockNumbers) + .difference(oldState.skippedBlockNumbers) + .intersection(state.blockNumbers) + .value() + _.map(newSkippedBlockNumbers, (skippedBlockNumber) => { + $blockList.children().last().remove() + $blockList.prepend(placeHolderBlock(skippedBlockNumber)) + }) + } + $blockList.children().last().remove() + $blockList.prepend(newBlockHtml(state.newBlock)) + } updateAllAges() } if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) @@ -142,3 +197,36 @@ if ($chainDetailsPage.length) { } }) } + +function newBlockHtml (blockHtml) { + return ` +
+ ${blockHtml} +
+ ` +} + +function placeHolderBlock (blockNumber) { + return ` +
+
+ + + + +
+
${blockNumber}
+
${window.localized['Block Processing']}
+
+
+
+ ` +} diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex index 302490c464..e34ce63418 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex @@ -1,4 +1,4 @@ -
+
@@ -6,8 +6,7 @@ @block, class: "tile-title", to: block_path(BlockScoutWeb.Endpoint, :show, @block), - "data-test": "block_number", - "data-block-number": to_string(@block.number) + "data-selector": "block-number" ) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex index 2811534741..cfcce5f33e 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex @@ -1,17 +1,20 @@ -
-
- <%= link(@block, to: block_path(BlockScoutWeb.Endpoint, :show, @block), class: "tile-title") %> -
- <%= Enum.count(@block.transactions) %> Transactions - -
- - <%= gettext "Miner" %> - - <%= render BlockScoutWeb.AddressView, - "_link.html", - address: @block.miner, - contract: false %> - +
+ <%= link( + @block, + class: "tile-title", + to: block_path(BlockScoutWeb.Endpoint, :show, @block), + "data-selector": "block-number" + ) %> +
+ <%= gettext("%{count} Transactions", count: Enum.count(@block.transactions)) %> +
+ + <%= gettext "Miner" %> + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @block.miner, + contract: false %> +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index 657ae52887..9516018d5a 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -56,7 +56,9 @@

<%= gettext "Blocks" %>

<%= for block <- @blocks do %> - <%= render BlockScoutWeb.ChainView, "_block.html", block: block %> +
+ <%= render BlockScoutWeb.ChainView, "_block.html", block: block %> +
<% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index 6c014469e4..7bebc9bb48 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -30,6 +30,7 @@