From 6abc7814c6fb5042bc43a865d4fc7c1c165d5f6b Mon Sep 17 00:00:00 2001 From: mistakia <1823355+mistakia@users.noreply.github.com> Date: Tue, 12 Mar 2024 01:17:52 -0400 Subject: [PATCH] feat: create `/live` page --- src/core/api/index.js | 3 +- src/core/api/sagas.js | 7 + src/core/api/service.js | 4 + src/core/nanodb/actions.js | 44 ++++++ src/core/nanodb/index.js | 7 + src/core/nanodb/reducer.js | 16 ++ src/core/nanodb/sagas.js | 25 +++ src/core/reducers.js | 4 +- src/core/sagas.js | 4 +- src/styles/toggle-button-group.styl | 14 ++ src/styles/variables.styl | 1 + src/views/components/app/app.js | 1 + .../components/metric-card/metric-card.js | 10 +- .../components/metric-card/metric-card.styl | 1 + src/views/components/posts/posts.js | 4 +- src/views/components/posts/posts.styl | 15 -- .../pages/confirmations/confirmations.js | 145 ++++++++++++++++++ .../pages/confirmations/confirmations.styl | 46 ++++++ src/views/pages/confirmations/index.js | 20 +++ src/views/routes.js | 2 + 20 files changed, 349 insertions(+), 24 deletions(-) create mode 100644 src/core/nanodb/actions.js create mode 100644 src/core/nanodb/index.js create mode 100644 src/core/nanodb/reducer.js create mode 100644 src/core/nanodb/sagas.js create mode 100644 src/styles/toggle-button-group.styl create mode 100644 src/views/pages/confirmations/confirmations.js create mode 100644 src/views/pages/confirmations/confirmations.styl create mode 100644 src/views/pages/confirmations/index.js diff --git a/src/core/api/index.js b/src/core/api/index.js index d6cb0a01..57612311 100644 --- a/src/core/api/index.js +++ b/src/core/api/index.js @@ -15,5 +15,6 @@ export { getNetworkStats, getWeight, getWeightHistory, - getDaily + getDaily, + get_block_confirmed_summary } from './sagas' diff --git a/src/core/api/sagas.js b/src/core/api/sagas.js index 11e40ef4..a7f84e1a 100644 --- a/src/core/api/sagas.js +++ b/src/core/api/sagas.js @@ -26,6 +26,7 @@ import { } from '@core/accounts/actions' import { blockRequestActions } from '@core/blocks/actions' import { dailyRequestActions } from '@core/ledger/actions' +import { block_confirmed_summary_request_actions } from '@core/nanodb/actions' function* fetchAPI(apiFunction, actions, opts = {}) { const { token } = yield select(getApp) @@ -120,3 +121,9 @@ export const getWeightHistory = fetch.bind( weightHistoryRequestActions ) export const getDaily = fetch.bind(null, api.getDaily, dailyRequestActions) + +export const get_block_confirmed_summary = fetch.bind( + null, + api.get_block_confirmed_summary, + block_confirmed_summary_request_actions +) diff --git a/src/core/api/service.js b/src/core/api/service.js index 80ed44b4..b0746746 100644 --- a/src/core/api/service.js +++ b/src/core/api/service.js @@ -81,6 +81,10 @@ export const api = { getWeightHistory() { const url = `${API_URL}/weight/history` return { url } + }, + get_block_confirmed_summary({ period = '10m' }) { + const url = `${API_URL}/nanodb/blocks/confirmed/summary?period=${period}` + return { url } } } diff --git a/src/core/nanodb/actions.js b/src/core/nanodb/actions.js new file mode 100644 index 00000000..8aa533b2 --- /dev/null +++ b/src/core/nanodb/actions.js @@ -0,0 +1,44 @@ +export const nanodb_actions = { + GET_BLOCK_CONFIRMED_SUMMARY: 'GET_BLOCK_CONFIRMED_SUMMARY', + + GET_BLOCK_CONFIRMED_SUMMARY_FAILED: 'GET_BLOCK_CONFIRMED_SUMMARY_FAILED', + GET_BLOCK_CONFIRMED_SUMMARY_PENDING: 'GET_BLOCK_CONFIRMED_SUMMARY_PENDING', + GET_BLOCK_CONFIRMED_SUMMARY_FULFILLED: + 'GET_BLOCK_CONFIRMED_SUMMARY_FULFILLED', + + get_block_confirmed_summary: (period = '10m') => ({ + type: nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY, + payload: { + period + } + }), + + get_block_confirmed_summary_failed: (params, error) => ({ + type: nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY_FAILED, + payload: { + params, + error + } + }), + + get_block_confirmed_summary_pending: (params) => ({ + type: nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY_PENDING, + payload: { + params + } + }), + + get_block_confirmed_summary_fulfilled: (params, data) => ({ + type: nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY_FULFILLED, + payload: { + params, + data + } + }) +} + +export const block_confirmed_summary_request_actions = { + failed: nanodb_actions.get_block_confirmed_summary_failed, + fulfilled: nanodb_actions.get_block_confirmed_summary_fulfilled, + pending: nanodb_actions.get_block_confirmed_summary_pending +} diff --git a/src/core/nanodb/index.js b/src/core/nanodb/index.js new file mode 100644 index 00000000..7ef4ff18 --- /dev/null +++ b/src/core/nanodb/index.js @@ -0,0 +1,7 @@ +export { + nanodb_actions, + block_confirmed_summary_request_actions +} from './actions' + +export { nanodb_reducer } from './reducer' +export { nanodb_sagas } from './sagas' diff --git a/src/core/nanodb/reducer.js b/src/core/nanodb/reducer.js new file mode 100644 index 00000000..8dd47ba8 --- /dev/null +++ b/src/core/nanodb/reducer.js @@ -0,0 +1,16 @@ +import { Map } from 'immutable' + +import { nanodb_actions } from './actions' + +export function nanodb_reducer(state = Map(), action) { + switch (action.type) { + case nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY_FULFILLED: + return state.set( + `block_confirmed_summary_${action.payload.params.period}`, + action.payload.data + ) + + default: + return state + } +} diff --git a/src/core/nanodb/sagas.js b/src/core/nanodb/sagas.js new file mode 100644 index 00000000..b88c38fe --- /dev/null +++ b/src/core/nanodb/sagas.js @@ -0,0 +1,25 @@ +import { takeEvery, fork, call } from 'redux-saga/effects' + +import { get_block_confirmed_summary } from '@core/api' +import { nanodb_actions } from './actions' + +export function* load_block_confirmed_summary({ payload }) { + yield call(get_block_confirmed_summary, payload) +} + +//= ==================================== +// WATCHERS +// ------------------------------------- + +export function* watch_get_block_confirmed_summary() { + yield takeEvery( + nanodb_actions.GET_BLOCK_CONFIRMED_SUMMARY, + load_block_confirmed_summary + ) +} + +//= ==================================== +// ROOT +// ------------------------------------- + +export const nanodb_sagas = [fork(watch_get_block_confirmed_summary)] diff --git a/src/core/reducers.js b/src/core/reducers.js index 6c7a6b4e..aad52788 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -13,6 +13,7 @@ import { networkReducer } from './network' import { notificationReducer } from './notifications' import { postsReducer } from './posts' import { postlistsReducer } from './postlists' +import { nanodb_reducer } from './nanodb' const rootReducer = (history) => combineReducers({ @@ -28,7 +29,8 @@ const rootReducer = (history) => network: networkReducer, notification: notificationReducer, posts: postsReducer, - postlists: postlistsReducer + postlists: postlistsReducer, + nanodb: nanodb_reducer }) export default rootReducer diff --git a/src/core/sagas.js b/src/core/sagas.js index 237db53f..b121138b 100644 --- a/src/core/sagas.js +++ b/src/core/sagas.js @@ -10,6 +10,7 @@ import { githubIssuesSagas } from './github-issues' import { ledgerSagas } from './ledger' import { networkSagas } from './network' import { postlistSagas } from './postlists' +import { nanodb_sagas } from './nanodb' export default function* rootSage() { yield all([ @@ -22,6 +23,7 @@ export default function* rootSage() { ...githubIssuesSagas, ...ledgerSagas, ...networkSagas, - ...postlistSagas + ...postlistSagas, + ...nanodb_sagas ]) } diff --git a/src/styles/toggle-button-group.styl b/src/styles/toggle-button-group.styl new file mode 100644 index 00000000..0dd023ef --- /dev/null +++ b/src/styles/toggle-button-group.styl @@ -0,0 +1,14 @@ +.toggle-button-group + flex 0 0 auto + border-radius 4px + border 1px solid $borderColor + background #ffffff + + .MuiToggleButton-root + border none + margin 4px + font-size 10px + padding 3px 8px + &:not(:first-child) + &:first-child + border-radius 4px \ No newline at end of file diff --git a/src/styles/variables.styl b/src/styles/variables.styl index 0f0a0dd1..39968701 100644 --- a/src/styles/variables.styl +++ b/src/styles/variables.styl @@ -1,4 +1,5 @@ $backgroundColor = #F0F0F0 +$textColor = #444444 $borderColor = #D0D0D0 diff --git a/src/views/components/app/app.js b/src/views/components/app/app.js index 5ae1193f..1306392f 100644 --- a/src/views/components/app/app.js +++ b/src/views/components/app/app.js @@ -10,6 +10,7 @@ import '@styles/typography.styl' import '@styles/doc.styl' import '@styles/header.styl' import '@styles/markdown.styl' +import '@styles/toggle-button-group.styl' export default class App extends React.Component { async componentDidMount() { diff --git a/src/views/components/metric-card/metric-card.js b/src/views/components/metric-card/metric-card.js index 32a71039..c8e880e2 100644 --- a/src/views/components/metric-card/metric-card.js +++ b/src/views/components/metric-card/metric-card.js @@ -19,7 +19,7 @@ export default class MetricCard extends React.Component { render() { const { - metrics, + metrics = [], title, subtitle, max, @@ -27,7 +27,8 @@ export default class MetricCard extends React.Component { tooltip, field, selectedField, - selectedLabel + selectedLabel, + body } = this.props const isSelectedField = field === selectedField const rows = metrics.map((p, i) => { @@ -66,7 +67,7 @@ export default class MetricCard extends React.Component { )} -
{rows}
+
{body || rows}
) } @@ -82,5 +83,6 @@ MetricCard.propTypes = { field: PropTypes.string, filter: PropTypes.func, selectedField: PropTypes.string, - selectedLabel: PropTypes.string + selectedLabel: PropTypes.string, + body: PropTypes.node } diff --git a/src/views/components/metric-card/metric-card.styl b/src/views/components/metric-card/metric-card.styl index 7465d9a4..6a673162 100644 --- a/src/views/components/metric-card/metric-card.styl +++ b/src/views/components/metric-card/metric-card.styl @@ -1,5 +1,6 @@ .metric__card flex 1 0 200px + max-width 300px border-radius 8px padding 16px margin 8px diff --git a/src/views/components/posts/posts.js b/src/views/components/posts/posts.js index 452b0eef..4c3aca2a 100644 --- a/src/views/components/posts/posts.js +++ b/src/views/components/posts/posts.js @@ -55,7 +55,7 @@ export default class Posts extends React.Component { exclusive onChange={this.handleChange} aria-label='age' - className='posts__age'> + className='toggle-button-group'> 3D 7D 1M @@ -67,7 +67,7 @@ export default class Posts extends React.Component { exclusive onChange={this.handleChange} aria-label='age' - className='posts__age'> + className='toggle-button-group'> 3D 7D 14D diff --git a/src/views/components/posts/posts.styl b/src/views/components/posts/posts.styl index fcdff3be..03c761bc 100644 --- a/src/views/components/posts/posts.styl +++ b/src/views/components/posts/posts.styl @@ -21,21 +21,6 @@ transform translate(-50%, -25px) position absolute -.posts__age - flex 0 0 auto - border-radius 4px - border 1px solid $borderColor - background #ffffff - - .MuiToggleButton-root - border none - margin 4px - font-size 10px - padding 3px 8px - &:not(:first-child) - &:first-child - border-radius 4px - @media (min-width 750px) .posts__container border-radius 16px diff --git a/src/views/pages/confirmations/confirmations.js b/src/views/pages/confirmations/confirmations.js new file mode 100644 index 00000000..465e95d4 --- /dev/null +++ b/src/views/pages/confirmations/confirmations.js @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import ImmutablePropTypes from 'react-immutable-proptypes' +import ToggleButton from '@mui/material/ToggleButton' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' + +import Seo from '@components/seo' +import Menu from '@components/menu' +import MetricCard from '@components/metric-card' + +import './confirmations.styl' + +const seconds_in_period = { + '10m': 600, + '30m': 1800, + '1h': 3600, + '4h': 14400, + '24h': 86400 +} + +export default function ConfirmationsPage({ + nanodb, + get_block_confirmed_summary +}) { + const [current_period, set_current_period] = useState('10m') + + useEffect(() => { + get_block_confirmed_summary(current_period) + }, [get_block_confirmed_summary, current_period]) + + const buckets = Object.values( + nanodb.getIn( + [ + `block_confirmed_summary_${current_period}`, + 'confirmation_latency_ms_by_bucket' + ], + {} + ) + ) + + const total_confirmations = buckets.reduce( + (acc, { confirmed_blocks = 0 }) => acc + confirmed_blocks, + 0 + ) + + const confirmations_per_second = + total_confirmations / seconds_in_period[current_period] + + const buckets_sorted_by_confirmed_blocks = buckets.sort( + (a, b) => a.confirmed_blocks - b.confirmed_blocks + ) + const median_bucket = + buckets_sorted_by_confirmed_blocks[ + Math.floor(buckets_sorted_by_confirmed_blocks.length / 2) + ] || {} + const confirmation_latency = Math.round(median_bucket.median / 1000) || 0 + + return ( + <> + +
+ {total_confirmations || ''}
} + /> + {confirmations_per_second || ''}} + /> + {confirmation_latency ? `${confirmation_latency}s` : ''} + } + /> + 120} + /> + 15} + /> + set_current_period(value)} + className='toggle-button-group confirmations-period'> + 10m + 30m + 1h + 4h + 24h + + +
+
+
Bucket
+
Blocks
+
Latency
+
+
+ {Array.from({ length: 62 }).map((_, index) => ( +
+
{index + 1}
+
+ {Math.floor(Math.random() * 1001)} +
+
+ {Math.floor(Math.random() * 400) + 1} +
+
+ ))} +
+
+ + + ) +} + +ConfirmationsPage.propTypes = { + nanodb: ImmutablePropTypes.map.isRequired, + get_block_confirmed_summary: PropTypes.func.isRequired +} diff --git a/src/views/pages/confirmations/confirmations.styl b/src/views/pages/confirmations/confirmations.styl new file mode 100644 index 00000000..255303df --- /dev/null +++ b/src/views/pages/confirmations/confirmations.styl @@ -0,0 +1,46 @@ +.confirmations-lead + position relative + display flex + max-width 1000px + margin 0 auto + +.confirmations-period + position absolute + right 0 + top 4px + +.buckets-container + padding 16px + display flex + overflow auto + flex-wrap wrap + +.buckets-body + display flex + margin-left 48px + +.buckets-head + position absolute + left 112px + width 48px + margin-bottom 32px + z-index 1 + .bucket-row + background darken($backgroundColor, 7%) + display flex + justify-content center + color lighten($textColor, 30%) + padding 0 4px + width 100px + + +.bucket-item + width 48px + +.bucket-row + display flex + align-items center + border-bottom 1px solid $borderColor + height 30px + padding 0 8px + diff --git a/src/views/pages/confirmations/index.js b/src/views/pages/confirmations/index.js new file mode 100644 index 00000000..96392679 --- /dev/null +++ b/src/views/pages/confirmations/index.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { nanodb_actions } from '@core/nanodb' + +import ConfirmationsPage from './confirmations' + +const map_state_to_props = createSelector( + (state) => state.get('nanodb'), + (nanodb) => ({ nanodb }) +) + +const map_dispatch_to_props = { + get_block_confirmed_summary: nanodb_actions.get_block_confirmed_summary +} + +export default connect( + map_state_to_props, + map_dispatch_to_props +)(ConfirmationsPage) diff --git a/src/views/routes.js b/src/views/routes.js index dc19b36d..8bc94aed 100644 --- a/src/views/routes.js +++ b/src/views/routes.js @@ -12,10 +12,12 @@ import NotFoundPage from '@pages/not-found' import RepresentativesPage from '@pages/representatives' import TelemetryPage from '@pages/telemetry' import LabelPage from '@pages/label' +import ConfirmationsPage from '@pages/confirmations' const Routes = () => ( +