From 9b7171411d588972fc0c00e5d3c3f869917981f3 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:38:14 +0100 Subject: [PATCH] feat: use nova models on visualizer three (#1053) * feat: use nova models on visualizer three * fix: remove unnecessary file and update client to nova * fix: set long timestamp without time reference --- api/src/initServices.ts | 4 + .../services/nova/stats/baseStatsService.ts | 56 +++++++ .../services/nova/stats/novaStatsService.ts | 37 +++++ .../nova/block/BlockTangleState.scss | 29 ++++ .../nova/block/BlockTangleState.tsx | 79 +++++----- client/src/app/types/visualizer.types.ts | 6 +- .../visualizer-threejs/VisualizerInstance.tsx | 8 +- .../visualizer-threejs/store/tangle.ts | 2 +- .../src/features/visualizer-threejs/types.ts | 5 +- .../wrapper/SelectedFeedInfo.tsx | 138 ++---------------- .../visualizer-threejs/wrapper/Wrapper.tsx | 6 +- client/src/models/graph/nova/INodeData.ts | 18 +++ client/src/services/nova/novaApiClient.ts | 14 ++ 13 files changed, 226 insertions(+), 176 deletions(-) create mode 100644 api/src/services/nova/stats/baseStatsService.ts create mode 100644 api/src/services/nova/stats/novaStatsService.ts create mode 100644 client/src/models/graph/nova/INodeData.ts diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 0873bba40..6a9f02792 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -25,6 +25,7 @@ import { NetworkService } from "./services/networkService"; import { NovaFeed } from "./services/nova/feed/novaFeed"; import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService"; import { NovaApiService } from "./services/nova/novaApiService"; +import { NovaStatsService } from "./services/nova/stats/novaStatsService"; import { ChronicleService } from "./services/stardust/chronicleService"; import { StardustFeed } from "./services/stardust/feed/stardustFeed"; import { InfluxDBService } from "./services/stardust/influx/influxDbService"; @@ -223,6 +224,9 @@ function initNovaServices(networkConfig: INetwork): void { ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService); } + const novaStatsService = new NovaStatsService(networkConfig); + ServiceFactory.register(`stats-${networkConfig.network}`, () => novaStatsService); + // eslint-disable-next-line no-void void NovaClient.create(novaClientParams).then((novaClient) => { ServiceFactory.register(`client-${networkConfig.network}`, () => novaClient); diff --git a/api/src/services/nova/stats/baseStatsService.ts b/api/src/services/nova/stats/baseStatsService.ts new file mode 100644 index 000000000..6399e38ed --- /dev/null +++ b/api/src/services/nova/stats/baseStatsService.ts @@ -0,0 +1,56 @@ +import { INetwork } from "../../../models/db/INetwork"; +import { IStatistics } from "../../../models/services/IStatistics"; +import { IStatsService } from "../../../models/services/IStatsService"; + +/** + * Class to handle stats service. + */ +export abstract class BaseStatsService implements IStatsService { + /** + * The network configuration. + */ + protected readonly _networkConfiguration: INetwork; + + /** + * The statistics. + */ + protected _statistics: IStatistics[]; + + /** + * Create a new instance of BaseStatsService. + * @param networkConfiguration The network configuration. + */ + constructor(networkConfiguration: INetwork) { + this._networkConfiguration = networkConfiguration; + this._statistics = [ + { + itemsPerSecond: 0, + confirmedItemsPerSecond: 0, + confirmationRate: 0, + }, + ]; + + setInterval(async () => this.updateStatistics(), 2000); + } + + /** + * Get the current stats. + * @returns The statistics for the network. + */ + public getStats(): IStatistics { + return this._statistics.at(-1); + } + + /** + * Get the stats history. + * @returns The historical statistics for the network. + */ + public getItemsPerSecondHistory(): number[] { + return this._statistics.map((s) => s.itemsPerSecond); + } + + /** + * Gather more statistics. + */ + protected abstract updateStatistics(): Promise; +} diff --git a/api/src/services/nova/stats/novaStatsService.ts b/api/src/services/nova/stats/novaStatsService.ts new file mode 100644 index 000000000..88bef4c80 --- /dev/null +++ b/api/src/services/nova/stats/novaStatsService.ts @@ -0,0 +1,37 @@ +// eslint-disable-next-line import/no-unresolved +import { Client } from "@iota/sdk-nova"; +import { BaseStatsService } from "./baseStatsService"; +import { ServiceFactory } from "../../../factories/serviceFactory"; +import logger from "../../../logger"; + +/** + * Class to handle stats service. + */ +export class NovaStatsService extends BaseStatsService { + /** + * Gather general statistics. + */ + protected async updateStatistics(): Promise { + try { + const client = ServiceFactory.get(`client-${this._networkConfiguration.network}`); + const response = await client.getInfo(); + + if (response) { + const metrics = response.nodeInfo.metrics; + this._statistics.push({ + itemsPerSecond: Number(metrics.blocksPerSecond), + confirmedItemsPerSecond: Number(metrics.confirmedBlocksPerSecond), + confirmationRate: Number(metrics.confirmationRate), + }); + + logger.debug(`[NovaStatsService] Updating network statistics for ${this._networkConfiguration.network}`); + + if (this._statistics.length > 30) { + this._statistics = this._statistics.slice(-30); + } + } + } catch (err) { + logger.debug(`[NovaStatsService] Update statistics failed: ${err}`); + } + } +} diff --git a/client/src/app/components/nova/block/BlockTangleState.scss b/client/src/app/components/nova/block/BlockTangleState.scss index 541b5d85d..c69f8c0ab 100644 --- a/client/src/app/components/nova/block/BlockTangleState.scss +++ b/client/src/app/components/nova/block/BlockTangleState.scss @@ -12,6 +12,35 @@ margin-top: 4px; } + .block-tangle-reference { + display: flex; + white-space: pre-wrap; + @include font-size(12px); + + color: $gray-5; + font-family: $inter; + font-weight: 500; + letter-spacing: 0.5px; + + @include tablet-down { + margin-left: 8px; + flex-direction: column; + white-space: normal; + } + + .block-tangle-reference__link { + color: var(--link-color); + cursor: pointer; + } + + .time-reference { + &:hover { + cursor: pointer; + text-decoration: underline; + } + } + } + .block-tangle-state { @include font-size(12px); diff --git a/client/src/app/components/nova/block/BlockTangleState.tsx b/client/src/app/components/nova/block/BlockTangleState.tsx index 825885655..e8a4619e6 100644 --- a/client/src/app/components/nova/block/BlockTangleState.tsx +++ b/client/src/app/components/nova/block/BlockTangleState.tsx @@ -1,10 +1,10 @@ import classNames from "classnames"; -import React, { useEffect, useState } from "react"; +import React from "react"; import Tooltip from "../../Tooltip"; import { BlockState, u64 } from "@iota/sdk-wasm-nova/web"; import { BlockFailureReason, BLOCK_FAILURE_REASON_STRINGS } from "@iota/sdk-wasm-nova/web/lib/types/models/block-failure-reason"; +import moment from "moment"; import "./BlockTangleState.scss"; -import { DateHelper } from "~/helpers/dateHelper"; export interface BlockTangleStateProps { /** @@ -24,45 +24,46 @@ export interface BlockTangleStateProps { } const BlockTangleState: React.FC = ({ status, issuingTime, failureReason }) => { - const [readableTimestamp, setReadableTimestamp] = useState(); - - useEffect(() => { - const timestamp = DateHelper.format(DateHelper.milliseconds(Number(issuingTime) / 1000000)); - setReadableTimestamp(timestamp); - }, [issuingTime]); + const blockIssueMoment = moment(Number(issuingTime) / 1000000); + const timeReference = blockIssueMoment.fromNow(); + const longTimestamp = blockIssueMoment.format("LLLL"); return ( -
- {status && ( - -
- {failureReason ? ( - - - {status} - - - ) : ( - {status} - )} -
-
- {readableTimestamp} -
-
- )} -
+ <> +
+ {status && ( + +
+ {failureReason ? ( + + + {status} + + + ) : ( + {status} + )} +
+
+ + {timeReference} + +
+
+ )} +
+ ); }; diff --git a/client/src/app/types/visualizer.types.ts b/client/src/app/types/visualizer.types.ts index d6b1889ee..30e90731f 100644 --- a/client/src/app/types/visualizer.types.ts +++ b/client/src/app/types/visualizer.types.ts @@ -1,7 +1,9 @@ import Viva from "vivagraphjs"; -import { IFeedBlockData } from "../../models/api/stardust/feed/IFeedBlockData"; +import { IFeedBlockData as IFeedBlockDataStardust } from "../../models/api/stardust/feed/IFeedBlockData"; +import { IFeedBlockData as IFeedBlockDataNova } from "../../models/api/nova/feed/IFeedBlockData"; import { INodeData } from "../../models/graph/stardust/INodeData"; export type TSelectNode = (node?: Viva.Graph.INode) => void; -export type TSelectFeedItem = IFeedBlockData | null; +export type TSelectFeedItem = IFeedBlockDataStardust | null; +export type TSelectFeedItemNova = IFeedBlockDataNova | null; diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index a4bf8f063..6cd1b9b94 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -26,9 +26,10 @@ import { NovaFeedClient } from "../../services/nova/novaFeedClient"; import { Wrapper } from "./wrapper/Wrapper"; import { CanvasElement } from "./enums"; import { useGetThemeMode } from "~/helpers/hooks/useGetThemeMode"; -import CameraControls from "./CameraControls"; +import { TSelectFeedItemNova } from "~/app/types/visualizer.types"; import { BasicBlockBody, IBlockMetadata } from "@iota/sdk-wasm-nova/web"; import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; +import CameraControls from "./CameraControls"; import "./Visualizer.scss"; const features = { @@ -65,6 +66,9 @@ const VisualizerInstance: React.FC> = const addToColorQueue = useTangleStore((s) => s.addToColorQueue); const blockMetadata = useTangleStore((s) => s.blockMetadata); const indexToBlockId = useTangleStore((s) => s.indexToBlockId); + const clickedInstanceId = useTangleStore((s) => s.clickedInstanceId); + + const selectedFeedItem: TSelectFeedItemNova = clickedInstanceId ? blockMetadata.get(clickedInstanceId) ?? null : null; const emitterRef = useRef(null); const feedServiceRef = useRef(null); @@ -230,7 +234,7 @@ const VisualizerInstance: React.FC> = networkConfig={networkConfig} onChangeFilter={() => {}} selectNode={() => {}} - selectedFeedItem={null} + selectedFeedItem={selectedFeedItem} setIsPlaying={setIsPlaying} isEdgeRenderingEnabled={isEdgeRenderingEnabled} setEdgeRenderingEnabled={(checked) => setEdgeRenderingEnabled(checked)} diff --git a/client/src/features/visualizer-threejs/store/tangle.ts b/client/src/features/visualizer-threejs/store/tangle.ts index 67300ddc1..5efd30480 100644 --- a/client/src/features/visualizer-threejs/store/tangle.ts +++ b/client/src/features/visualizer-threejs/store/tangle.ts @@ -2,7 +2,7 @@ import { Color } from "three"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { ZOOM_DEFAULT, ANIMATION_TIME_SECONDS } from "../constants"; -import { IFeedBlockData } from "~models/api/stardust/feed/IFeedBlockData"; +import { IFeedBlockData } from "~models/api/nova/feed/IFeedBlockData"; interface IPosition { x: number; diff --git a/client/src/features/visualizer-threejs/types.ts b/client/src/features/visualizer-threejs/types.ts index 8e881bd26..d290f27e6 100644 --- a/client/src/features/visualizer-threejs/types.ts +++ b/client/src/features/visualizer-threejs/types.ts @@ -1,8 +1,5 @@ -import { IFeedBlockData } from "../../models/api/stardust/feed/IFeedBlockData"; -import { IFeedBlockMetadata } from "../../models/api/stardust/feed/IFeedBlockMetadata"; +import { IFeedBlockData } from "../../models/api/nova/feed/IFeedBlockData"; export type TFeedBlockAdd = (newBlock: IFeedBlockData) => void; -export type TFeedBlockMetadataUpdate = (metadataUpdate: { [id: string]: IFeedBlockMetadata }) => void; - export type TangleMeshType = THREE.Mesh; diff --git a/client/src/features/visualizer-threejs/wrapper/SelectedFeedInfo.tsx b/client/src/features/visualizer-threejs/wrapper/SelectedFeedInfo.tsx index 0ee810851..3ad097dfa 100644 --- a/client/src/features/visualizer-threejs/wrapper/SelectedFeedInfo.tsx +++ b/client/src/features/visualizer-threejs/wrapper/SelectedFeedInfo.tsx @@ -1,47 +1,25 @@ -import { CONFLICT_REASON_STRINGS, ConflictReason, hexToUtf8 } from "@iota/sdk-wasm/web"; -import classNames from "classnames"; -import React, { useContext, useState } from "react"; +import React from "react"; import { Link } from "react-router-dom"; -import BlockTangleState from "~/app/components/stardust/block/BlockTangleState"; +import BlockTangleState from "~/app/components/nova/block/BlockTangleState"; import TruncatedId from "~/app/components/stardust/TruncatedId"; -import NetworkContext from "~/app/context/NetworkContext"; -import { TSelectNode } from "~/app/types/visualizer.types"; import CloseIcon from "~assets/close.svg?react"; -import DropdownIcon from "~assets/dropdown-arrow.svg?react"; -import { DateHelper } from "~helpers/dateHelper"; -import { formatAmount } from "~helpers/stardust/valueFormatHelper"; import { useTangleStore } from "../store"; import { INetwork } from "~models/config/INetwork"; +import { useBlockMetadata } from "~/helpers/nova/hooks/useBlockMetadata"; +import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; import "./KeyPanel.scss"; export const SelectedFeedInfo = ({ network, - selectNode, + selectedFeedItem, networkConfig, }: { readonly networkConfig: INetwork; readonly network: string; - readonly selectNode: TSelectNode; + readonly selectedFeedItem: IFeedBlockData; }) => { - const clickedInstanceId = useTangleStore((state) => state.clickedInstanceId); const setClickedInstanceId = useTangleStore((state) => state.setClickedInstanceId); - const blockMetadata = useTangleStore((state) => state.blockMetadata); - - const { tokenInfo } = useContext(NetworkContext); - const [isExpanded, setIsExpanded] = useState(false); - const [isFormatAmountsFull, setIsFormatAmountsFull] = useState(null); - const getStatus = (referenced?: number) => (referenced ? "referenced" : undefined); - - const getConflictReasonMessage = (conflictReason?: ConflictReason) => - conflictReason ? CONFLICT_REASON_STRINGS[conflictReason] : undefined; - - // eslint-disable-next-line no-extra-boolean-cast - const selectedFeedItem = !!clickedInstanceId ? blockMetadata?.get(clickedInstanceId) : undefined; - const properties = selectedFeedItem?.properties; - - if (!selectedFeedItem) { - return null; - } + const [selectedBlockMetadata, isMetadataLoading] = useBlockMetadata(network, selectedFeedItem.blockId); return (
@@ -54,110 +32,20 @@ export const SelectedFeedInfo = ({
Block - {selectedFeedItem.payloadType !== "Milestone" && selectedFeedItem.metadata && ( - + + {selectedBlockMetadata.metadata && !isMetadataLoading && ( - - )} + )} +
- {properties?.transactionId && ( - -
Transaction id
-
- - - -
-
- )} - {properties?.tag && ( - -
Tag
-
{hexToUtf8(properties.tag)}
-
Hex
-
{properties.tag}
-
- )} - {selectedFeedItem.metadata?.milestone !== undefined && ( - - {properties?.milestoneId && ( - -
Milestone id
-
- - - -
-
- )} -
Milestone index
-
- - {selectedFeedItem.metadata.milestone} - -
- {properties?.timestamp && ( - -
Timestamp
-
- {DateHelper.formatShort(DateHelper.milliseconds(properties.timestamp))} -
-
- )} -
- )} - {selectedFeedItem?.value !== undefined && selectedFeedItem.metadata?.milestone === undefined && ( - -
Value
-
- setIsFormatAmountsFull(!isFormatAmountsFull)} className="pointer margin-r-5"> - {formatAmount(selectedFeedItem?.value, tokenInfo, isFormatAmountsFull ?? undefined)} - -
-
- )} - {selectedFeedItem?.reattachments && selectedFeedItem.reattachments.length > 0 && ( - -
setIsExpanded(!isExpanded)} - > -
- -
-
Reattachments
-
-
- {selectedFeedItem.reattachments.map((item, index) => ( -
- - - - {item?.metadata && ( - - - - )} -
- ))} -
-
- )}
diff --git a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx index 4d5d459d3..84c8d0b31 100644 --- a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx +++ b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx @@ -1,6 +1,6 @@ import React from "react"; import Modal from "~/app/components/Modal"; -import { TSelectFeedItem, TSelectNode } from "~/app/types/visualizer.types"; +import { TSelectFeedItemNova, TSelectNode } from "~/app/types/visualizer.types"; import { INetwork } from "~/models/config/INetwork"; import { KeyPanel } from "./KeyPanel"; import mainHeader from "~assets/modals/visualizer/main-header.json"; @@ -29,7 +29,7 @@ export const Wrapper = ({ readonly networkConfig: INetwork; readonly onChangeFilter: React.ChangeEventHandler; readonly selectNode: TSelectNode; - readonly selectedFeedItem: TSelectFeedItem; + readonly selectedFeedItem: TSelectFeedItemNova; readonly setIsPlaying: (isPlaying: boolean) => void; readonly isEdgeRenderingEnabled?: boolean; readonly setEdgeRenderingEnabled?: (isEnabled: boolean) => void; @@ -69,7 +69,7 @@ export const Wrapper = ({ - + {selectedFeedItem && } ); diff --git a/client/src/models/graph/nova/INodeData.ts b/client/src/models/graph/nova/INodeData.ts new file mode 100644 index 000000000..45e75fc47 --- /dev/null +++ b/client/src/models/graph/nova/INodeData.ts @@ -0,0 +1,18 @@ +import { IFeedBlockData } from "../../api/nova/feed/IFeedBlockData"; + +export interface INodeData { + /** + * The feed item. + */ + feedItem: IFeedBlockData; + + /** + * When was the node added. + */ + added: number; + + /** + * The graph number. + */ + graphId?: number; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 4dc56ab8f..493dc5c50 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -13,6 +13,8 @@ import { IBlockDetailsRequest } from "~/models/api/nova/block/IBlockDetailsReque import { IBlockDetailsResponse } from "~/models/api/nova/block/IBlockDetailsResponse"; import { IRewardsRequest } from "~/models/api/nova/IRewardsRequest"; import { IRewardsResponse } from "~/models/api/nova/IRewardsResponse"; +import { IStatsGetRequest } from "~/models/api/stats/IStatsGetRequest"; +import { IStatsGetResponse } from "~/models/api/stats/IStatsGetResponse"; /** * Class to handle api communications on nova. @@ -84,4 +86,16 @@ export class NovaApiClient extends ApiClient { public async getRewards(request: IRewardsRequest): Promise { return this.callApi(`nova/output/rewards/${request.network}/${request.outputId}`, "get"); } + + /** + * Get the stats. + * @param request The request to send. + * @returns The response from the request. + */ + public async stats(request: IStatsGetRequest): Promise { + return this.callApi( + `stats/${request.network}?includeHistory=${request.includeHistory ? "true" : "false"}`, + "get", + ); + } }