diff --git a/src/core/api/index.js b/src/core/api/index.js index d6cb0a01..4553d010 100644 --- a/src/core/api/index.js +++ b/src/core/api/index.js @@ -15,5 +15,11 @@ export { getNetworkStats, getWeight, getWeightHistory, - getDaily + getDaily, + get_blocks_confirmed_summary, + get_accounts_unconfirmed_summary, + get_blocks_unconfirmed_summary } from './sagas' + +export { api_reducer } from './reducer' +export { get_request_history } from './selectors' diff --git a/src/core/api/reducer.js b/src/core/api/reducer.js new file mode 100644 index 00000000..0494a751 --- /dev/null +++ b/src/core/api/reducer.js @@ -0,0 +1,41 @@ +import { Map } from 'immutable' + +import { nanodb_actions } from '@core/nanodb/actions' + +const initialState = new Map({ + request_history: new Map() +}) + +export function api_reducer(state = initialState, { payload, type }) { + switch (type) { + case nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_PENDING: + return state.setIn( + [ + 'request_history', + `GET_BLOCKS_CONFIRMED_SUMMARY_${payload.params.period}` + ], + true + ) + + case nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_FAILED: + return state.deleteIn([ + 'request_history', + `GET_BLOCKS_CONFIRMED_SUMMARY_${payload.params.period}` + ]) + + case nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_PENDING: + return state.setIn( + ['request_history', 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY'], + true + ) + + case nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FAILED: + return state.deleteIn([ + 'request_history', + 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY' + ]) + + default: + return state + } +} diff --git a/src/core/api/sagas.js b/src/core/api/sagas.js index 11e40ef4..7358aa34 100644 --- a/src/core/api/sagas.js +++ b/src/core/api/sagas.js @@ -26,6 +26,11 @@ import { } from '@core/accounts/actions' import { blockRequestActions } from '@core/blocks/actions' import { dailyRequestActions } from '@core/ledger/actions' +import { + block_confirmed_summary_request_actions, + accounts_unconfirmed_summary_request_actions, + blocks_unconfirmed_summary_request_actions +} from '@core/nanodb/actions' function* fetchAPI(apiFunction, actions, opts = {}) { const { token } = yield select(getApp) @@ -120,3 +125,21 @@ export const getWeightHistory = fetch.bind( weightHistoryRequestActions ) export const getDaily = fetch.bind(null, api.getDaily, dailyRequestActions) + +export const get_blocks_confirmed_summary = fetch.bind( + null, + api.get_blocks_confirmed_summary, + block_confirmed_summary_request_actions +) + +export const get_accounts_unconfirmed_summary = fetch.bind( + null, + api.get_accounts_unconfirmed_summary, + accounts_unconfirmed_summary_request_actions +) + +export const get_blocks_unconfirmed_summary = fetch.bind( + null, + api.get_blocks_unconfirmed_summary, + blocks_unconfirmed_summary_request_actions +) diff --git a/src/core/api/selectors.js b/src/core/api/selectors.js new file mode 100644 index 00000000..83ed9cab --- /dev/null +++ b/src/core/api/selectors.js @@ -0,0 +1,2 @@ +export const get_request_history = (state) => + state.getIn(['api', 'request_history']) diff --git a/src/core/api/service.js b/src/core/api/service.js index 80ed44b4..4cef27ec 100644 --- a/src/core/api/service.js +++ b/src/core/api/service.js @@ -81,6 +81,18 @@ export const api = { getWeightHistory() { const url = `${API_URL}/weight/history` return { url } + }, + get_blocks_confirmed_summary({ period = '10m' }) { + const url = `${API_URL}/nanodb/blocks/confirmed/summary?period=${period}` + return { url } + }, + get_accounts_unconfirmed_summary() { + const url = `${API_URL}/nanodb/accounts/unconfirmed/summary` + return { url } + }, + get_blocks_unconfirmed_summary() { + const url = `${API_URL}/nanodb/blocks/unconfirmed/summary` + return { url } } } diff --git a/src/core/nanodb/actions.js b/src/core/nanodb/actions.js new file mode 100644 index 00000000..d40372af --- /dev/null +++ b/src/core/nanodb/actions.js @@ -0,0 +1,125 @@ +export const nanodb_actions = { + GET_BLOCKS_CONFIRMED_SUMMARY: 'GET_BLOCKS_CONFIRMED_SUMMARY', + + GET_BLOCKS_CONFIRMED_SUMMARY_FAILED: 'GET_BLOCKS_CONFIRMED_SUMMARY_FAILED', + GET_BLOCKS_CONFIRMED_SUMMARY_PENDING: 'GET_BLOCKS_CONFIRMED_SUMMARY_PENDING', + GET_BLOCKS_CONFIRMED_SUMMARY_FULFILLED: + 'GET_BLOCKS_CONFIRMED_SUMMARY_FULFILLED', + + GET_ACCOUNTS_UNCONFIRMED_SUMMARY: 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY', + + GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FAILED: + 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FAILED', + GET_ACCOUNTS_UNCONFIRMED_SUMMARY_PENDING: + 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY_PENDING', + GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FULFILLED: + 'GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FULFILLED', + + GET_BLOCKS_UNCONFIRMED_SUMMARY: 'GET_BLOCKS_UNCONFIRMED_SUMMARY', + + GET_BLOCKS_UNCONFIRMED_SUMMARY_FAILED: + 'GET_BLOCKS_UNCONFIRMED_SUMMARY_FAILED', + GET_BLOCKS_UNCONFIRMED_SUMMARY_PENDING: + 'GET_BLOCKS_UNCONFIRMED_SUMMARY_PENDING', + GET_BLOCKS_UNCONFIRMED_SUMMARY_FULFILLED: + 'GET_BLOCKS_UNCONFIRMED_SUMMARY_FULFILLED', + + get_blocks_confirmed_summary: (period = '10m') => ({ + type: nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY, + payload: { + period + } + }), + + get_blocks_confirmed_summary_failed: (params, error) => ({ + type: nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_FAILED, + payload: { + params, + error + } + }), + + get_blocks_confirmed_summary_pending: (params) => ({ + type: nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_PENDING, + payload: { + params + } + }), + + get_blocks_confirmed_summary_fulfilled: (params, data) => ({ + type: nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_FULFILLED, + payload: { + params, + data + } + }), + + get_accounts_unconfirmed_summary: () => ({ + type: nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY + }), + + get_accounts_unconfirmed_summary_failed: (params, error) => ({ + type: nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FAILED, + payload: { + params, + error + } + }), + + get_accounts_unconfirmed_summary_pending: (params) => ({ + type: nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_PENDING, + payload: { + params + } + }), + get_accounts_unconfirmed_summary_fulfilled: (params, data) => ({ + type: nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FULFILLED, + payload: { + params, + data + } + }), + + get_blocks_unconfirmed_summary: () => ({ + type: nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY + }), + + get_blocks_unconfirmed_summary_failed: (params, error) => ({ + type: nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY_FAILED, + payload: { + params, + error + } + }), + get_blocks_unconfirmed_summary_pending: (params) => ({ + type: nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY_PENDING, + payload: { + params + } + }), + get_blocks_unconfirmed_summary_fulfilled: (params, data) => ({ + type: nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY_FULFILLED, + payload: { + params, + data + } + }) +} + +export const block_confirmed_summary_request_actions = { + failed: nanodb_actions.get_blocks_confirmed_summary_failed, + fulfilled: nanodb_actions.get_blocks_confirmed_summary_fulfilled, + pending: nanodb_actions.get_blocks_confirmed_summary_pending +} + +export const accounts_unconfirmed_summary_request_actions = { + failed: nanodb_actions.get_accounts_unconfirmed_summary_failed, + fulfilled: nanodb_actions.get_accounts_unconfirmed_summary_fulfilled, + pending: nanodb_actions.get_accounts_unconfirmed_summary_pending +} + +export const blocks_unconfirmed_summary_request_actions = { + failed: nanodb_actions.get_blocks_unconfirmed_summary_failed, + fulfilled: nanodb_actions.get_blocks_unconfirmed_summary_fulfilled, + pending: nanodb_actions.get_blocks_unconfirmed_summary_pending +} diff --git a/src/core/nanodb/index.js b/src/core/nanodb/index.js new file mode 100644 index 00000000..b53c4e3f --- /dev/null +++ b/src/core/nanodb/index.js @@ -0,0 +1,9 @@ +export { + nanodb_actions, + block_confirmed_summary_request_actions, + accounts_unconfirmed_summary_request_actions, + blocks_unconfirmed_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..675455b3 --- /dev/null +++ b/src/core/nanodb/reducer.js @@ -0,0 +1,22 @@ +import { Map } from 'immutable' + +import { nanodb_actions } from './actions' + +export function nanodb_reducer(state = Map(), action) { + switch (action.type) { + case nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY_FULFILLED: + return state.set( + `block_confirmed_summary_${action.payload.params.period}`, + action.payload.data + ) + + case nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY_FULFILLED: + return state.set('accounts_unconfirmed_summary', action.payload.data) + + case nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY_FULFILLED: + return state.set('blocks_unconfirmed_summary', 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..60fffee8 --- /dev/null +++ b/src/core/nanodb/sagas.js @@ -0,0 +1,70 @@ +import { takeEvery, fork, call, select } from 'redux-saga/effects' + +import { + get_blocks_confirmed_summary, + get_accounts_unconfirmed_summary, + get_blocks_unconfirmed_summary, + get_request_history +} from '@core/api' +import { nanodb_actions } from './actions' + +export function* load_block_confirmed_summary({ payload }) { + const request_history = yield select(get_request_history) + + if (request_history.has(`GET_BLOCKS_CONFIRMED_SUMMARY_${payload.period}`)) { + return + } + + yield call(get_blocks_confirmed_summary, payload) +} + +export function* load_accounts_unconfirmed_summary() { + const request_history = yield select(get_request_history) + if (request_history.has('GET_ACCOUNTS_UNCONFIRMED_SUMMARY')) { + return + } + yield call(get_accounts_unconfirmed_summary) +} + +export function* load_blocks_unconfirmed_summary() { + const request_history = yield select(get_request_history) + if (request_history.has('GET_BLOCKS_UNCONFIRMED_SUMMARY')) { + return + } + yield call(get_blocks_unconfirmed_summary) +} + +//= ==================================== +// WATCHERS +// ------------------------------------- + +export function* watch_get_blocks_confirmed_summary() { + yield takeEvery( + nanodb_actions.GET_BLOCKS_CONFIRMED_SUMMARY, + load_block_confirmed_summary + ) +} + +export function* watch_get_accounts_unconfirmed_summary() { + yield takeEvery( + nanodb_actions.GET_ACCOUNTS_UNCONFIRMED_SUMMARY, + load_accounts_unconfirmed_summary + ) +} + +export function* watch_get_blocks_unconfirmed_summary() { + yield takeEvery( + nanodb_actions.GET_BLOCKS_UNCONFIRMED_SUMMARY, + load_blocks_unconfirmed_summary + ) +} + +//= ==================================== +// ROOT +// ------------------------------------- + +export const nanodb_sagas = [ + fork(watch_get_blocks_confirmed_summary), + fork(watch_get_accounts_unconfirmed_summary), + fork(watch_get_blocks_unconfirmed_summary) +] diff --git a/src/core/reducers.js b/src/core/reducers.js index 6c7a6b4e..e85abb34 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -13,6 +13,8 @@ import { networkReducer } from './network' import { notificationReducer } from './notifications' import { postsReducer } from './posts' import { postlistsReducer } from './postlists' +import { nanodb_reducer } from './nanodb' +import { api_reducer } from './api' const rootReducer = (history) => combineReducers({ @@ -28,7 +30,9 @@ const rootReducer = (history) => network: networkReducer, notification: notificationReducer, posts: postsReducer, - postlists: postlistsReducer + postlists: postlistsReducer, + nanodb: nanodb_reducer, + api: api_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/live/index.js b/src/views/pages/live/index.js new file mode 100644 index 00000000..9dc46ce3 --- /dev/null +++ b/src/views/pages/live/index.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { nanodb_actions } from '@core/nanodb' + +import LivePage from './live' + +const map_state_to_props = createSelector( + (state) => state.get('nanodb'), + (nanodb) => ({ nanodb }) +) + +const map_dispatch_to_props = { + get_blocks_confirmed_summary: nanodb_actions.get_blocks_confirmed_summary, + get_accounts_unconfirmed_summary: + nanodb_actions.get_accounts_unconfirmed_summary, + get_blocks_unconfirmed_summary: nanodb_actions.get_blocks_unconfirmed_summary +} + +export default connect(map_state_to_props, map_dispatch_to_props)(LivePage) diff --git a/src/views/pages/live/live.js b/src/views/pages/live/live.js new file mode 100644 index 00000000..ee930199 --- /dev/null +++ b/src/views/pages/live/live.js @@ -0,0 +1,319 @@ +import React, { useEffect, useState, useMemo } 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 percentile from 'percentile' + +import Seo from '@components/seo' +import Menu from '@components/menu' +import MetricCard from '@components/metric-card' + +import './live.styl' + +const seconds_in_period = { + '10m': 600, + '30m': 1800, + '1h': 3600, + '4h': 14400, + '24h': 86400 +} + +export default function LivePage({ + nanodb, + get_blocks_confirmed_summary, + get_accounts_unconfirmed_summary, + get_blocks_unconfirmed_summary +}) { + const [current_period, set_current_period] = useState('10m') + + useEffect(() => { + get_blocks_confirmed_summary(current_period) + }, [get_blocks_confirmed_summary, current_period]) + + useEffect(() => { + get_accounts_unconfirmed_summary() + }, [get_accounts_unconfirmed_summary]) + + useEffect(() => { + get_blocks_unconfirmed_summary() + }, [get_blocks_unconfirmed_summary]) + + const confirmation_latency_by_bucket = useMemo( + () => + nanodb.getIn( + [ + `block_confirmed_summary_${current_period}`, + 'confirmation_latency_ms_by_bucket' + ], + {} + ), + [nanodb, current_period] + ) + + const bucket_values = useMemo( + () => Object.values(confirmation_latency_by_bucket), + [confirmation_latency_by_bucket] + ) + const total_confirmations = useMemo( + () => + bucket_values.reduce( + (acc, { confirmed_blocks = 0 }) => acc + confirmed_blocks, + 0 + ), + [bucket_values] + ) + + const confirmations_per_second = useMemo( + () => + parseFloat( + (total_confirmations / seconds_in_period[current_period]).toFixed(3) + ), + [total_confirmations, current_period] + ) + + const buckets_sorted_by_confirmed_blocks = useMemo( + () => + bucket_values + .filter((b) => b.confirmed_blocks) + .sort((a, b) => a.confirmed_blocks - b.confirmed_blocks), + [bucket_values] + ) + const median_bucket = useMemo( + () => + buckets_sorted_by_confirmed_blocks[ + Math.floor(buckets_sorted_by_confirmed_blocks.length / 2) + ] || {}, + [buckets_sorted_by_confirmed_blocks] + ) + + const confirmation_latency = median_bucket.median || 0 + + const unconfirmed_accounts_count = nanodb.getIn( + ['accounts_unconfirmed_summary', 'unconfirmed_accounts_count'], + null + ) + + const unconfirmed_blocks_by_bucket = useMemo( + () => + nanodb.getIn( + ['blocks_unconfirmed_summary', 'unconfirmed_blocks_by_bucket'], + {} + ), + [nanodb] + ) + + const unconfirmed_blocks = Object.values(unconfirmed_blocks_by_bucket).reduce( + (acc, value) => acc + value, + 0 + ) + const confirmed_percentile_values = percentile( + [0, 25, 75, 100], + bucket_values.map((b) => b.confirmed_blocks) + ) + const confirmed_percentile = { + min: confirmed_percentile_values[0], + p25: confirmed_percentile_values[1], + p75: confirmed_percentile_values[2], + max: confirmed_percentile_values[3] + } + + const unconfirmed_percentile_values = percentile( + [0, 25, 75, 100], + Object.values(unconfirmed_blocks_by_bucket).map((b) => b) + ) + const unconfirmed_percentile = { + min: unconfirmed_percentile_values[0], + p25: unconfirmed_percentile_values[1], + p75: unconfirmed_percentile_values[2], + max: unconfirmed_percentile_values[3] + } + + console.log({ + confirmed_percentile, + unconfirmed_percentile + }) + + return ( + <> + +
+ {total_confirmations || '-'}
} + /> + {confirmations_per_second || '-'}} + /> + + {confirmation_latency ? `${confirmation_latency}ms` : '-'} + + } + /> + + {unconfirmed_blocks === null || unconfirmed_blocks === undefined + ? '-' + : unconfirmed_blocks} + + } + /> + + {unconfirmed_accounts_count === null + ? '-' + : unconfirmed_accounts_count} + + } + /> + set_current_period(value)} + className='toggle-button-group confirmations-period'> + 10m + 30m + 1h + 4h + 24h + + +
+
+
Bucket #
+
Unconfirmed
+
Latency
+
Confs.
+
+
+ {Array.from({ length: 62 }).map((_, index) => { + const confirmed_blocks = + confirmation_latency_by_bucket[`bucket_${index}`] + ?.confirmed_blocks || 0 + const unconfirmed_blocks = + unconfirmed_blocks_by_bucket[`bucket_${index}`] || 0 + let confirmed_color = 'transparent' + let unconfirmed_color = 'transparent' + // Assuming percentile values are available for confirmed and unconfirmed blocks + + if (confirmed_blocks < confirmed_percentile.p25) { + const maxPercent = + (confirmed_percentile.p25 - confirmed_blocks) / + (confirmed_percentile.p25 - confirmed_percentile.min) / + 1.5 || 0 + confirmed_color = `rgba(253, 162, 145, ${maxPercent})` + } else if ( + confirmed_blocks >= confirmed_percentile.p25 && + confirmed_blocks <= confirmed_percentile.p75 + ) { + const percent = + (confirmed_blocks - confirmed_percentile.p25) / + (confirmed_percentile.max - confirmed_percentile.p25) || 0 + confirmed_color = `rgba(46, 163, 221, ${percent})` + } else { + const maxPercent = + (confirmed_blocks - confirmed_percentile.p75) / + (confirmed_percentile.max - confirmed_percentile.p75) / + 1.5 || 0 + confirmed_color = `rgba(46, 163, 221, ${maxPercent})` + } + + if (unconfirmed_blocks < unconfirmed_percentile.p25) { + const maxPercent = + (unconfirmed_percentile.p25 - unconfirmed_blocks) / + (unconfirmed_percentile.p25 - unconfirmed_percentile.min) / + 1.5 || 0 + unconfirmed_color = `rgba(253, 162, 145, ${maxPercent})` + } else if ( + unconfirmed_blocks >= unconfirmed_percentile.p25 && + unconfirmed_blocks <= unconfirmed_percentile.p75 + ) { + const percent = + (unconfirmed_blocks - unconfirmed_percentile.p25) / + (unconfirmed_percentile.max - unconfirmed_percentile.p25) || 0 + unconfirmed_color = `rgba(46, 163, 221, ${percent})` + } else { + const maxPercent = + (unconfirmed_blocks - unconfirmed_percentile.p75) / + (unconfirmed_percentile.max - unconfirmed_percentile.p75) / + 1.5 || 0 + unconfirmed_color = `rgba(46, 163, 221, ${maxPercent})` + } + + return ( +
+
{index}
+
+ {unconfirmed_blocks_by_bucket[`bucket_${index}`] ?? ''} +
+
+ {confirmation_latency_by_bucket[`bucket_${index}`]?.median + ? `${ + confirmation_latency_by_bucket[`bucket_${index}`] + .median >= 1000 + ? ( + confirmation_latency_by_bucket[`bucket_${index}`] + .median / 1000 + ).toFixed(1) + : confirmation_latency_by_bucket[`bucket_${index}`] + .median + }${ + confirmation_latency_by_bucket[`bucket_${index}`] + .median >= 1000 + ? 's' + : 'ms' + }` + : ''} +
+
+ {confirmed_blocks || ''} +
+
+ ) + })} +
+
+ + + ) +} + +LivePage.propTypes = { + nanodb: ImmutablePropTypes.map.isRequired, + get_blocks_confirmed_summary: PropTypes.func.isRequired, + get_accounts_unconfirmed_summary: PropTypes.func.isRequired, + get_blocks_unconfirmed_summary: PropTypes.func.isRequired +} diff --git a/src/views/pages/live/live.styl b/src/views/pages/live/live.styl new file mode 100644 index 00000000..c1fcb84f --- /dev/null +++ b/src/views/pages/live/live.styl @@ -0,0 +1,57 @@ +.live-lead + position relative + display flex + flex-wrap wrap + max-width 1000px + margin 8px auto 0 + padding 48px 0 16px + + .metric__card + flex 1 + +.confirmations-period + position absolute + right 8px + top 8px + +.buckets-container + padding 16px + display flex + overflow auto + flex-wrap wrap + +.buckets-body + display flex + margin-left 100px + +.buckets-head + position absolute + left 18px + width 100px + 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 64px + +.bucket-row + display flex + align-items center + border-bottom 1px solid $borderColor + height 30px + padding 0 8px + +.bucket-title + font-family 'IBM Plex Mono', monospace + background-color lighten($backgroundColor, 70%) + font-size 14px + font-weight 600 + diff --git a/src/views/routes.js b/src/views/routes.js index dc19b36d..a72386f9 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 LivePage from '@pages/live' const Routes = () => ( +