diff --git a/__tests__/TestHelper.js b/__tests__/TestHelper.js index 6adcce7b8..3de8ded54 100644 --- a/__tests__/TestHelper.js +++ b/__tests__/TestHelper.js @@ -375,7 +375,10 @@ const TestHelper = { finalizedPoint: 1, geoLocation: null, height: 120, - name: 'node' + name: 'node', + restVersion: null, + isHealthy: null, + isSslEnabled: null }, geoLocationCommonField: { city: 'ABC City', diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index a1d465dd7..8ae44b0df 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -10,46 +10,46 @@ describe('Node Service', () => { const nodeWatchServiceNodeResponse = [ { + ...nodeCommonField, roles: 1, restVersion: null, isHealthy: null, - isSslEnabled: null, - ...nodeCommonField + isSslEnabled: null }, { + ...nodeCommonField, roles: 1, // Peer node (light) restVersion: '2.4.4', isHealthy: null, - isSslEnabled: true, - ...nodeCommonField + isSslEnabled: true }, { + ...nodeCommonField, roles: 3, restVersion: '2.4.4', isHealthy: true, - isSslEnabled: true, - ...nodeCommonField + isSslEnabled: true }, { + ...nodeCommonField, roles: 5, // Peer Voting node (light) restVersion: '2.4.4', isHealthy: null, - isSslEnabled: true, - ...nodeCommonField + isSslEnabled: true }, { + ...nodeCommonField, roles: 5, restVersion: null, isHealthy: null, - isSslEnabled: null, - ...nodeCommonField + isSslEnabled: null }, { + ...nodeCommonField, roles: 7, restVersion: '2.4.4', isHealthy: true, - isSslEnabled: true, - ...nodeCommonField + isSslEnabled: true } ]; @@ -72,15 +72,14 @@ describe('Node Service', () => { networkGenerationHashSeed: '57F7DA205008026C776CB6AED843393F04CD458E0AA2D9F1D5F31A402072B2D6' }; - const runNodeWatchFailResponseTests = (nodeWatchMethod, NodeServiceMethod) => { - it('throws error when node watch fail response', async () => { + const runNodeServiceThrowErrorTests = (nodeServiceMethod, params, expectedError) => { + it('throws error when node service fail response', async () => { // Arrange: - const error = new Error(`node watch ${nodeWatchMethod} error`); - - jest.spyOn(NodeWatchService, nodeWatchMethod).mockRejectedValue(error); + jest.spyOn(NodeWatchService, 'getNodes').mockRejectedValue(new Error()); + jest.spyOn(NodeWatchService, 'getNodeByMainPublicKey').mockRejectedValue(new Error()); // Act + Assert: - await expect(NodeService[NodeServiceMethod]()).rejects.toThrow(error); + await expect(NodeService[nodeServiceMethod](params)).rejects.toThrow(expectedError); }); }; @@ -151,7 +150,7 @@ describe('Node Service', () => { ]); }); - runNodeWatchFailResponseTests('getNodes', 'getAvailableNodes'); + runNodeServiceThrowErrorTests('getAvailableNodes', undefined, 'Failed to get available nodes'); }); describe('getNodeStats', () => { @@ -258,7 +257,7 @@ describe('Node Service', () => { nodeWatchServiceNodeResponse[3] ].forEach(lightNode => runLightRestNodeTests(lightNode)); - runNodeWatchFailResponseTests('getNodeByMainPublicKey', 'getNodeInfo'); + runNodeServiceThrowErrorTests('getNodeInfo', 'public key 123', 'Failed to get node info for public key public key 123'); }); describe('getAPINodeList', () => { @@ -292,4 +291,76 @@ describe('Node Service', () => { ]); }); }); + + describe('getNodeHeightAndFinalizedHeightStats', () => { + it('returns node height and finalized height count and group by version', async () => { + // Arrange: + const mockApiResponse = [ + { + ...nodeCommonField, + version: '1.0.3.6', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.6', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.5', + height: 120, + finalizedHeight: 99 + }, + { + ...nodeCommonField, + version: '1.0.3.5', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.4', + height: 120, + finalizedHeight: 100 + } + ]; + + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(mockApiResponse); + + // Act: + const result = await NodeService.getNodeHeightAndFinalizedHeightStats(); + + // Assert: + expect(result).toEqual([ + { + 'data': [{'x': 120, 'y': 2, 'z': 2}], + 'name': '1.0.3.6 - Height' + }, + { + 'data': [{'x': 100, 'y': 2, 'z': 2}], + 'name': '1.0.3.6 - Finalized Height'}, + { + 'data': [{'x': 120, 'y': 2, 'z': 2}], + 'name': '1.0.3.5 - Height' + }, + { + 'data': [{'x': 99, 'y': 1, 'z': 1}, {'x': 100, 'y': 1, 'z': 1}], + 'name': '1.0.3.5 - Finalized Height' + }, + { + 'data': [{'x': 120, 'y': 1, 'z': 1}], + 'name': '1.0.3.4 - Height' + }, + { + 'data': [{'x': 100, 'y': 1, 'z': 1}], + 'name': '1.0.3.4 - Finalized Height' + } + ]); + }); + + runNodeServiceThrowErrorTests('getNodeHeightAndFinalizedHeightStats', undefined, 'Failed to get node height stats'); + }); }); diff --git a/package-lock.json b/package-lock.json index 5494798a3..90ed4f714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "nem-sdk": "^1.6.8", "net": "^1.0.2", "symbol-sdk": "^2.0.3", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tls": "^0.0.1", "url-parse": "^1.5.10", "vue": "^2.6.14", @@ -23939,11 +23938,6 @@ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, - "node_modules/symbol-statistics-service-typescript-fetch-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/symbol-statistics-service-typescript-fetch-client/-/symbol-statistics-service-typescript-fetch-client-1.1.5.tgz", - "integrity": "sha512-hcVD9jq+S2kZCYaXTDglU43y3Dn5K6W3O45Y/pMAfiuYDCdVaHczAPDE/ZKGo0kZXy69e1sn/riAzfDfn8yFBw==" - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index bfc436d3f..eb06c82e7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "nem-sdk": "^1.6.8", "net": "^1.0.2", "symbol-sdk": "^2.0.3", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tls": "^0.0.1", "url-parse": "^1.5.10", "vue": "^2.6.14", diff --git a/src/components/Chart.vue b/src/components/Chart.vue index 74d413f97..ecb415975 100644 --- a/src/components/Chart.vue +++ b/src/components/Chart.vue @@ -149,6 +149,11 @@ export default { dataLabels: { position: 'top' } + }, + bubble: { + zScaling: true, + minBubbleRadius: 10, + maxBubbleRadius: 50 } }, title: { @@ -189,7 +194,10 @@ export default { : {} }, tooltip: { - enabled: true + enabled: true, + z: { + title: 'Count: ' + }, }, legend: { show: true, diff --git a/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue b/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue new file mode 100644 index 000000000..48992cc2f --- /dev/null +++ b/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/widgets/NodeHeightStatsWidget.vue b/src/components/widgets/NodeHeightStatsWidget.vue deleted file mode 100644 index f44b0d587..000000000 --- a/src/components/widgets/NodeHeightStatsWidget.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/src/config/default.json b/src/config/default.json index 34a3a9a0a..5188f549c 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -2,7 +2,6 @@ "apiNodePort": 3001, "endpoints": { "marketData": "https://min-api.cryptocompare.com/", - "statisticsService": "https://symbol.services", "nodeWatch": "https://nodewatch.symbol.tools" }, "networkConfig": { diff --git a/src/config/i18n/en-us.json b/src/config/i18n/en-us.json index 11467299a..1e7bba8c6 100644 --- a/src/config/i18n/en-us.json +++ b/src/config/i18n/en-us.json @@ -383,8 +383,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/es.json b/src/config/i18n/es.json index d857ef556..61d181c2d 100644 --- a/src/config/i18n/es.json +++ b/src/config/i18n/es.json @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/ja.json b/src/config/i18n/ja.json index bc9a71662..112332c20 100644 --- a/src/config/i18n/ja.json +++ b/src/config/i18n/ja.json @@ -383,8 +383,7 @@ "allNodes": "合計数", "nodeCountByRoles": "ロールごとの数", "chainInfo": "チェーン情報", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "詳細を表示", "nodeCountChartTitle": "Node count over time", "timestamp": "タイムスタンプ", diff --git a/src/config/i18n/ko.json b/src/config/i18n/ko.json index d893cd6b2..1b54f5d77 100644 --- a/src/config/i18n/ko.json +++ b/src/config/i18n/ko.json @@ -383,8 +383,7 @@ "allNodes": "총 개수", "nodeCountByRoles": "역할 개수", "chainInfo": "체인 정보", - "nodeHeightStatsTitle": "체인 높이의 총 노드 수", - "nodeFinalizedHeightStatsTitle": "최종화 높이의 총 노드 수", + "nodeHeightAndFinalizedHeightStatsTitle": "노드 높이와 최종화 높이의 수", "viewMoreStatistics": "상태 더보기", "nodeCountChartTitle": "노드 카운트 시간 초과", "timestamp": "타임스탬프", diff --git a/src/config/i18n/pt.json b/src/config/i18n/pt.json index 420e8cde8..323dd4379 100644 --- a/src/config/i18n/pt.json +++ b/src/config/i18n/pt.json @@ -380,8 +380,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/ru.json b/src/config/i18n/ru.json index f02bcd7f2..b0b6a7428 100644 --- a/src/config/i18n/ru.json +++ b/src/config/i18n/ru.json @@ -383,8 +383,7 @@ "allNodes": "Общее количество", "nodeCountByRoles": "Подсчет по ролям", "chainInfo": "Информация о сети", - "nodeHeightStatsTitle": "Количество нод по высоте цепи", - "nodeFinalizedHeightStatsTitle": "Количество нод по завершенной высоте", + "nodeHeightAndFinalizedHeightStatsTitle": "Количество нод по высоте и завершенной высоте", "viewMoreStatistics": "Просмотр Дополнительной Статистики", "nodeCountChartTitle": "Количество нод с течением времени", "timestamp": "Отметка времени", diff --git a/src/config/i18n/ua.json b/src/config/i18n/ua.json index f0b9ed198..87cfa0864 100644 --- a/src/config/i18n/ua.json +++ b/src/config/i18n/ua.json @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/zh.json b/src/config/i18n/zh.json index dfae37806..de3375db4 100644 --- a/src/config/i18n/zh.json +++ b/src/config/i18n/zh.json @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index 5865b3b9b..739a2ec84 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -115,7 +115,7 @@ class NodeService { .sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)); } catch (e) { console.error(e); - throw Error('node watch getNodes error'); + throw Error('Failed to get available nodes'); } }; @@ -231,7 +231,7 @@ class NodeService { return formattedNode; } catch (e) { console.error(e); - throw Error('node watch getNodeByMainPublicKey error'); + throw Error('Failed to get node info for public key ' + publicKey); } }; @@ -251,28 +251,84 @@ class NodeService { return nodeTypes; }; - static getNodeHeightStats = async () => { + static getNodeHeightAndFinalizedHeightStats = async () => { try { - const data = await http.statisticServiceRestClient().getNodeHeightStats(); - - return [ - { - name: 'Height', - data: data.height.map(el => ({ - x: '' + parseInt(el.value), - y: parseInt(el.count) + const data = await NodeWatchService.getNodes(); + + const nodesByVersion = {}; + + data.forEach(node => { + const {version} = node; + + // Initialize the array for this version if it doesn't exist yet + if (!nodesByVersion[version]) { + nodesByVersion[version] = { + heights: [ + { + height: node.height, + count: 1 + } + ], + finalizedHeights: [ + { + finalizedHeight: node.finalizedHeight, + count: 1 + } + ] + }; + } else { + // Check if height already exists + const existingHeight = nodesByVersion[version].heights.find(h => h.height === node.height); + if (existingHeight) { + existingHeight.count++; + } else { + nodesByVersion[version].heights.push({ + height: node.height, + count: 1 + }); + } + + // Check if finalized height already exists + const existingFinalizedHeight = nodesByVersion[version] + .finalizedHeights.find(h => h.finalizedHeight === node.finalizedHeight); + if (existingFinalizedHeight) { + existingFinalizedHeight.count++; + } else { + nodesByVersion[version].finalizedHeights.push({ + finalizedHeight: node.finalizedHeight, + count: 1 + }); + } + } + }); + + const result = []; + + Object.keys(nodesByVersion).forEach(version => { + // Add height series for this version + result.push({ + name: `${version} - Height`, + data: nodesByVersion[version].heights.map(item => ({ + x: item.height, + y: item.count, + z: item.count })) - }, - { - name: 'Finalized Height', - data: data.finalizedHeight.map(el => ({ - x: '' + parseInt(el.value), - y: parseInt(el.count) + }); + + // Add finalized height series for this version + result.push({ + name: `${version} - Finalized Height`, + data: nodesByVersion[version].finalizedHeights.map(item => ({ + x: item.finalizedHeight, + y: item.count, + z: item.count })) - } - ]; + }); + }); + + return result; } catch (e) { - throw Error('Statistics service getNodeHeightStats error: ', e); + throw Error('Failed to get node height stats'); } }; diff --git a/src/infrastructure/NodeWatchService.js b/src/infrastructure/NodeWatchService.js index 3198ce972..609866cf0 100644 --- a/src/infrastructure/NodeWatchService.js +++ b/src/infrastructure/NodeWatchService.js @@ -19,6 +19,11 @@ class NodeWatchService { return response.data; } + static async getNodeCount() { + const response = await this.get('/api/symbol/nodes/count'); + return response.data; + } + static async get(route) { try { const response = await Axios.get(`${globalConfig.endpoints.nodeWatch}${route}`); diff --git a/src/infrastructure/StatisticService.js b/src/infrastructure/StatisticService.js index b07079877..5669723b4 100644 --- a/src/infrastructure/StatisticService.js +++ b/src/infrastructure/StatisticService.js @@ -16,8 +16,7 @@ * */ import Constants from '../config/constants'; -import globalConfig from '../config/globalConfig'; -import Axios from 'axios'; +import { NodeWatchService } from '../infrastructure'; class StatisticService { /** @@ -145,20 +144,12 @@ class StatisticService { } static getNodeCountSeries = async () => { - const data = await StatisticService.fetchFromStatisticsService('/timeSeries/nodeCount'); + const data = await NodeWatchService.getNodeCount(); const chartData = StatisticService.formatChartData(data, ['1', '2', '3', '4', '5', '6', '7', 'total']); return chartData.map(el => ({ ...el, name: Constants.RoleType[el.name] || el.name })); } - static fetchFromStatisticsService = async route => { - if (this.isUrlProvided()) - return (await Axios.get(globalConfig.endpoints.statisticsService + route)).data; - - else - throw Error('Statistics service endpoint is not provided'); - } - static formatChartData = (data, includeKeys) => { const aggreagatedData = {}; const isKeyIncluded = key => !includeKeys || includeKeys.includes(key); @@ -189,15 +180,6 @@ class StatisticService { return chartData; } - - static isUrlProvided () { - try { - new URL(globalConfig?.endpoints?.statisticsService); // eslint-disable-line no-new - return true; - } catch (e) { - return false; - } - } } export default StatisticService; diff --git a/src/infrastructure/http.js b/src/infrastructure/http.js index 3e2ce43bb..c99de2ce6 100644 --- a/src/infrastructure/http.js +++ b/src/infrastructure/http.js @@ -20,10 +20,6 @@ import accountLabels from '../config/accountLabels'; import globalConfig from '../config/globalConfig'; import { NamespaceService } from '../infrastructure'; import * as symbol from 'symbol-sdk'; -import { - Configuration, - NodeApi -} from 'symbol-statistics-service-typescript-fetch-client'; let NODE_URL; @@ -190,21 +186,4 @@ export default class http { static get transactionPaginationStreamer() { return new symbol.TransactionPaginationStreamer(this.createRepositoryFactory.createTransactionRepository()); } - - static statisticServiceRestClient() { - try { - const statisticsServiceUrl = globalConfig.endpoints.statisticsService; - - if (statisticsServiceUrl && statisticsServiceUrl.length) { - return new NodeApi(new Configuration({ - fetchApi: fetch, - basePath: statisticsServiceUrl - })); - } else { - throw Error('Statistics service endpoint is not provided'); - } - } catch (error) { - console.error(error); - } - } } diff --git a/src/store/node.js b/src/store/node.js index 09b5ae0d2..de81f0d04 100644 --- a/src/store/node.js +++ b/src/store/node.js @@ -63,8 +63,7 @@ export default { hostInfoManager: (state, getters) => ({ loading: getters.timeline?.loading || getters.info?.loading, - error: !StatisticService.isUrlProvided() || - getters.timeline?.error || + error: getters.timeline?.error || getters.info?.error }) }, diff --git a/src/store/statistic.js b/src/store/statistic.js index fd2298278..70a4df567 100644 --- a/src/store/statistic.js +++ b/src/store/statistic.js @@ -29,8 +29,8 @@ import { Order } from 'symbol-sdk'; const managers = [ new DataSet( - 'nodeHeightStats', - () => NodeService.getNodeHeightStats() + 'nodeHeightAndFinalizedHeightStats', + () => NodeService.getNodeHeightAndFinalizedHeightStats() ) ]; @@ -123,7 +123,7 @@ export default { async uninitialize ({ commit, dispatch, getters }) { const callback = async () => {}; - getters.nodeHeightStats?.uninitialize(); + getters.nodeHeightAndFinalizedHeightStats?.uninitialize(); await LOCK.uninitialize(callback, commit, dispatch, getters); }, @@ -134,7 +134,7 @@ export default { context.commit('setLoadingBlockTimeDifference', true); context.commit('setLoadingTransactionPerBlock', true); context.commit('setLoadingNodeCountSeries', true); - context.getters.nodeHeightStats.setStore(context).initialFetch(); + context.getters.nodeHeightAndFinalizedHeightStats.setStore(context).initialFetch(); context.commit('setError', false); try { diff --git a/src/views/Statistics.vue b/src/views/Statistics.vue index fa78acdfd..4b9d53fa4 100644 --- a/src/views/Statistics.vue +++ b/src/views/Statistics.vue @@ -33,12 +33,10 @@ - - - - - + + + @@ -50,7 +48,7 @@ import NetworkFeesWidget from '../components/widgets/NetworkFeesWidget.vue'; import NetworkRentalFeesWidget from '../components/widgets/NetworkRentalFeesWidget.vue'; import ChartBlockTimeDifference from '../components/widgets/ChartBlockTimeDifference.vue'; import ChartTransactionPerBlock from '../components/widgets/ChartTransactionPerBlock.vue'; -import NodeHeightStatsWidget from '../components/widgets/NodeHeightStatsWidget.vue'; +import NodeHeightAndFinalizedHeightStatsWidget from '../components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue'; import ChartNodeCount from '../components/widgets/ChartNodeCount.vue'; export default { @@ -60,7 +58,7 @@ export default { NetworkRentalFeesWidget, ChartBlockTimeDifference, ChartTransactionPerBlock, - NodeHeightStatsWidget, + NodeHeightAndFinalizedHeightStatsWidget, ChartNodeCount }, mounted () {