├── .eslintignore ├── .gitignore ├── .env ├── .vscode └── settings.json ├── src ├── assets │ ├── iota-core │ │ └── brand.json │ ├── fonts │ │ ├── dm-sans │ │ │ ├── dm-sans-v6-latin-500.woff │ │ │ ├── dm-sans-v6-latin-500.woff2 │ │ │ ├── dm-sans-v6-latin-700.woff │ │ │ ├── dm-sans-v6-latin-700.woff2 │ │ │ ├── dm-sans-v6-latin-italic.woff │ │ │ ├── dm-sans-v6-latin-500italic.woff │ │ │ ├── dm-sans-v6-latin-700italic.woff │ │ │ ├── dm-sans-v6-latin-italic.woff2 │ │ │ ├── dm-sans-v6-latin-regular.woff │ │ │ ├── dm-sans-v6-latin-regular.woff2 │ │ │ ├── dm-sans-v6-latin-500italic.woff2 │ │ │ └── dm-sans-v6-latin-700italic.woff2 │ │ └── ibm-plex │ │ │ ├── ibm-plex-mono-v6-latin-300.woff │ │ │ ├── ibm-plex-mono-v6-latin-300.woff2 │ │ │ ├── ibm-plex-mono-v6-latin-500.woff │ │ │ ├── ibm-plex-mono-v6-latin-500.woff2 │ │ │ ├── ibm-plex-mono-v6-latin-italic.woff │ │ │ ├── ibm-plex-mono-v6-latin-italic.woff2 │ │ │ ├── ibm-plex-mono-v6-latin-regular.woff │ │ │ └── ibm-plex-mono-v6-latin-regular.woff2 │ ├── dropdown-arrow.svg │ ├── chevron-down.svg │ ├── health-bad.svg │ ├── health-good.svg │ ├── health-warning.svg │ ├── play.svg │ ├── ellipsis.svg │ ├── download.svg │ ├── upload.svg │ ├── slot.svg │ ├── banner-curve.svg │ ├── uptime.svg │ ├── copy.svg │ ├── pause.svg │ ├── confirmation.svg │ ├── toggle.svg │ ├── chevron-left.svg │ ├── eye.svg │ ├── chevron-right.svg │ ├── home.svg │ ├── analytics.svg │ ├── padlock.svg │ ├── memory.svg │ ├── padlock-unlocked.svg │ ├── peers.svg │ ├── eye-closed.svg │ ├── visualizer.svg │ ├── moon.svg │ ├── search.svg │ ├── navigate-to.svg │ ├── close.svg │ ├── db-icon.svg │ ├── sun.svg │ ├── pruning.svg │ └── settings.svg ├── react-app-env.d.ts ├── models │ ├── hexEncodedTypes.ts │ ├── visualizer │ │ ├── visualizerVertexOperation.ts │ │ ├── IVerticesCounts.ts │ │ └── IVertex.ts │ ├── IBrandConfiguration.ts │ ├── websocket │ │ ├── IVisualizerTipInfo.ts │ │ ├── IVisualizerBlockStateInfo.ts │ │ ├── IGossipMetrics.ts │ │ ├── IPublicNodeStatus.ts │ │ ├── IDatabaseSizesMetric.ts │ │ ├── IDatabaseSizesMetrics.ts │ │ ├── IWebSocketMessage.ts │ │ ├── ISyncStatus.ts │ │ ├── INodeInfoExtended.ts │ │ ├── webSocketTopic.ts │ │ └── IVisualizerVertex.ts │ ├── peers │ │ ├── IPeersResponse.ts │ │ ├── IPeerGossipMetrics.ts │ │ └── IPeer.ts │ ├── ITypeBase.ts │ ├── tangle │ │ ├── IPayloadSignedTransaction.ts │ │ ├── IPayloadCandidacyAnnouncement.ts │ │ ├── IBlock.ts │ │ ├── IBlockHeader.ts │ │ ├── IBlockBodyBasic.ts │ │ ├── IPayloadTaggedData.ts │ │ ├── blockBodyTypes.ts │ │ ├── IBlockBodyValidation.ts │ │ └── payloadTypes.ts │ ├── info │ │ ├── INodeInfoProtocol.ts │ │ ├── INodeInfoProtocolParameters.ts │ │ ├── INodeInfo.ts │ │ └── INetworkMetrics.ts │ ├── IResponse.ts │ ├── clients │ │ ├── singleNodeClientOptions.ts │ │ └── clientError.ts │ └── IClient.ts ├── app │ ├── routes │ │ ├── PeerRouteProps.ts │ │ ├── Login.scss │ │ ├── Unavailable.scss │ │ ├── LoginState.ts │ │ ├── PeerState.ts │ │ ├── Unavailable.tsx │ │ ├── PeersState.ts │ │ ├── VisualizerState.ts │ │ ├── Home.scss │ │ ├── HomeState.ts │ │ ├── Peer.scss │ │ ├── Peers.scss │ │ └── Visualizer.scss │ ├── components │ │ ├── layout │ │ │ ├── NavPanelState.ts │ │ │ ├── TabPanelState.ts │ │ │ ├── ToggleButtonState.ts │ │ │ ├── NavMenuProps.ts │ │ │ ├── SpinnerProps.ts │ │ │ ├── NavMenuState.ts │ │ │ ├── BreakpointState.ts │ │ │ ├── BlockButtonState.ts │ │ │ ├── ToggleButtonProps.ts │ │ │ ├── HeaderProps.ts │ │ │ ├── HealthIndicatorProps.ts │ │ │ ├── PaginationState.tsx │ │ │ ├── BlockButtonProps.ts │ │ │ ├── MicroGraphState.ts │ │ │ ├── GraphState.ts │ │ │ ├── HealthIndicator.scss │ │ │ ├── DialogProps.ts │ │ │ ├── TabPanelProps.ts │ │ │ ├── BreakpointProps.ts │ │ │ ├── InfoPanelProps.ts │ │ │ ├── MicroGraphProps.ts │ │ │ ├── NavMenu.scss │ │ │ ├── Spinner.scss │ │ │ ├── Spinner.tsx │ │ │ ├── ToggleButton.scss │ │ │ ├── MicroGraph.scss │ │ │ ├── GraphProps.ts │ │ │ ├── PaginationProps.ts │ │ │ ├── HealthIndicator.tsx │ │ │ ├── HeaderState.ts │ │ │ ├── Header.scss │ │ │ ├── AsyncComponent.tsx │ │ │ ├── Dialog.tsx │ │ │ ├── BlockButton.scss │ │ │ ├── Tooltip.tsx │ │ │ ├── TabPanel.scss │ │ │ ├── NavPanelProps.ts │ │ │ ├── Tooltip.scss │ │ │ ├── ToggleButton.tsx │ │ │ ├── NavPanel.scss │ │ │ ├── Graph.scss │ │ │ ├── Pagination.scss │ │ │ ├── InfoPanel.tsx │ │ │ ├── Breakpoint.tsx │ │ │ ├── BlockButton.tsx │ │ │ ├── Dialog.scss │ │ │ ├── NavMenu.tsx │ │ │ ├── TabPanel.tsx │ │ │ └── InfoPanel.scss │ │ └── tangle │ │ │ ├── PeersSummaryState.ts │ │ │ └── PeersSummaryPanel.scss │ ├── AppState.ts │ └── App.scss ├── scss │ ├── variables.scss │ ├── themes │ │ ├── dark.scss │ │ └── light.scss │ ├── media-queries.scss │ ├── fonts.scss │ ├── fonts │ │ ├── ibm-plex-mono.scss │ │ └── dm-sans.scss │ ├── standard.scss │ ├── forms.scss │ └── layout.scss ├── index.scss ├── factories │ └── serviceFactory.ts ├── utils │ ├── dataHelper.ts │ ├── clipboardHelper.ts │ ├── brandHelper.ts │ └── downloadHelper.ts ├── services │ ├── settingsService.ts │ ├── themeService.ts │ ├── nodeConfigService.ts │ ├── dashboardConfigService.ts │ ├── eventAggregator.ts │ ├── tangleService.ts │ ├── localStorageService.ts │ └── sessionStorageService.ts └── index.tsx ├── craco.config.js ├── .github ├── codeql │ └── codeql-config.yml ├── dependabot.yml ├── SUPPORT.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── create-task.yml │ └── bug-report.yml ├── workflows │ ├── main.yml │ ├── build-stardust.yml │ └── codeql-analysis.yml ├── pull_request_template.md └── SECURITY.md ├── public ├── branding │ └── iota-core │ │ ├── favicon.ico │ │ └── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ └── site.webmanifest └── index.html ├── typings └── global.d.ts ├── .stylelintrc.json ├── tsconfig.json ├── README.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | EXTEND_ESLINT=true 2 | SKIP_PREFLIGHT_CHECK=true 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /src/assets/iota-core/brand.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IOTA-Core" 3 | } -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | eslint: { 3 | enable: false 4 | } 5 | }; -------------------------------------------------------------------------------- /src/models/hexEncodedTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hex encoded bytes. 3 | */ 4 | export declare type HexEncodedString = string; 5 | -------------------------------------------------------------------------------- /src/models/visualizer/visualizerVertexOperation.ts: -------------------------------------------------------------------------------- 1 | export type VisualizerVertexOperation = "add" | "update" | "delete"; 2 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Config" 2 | 3 | queries: 4 | - uses: security-and-quality 5 | paths: 6 | - src 7 | -------------------------------------------------------------------------------- /public/branding/iota-core/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon.ico -------------------------------------------------------------------------------- /src/app/routes/PeerRouteProps.ts: -------------------------------------------------------------------------------- 1 | export interface PeerRouteProps { 2 | /** 3 | * The peer id. 4 | */ 5 | id: string; 6 | } 7 | -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/favicon.ico -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/app/components/layout/NavPanelState.ts: -------------------------------------------------------------------------------- 1 | export interface NavPanelState { 2 | /** 3 | * The image logo. 4 | */ 5 | logoSrc: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/layout/TabPanelState.ts: -------------------------------------------------------------------------------- 1 | export interface TabPanelState { 2 | /** 3 | * The active tab. 4 | */ 5 | activeTab: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-500.woff -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-500.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-700.woff -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-700.woff2 -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff -------------------------------------------------------------------------------- /src/models/IBrandConfiguration.ts: -------------------------------------------------------------------------------- 1 | export interface IBrandConfiguration { 2 | /** 3 | * The name of the application. 4 | */ 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff2 -------------------------------------------------------------------------------- /src/models/websocket/IVisualizerTipInfo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IVisualizerTipInfo { 3 | id: string; 4 | isTip: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/components/layout/ToggleButtonState.ts: -------------------------------------------------------------------------------- 1 | export interface ToggleButtonState { 2 | /** 3 | * Is the button checked. 4 | */ 5 | value: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff2 -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/public/branding/iota-core/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff -------------------------------------------------------------------------------- /src/models/peers/IPeersResponse.ts: -------------------------------------------------------------------------------- 1 | import { IPeer } from "./IPeer"; 2 | 3 | /** 4 | * Peer details. 5 | */ 6 | export interface IPeersResponse { 7 | peers: IPeer[]; 8 | } 9 | -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | /** Global definitions for developement **/ 2 | 3 | // for style loader 4 | declare module '*.css' { 5 | const styles: any; 6 | export = styles; 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/dropdown-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger/node-dashboard/HEAD/src/assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff2 -------------------------------------------------------------------------------- /src/models/websocket/IVisualizerBlockStateInfo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IVisualizerBlockStateInfo { 3 | id: string; 4 | blockState: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/websocket/IGossipMetrics.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IGossipMetrics { 3 | incoming: number; 4 | new: number; 5 | outgoing: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/models/ITypeBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type of the object. 3 | */ 4 | export interface ITypeBase { 5 | /** 6 | * The type of the object. 7 | */ 8 | type: T; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/models/tangle/IPayloadSignedTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { ITypeBase } from "../ITypeBase"; 2 | 3 | /** 4 | * Signed Transaction payload. 5 | */ 6 | export type IPayloadSignedTransaction = ITypeBase<1>; 7 | -------------------------------------------------------------------------------- /src/models/tangle/IPayloadCandidacyAnnouncement.ts: -------------------------------------------------------------------------------- 1 | import type { ITypeBase } from "../ITypeBase"; 2 | 3 | /** 4 | * Candidacy announcement payload. 5 | */ 6 | export type IPayloadCandidacyAnnouncement = ITypeBase<2>; 7 | -------------------------------------------------------------------------------- /src/models/websocket/IPublicNodeStatus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IPublicNodeStatus { 3 | isNodeHealthy: boolean; 4 | isNetworkHealthy: boolean; 5 | pruningEpoch: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/layout/NavMenuProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface NavMenuProps { 4 | /** 5 | * The child controls. 6 | */ 7 | children?: ReactNode[] | ReactNode; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/layout/SpinnerProps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The props for the Spinner component. 3 | */ 4 | export interface SpinnerProps { 5 | /** 6 | * Show in compact mode. 7 | */ 8 | compact?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/health-bad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/components/layout/NavMenuState.ts: -------------------------------------------------------------------------------- 1 | export interface NavMenuState { 2 | /** 3 | * The image logo. 4 | */ 5 | logoSrc: string; 6 | 7 | /** 8 | * Is the menu open 9 | */ 10 | isOpen: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/health-good.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/health-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/models/websocket/IDatabaseSizesMetric.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IDatabaseSizesMetric { 3 | permanent: string; 4 | prunable: string; 5 | txRetainer: string; 6 | total: string; 7 | ts: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/models/websocket/IDatabaseSizesMetrics.ts: -------------------------------------------------------------------------------- 1 | import { IDatabaseSizesMetric } from "./IDatabaseSizesMetric"; 2 | 3 | /* eslint-disable camelcase */ 4 | export interface IDatabaseSizesMetrics { 5 | databaseSizes: IDatabaseSizesMetric[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/layout/BreakpointState.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The state for the Breakpoint component. 4 | */ 5 | export interface BreakpointState { 6 | /** 7 | * Show or hide the children. 8 | */ 9 | isVisible: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/BlockButtonState.ts: -------------------------------------------------------------------------------- 1 | export interface BlockButtonState { 2 | /** 3 | * Is the message active. 4 | */ 5 | active: boolean; 6 | 7 | /** 8 | * The message to show. 9 | */ 10 | message: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/models/websocket/IWebSocketMessage.ts: -------------------------------------------------------------------------------- 1 | export interface IWebSocketMessage { 2 | /** 3 | * The type of the message. 4 | */ 5 | type: number; 6 | 7 | /** 8 | * The data for the message. 9 | */ 10 | data: unknown; 11 | } 12 | -------------------------------------------------------------------------------- /src/models/tangle/IBlock.ts: -------------------------------------------------------------------------------- 1 | import { BlockBodyTypes } from "./blockBodyTypes"; 2 | import { IBlockHeader } from "./IBlockHeader"; 3 | 4 | /** 5 | * Block. 6 | */ 7 | export interface IBlock { 8 | header: IBlockHeader; 9 | body: BlockBodyTypes; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/models/tangle/IBlockHeader.ts: -------------------------------------------------------------------------------- 1 | import { HexEncodedString } from "../hexEncodedTypes"; 2 | 3 | /** 4 | * Block Header. 5 | */ 6 | export interface IBlockHeader { 7 | /** 8 | * The ID of the issuer. 9 | */ 10 | issuerId: HexEncodedString; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/components/layout/ToggleButtonProps.ts: -------------------------------------------------------------------------------- 1 | export interface ToggleButtonProps { 2 | /** 3 | * Is the button checked. 4 | */ 5 | value: boolean; 6 | 7 | /** 8 | * The value changed. 9 | */ 10 | onChanged(value: boolean): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/models/info/INodeInfoProtocol.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Protocol Info. 3 | */ 4 | export interface INodeInfoProtocolParameterParameters { 5 | /** 6 | * The human friendly name of the network on which the node operates on. 7 | */ 8 | networkName: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/models/websocket/ISyncStatus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface ISyncStatus { 3 | currentSlot: number; 4 | currentEpoch: number; 5 | latestAcceptedBlockSlot: number; 6 | latestFinalizedSlot: number; 7 | latestCommitmentSlot: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/layout/HeaderProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | /** 4 | * The props for the Header component. 5 | */ 6 | export interface HeaderProps { 7 | /** 8 | * The child controls. 9 | */ 10 | children?: ReactNode[] | ReactNode; 11 | } 12 | -------------------------------------------------------------------------------- /src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $content-width-desktop: 1080px; 2 | 3 | $spacing-tiny: 10px; 4 | $spacing-small: 16px; 5 | $spacing-medium: 24px; 6 | $spacing-large: 32px; 7 | 8 | $form-input-radius: 8px; 9 | 10 | $success: #28a745; 11 | $danger: #dc3545; 12 | $info: #17a2b8; 13 | $warning: #ffc107; 14 | -------------------------------------------------------------------------------- /src/assets/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/websocket/INodeInfoExtended.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface INodeInfoExtended { 3 | version: string; 4 | latestVersion: string; 5 | uptime: string; 6 | nodeId: string; 7 | multiAddress: string; 8 | alias: string; 9 | memoryUsage: string; 10 | } 11 | -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/models/peers/IPeerGossipMetrics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Peer Gossip metrics. 3 | */ 4 | export interface IPeerGossipMetrics { 5 | /** 6 | * The total amount of received packets. 7 | */ 8 | packetsReceived: number; 9 | /** 10 | * The total amount of sent packets. 11 | */ 12 | packetsSent: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/components/layout/HealthIndicatorProps.ts: -------------------------------------------------------------------------------- 1 | export interface HealthIndicatorProps { 2 | /** 3 | * The label for the indicator. 4 | */ 5 | label: string; 6 | 7 | /** 8 | * Is the indicator healthy. 9 | */ 10 | healthy: boolean; 11 | 12 | /** 13 | * Class names. 14 | */ 15 | className?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/layout/PaginationState.tsx: -------------------------------------------------------------------------------- 1 | export interface PaginationState { 2 | /** 3 | * Pagination last page. 4 | */ 5 | lastPage: number; 6 | 7 | /** 8 | * Pagination range. 9 | */ 10 | paginationRange: (number|string)[]; 11 | 12 | /** 13 | * Is mobile view. 14 | */ 15 | isMobile: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/tangle/IBlockBodyBasic.ts: -------------------------------------------------------------------------------- 1 | import { ITypeBase } from "../ITypeBase"; 2 | import { PayloadTypes } from "./payloadTypes"; 3 | 4 | /** 5 | * Basic block body. 6 | */ 7 | export interface IBlockBodyBasic extends ITypeBase<0> { 8 | /** 9 | * The inner payload of the block. Can be nil. 10 | */ 11 | payload: PayloadTypes | null; 12 | } 13 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-property-sort-order-smacss" 5 | ], 6 | "plugins": [ 7 | "stylelint-scss" 8 | ], 9 | "rules": { 10 | "at-rule-no-unknown": null, 11 | "scss/at-rule-no-unknown": true, 12 | "color-hex-length": "long" 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/components/layout/BlockButtonProps.ts: -------------------------------------------------------------------------------- 1 | export interface BlockButtonProps { 2 | /** 3 | * The type of button to show. 4 | */ 5 | buttonType: "copy"; 6 | 7 | /** 8 | * Position to show label. 9 | */ 10 | labelPosition: "top" | "right"; 11 | 12 | /** 13 | * The button click. 14 | */ 15 | onClick(): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/tangle/IPayloadTaggedData.ts: -------------------------------------------------------------------------------- 1 | import { HexEncodedString } from "../hexEncodedTypes"; 2 | import type { ITypeBase } from "../ITypeBase"; 3 | 4 | /** 5 | * Tagged data payload. 6 | */ 7 | export interface IPayloadTaggedData extends ITypeBase<0> { 8 | /** 9 | * The tag to use to categorize the data. 10 | */ 11 | tag: HexEncodedString; 12 | } 13 | -------------------------------------------------------------------------------- /src/models/websocket/webSocketTopic.ts: -------------------------------------------------------------------------------- 1 | export enum WebSocketTopic { 2 | SyncStatus = 0, 3 | PublicNodeStatus = 1, 4 | NodeInfoExtended = 2, 5 | GossipMetrics = 3, 6 | PeerMetrics = 4, 7 | NetworkMetrics = 5, 8 | DatabaseSizeMetric = 6, 9 | VisualizerVertex = 7, 10 | VisualizerBlockStateInfo = 8, 11 | VisualizerTipInfo = 9, 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/slot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/MicroGraphState.ts: -------------------------------------------------------------------------------- 1 | export interface MicroGraphState { 2 | /** 3 | * Graph points. 4 | */ 5 | graphPoints?: { 6 | type: string; 7 | x: number; 8 | y: number; 9 | }[]; 10 | 11 | /** 12 | * Circle position. 13 | */ 14 | circle?: { 15 | x: number; 16 | y: number; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/tangle/PeersSummaryState.ts: -------------------------------------------------------------------------------- 1 | export interface PeersSummaryState { 2 | /** 3 | * The list of peers. 4 | */ 5 | peers?: { 6 | id: string; 7 | alias?: string; 8 | connected: boolean; 9 | address?: string; 10 | }[]; 11 | 12 | /** 13 | * Hide any secure details. 14 | */ 15 | blindMode: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/websocket/IVisualizerVertex.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IVisualizerVertex { 3 | id: string; 4 | parents: string; 5 | blockState: string; 6 | isBasicBlockTaggedData: boolean; 7 | isBasicBlockSignedTransaction: boolean; 8 | isBasicBlockCandidacyAnnouncement: boolean; 9 | isValidationBlock: boolean; 10 | isTip: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/banner-curve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/GraphState.ts: -------------------------------------------------------------------------------- 1 | export interface GraphState { 2 | /** 3 | * Path to draw. 4 | */ 5 | paths?: { 6 | path: string; 7 | className: string; 8 | }[]; 9 | 10 | /** 11 | * Text to draw. 12 | */ 13 | text?: { 14 | x: number; 15 | y: number; 16 | anchor?: string; 17 | content: string; 18 | }[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/routes/Login.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .login { 6 | display: flex; 7 | flex: 1; 8 | justify-content: center; 9 | padding: 60px; 10 | 11 | @include desktop-down { 12 | padding: $spacing-small; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | max-width: $content-width-desktop; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/routes/Unavailable.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .unavailable { 6 | display: flex; 7 | flex: 1; 8 | justify-content: center; 9 | padding: 40px 60px; 10 | 11 | @include desktop-down { 12 | padding: $spacing-small; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | max-width: $content-width-desktop; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/layout/HealthIndicator.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | 4 | .health-indicator { 5 | display: flex; 6 | align-items: center; 7 | 8 | .label { 9 | @include font-size(10px); 10 | 11 | margin-left: $spacing-small; 12 | color: var(--text-color-secondary); 13 | font-family: $font-sans; 14 | font-weight: 500; 15 | text-transform: uppercase; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/uptime.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/models/tangle/blockBodyTypes.ts: -------------------------------------------------------------------------------- 1 | import { IBlockBodyBasic } from "./IBlockBodyBasic"; 2 | import { IBlockBodyValidation } from "./IBlockBodyValidation"; 3 | 4 | /** 5 | * The global types for the block bodies. 6 | */ 7 | export const BLOCK_BODY_TYPE_BASIC = 0; 8 | export const BLOCK_BODY_TYPE_VALIDATION = 1; 9 | 10 | /** 11 | * All of the block body types. 12 | */ 13 | export declare type BlockBodyTypes = IBlockBodyBasic | IBlockBodyValidation; 14 | -------------------------------------------------------------------------------- /src/models/info/INodeInfoProtocolParameters.ts: -------------------------------------------------------------------------------- 1 | import type { INodeInfoProtocolParameterParameters } from "./INodeInfoProtocol"; 2 | 3 | /** 4 | * The Protocol Info. 5 | */ 6 | export interface INodeInfoProtocolParameter { 7 | /** 8 | * The start epoch for the protocol parameters. 9 | */ 10 | startEpoch: number; 11 | /** 12 | * The protocol parameters. 13 | */ 14 | parameters: INodeInfoProtocolParameterParameters; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/DialogProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | /** 4 | * The props for the Dialog component. 5 | */ 6 | export interface DialogProps { 7 | /** 8 | * The title to show on the dialog. 9 | */ 10 | title: string; 11 | 12 | /** 13 | * The child controls. 14 | */ 15 | children?: ReactNode[] | ReactNode; 16 | 17 | /** 18 | * The dialog actions. 19 | */ 20 | actions?: ReactNode[] | ReactNode; 21 | } 22 | -------------------------------------------------------------------------------- /src/models/info/INodeInfo.ts: -------------------------------------------------------------------------------- 1 | import { INodeInfoProtocolParameter } from "./INodeInfoProtocolParameters"; 2 | /** 3 | * Response from the /info endpoint. 4 | */ 5 | export interface INodeInfo { 6 | /** 7 | * The name of the node. 8 | */ 9 | name: string; 10 | /** 11 | * The version of node. 12 | */ 13 | version: string; 14 | /** 15 | * The protocol parameters of the node. 16 | */ 17 | protocolParameters: INodeInfoProtocolParameter[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/routes/LoginState.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface LoginState { 3 | /** 4 | * The user. 5 | */ 6 | user: string; 7 | 8 | /** 9 | * The password. 10 | */ 11 | password: string; 12 | 13 | /** 14 | * Is the component busy. 15 | */ 16 | isBusy: boolean; 17 | 18 | /** 19 | * Any error from the login. 20 | */ 21 | error: boolean; 22 | 23 | /** 24 | * Redirect after login. 25 | */ 26 | redirect: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/routes/PeerState.ts: -------------------------------------------------------------------------------- 1 | import { IPeerGossipMetrics } from "../../models/peers/IPeerGossipMetrics"; 2 | 3 | export interface PeerState { 4 | alias?: string; 5 | address: string; 6 | isConnected: boolean; 7 | receivedPacketsDiff: number[]; 8 | sentPacketsDiff: number[]; 9 | gossipMetrics?: IPeerGossipMetrics; 10 | relation: string; 11 | lastUpdateTime: number; 12 | 13 | /** 14 | * Hide any secure details. 15 | */ 16 | blindMode: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/layout/TabPanelProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface TabPanelProps { 4 | /** 5 | * The labels to display on the panel. 6 | */ 7 | tabs: string[]; 8 | 9 | /** 10 | * The active tab. 11 | */ 12 | activeTab: string; 13 | 14 | /** 15 | * The child controls. 16 | */ 17 | children?: ReactNode[]; 18 | 19 | /** 20 | * The tab changed. 21 | */ 22 | onTabChanged?(activeTab: string): void; 23 | } 24 | -------------------------------------------------------------------------------- /src/models/IResponse.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Base response data. 5 | */ 6 | export interface IResponse { 7 | /** 8 | * Optional error in the response. 9 | */ 10 | error?: { 11 | /** 12 | * The code for the error response. 13 | */ 14 | code: string; 15 | 16 | /** 17 | * A more descriptive version of the error. 18 | */ 19 | message: string; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/AppState.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AppState { 3 | /** 4 | * Is the user logged in. 5 | */ 6 | isLoggedIn: boolean; 7 | 8 | /** 9 | * What is the current theme. 10 | */ 11 | theme: string; 12 | 13 | /** 14 | * Is the app online. 15 | */ 16 | online: boolean; 17 | 18 | /** 19 | * The node health. 20 | */ 21 | isNodeHealthy: boolean; 22 | 23 | /** 24 | * The network health. 25 | */ 26 | isNetworkHealthy: boolean; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/components/layout/BreakpointProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | /** 4 | * The props for the Breakpoint component. 5 | */ 6 | export interface BreakpointProps { 7 | /** 8 | * The size to show/hide the component. 9 | */ 10 | size: "phone" | "tablet" | "desktop"; 11 | 12 | /** 13 | * Show or hide the component if window above or below size. 14 | */ 15 | aboveBelow: "above" | "below"; 16 | 17 | /** 18 | * The child controls. 19 | */ 20 | children?: ReactNode[] | ReactNode; 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /public/branding/iota-core/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IOTA-Core", 3 | "short_name": "IOTA-Core", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/app/App.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/media-queries"; 2 | @import "../scss/variables"; 3 | 4 | .app { 5 | display: flex; 6 | flex: 1; 7 | flex-direction: row; 8 | align-items: stretch; 9 | min-width: 320px; 10 | overflow: hidden; 11 | 12 | .scroll-content { 13 | overflow: auto; 14 | } 15 | 16 | @include tablet-down { 17 | .health-indicators { 18 | padding: $spacing-small 20px; 19 | } 20 | } 21 | 22 | @include phone-down { 23 | .health-indicators { 24 | padding: $spacing-small 20px $spacing-small 66px; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/models/tangle/IBlockBodyValidation.ts: -------------------------------------------------------------------------------- 1 | import { HexEncodedString } from "../hexEncodedTypes"; 2 | import type { ITypeBase } from "../ITypeBase"; 3 | 4 | /** 5 | * Validation block body. 6 | */ 7 | export interface IBlockBodyValidation extends ITypeBase<1> { 8 | /** 9 | * The highest version of the protocol that is supported by the validator. 10 | */ 11 | highestSupportedVersion: number; 12 | 13 | /** 14 | * The hash of the protocol parameters for the HighestSupportedVersion. 15 | */ 16 | protocolParametersHash: HexEncodedString; 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/themes/dark.scss: -------------------------------------------------------------------------------- 1 | .theme-dark { 2 | --background: #22293e; 3 | --panel-background: #2b3659; 4 | --panel-border: #212a44; 5 | --panel-background-highlight: #353f60; 6 | --text-color-primary: #f1f4fa; 7 | --text-color-secondary: #9aadce; 8 | --accent-primary: #108cff; 9 | --bar-color-1: #309cff; 10 | --bar-color-2: #1f629f; 11 | --bar-color-3: #b7dcff; 12 | --bar-color-4: #e4f2ff; 13 | --scroll-thumb: #2b3659; 14 | --scroll-background: #212a44; 15 | --drop-shadow: #22293e; 16 | --dialog-shield: #aaaaaa; 17 | --tooltip-background: #9aadce; 18 | --tooltip-text: #22293e; 19 | } 20 | -------------------------------------------------------------------------------- /src/scss/themes/light.scss: -------------------------------------------------------------------------------- 1 | .theme-light { 2 | --background: #f6f9ff; 3 | --panel-background: #ffffff; 4 | --panel-background-highlight: #f6f8fc; 5 | --panel-border: #eef4ff; 6 | --text-color-primary: #25395f; 7 | --text-color-secondary: #9aadce; 8 | --accent-primary: #108cff; 9 | --bar-color-1: #309cff; 10 | --bar-color-2: #b7dcff; 11 | --bar-color-3: #1f629f; 12 | --bar-color-4: #0a3257; 13 | --scroll-thumb: #9aadce; 14 | --scroll-background: #ffffff; 15 | --drop-shadow: #aaaaaa; 16 | --dialog-shield: #111111; 17 | --tooltip-background: #9aadce; 18 | --tooltip-text: #ffffff; 19 | } 20 | -------------------------------------------------------------------------------- /src/models/info/INetworkMetrics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Response from the /info endpoint. 3 | */ 4 | export interface INetworkMetrics { 5 | /** 6 | * Current rate of new blocks per second, it's updated when a commitment is committed. 7 | */ 8 | blocksPerSecond: string; 9 | /** 10 | * Current rate of confirmed blocks per second, it's updated when a commitment is committed. 11 | */ 12 | confirmedBlocksPerSecond: string; 13 | /** 14 | * Ratio of confirmed blocks in relation to new blocks up until the latest commitment is committed. 15 | */ 16 | confirmationRate: string; 17 | } 18 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Community resources 2 | 3 | If you have a general or technical question, you can use one of the following resources instead of submitting an issue: 4 | 5 | - [**Developer documentation:**](https://docs.iota.org/) For official information about developing with IOTA technology 6 | - [**Discord:**](https://discord.iota.org/) For real-time chats with the developers and community members 7 | - [**IOTA cafe:**](https://iota.cafe/) For technical discussions with the Research and Development Department at the IOTA Foundation 8 | - [**StackExchange:**](https://iota.stackexchange.com/) For technical and troubleshooting questions -------------------------------------------------------------------------------- /src/app/components/layout/InfoPanelProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface InfoPanelProps { 4 | /** 5 | * The caption for the panel. 6 | */ 7 | caption: string; 8 | 9 | /** 10 | * The value for the panel. 11 | */ 12 | value: string | undefined; 13 | 14 | /** 15 | * The icon to display. 16 | */ 17 | icon: ReactNode; 18 | 19 | /** 20 | * The style for the icon. 21 | */ 22 | iconStyle: "green" | "orange" | "blue" | "purple" | "grey"; 23 | 24 | /** 25 | * Class names. 26 | */ 27 | className?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/models/visualizer/IVerticesCounts.ts: -------------------------------------------------------------------------------- 1 | export interface IVerticesCounts { 2 | /** 3 | * How many vertices are there. 4 | */ 5 | total: number; 6 | /** 7 | * How many accepted vertices. 8 | */ 9 | accepted: number; 10 | /** 11 | * How many confirmed vertices. 12 | */ 13 | confirmed: number; 14 | /** 15 | * How many finalized vertices. 16 | */ 17 | finalized: number; 18 | /** 19 | * How many transaction vertices. 20 | */ 21 | transactions: number; 22 | /** 23 | * How many tip vertices. 24 | */ 25 | tips: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/layout/MicroGraphProps.ts: -------------------------------------------------------------------------------- 1 | export interface MicroGraphProps { 2 | /** 3 | * The label for the micro graph. 4 | */ 5 | label: string; 6 | 7 | /** 8 | * The value for the micro graph. 9 | */ 10 | value: string; 11 | 12 | /** 13 | * The graph values. 14 | */ 15 | values: number[]; 16 | 17 | /** 18 | * Class names. 19 | */ 20 | className?: string; 21 | 22 | /** 23 | * Width for the graph. 24 | */ 25 | graphWidth?: number; 26 | 27 | /** 28 | * Height for the graph. 29 | */ 30 | graphHeight?: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/models/tangle/payloadTypes.ts: -------------------------------------------------------------------------------- 1 | import { IPayloadCandidacyAnnouncement } from "./IPayloadCandidacyAnnouncement"; 2 | import type { IPayloadSignedTransaction } from "./IPayloadSignedTransaction"; 3 | import type { IPayloadTaggedData } from "./IPayloadTaggedData"; 4 | 5 | /** 6 | * The global types for the payloads. 7 | */ 8 | export const PAYLOAD_TYPE_TAGGED_DATA = 0; 9 | export const PAYLOAD_TYPE_SIGNED_TRANSACTION = 1; 10 | export const PAYLOAD_TYPE_CANDIDACY_ANNOUNCEMENT = 2; 11 | 12 | /** 13 | * All of the payload types. 14 | */ 15 | export declare type PayloadTypes = IPayloadTaggedData | IPayloadSignedTransaction | IPayloadCandidacyAnnouncement; 16 | -------------------------------------------------------------------------------- /src/app/components/layout/NavMenu.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | .nav-menu { 6 | display: flex; 7 | align-items: center; 8 | 9 | button { 10 | border: 0; 11 | outline: 0; 12 | background: none; 13 | cursor: pointer; 14 | 15 | .logo { 16 | width: 28px; 17 | height: 28px; 18 | } 19 | } 20 | 21 | .popup-container { 22 | display: flex; 23 | position: fixed; 24 | z-index: 10; 25 | top: 0; 26 | right: 0; 27 | bottom: 0; 28 | left: 0; 29 | align-items: stretch; 30 | justify-content: stretch; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scss/media-queries.scss: -------------------------------------------------------------------------------- 1 | $desktop-width: 1024px; 2 | $tablet-width: 768px; 3 | $phone-width: 480px; 4 | 5 | @mixin desktop-down { 6 | @media (max-width: #{$desktop-width}) { 7 | @content; 8 | } 9 | } 10 | 11 | @mixin tablet-down { 12 | @media (max-width: #{$tablet-width}) { 13 | @content; 14 | } 15 | } 16 | 17 | @mixin tablet-down-only { 18 | @media (max-width: #{$tablet-width}) and (min-width: #{$phone-width + 1}) { 19 | @content; 20 | } 21 | } 22 | 23 | @mixin phone-down { 24 | @media (max-width: #{$phone-width}) { 25 | @content; 26 | } 27 | } 28 | 29 | @include phone-down { 30 | .phone-down-hide { 31 | display: none; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Request a feature 4 | url: https://github.com/iotaledger/node-dashboard/discussions/categories/features-enhancements 5 | about: Propose a feature or enhancement in GitHub discussions 6 | - name: Security vulnerabilities 7 | url: security@iota.org 8 | about: Please report security vulnerabilities here. 9 | - name: General feedback 10 | url: https://github.com/iotaledger/node-dashboard/discussions/categories/feedback 11 | about: Leave us some feedback 12 | - name: Discord 13 | url: https://discord.iota.org/ 14 | about: Please ask and answer questions here. 15 | -------------------------------------------------------------------------------- /src/app/components/layout/Spinner.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/variables'; 2 | 3 | .spinner { 4 | position: relative; 5 | top: 20px; 6 | left: 20px; 7 | width: 40px; 8 | height: 40px; 9 | animation: pulse 1s ease-in-out infinite; 10 | border-radius: 100%; 11 | background-color: var(--text-color-secondary); 12 | 13 | &.spinner--compact { 14 | top: 10px; 15 | left: 10px; 16 | width: 20px; 17 | height: 20px; 18 | } 19 | } 20 | 21 | @keyframes pulse { 22 | 0% { 23 | transform: translate(-50%, -50%) scale(0); 24 | opacity: 1; 25 | } 26 | 27 | 100% { 28 | transform: translate(-50%, -50%) scale(1); 29 | opacity: 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/models/clients/singleNodeClientOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options used when constructing SingleNodeClient. 3 | */ 4 | export interface SingleNodeClientOptions { 5 | /** 6 | * Base path for API location, defaults to /api/. 7 | */ 8 | basePath?: string; 9 | /** 10 | * Timeout for API requests. 11 | */ 12 | timeout?: number; 13 | /** 14 | * Username for the endpoint. 15 | */ 16 | userName?: string; 17 | /** 18 | * Password for the endpoint. 19 | */ 20 | password?: string; 21 | /** 22 | * Additional headers to include in the requests. 23 | */ 24 | headers?: { 25 | [id: string]: string; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/components/layout/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import "./Spinner.scss"; 4 | import { SpinnerProps } from "./SpinnerProps"; 5 | 6 | /** 7 | * Component which will display a spinner. 8 | */ 9 | class Spinner extends Component { 10 | /** 11 | * Render the component. 12 | * @returns The node to render. 13 | */ 14 | public render(): ReactNode { 15 | return ( 16 |
20 | ); 21 | } 22 | } 23 | 24 | export default Spinner; 25 | -------------------------------------------------------------------------------- /src/app/components/layout/ToggleButton.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | 4 | .toggle-button { 5 | width: 56px; 6 | height: 30px; 7 | border: 1px solid var(--panel-border); 8 | border-radius: $spacing-small; 9 | outline: 0; 10 | background-color: var(--panel-background-highlight); 11 | 12 | svg { 13 | color: var(--text-color-secondary); 14 | } 15 | 16 | .icon-container { 17 | position: relative; 18 | left: -11px; 19 | } 20 | 21 | &.toggle-button--checked { 22 | background-color: #61e884; 23 | 24 | svg { 25 | color: #ffffff; 26 | } 27 | 28 | .icon-container { 29 | left: 11px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Client Build Main/Develop 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "src/**" 8 | - ".github/workflows/main.yml" 9 | pull_request: 10 | branches: [main] 11 | paths: 12 | - "src/**" 13 | - ".github/workflows/main.yml" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js 16.16 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: "16.16" 25 | - name: npm 8.18 26 | run: npm install -g npm@8.18 27 | - name: Client Build 28 | run: | 29 | npm install 30 | npm run build 31 | -------------------------------------------------------------------------------- /.github/workflows/build-stardust.yml: -------------------------------------------------------------------------------- 1 | name: Client Build Stardust 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | paths: 7 | - "src/**" 8 | - ".github/workflows/build-stardust.yml" 9 | pull_request: 10 | branches: [develop] 11 | paths: 12 | - "src/**" 13 | - ".github/workflows/build-stardust.yml" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js 16.16 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: "16.16" 25 | - name: npm 8.18 26 | run: npm install -g npm@8.18 27 | - name: Client Build 28 | run: | 29 | npm install 30 | npm run build -------------------------------------------------------------------------------- /src/models/peers/IPeer.ts: -------------------------------------------------------------------------------- 1 | import type { IPeerGossipMetrics } from "./IPeerGossipMetrics"; 2 | 3 | /** 4 | * Peer details. 5 | */ 6 | export interface IPeer { 7 | /** 8 | * The libp2p identifier of the peer. 9 | */ 10 | id: string; 11 | /** 12 | * The libp2p multi addresses of the peer. 13 | */ 14 | multiAddresses: string[]; 15 | /** 16 | * The alias to identify the peer. 17 | */ 18 | alias?: string; 19 | /** 20 | * The relation (manual, autopeered) of the peer. 21 | */ 22 | relation: string; 23 | /** 24 | * Whether the peer is connected. 25 | */ 26 | connected: boolean; 27 | /** 28 | * The gossip metrics for this peer. 29 | */ 30 | gossipMetrics: IPeerGossipMetrics; 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | "forceConsistentCasingInFileNames": true, 21 | "downlevelIteration": true, 22 | "typeRoots": [ 23 | "./typings", 24 | "./node_modules/@types" 25 | ], 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": [ 29 | "src" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/MicroGraph.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | 4 | .micro-graph { 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: flex-start; 8 | width: 130px; 9 | 10 | .label { 11 | @include font-size(10px); 12 | 13 | color: var(--text-color-secondary); 14 | font-family: $font-sans; 15 | font-weight: 500; 16 | text-transform: uppercase; 17 | white-space: nowrap; 18 | } 19 | 20 | .value { 21 | @include font-size(16px); 22 | 23 | color: var(--text-color-primary); 24 | font-family: $font-sans; 25 | font-weight: bold; 26 | white-space: nowrap; 27 | } 28 | 29 | .canvas { 30 | color: var(--accent-primary); 31 | 32 | svg { 33 | overflow: visible; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/layout/GraphProps.ts: -------------------------------------------------------------------------------- 1 | export interface GraphProps { 2 | /** 3 | * The caption for the graph. 4 | */ 5 | caption: string; 6 | 7 | /** 8 | * The graph series. 9 | */ 10 | series: { 11 | className: string; 12 | label: string; 13 | values: number[]; 14 | }[]; 15 | 16 | /** 17 | * The interval between items in the graph. 18 | */ 19 | timeInterval?: number; 20 | 21 | /** 22 | * The number of time markers to show. 23 | */ 24 | timeMarkers?: number; 25 | 26 | /** 27 | * The end time. 28 | */ 29 | endTime?: number; 30 | 31 | /** 32 | * The maximum number of items to show. 33 | */ 34 | seriesMaxLength: number; 35 | 36 | /** 37 | * Class names. 38 | */ 39 | className?: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/scss/fonts.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable annotation-no-unknown */ 2 | @import "./fonts/dm-sans"; 3 | @import "./fonts/ibm-plex-mono"; 4 | 5 | $font-sans: "DM Sans", sans-serif; 6 | $font-mono: "IBM Plex Mono", monospace; 7 | 8 | $rem-base-font: 16px !default; 9 | 10 | @mixin font-size($font-size, $line-height: 0) { 11 | font-size: $font-size; 12 | font-size: calc($font-size / $rem-base-font * 1rem); 13 | 14 | @if $line-height > 0 { 15 | line-height: $line-height; 16 | line-height: calc($line-height / $rem-base-font * 1rem); 17 | } 18 | } 19 | 20 | .font-weight-bold { 21 | font-weight: bold !important; 22 | } 23 | 24 | .font-weight-normal { 25 | font-weight: normal !important; 26 | } 27 | 28 | .font-weight-light { 29 | font-weight: 300 !important; 30 | } 31 | 32 | .font-italic { 33 | font-style: italic !important; 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/confirmation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/PaginationProps.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationProps { 2 | 3 | /** 4 | * The total number of pages. 5 | */ 6 | totalCount: number; 7 | 8 | /** 9 | * The number of current page. 10 | */ 11 | currentPage: number; 12 | 13 | /** 14 | * The total number of sibling pages. 15 | */ 16 | siblingsCount: number; 17 | 18 | /** 19 | * Number of results per page. 20 | */ 21 | pageSize: number; 22 | 23 | /** 24 | * Define limit of remaining pages above which the extra page range will be shown. 25 | */ 26 | extraPageRangeLimit?: number; 27 | 28 | /** 29 | * The optional additional CSS classes. 30 | */ 31 | classNames?: string; 32 | 33 | /** 34 | * Page changed. 35 | * @param page Page navigated to. 36 | */ 37 | onPageChange(page: number): void; 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/HealthIndicator.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import { ReactComponent as HealthBadIcon } from "../../../assets/health-bad.svg"; 4 | import { ReactComponent as HealthGoodIcon } from "../../../assets/health-good.svg"; 5 | import "./HealthIndicator.scss"; 6 | import { HealthIndicatorProps } from "./HealthIndicatorProps"; 7 | 8 | /** 9 | * Health Indicator. 10 | */ 11 | class HealthIndicator extends Component { 12 | /** 13 | * Render the component. 14 | * @returns The node to render. 15 | */ 16 | public render(): ReactNode { 17 | return ( 18 |
19 | {this.props.healthy ? : } 20 | {this.props.label} 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default HealthIndicator; 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - '.github/codeql/**' 10 | - '.github/workflows/codeql-analysis.yml' 11 | pull_request: 12 | paths: 13 | - 'src/**' 14 | - '.github/codeql/**' 15 | - '.github/workflows/codeql-analysis.yml' 16 | schedule: 17 | - cron: '0 0 * * *' 18 | 19 | jobs: 20 | CodeQL-Build: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 2 28 | 29 | - run: git checkout HEAD^2 30 | if: ${{ github.event_name == 'pull_request' }} 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v1 34 | with: 35 | languages: javascript 36 | config-file: ./.github/codeql/codeql-config.yml 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v1 40 | -------------------------------------------------------------------------------- /src/models/clients/clientError.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Class to represent errors from Client. 6 | */ 7 | export class ClientError extends Error { 8 | /** 9 | * The route the request was made to. 10 | */ 11 | public route: string; 12 | 13 | /** 14 | * The HTTP status code returned. 15 | */ 16 | public httpStatus: number; 17 | 18 | /** 19 | * The code return in the payload. 20 | */ 21 | public code?: string; 22 | 23 | /** 24 | * Create a new instance of ClientError. 25 | * @param message The message for the error. 26 | * @param route The route the request was made to. 27 | * @param httpStatus The http status code. 28 | * @param code The code in the payload. 29 | */ 30 | constructor(message: string, route: string, httpStatus: number, code?: string) { 31 | super(message); 32 | this.route = route; 33 | this.httpStatus = httpStatus; 34 | this.code = code; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/analytics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/HeaderState.ts: -------------------------------------------------------------------------------- 1 | export interface HeaderState { 2 | /** 3 | * The node health. 4 | */ 5 | isNodeHealthy: boolean; 6 | 7 | /** 8 | * The network health. 9 | */ 10 | isNetworkHealthy: boolean; 11 | 12 | /** 13 | * Bps for micro graph. 14 | */ 15 | bps: string; 16 | 17 | /** 18 | * Bps values for micro graph. 19 | */ 20 | bpsValues: number[]; 21 | 22 | /** 23 | * Total database size for micro graph. 24 | */ 25 | dbSizeTotalFormatted: string; 26 | 27 | /** 28 | * Total database size values for micro graph. 29 | */ 30 | dbSizeTotal: number[]; 31 | 32 | /** 33 | * Memory size for micro graph. 34 | */ 35 | memorySizeFormatted: string; 36 | 37 | /** 38 | * Memory size values for micro graph. 39 | */ 40 | memorySize: number[]; 41 | 42 | /** 43 | * Is the auth logged in. 44 | */ 45 | isLoggedIn: boolean; 46 | 47 | /** 48 | * Is the app online. 49 | */ 50 | online: boolean; 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/padlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/components/layout/Header.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/media-queries'; 2 | @import '../../../scss/variables'; 3 | 4 | .header { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 80px; 9 | padding: $spacing-small; 10 | background-color: var(--panel-background); 11 | 12 | @include tablet-down { 13 | justify-content: flex-start; 14 | height: 60px; 15 | padding-left: 0; 16 | } 17 | 18 | @include phone-down { 19 | padding-left: $spacing-small; 20 | } 21 | 22 | .content { 23 | display: flex; 24 | flex: 1; 25 | flex-direction: row; 26 | max-width: $content-width-desktop; 27 | 28 | .child { 29 | padding: $spacing-tiny $spacing-medium; 30 | border-right: 1px solid var(--panel-border); 31 | 32 | @include desktop-down { 33 | padding: $spacing-tiny; 34 | } 35 | 36 | &.child-fill { 37 | flex: 1; 38 | min-width: 250px; 39 | padding-right: 40px; 40 | 41 | @include tablet-down { 42 | min-width: unset; 43 | padding-right: 0; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/models/IClient.ts: -------------------------------------------------------------------------------- 1 | import { HexEncodedString } from "./hexEncodedTypes"; 2 | import type { INodeInfo } from "./info/INodeInfo"; 3 | import { IPeer } from "./peers/IPeer"; 4 | import { IBlock } from "./tangle/IBlock"; 5 | /** 6 | * Client interface definition for API communication. 7 | */ 8 | export interface IClient { 9 | /** 10 | * Get the info about the node. 11 | * @returns The node information. 12 | */ 13 | info(): Promise; 14 | /** 15 | * Get the block data by id. 16 | * @param blockId The block to get the data for. 17 | * @returns The block data. 18 | */ 19 | block(blockId: HexEncodedString): Promise; 20 | /** 21 | * Add a new peer. 22 | * @param multiAddress The address of the peer to add. 23 | * @param alias An optional alias for the peer. 24 | * @returns The details for the created peer. 25 | */ 26 | peerAdd(multiAddress: string, alias?: string): Promise; 27 | /** 28 | * Delete a peer. 29 | * @param peerId The peer to delete. 30 | * @returns Nothing. 31 | */ 32 | peerDelete(peerId: string): Promise; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import './scss/layout'; 2 | @import './scss/forms'; 3 | @import './scss/standard'; 4 | @import './scss/media-queries'; 5 | @import './scss/themes/dark'; 6 | @import './scss/themes/light'; 7 | @import './scss/variables'; 8 | 9 | html { 10 | height: 100%; 11 | } 12 | 13 | * { 14 | box-sizing: border-box; 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | body { 20 | display: flex; 21 | height: 100vh; 22 | overflow: hidden; 23 | background-color: var(--background); 24 | 25 | #root { 26 | display: flex; 27 | flex: 1; 28 | 29 | .success { 30 | color: $success; 31 | } 32 | 33 | .warning { 34 | color: $warning; 35 | } 36 | 37 | .danger { 38 | color: $danger; 39 | } 40 | 41 | .info { 42 | color: $info; 43 | } 44 | } 45 | } 46 | 47 | ::-webkit-scrollbar-track { 48 | border-radius: 0; 49 | background-color: var(--scroll-background); 50 | } 51 | 52 | ::-webkit-scrollbar { 53 | width: 12px; 54 | background-color: var(--scroll-background); 55 | } 56 | 57 | ::-webkit-scrollbar-thumb { 58 | border-radius: 0; 59 | background-color: var(--scroll-thumb); 60 | } 61 | -------------------------------------------------------------------------------- /src/app/components/tangle/PeersSummaryPanel.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | 4 | .peers-summary { 5 | padding: $spacing-small; 6 | 7 | .peers-summary--icon-button { 8 | border: 0; 9 | outline: 0; 10 | background: none; 11 | color: var(--text-color-secondary); 12 | cursor: pointer; 13 | 14 | &:hover { 15 | color: var(--accent-primary); 16 | } 17 | } 18 | 19 | .peers-summary--item { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | min-height: 90px; 24 | margin-bottom: $spacing-small; 25 | padding: $spacing-small; 26 | overflow: hidden; 27 | border-radius: $spacing-small; 28 | background-color: var(--panel-background-highlight); 29 | 30 | &:focus { 31 | border: 0; 32 | } 33 | 34 | .peer-health-icon { 35 | width: 16px; 36 | } 37 | 38 | .peer-id { 39 | @include font-size(14px); 40 | 41 | margin-left: $spacing-small; 42 | color: var(--text-color-primary); 43 | font-family: $font-sans; 44 | font-weight: 500; 45 | word-break: break-all; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/memory.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/padlock-unlocked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/components/layout/AsyncComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | 3 | /** 4 | * Base component for component with async requests. 5 | */ 6 | class AsyncComponent extends Component { 7 | /** 8 | * Is the component mounted. 9 | */ 10 | protected _isMounted?: boolean; 11 | 12 | /** 13 | * The component mounted. 14 | */ 15 | public componentDidMount(): void { 16 | this._isMounted = true; 17 | } 18 | 19 | /** 20 | * The component will unmount so update flag. 21 | */ 22 | public componentWillUnmount(): void { 23 | this._isMounted = false; 24 | } 25 | 26 | /** 27 | * Set the state if the component is still mounted. 28 | * @param state The state to set. 29 | * @param callback The callback for the setState. 30 | */ 31 | public setState( 32 | state: ((prevState: Readonly, props: Readonly

) => (Pick | S | null)) | (Pick | S | null), 33 | callback?: () => void 34 | ): void { 35 | if (this._isMounted) { 36 | super.setState(state, callback); 37 | } 38 | } 39 | } 40 | 41 | export default AsyncComponent; 42 | -------------------------------------------------------------------------------- /src/app/components/layout/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from "react"; 2 | import "./Dialog.scss"; 3 | import { DialogProps } from "./DialogProps"; 4 | 5 | /** 6 | * Component which will display a dialog. 7 | */ 8 | class Dialog extends Component { 9 | /** 10 | * Render the component. 11 | * @returns The node to render. 12 | */ 13 | public render(): ReactNode { 14 | return ( 15 | 16 |

17 |
18 |
19 |
20 |

{this.props.title}

21 |
22 |
23 | {this.props.children} 24 |
25 |
26 | {this.props.actions} 27 |
28 |
29 |
30 | 31 | ); 32 | } 33 | } 34 | 35 | export default Dialog; 36 | -------------------------------------------------------------------------------- /src/scss/fonts/ibm-plex-mono.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'IBM Plex Mono'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local(''), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff2') format('woff2'), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-300.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'IBM Plex Mono'; 10 | font-style: italic; 11 | font-weight: 400; 12 | src: local(''), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff2') format('woff2'), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-italic.woff') format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'IBM Plex Mono'; 17 | font-style: normal; 18 | font-weight: 400; 19 | src: local(''), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff2') format('woff2'), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-regular.woff') format('woff'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'IBM Plex Mono'; 24 | font-style: normal; 25 | font-weight: 500; 26 | src: local(''), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff2') format('woff2'), url('../../assets/fonts/ibm-plex/ibm-plex-mono-v6-latin-500.woff') format('woff'); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/routes/Unavailable.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { RouteComponentProps } from "react-router-dom"; 3 | import AsyncComponent from "../components/layout/AsyncComponent"; 4 | import "./Unavailable.scss"; 5 | 6 | /** 7 | * Component which will show the unavailable page. 8 | */ 9 | class Unavailable extends AsyncComponent> { 10 | /** 11 | * Create a new instance of Unavailable. 12 | * @param props The props. 13 | */ 14 | constructor(props: RouteComponentProps) { 15 | super(props); 16 | 17 | this.state = {}; 18 | } 19 | 20 | /** 21 | * Render the component. 22 | * @returns The node to render. 23 | */ 24 | public render(): ReactNode { 25 | return ( 26 |
27 |
28 |
29 |

Service Unavailable

30 |

The node is currently unavailable or is not synced, please try again later.

31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Unavailable; 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description of change 2 | 3 | Please write a summary of your changes and why you made them. Be sure to reference any related issues by adding `fixes # (issue)`. 4 | 5 | ## Type of change 6 | 7 | Choose a type of change, and delete any options that are not relevant. 8 | 9 | - Bug fix (a non-breaking change which fixes an issue) 10 | - Enhancement (a non-breaking change which adds functionality) 11 | - Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - Documentation Fix 13 | 14 | ## How the change has been tested 15 | 16 | Describe the tests that you ran to verify your changes. 17 | 18 | Make sure to provide instructions for the maintainer as well as any relevant configurations. 19 | 20 | ## Change checklist 21 | 22 | Add an `x` to the boxes that are relevant to your changes, and delete any items that are not. 23 | 24 | - [] My code follows the contribution guidelines for this project 25 | - [] I have performed a self-review of my own code 26 | - [] I have commented my code, particularly in hard-to-understand areas 27 | - [] I have made corresponding changes to the documentation 28 | - [] I have added tests that prove my fix is effective or that my feature works 29 | - [] New and existing unit tests pass locally with my changes 30 | -------------------------------------------------------------------------------- /src/assets/peers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/BlockButton.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | 4 | .block-button { 5 | position: relative; 6 | 7 | .block-button-btn { 8 | border: 0; 9 | outline: none; 10 | background: none; 11 | cursor: pointer; 12 | 13 | svg { 14 | color: var(--text-color-secondary); 15 | } 16 | 17 | &:hover { 18 | svg { 19 | color: var(--text-color-primary); 20 | } 21 | } 22 | 23 | &:focus { 24 | svg { 25 | color: var(--accent-primary); 26 | } 27 | } 28 | } 29 | 30 | .block-button--message { 31 | @include font-size(10px); 32 | 33 | position: absolute; 34 | min-width: 200px; 35 | animation: fade 2s linear; 36 | animation-fill-mode: forwards; 37 | opacity: 1; 38 | color: var(--accent-primary); 39 | font-family: $font-sans; 40 | font-weight: bold; 41 | text-transform: uppercase; 42 | 43 | &.block-button--message--right { 44 | top: 3px; 45 | margin-left: 10px; 46 | } 47 | 48 | &.block-button--message--top { 49 | top: -15px; 50 | left: -10px; 51 | } 52 | } 53 | 54 | @keyframes fade { 55 | 0%, 56 | 100% { 57 | opacity: 0; 58 | } 59 | 60 | 50% { 61 | opacity: 1; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/routes/PeersState.ts: -------------------------------------------------------------------------------- 1 | export interface PeersState { 2 | /** 3 | * The peers. 4 | */ 5 | peers: { 6 | id: string; 7 | alias?: string; 8 | address?: string; 9 | connected: boolean; 10 | relation: string; 11 | receivedPacketsTotal: number[]; 12 | sentPacketsTotal: number[]; 13 | receivedPacketsDiff: number[]; 14 | sentPacketsDiff: number[]; 15 | lastUpdateTime: number; 16 | }[]; 17 | 18 | /** 19 | * The type of dialog to show. 20 | */ 21 | dialogType?: "add" | "edit" | "promote" | "delete"; 22 | 23 | /** 24 | * Is the an edit type dialog. 25 | */ 26 | dialogIsEdit?: boolean; 27 | 28 | /** 29 | * The peer to operate on. 30 | */ 31 | dialogPeerIdOriginal?: string; 32 | 33 | /** 34 | * Is the dialog busy. 35 | */ 36 | dialogBusy?: boolean; 37 | 38 | /** 39 | * Status message to display in dialog. 40 | */ 41 | dialogStatus?: string; 42 | 43 | /** 44 | * Address for adding a peer. 45 | */ 46 | dialogPeerMultiAddress: string; 47 | 48 | /** 49 | * Alias for adding a peer. 50 | */ 51 | dialogPeerAlias: string; 52 | 53 | /** 54 | * Hide any secure details. 55 | */ 56 | blindMode: boolean; 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/eye-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/visualizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-param */ 2 | /* eslint-disable jsdoc/require-returns */ 3 | import React, { useRef } from "react"; 4 | import "./Tooltip.scss"; 5 | 6 | interface TooltipProps { 7 | tooltipContent: string | React.ReactNode; 8 | children: React.ReactNode; 9 | } 10 | 11 | /** 12 | * Component to display a tooltip on hover. 13 | */ 14 | const Tooltip: React.FC = ({ children, tooltipContent }) => { 15 | const tooltip = useRef(null); 16 | 17 | const onEnter = () => { 18 | if (tooltip?.current) { 19 | tooltip.current.style.visibility = "visible"; 20 | tooltip.current.style.opacity = "1"; 21 | } 22 | }; 23 | 24 | const onLeave = () => { 25 | if (tooltip?.current) { 26 | tooltip.current.style.visibility = "hidden"; 27 | tooltip.current.style.opacity = "0"; 28 | } 29 | }; 30 | 31 | return ( 32 |
33 |
34 |
35 | {tooltipContent} 36 |
37 |
38 | {children} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Tooltip; 45 | -------------------------------------------------------------------------------- /src/factories/serviceFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory for creating services. 3 | */ 4 | export class ServiceFactory { 5 | /** 6 | * Store the service callbacks. 7 | */ 8 | private static readonly _services: { [name: string]: (serviceName: string) => unknown } = {}; 9 | 10 | /** 11 | * Store the created instances. 12 | */ 13 | private static readonly _instances: { [name: string]: unknown } = {}; 14 | 15 | /** 16 | * Register a new service. 17 | * @param name The name of the service. 18 | * @param instanceCallback The callback to create an instance. 19 | */ 20 | public static register(name: string, instanceCallback: (serviceName: string) => unknown): void { 21 | this._services[name] = instanceCallback; 22 | } 23 | 24 | /** 25 | * Unregister a service. 26 | * @param name The name of the service to unregister. 27 | */ 28 | public static unregister(name: string): void { 29 | delete this._services[name]; 30 | } 31 | 32 | /** 33 | * Get a service instance. 34 | * @param name The name of the service to get. 35 | * @returns An instance of the service. 36 | */ 37 | public static get(name: string): T { 38 | if (!this._instances[name] && this._services[name]) { 39 | this._instances[name] = this._services[name](name); 40 | } 41 | return this._instances[name] as T; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/components/layout/TabPanel.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/variables'; 3 | @import '../../../scss/media-queries'; 4 | 5 | .tab-panel { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: stretch; 9 | 10 | .tab-panel--buttons { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | margin-bottom: $spacing-tiny; 15 | 16 | @include desktop-down { 17 | margin-bottom: 0; 18 | } 19 | 20 | .tab-panel--button { 21 | @include font-size(18px); 22 | 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | margin-right: 60px; 27 | margin-bottom: $spacing-small; 28 | border: 0; 29 | outline: 0; 30 | background: none; 31 | color: var(--text-color-secondary); 32 | font-family: $font-sans; 33 | font-weight: bold; 34 | text-decoration: none; 35 | cursor: pointer; 36 | 37 | .underline { 38 | width: 60%; 39 | height: 2px; 40 | margin-top: 4px; 41 | background-color: transparent; 42 | } 43 | 44 | &:focus { 45 | .underline { 46 | background-color: var(--accent-primary); 47 | } 48 | } 49 | 50 | &.tab-panel--button__selected { 51 | color: var(--text-color-primary); 52 | 53 | .underline { 54 | background-color: var(--accent-primary); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Node Dashboard 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/models/visualizer/IVertex.ts: -------------------------------------------------------------------------------- 1 | export interface IVertex { 2 | /** 3 | * What is the id for the vertex. 4 | */ 5 | fullId?: string; 6 | 7 | /** 8 | * What is the short id for the vertex. 9 | */ 10 | shortId: string; 11 | 12 | /** 13 | * Slot of the block. 14 | */ 15 | slot?: number; 16 | 17 | /** 18 | * Parent Ids. 19 | */ 20 | parents?: string; 21 | 22 | /** 23 | * Block State. 24 | */ 25 | blockState?: string; 26 | 27 | /** 28 | * Is the block a basic block tagged data. 29 | */ 30 | isBasicBlockTaggedData?: boolean; 31 | 32 | /** 33 | * Is the block a basic block signed transaction. 34 | */ 35 | isBasicBlockSignedTransaction?: boolean; 36 | 37 | /** 38 | * Is the block a basic block candidacy announcement. 39 | */ 40 | isBasicBlockCandidacyAnnouncement?: boolean; 41 | 42 | /** 43 | * Is the block a validation block. 44 | */ 45 | isValidationBlock?: boolean; 46 | 47 | /** 48 | * Is the block a tip. 49 | */ 50 | isTip?: boolean; 51 | 52 | /** 53 | * Is the block accepted. 54 | */ 55 | isAccepted?: boolean; 56 | 57 | /** 58 | * Is the block confirmed. 59 | */ 60 | isConfirmed?: boolean; 61 | 62 | /** 63 | * Is the block finalized. 64 | */ 65 | isFinalized?: boolean; 66 | 67 | /** 68 | * Is it selected. 69 | */ 70 | isSelected?: boolean; 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/layout/NavPanelProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface NavPanelProps { 4 | /** 5 | * The buttons to display on the panel. 6 | */ 7 | middle: { 8 | /** 9 | * The label for the button. 10 | */ 11 | label: string; 12 | /** 13 | * The icon content for the button. 14 | */ 15 | icon: ReactNode; 16 | /** 17 | * The route to navigate for the button. 18 | */ 19 | route?: string; 20 | /** 21 | * The function to trigger for the button. 22 | */ 23 | function?: () => void; 24 | /** 25 | * Is the button visible. 26 | */ 27 | hidden?: boolean; 28 | }[]; 29 | 30 | /** 31 | * The buttons to display on the panel. 32 | */ 33 | end: { 34 | /** 35 | * The label for the button. 36 | */ 37 | label: string; 38 | /** 39 | * The icon content for the button. 40 | */ 41 | icon: ReactNode; 42 | /** 43 | * The route to navigate for the button. 44 | */ 45 | route?: string; 46 | /** 47 | * The function to trigger for the button. 48 | */ 49 | function?: () => void; 50 | /** 51 | * Is the button visible. 52 | */ 53 | hidden?: boolean; 54 | }[]; 55 | 56 | /** 57 | * Show the panel full width. 58 | */ 59 | fullWidth: boolean; 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/dataHelper.ts: -------------------------------------------------------------------------------- 1 | import { HexEncodedString } from "../models/hexEncodedTypes"; 2 | 3 | /** 4 | * Class to help with processing of data. 5 | */ 6 | export class DataHelper { 7 | /** 8 | * Computes a slotIndex from a block, transaction or slotCommitment Id. 9 | * @param id The block, transaction or slotCommitment Id. 10 | * @returns The slotIndex. 11 | */ 12 | public static computeSlotIndex( 13 | id: HexEncodedString 14 | ): number { 15 | const numberString = id.slice(-8); 16 | const chunks = []; 17 | 18 | for ( 19 | let charsLength = numberString.length, i = 0; 20 | i < charsLength; 21 | i += 2 22 | ) { 23 | chunks.push(numberString.slice(i, i + 2)); 24 | } 25 | const separated = chunks.map(n => Number.parseInt(n, 16)); 26 | const buf = Uint8Array.from(separated).buffer; 27 | const view = new DataView(buf); 28 | 29 | return view.getUint32(0, true); 30 | } 31 | 32 | /** 33 | * Sort a list of peers. 34 | * @param peers The peers to sort. 35 | * @returns The sorted peers. 36 | */ 37 | public static sortPeers(peers: T[]): T[] { 38 | return peers.sort((a, b) => { 39 | if (a.connected !== b.connected) { 40 | return a.connected ? -1 : 1; 41 | } 42 | 43 | return (a.alias ?? a.id).localeCompare(b.alias ?? b.id); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/settingsService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceFactory } from "../factories/serviceFactory"; 2 | import { EventAggregator } from "./eventAggregator"; 3 | import { LocalStorageService } from "./localStorageService"; 4 | 5 | /** 6 | * Class to use for storing settings. 7 | */ 8 | export class SettingsService { 9 | /** 10 | * The blind mode setting. 11 | */ 12 | private _blindMode: boolean; 13 | 14 | /** 15 | * The storage servie. 16 | */ 17 | private readonly _storageService: LocalStorageService; 18 | 19 | /** 20 | * Create a new instance of SettingsService. 21 | */ 22 | constructor() { 23 | this._storageService = ServiceFactory.get("local-storage"); 24 | this._blindMode = false; 25 | } 26 | 27 | /** 28 | * Initialize the service. 29 | */ 30 | public initialize(): void { 31 | this._blindMode = this._storageService.load("blindMode") ?? false; 32 | } 33 | 34 | /** 35 | * Get the blind mode setting. 36 | * @returns The blind mode. 37 | */ 38 | public getBlindMode(): boolean { 39 | return this._blindMode; 40 | } 41 | 42 | /** 43 | * Set the blind mode setting. 44 | * @param blindMode The new blind mode. 45 | */ 46 | public setBlindMode(blindMode: boolean): void { 47 | this._blindMode = blindMode; 48 | this._storageService.save("blindMode", this._blindMode); 49 | EventAggregator.publish("settings.blindMode", this._blindMode); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/routes/VisualizerState.ts: -------------------------------------------------------------------------------- 1 | import { IBlock } from "../../models/tangle/IBlock"; 2 | import { IVertex } from "../../models/visualizer/IVertex"; 3 | 4 | export interface VisualizerState { 5 | /** 6 | * Blocks. 7 | */ 8 | total: string; 9 | 10 | /** 11 | * Blocks per second. 12 | */ 13 | bps: string; 14 | 15 | /** 16 | * Tips. 17 | */ 18 | tips: string; 19 | 20 | /** 21 | * Accepted. 22 | */ 23 | accepted: string; 24 | 25 | /** 26 | * Confirmed. 27 | */ 28 | confirmed: string; 29 | 30 | /** 31 | * Finalized. 32 | */ 33 | finalized: string; 34 | 35 | /** 36 | * Transactions. 37 | */ 38 | transactions: string; 39 | 40 | /** 41 | * Is the rendering active. 42 | */ 43 | isActive: boolean; 44 | 45 | /** 46 | * The vertex that is selected. 47 | */ 48 | selected?: { 49 | /** 50 | * The vertex that is selected. 51 | */ 52 | vertex: IVertex; 53 | 54 | /** 55 | * Select item vertex state. 56 | */ 57 | vertexState: string; 58 | 59 | /** 60 | * Select item block state title. 61 | */ 62 | blockStateTitle?: string; 63 | 64 | /** 65 | * Select item payload title. 66 | */ 67 | payloadTitle?: string; 68 | 69 | /** 70 | * Select item block. 71 | */ 72 | block?: IBlock; 73 | }; 74 | 75 | /** 76 | * What is the theme. 77 | */ 78 | theme: string; 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/clipboardHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper methods for clipboard. 3 | */ 4 | export class ClipboardHelper { 5 | /** 6 | * Copy the text to the clipboard. 7 | * @param text The text to copy. 8 | * @returns True id the text was copied. 9 | */ 10 | public static copy(text: string | undefined): boolean { 11 | if (text !== undefined && text !== null) { 12 | try { 13 | const textArea = document.createElement("textarea"); 14 | 15 | // Prevent zooming on iOS 16 | textArea.style.fontSize = "12pt"; 17 | // Reset box model 18 | textArea.style.border = "0"; 19 | textArea.style.padding = "0"; 20 | textArea.style.margin = "0"; 21 | // Move element out of screen horizontally 22 | textArea.style.position = "absolute"; 23 | textArea.style.left = "-9999px"; 24 | // Move element to the same position vertically 25 | const yPosition = window.pageYOffset || document.documentElement.scrollTop; 26 | textArea.style.top = `${yPosition}px`; 27 | 28 | textArea.setAttribute("readonly", ""); 29 | textArea.value = text; 30 | 31 | document.body.append(textArea); 32 | 33 | textArea.select(); 34 | document.execCommand("Copy"); 35 | textArea.remove(); 36 | 37 | return true; 38 | } catch { 39 | // Not much we can do 40 | return false; 41 | } 42 | } else { 43 | return false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/scss/fonts/dm-sans.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'DM Sans'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'DM Sans'; 10 | font-style: italic; 11 | font-weight: 400; 12 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-italic.woff') format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'DM Sans'; 17 | font-style: normal; 18 | font-weight: 500; 19 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-500.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-500.woff') format('woff'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'DM Sans'; 24 | font-style: italic; 25 | font-weight: 500; 26 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-500italic.woff') format('woff'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'DM Sans'; 31 | font-style: normal; 32 | font-weight: 700; 33 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-700.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-700.woff') format('woff'); 34 | } 35 | 36 | @font-face { 37 | font-family: 'DM Sans'; 38 | font-style: italic; 39 | font-weight: 700; 40 | src: local(''), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff2') format('woff2'), url('../../assets/fonts/dm-sans/dm-sans-v6-latin-700italic.woff') format('woff'); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/layout/Tooltip.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | 4 | .tooltip { 5 | position: relative; 6 | display: flex; 7 | 8 | .tooltip__wrapper { 9 | visibility: hidden; 10 | position: absolute; 11 | top: 100%; 12 | margin-top: 10px; 13 | margin-left: 20px; 14 | padding: 12px; 15 | border-radius: 0.25rem; 16 | background: var(--tooltip-background); 17 | white-space: nowrap; 18 | color: var(--tooltip-text); 19 | z-index: 1; 20 | opacity: 0; 21 | transition: all 250ms; 22 | right: 0; 23 | width: 300px; 24 | text-align: center; 25 | white-space: break-spaces; 26 | font-family: $font-sans; 27 | word-break: keep-all; 28 | 29 | .tooltip__arrow { 30 | background: var(--tooltip-background); 31 | width: 12px; 32 | height: 12px; 33 | position: absolute; 34 | top: -6px; 35 | right: 150px; 36 | transform: rotate(45deg); 37 | } 38 | 39 | @include desktop-down { 40 | display: none; 41 | } 42 | 43 | @include tablet-down { 44 | display: none; 45 | } 46 | 47 | @include phone-down { 48 | display: none; 49 | } 50 | } 51 | 52 | .tooltip__children { 53 | white-space: nowrap; 54 | font-weight: 600; 55 | cursor: pointer; 56 | 57 | @include desktop-down { 58 | cursor: text; 59 | } 60 | 61 | @include tablet-down { 62 | cursor: text; 63 | } 64 | 65 | @include phone-down { 66 | cursor: text; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/navigate-to.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/services/themeService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceFactory } from "../factories/serviceFactory"; 2 | import { EventAggregator } from "./eventAggregator"; 3 | import { LocalStorageService } from "./localStorageService"; 4 | 5 | /** 6 | * Class the help with themes. 7 | */ 8 | export class ThemeService { 9 | /** 10 | * The theme. 11 | */ 12 | private _theme: string; 13 | 14 | /** 15 | * Create a new instance of ThemeService. 16 | */ 17 | constructor() { 18 | this._theme = "light"; 19 | } 20 | 21 | /** 22 | * Initialize the theme. 23 | */ 24 | public initialize(): void { 25 | const storageService = ServiceFactory.get("local-storage"); 26 | 27 | const theme = storageService.load("theme"); 28 | 29 | this.apply(theme, false); 30 | } 31 | 32 | /** 33 | * Apply a theme. 34 | * @param theme The theme to apply. 35 | * @param save Save the theme. 36 | */ 37 | public apply(theme: string, save: boolean): void { 38 | const currentTheme = this._theme; 39 | this._theme = theme ?? "light"; 40 | 41 | document.body.classList.remove(`theme-${currentTheme}`); 42 | document.body.classList.add(`theme-${this._theme}`); 43 | 44 | EventAggregator.publish("theme", this._theme); 45 | 46 | if (save) { 47 | this.save(); 48 | } 49 | } 50 | 51 | /** 52 | * Get the theme. 53 | * @returns The theme. 54 | */ 55 | public get(): string { 56 | return this._theme; 57 | } 58 | 59 | /** 60 | * Save theme. 61 | */ 62 | public save(): void { 63 | const storageService = ServiceFactory.get("local-storage"); 64 | storageService.save("theme", this._theme); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/services/nodeConfigService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceFactory } from "../factories/serviceFactory"; 2 | import { SessionStorageService } from "./sessionStorageService"; 3 | import { TangleService } from "./tangleService"; 4 | 5 | /** 6 | * Service to handle getting confiuration from the node. 7 | */ 8 | export class NodeConfigService { 9 | /** 10 | * The network id. 11 | */ 12 | private _networkId: string; 13 | 14 | /** 15 | * The storage servie. 16 | */ 17 | private readonly _storageService: SessionStorageService; 18 | 19 | /** 20 | * Create a new instance of NodeConfigService. 21 | */ 22 | constructor() { 23 | this._storageService = ServiceFactory.get("session-storage"); 24 | this._networkId = ""; 25 | } 26 | 27 | /** 28 | * Initialise NodeConfigService. 29 | */ 30 | public async initialize(): Promise { 31 | this._networkId = this._storageService.load("networkId"); 32 | 33 | if (!this._networkId) { 34 | const tangleService = ServiceFactory.get("tangle"); 35 | 36 | try { 37 | const info = await tangleService.info(); 38 | this.setNetworkId(info.protocolParameters[0].parameters.networkName); 39 | } catch {} 40 | } 41 | } 42 | 43 | /** 44 | * Get the network id. 45 | * @returns The network id. 46 | */ 47 | public getNetworkId(): string { 48 | return this._networkId; 49 | } 50 | 51 | /** 52 | * Set the network id. 53 | * @param networkId The new network id. 54 | */ 55 | public setNetworkId(networkId: string): void { 56 | this._networkId = networkId; 57 | this._storageService.save("networkId", this._networkId); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/services/dashboardConfigService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceFactory } from "../factories/serviceFactory"; 2 | import { FetchHelper } from "../utils/fetchHelper"; 3 | import { AuthService } from "./authService"; 4 | /** 5 | * Service to handle getting confiuration from the dashboard backend. 6 | */ 7 | export class DashboardConfigService { 8 | /** 9 | * The explorer URL. 10 | */ 11 | private _explorerURL: string; 12 | 13 | /** 14 | * The auth service. 15 | */ 16 | private readonly _authService: AuthService; 17 | 18 | /** 19 | * Create a new instance of DashboardConfigService. 20 | */ 21 | constructor() { 22 | this._authService = ServiceFactory.get("auth"); 23 | this._explorerURL = ""; 24 | } 25 | 26 | /** 27 | * Initialise DashboardConfigService. 28 | */ 29 | public async initialize(): Promise { 30 | try { 31 | this._explorerURL = await this.getExplorerURLBackend(); 32 | } catch {} 33 | } 34 | 35 | /** 36 | * Get the explorer URL. 37 | * @returns The explorer URL. 38 | */ 39 | public getExplorerURL(): string { 40 | return this._explorerURL; 41 | } 42 | 43 | /** 44 | * Get the explorer URL from the backend. 45 | * @returns The explorer URL. 46 | */ 47 | private async getExplorerURLBackend(): Promise { 48 | const headers = this._authService.buildAuthHeaders(); 49 | 50 | const response = await FetchHelper.json( 53 | `${window.location.protocol}//${window.location.host}`, 54 | "/dashboard/settings", 55 | "get", 56 | undefined, 57 | headers); 58 | 59 | return response.explorerUrl; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/create-task.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: New task 3 | description: Create a new work task 4 | title: '[Task]: ' 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: '## Creating a task' 10 | - type: markdown 11 | attributes: 12 | value: | 13 | This form should be used by official maintainers only to create new work tasks. Most tasks should be assigned to a milestone and the task management project. 14 | 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: Task description 19 | description: Describe the task that needs to be completed. 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: requirements 25 | attributes: 26 | label: Requirements 27 | description: What are the requirements for this task, this could be a checklist of subtasks. 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: acceptance_criteria 33 | attributes: 34 | label: Acceptance criteria 35 | description: What is the criteria for this task to be marked as done? This will help anyone approving any PRs related to this task. 36 | validations: 37 | required: true 38 | 39 | - type: checkboxes 40 | id: checklist 41 | attributes: 42 | label: Creation checklist 43 | description: 'Before submitting this task please ensure you have done the following if necessary:' 44 | options: 45 | - label: I have assigned this task to the correct people 46 | required: false 47 | - label: I have added the most appropriate labels 48 | required: false 49 | - label: I have linked the correct milestone and/or project 50 | required: false 51 | -------------------------------------------------------------------------------- /src/app/components/layout/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import "./ToggleButton.scss"; 4 | import { ReactComponent as ToggleIcon } from "../../../assets/toggle.svg"; 5 | import { ToggleButtonProps } from "./ToggleButtonProps"; 6 | import { ToggleButtonState } from "./ToggleButtonState"; 7 | 8 | /** 9 | * Component which will display a toggle button. 10 | */ 11 | class ToggleButton extends Component { 12 | /** 13 | * Create a new instance of ToggleButton. 14 | * @param props The props. 15 | */ 16 | constructor(props: ToggleButtonProps) { 17 | super(props); 18 | 19 | this.state = { 20 | value: props.value 21 | }; 22 | } 23 | 24 | /** 25 | * The component did update. 26 | * @param prevProps The previous properties. 27 | */ 28 | public componentDidUpdate(prevProps: ToggleButtonProps): void { 29 | if (this.props.value !== prevProps.value) { 30 | this.setState({ value: this.props.value }); 31 | } 32 | } 33 | 34 | /** 35 | * Render the component. 36 | * @returns The node to render. 37 | */ 38 | public render(): ReactNode { 39 | return ( 40 | 57 | ); 58 | } 59 | } 60 | 61 | export default ToggleButton; 62 | -------------------------------------------------------------------------------- /src/scss/standard.scss: -------------------------------------------------------------------------------- 1 | @import "./fonts"; 2 | @import "./variables"; 3 | 4 | h1 { 5 | @include font-size(24px); 6 | 7 | color: var(--text-color-primary); 8 | font-family: $font-sans; 9 | font-weight: bold; 10 | text-decoration: none; 11 | white-space: nowrap; 12 | } 13 | 14 | h2 { 15 | @include font-size(18px); 16 | 17 | color: var(--text-color-primary); 18 | font-family: $font-sans; 19 | font-weight: bold; 20 | text-decoration: none; 21 | } 22 | 23 | h3 { 24 | @include font-size(14px); 25 | 26 | color: var(--text-color-primary); 27 | font-family: $font-sans; 28 | font-weight: bold; 29 | text-decoration: none; 30 | } 31 | 32 | h4 { 33 | @include font-size(10px); 34 | 35 | color: var(--text-color-secondary); 36 | font-family: $font-sans; 37 | font-weight: 500; 38 | text-decoration: none; 39 | text-transform: uppercase; 40 | } 41 | 42 | p { 43 | @include font-size(14px); 44 | 45 | color: var(--text-color-primary); 46 | font-family: $font-sans; 47 | } 48 | 49 | a { 50 | outline: 0; 51 | text-decoration: none; 52 | 53 | &:focus { 54 | border-bottom: 1px solid var(--accent-primary); 55 | } 56 | } 57 | 58 | hr { 59 | height: 1px; 60 | margin: $spacing-medium 0; 61 | border: 0; 62 | background-color: var(--panel-border); 63 | } 64 | 65 | .secondary { 66 | color: var(--text-color-secondary); 67 | } 68 | 69 | .icon-button { 70 | padding: $spacing-tiny; 71 | border: 0; 72 | border-radius: $spacing-tiny; 73 | outline: 0; 74 | background: none; 75 | cursor: pointer; 76 | 77 | &:focus { 78 | box-shadow: 0 0 3px 0 var(--accent-primary); 79 | } 80 | } 81 | 82 | .word-break { 83 | word-break: break-word; 84 | } 85 | 86 | .word-break-all { 87 | word-break: break-all; 88 | } 89 | 90 | .hide-overflow { 91 | overflow: hidden; 92 | } 93 | 94 | .d-none { 95 | display: none !important; 96 | } 97 | 98 | .text-ellipsis { 99 | text-overflow: ellipsis; 100 | } 101 | -------------------------------------------------------------------------------- /src/utils/brandHelper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 3 | import { IBrandConfiguration } from "../models/IBrandConfiguration"; 4 | 5 | export class BrandHelper { 6 | /** 7 | * The brand id from the environment. 8 | */ 9 | private static readonly _brandId?: string = process.env.REACT_APP_BRAND_ID; 10 | 11 | /** 12 | * The brand configuration. 13 | */ 14 | private static _brandConfiguration: IBrandConfiguration; 15 | 16 | /** 17 | * Initialize the branding. 18 | * @returns The brand configuration. 19 | */ 20 | public static initialize(): IBrandConfiguration | undefined { 21 | if (BrandHelper._brandId) { 22 | BrandHelper._brandConfiguration = require(`../assets/${BrandHelper._brandId}/brand.json`); 23 | document.title = `${BrandHelper._brandConfiguration.name} Dashboard`; 24 | 25 | return BrandHelper._brandConfiguration; 26 | } 27 | } 28 | 29 | /** 30 | * Get the configuration. 31 | * @returns The configuration. 32 | */ 33 | public static getConfiguration(): IBrandConfiguration { 34 | return BrandHelper._brandConfiguration; 35 | } 36 | 37 | /** 38 | * Get the logo for the navigation panel. 39 | * @param theme The current theme. 40 | * @returns The navigation panel logo. 41 | */ 42 | public static async getLogoNavigation(theme: string): Promise { 43 | const logo = await import(`../assets/${BrandHelper._brandId}/themes/${theme}/logo-navigation.svg`); 44 | return logo.default; 45 | } 46 | 47 | /** 48 | * Get the logo for the home page banner. 49 | * @param theme The current theme. 50 | * @returns The banner panel logo. 51 | */ 52 | public static async getBanner(theme: string): Promise { 53 | const banner = await import(`../assets/${BrandHelper._brandId}/themes/${theme}/banner.svg`); 54 | return banner.default; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/layout/NavPanel.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | .nav-panel { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: space-between; 10 | width: 120px; 11 | padding-top: 22px; 12 | overflow: auto; 13 | border-right: 1px solid var(--panel-border); 14 | background-color: var(--panel-background); 15 | 16 | @include tablet-down { 17 | padding-top: $spacing-small; 18 | } 19 | 20 | a { 21 | &:focus { 22 | border: 0; 23 | outline: none; 24 | } 25 | } 26 | 27 | .logo { 28 | width: 36px; 29 | height: 36px; 30 | 31 | @include tablet-down { 32 | width: 28px; 33 | height: 28px; 34 | } 35 | } 36 | 37 | .nav-panel-middle { 38 | display: flex; 39 | flex-direction: column; 40 | margin: $spacing-large 0; 41 | } 42 | 43 | .nav-panel-end { 44 | display: flex; 45 | flex-direction: column; 46 | margin: $spacing-large 0; 47 | } 48 | 49 | .nav-panel--button { 50 | @include font-size(12px); 51 | 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | height: 50px; 56 | border: 0; 57 | outline: 0; 58 | background: none; 59 | color: var(--text-color-secondary); 60 | font-family: $font-sans; 61 | font-weight: bold; 62 | text-decoration: none; 63 | cursor: pointer; 64 | 65 | &.nav-panel--button__selected { 66 | color: var(--accent-primary); 67 | } 68 | 69 | .nav-panel-button-label { 70 | margin-top: $spacing-tiny; 71 | } 72 | 73 | +.nav-panel--button { 74 | margin-top: $spacing-large; 75 | } 76 | } 77 | 78 | &.full-width { 79 | width: 100%; 80 | 81 | .nav-panel--button { 82 | flex-direction: row; 83 | 84 | .nav-panel-button-label { 85 | margin-top: 0; 86 | margin-left: $spacing-small; 87 | } 88 | 89 | +.nav-panel--button { 90 | margin-top: $spacing-small; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/scss/forms.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-class-pattern */ 2 | @import "./fonts"; 3 | @import "./variables"; 4 | 5 | .select-wrapper { 6 | display: inline-block; 7 | position: relative; 8 | 9 | svg { 10 | position: absolute; 11 | z-index: 2; 12 | top: 16px; 13 | right: 18px; 14 | color: var(--text-color-primary); 15 | pointer-events: none; 16 | } 17 | 18 | select { 19 | @include font-size(14px); 20 | 21 | height: 40px; 22 | margin: 0; 23 | padding: 0 48px 0 20px; 24 | border: 1px solid var(--text-color-secondary); 25 | border-radius: $form-input-radius; 26 | outline: none; 27 | background-color: transparent; 28 | color: var(--text-color-primary); 29 | font-family: $font-sans; 30 | appearance: none; 31 | 32 | &:focus { 33 | box-shadow: 0 0 3px 0 var(--accent-primary); 34 | } 35 | 36 | &:-ms-expand { 37 | display: none; 38 | } 39 | 40 | &:-moz-focusring { 41 | color: transparent; 42 | text-shadow: 0 0 0 var(--text-color-primary); 43 | } 44 | 45 | option { 46 | background-color: var(--panel-background); 47 | color: var(--text-color-primary); 48 | } 49 | } 50 | } 51 | 52 | input { 53 | @include font-size(14px); 54 | 55 | height: 40px; 56 | margin: 0; 57 | padding: 0 48px 0 20px; 58 | border: 1px solid var(--text-color-secondary); 59 | border-radius: $form-input-radius; 60 | outline: none; 61 | background-color: transparent; 62 | color: var(--text-color-primary); 63 | font-family: $font-sans; 64 | appearance: none; 65 | 66 | &:focus { 67 | box-shadow: 0 0 3px 0 var(--accent-primary); 68 | } 69 | 70 | &:disabled { 71 | opacity: 0.5; 72 | } 73 | 74 | &.input--stretch { 75 | width: 100%; 76 | } 77 | } 78 | 79 | input[type="file"]::file-selector-button { 80 | display: none; 81 | } 82 | 83 | .file-wrapper { 84 | display: inline-block; 85 | position: relative; 86 | 87 | input { 88 | padding-left: 30px; 89 | } 90 | 91 | svg { 92 | position: absolute; 93 | top: 5px; 94 | left: 0; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/services/eventAggregator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class to manage event aggregation. 3 | */ 4 | export class EventAggregator { 5 | /** 6 | * The stored subscriptions. 7 | */ 8 | private static readonly _subscriptions: { 9 | [eventName: string]: { 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | [subscriberId: string]: (data: any) => void; 12 | }; 13 | } = {}; 14 | 15 | /** 16 | * Subscribe to an event. 17 | * @param eventName The name of the event to subscribe to. 18 | * @param subscriberId The id of the subscriber. 19 | * @param handler The handle to call on a publish. 20 | */ 21 | public static subscribe( 22 | eventName: string, 23 | subscriberId: string, 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | handler: (data: any) => void | Promise): void { 26 | EventAggregator._subscriptions[eventName] ||= {}; 27 | EventAggregator._subscriptions[eventName][subscriberId] = handler; 28 | } 29 | 30 | /** 31 | * Unsubscribe from an event. 32 | * @param eventName The name of the event to subscribe to. 33 | * @param subscriberId The id of the subscriber. 34 | */ 35 | public static unsubscribe(eventName: string, subscriberId: string): void { 36 | if (EventAggregator._subscriptions[eventName]) { 37 | delete EventAggregator._subscriptions[eventName][subscriberId]; 38 | } 39 | } 40 | 41 | /** 42 | * Publish the event. 43 | * @param eventName The name of the event to publish. 44 | * @param data The data to publish with the event. 45 | */ 46 | public static publish(eventName: string, data?: unknown): void { 47 | setTimeout( 48 | () => { 49 | if (EventAggregator._subscriptions[eventName]) { 50 | for (const subscriberId in EventAggregator._subscriptions[eventName]) { 51 | EventAggregator._subscriptions[eventName][subscriberId](data); 52 | } 53 | } 54 | }, 55 | 0); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/components/layout/Graph.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | .graph { 6 | display: flex; 7 | flex: 1; 8 | flex-direction: column; 9 | 10 | .bar-color-1 { 11 | background-color: var(--bar-color-1); 12 | fill: var(--bar-color-1); 13 | } 14 | 15 | .bar-color-2 { 16 | background-color: var(--bar-color-2); 17 | fill: var(--bar-color-2); 18 | } 19 | 20 | .bar-color-3 { 21 | background-color: var(--bar-color-3); 22 | fill: var(--bar-color-3); 23 | } 24 | 25 | .bar-color-4 { 26 | background-color: var(--bar-color-4); 27 | fill: var(--bar-color-4); 28 | } 29 | 30 | .title-row { 31 | display: flex; 32 | flex: 1; 33 | flex-direction: row; 34 | justify-content: space-between; 35 | margin-bottom: $spacing-large; 36 | 37 | .caption { 38 | @include font-size(10px); 39 | 40 | color: var(--text-color-secondary); 41 | font-family: $font-sans; 42 | font-weight: 500; 43 | text-transform: uppercase; 44 | } 45 | 46 | .key { 47 | display: flex; 48 | flex-direction: row; 49 | align-items: center; 50 | margin-left: $spacing-small; 51 | 52 | .key-color { 53 | width: $spacing-small; 54 | height: $spacing-tiny; 55 | border-radius: $spacing-tiny; 56 | } 57 | 58 | .key-label { 59 | @include font-size(10px); 60 | 61 | margin-left: $spacing-small; 62 | color: var(--text-color-secondary); 63 | font-family: $font-sans; 64 | font-weight: 500; 65 | } 66 | 67 | @include tablet-down { 68 | display: none; 69 | } 70 | } 71 | } 72 | 73 | .canvas { 74 | flex: 1; 75 | color: var(--accent-primary); 76 | 77 | svg { 78 | width: 100%; 79 | height: 200px; 80 | overflow: visible; 81 | 82 | .axis-label { 83 | @include font-size(10px); 84 | 85 | fill: var(--text-color-secondary); 86 | font-family: $font-sans; 87 | font-weight: 500; 88 | } 89 | 90 | .axis-color { 91 | stroke: var(--text-color-secondary); 92 | opacity: 0.2; 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/components/layout/Pagination.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | 6 | .pagination { 7 | display: flex; 8 | align-self: center; 9 | list-style-type: none; 10 | justify-content: center; 11 | margin-top: 24px; 12 | 13 | .pagination-item { 14 | @include font-size(14px); 15 | 16 | padding: 0 12px; 17 | height: 32px; 18 | min-width: 32px; 19 | text-align: center; 20 | margin: auto 4px; 21 | color: var(--accent-primary); 22 | display: flex; 23 | box-sizing: border-box; 24 | align-items: center; 25 | border-radius: 6px; 26 | font-family: $font-mono; 27 | 28 | &.dots:hover { 29 | background-color: transparent; 30 | cursor: default; 31 | } 32 | &:hover { 33 | background-color: var(--panel-background-highlight); 34 | cursor: pointer; 35 | } 36 | 37 | &.selected { 38 | background-color: var(--background); 39 | } 40 | 41 | .arrow { 42 | &::before { 43 | position: relative; 44 | content: ''; 45 | display: inline-block; 46 | width: 0.4em; 47 | height: 0.4em; 48 | border-right: 0.12em solid; 49 | border-top: 0.12em solid; 50 | border-right-color: var(--text-color-primary); 51 | border-top-color: var(--text-color-primary); 52 | } 53 | 54 | &.left { 55 | transform: rotate(-135deg) translate(-25%); 56 | } 57 | 58 | &.right { 59 | transform: rotate(45deg); 60 | } 61 | } 62 | 63 | &.disabled { 64 | pointer-events: none; 65 | 66 | .arrow::before { 67 | border-right: 0.12em solid; 68 | border-top: 0.12em solid; 69 | border-right-color: var(--text-color-secondary); 70 | border-top-color: var(--text-color-secondary); 71 | } 72 | 73 | &:hover { 74 | background-color: transparent; 75 | cursor: default; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 |

Responsible disclosure policy

2 | 3 | At the IOTA Foundation, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. If you've discovered a vulnerability, please follow the guidelines below to report it to our security team: 4 |
    5 |
  • E-mail your findings to security@iota.org. If the report contains highly sensitive information, please consider encrypting your findings using our contact@iota.org (466385BD0B40D9550F93C04746A440CCE5664A64) PGP key.
  • 6 |
7 | Please follow these rules when testing/reporting vulnerabilities: 8 |
    9 |
  • Do not take advantage of the vulnerability you have discovered, for example by downloading more data than is necessary to demonstrate the vulnerability.
  • 10 |
  • Do not read, modify or delete data that you don't own.
  • 11 |
  • We ask that you do not to disclosure the problem to third parties until it has been resolved.
  • 12 |
  • The scope of the program is limited to technical vulnerabilities in IOTA Foundations's web applications and open source software packages distributed through GitHub, please do not try to test physical security or attempt phishing attacks against our employees, and so on.
  • 13 |
  • Out of concern for the availability of our services to all users, please do not attempt to carry out DoS attacks, leverage black hat SEO techniques, spam people, and do other similarly questionable things. We also discourage the use of any vulnerability testing tools that automatically generate significant volumes of traffic.
  • 14 |
15 | What we promise: 16 |
    17 |
  • We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date.
  • 18 |
  • If you have followed the instructions above, we will not take any legal action against you in regard to the report.
  • 19 |
  • We will keep you informed during all stages of resolving the problem.
  • 20 |
  • To show our appreciation for your effort and cooperation during the report, we will list your name and a link to a personal website/social network profile on the page below so that the public can know you've helped keep the IOTA Foundation secure.
  • 21 |
22 | We sincerely appreciate the efforts of security researchers in keeping our community safe. 23 | 24 | -------------------------------------------------------------------------------- /src/app/components/layout/InfoPanel.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import "./InfoPanel.scss"; 4 | import { InfoPanelProps } from "./InfoPanelProps"; 5 | import Tooltip from "./Tooltip"; 6 | 7 | const SYNC_STATUS_CAPTION = "Finalized Slot / Committed Slot"; 8 | 9 | /** 10 | * Info panel. 11 | */ 12 | class InfoPanel extends Component { 13 | /** 14 | * Render the component. 15 | * @returns The node to render. 16 | */ 17 | public render(): ReactNode { 18 | let latestFinalizedSlot = ""; 19 | let latestCommitmentSlot = ""; 20 | if (this.props.caption === SYNC_STATUS_CAPTION && this.props.value) { 21 | const slots = this.props.value.split("/"); 22 | latestFinalizedSlot = slots[0]; 23 | latestCommitmentSlot = slots[1]; 24 | } 25 | return ( 26 |
27 |
28 |
29 |
30 | {this.props.icon} 31 |
32 |
33 |
34 |

{this.props.caption}

35 | { 36 | this.props.caption === SYNC_STATUS_CAPTION ? 37 | 40 | { 41 | this.props.value ? 42 |
43 | {latestFinalizedSlot} / {latestCommitmentSlot} 44 |
: 45 | "-" 46 | } 47 |
: 48 |
{this.props.value ?? "-"}
49 | } 50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | export default InfoPanel; 57 | -------------------------------------------------------------------------------- /src/app/routes/Home.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .home { 6 | display: flex; 7 | flex: 1; 8 | justify-content: center; 9 | padding: 60px; 10 | 11 | @include desktop-down { 12 | padding: $spacing-small; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | max-width: $content-width-desktop; 18 | 19 | .banner { 20 | flex: 1; 21 | height: 140px; 22 | 23 | @include desktop-down { 24 | height: unset; 25 | } 26 | 27 | .node-info { 28 | display: flex; 29 | flex: 1; 30 | flex-direction: column; 31 | justify-content: space-between; 32 | padding: $spacing-small; 33 | 34 | @include desktop-down { 35 | * + .secondary { 36 | margin-top: $spacing-tiny; 37 | } 38 | } 39 | } 40 | 41 | .banner-curve { 42 | color: var(--panel-background-highlight); 43 | 44 | @include desktop-down { 45 | display: none; 46 | } 47 | } 48 | 49 | .banner-image { 50 | display: flex; 51 | flex: 1; 52 | align-items: center; 53 | justify-content: center; 54 | border-radius: 0 16px 16px 0; 55 | background-color: var(--panel-background-highlight); 56 | 57 | @include desktop-down { 58 | display: none; 59 | } 60 | } 61 | } 62 | 63 | .blocks-graph-panel { 64 | .graph { 65 | padding: 24px; 66 | } 67 | } 68 | 69 | .info-col { 70 | align-items: stretch; 71 | width: 66%; 72 | overflow: hidden; 73 | 74 | @include desktop-down { 75 | width: unset; 76 | } 77 | } 78 | 79 | .peers-summary-col { 80 | width: 33%; 81 | overflow: hidden; 82 | 83 | @include desktop-down { 84 | width: unset; 85 | } 86 | } 87 | 88 | .peers-summary-panel { 89 | margin-left: $spacing-small; 90 | 91 | @include desktop-down { 92 | margin-top: $spacing-small; 93 | margin-left: 0; 94 | } 95 | } 96 | 97 | .info-panel + .info-panel { 98 | margin-left: $spacing-small; 99 | 100 | @include tablet-down { 101 | margin-top: $spacing-small; 102 | margin-left: 0; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/app/components/layout/Breakpoint.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode } from "react"; 2 | import { BreakpointProps } from "./BreakpointProps"; 3 | import { BreakpointState } from "./BreakpointState"; 4 | 5 | /** 6 | * Component to show/hide children based on media size breakpoints. 7 | */ 8 | class Breakpoint extends Component { 9 | /** 10 | * The size for the breakpoints. 11 | */ 12 | private static readonly SIZE_BREAKPOINTS = { 13 | "phone": 480, 14 | "tablet": 768, 15 | "desktop": 1024 16 | }; 17 | 18 | /** 19 | * The resize method 20 | */ 21 | private readonly _resize: () => void; 22 | 23 | /** 24 | * Create a new instance of Breakpoint. 25 | * @param props The props. 26 | */ 27 | constructor(props: BreakpointProps) { 28 | super(props); 29 | 30 | this._resize = () => this.resize(); 31 | 32 | this.state = { 33 | isVisible: this.calculateVisible() 34 | }; 35 | } 36 | 37 | /** 38 | * The component mounted. 39 | */ 40 | public componentDidMount(): void { 41 | window.addEventListener("resize", this._resize); 42 | } 43 | 44 | /** 45 | * The component will unmount so update flag. 46 | */ 47 | public componentWillUnmount(): void { 48 | window.removeEventListener("resize", this._resize); 49 | } 50 | 51 | /** 52 | * Render the component. 53 | * @returns The node to render. 54 | */ 55 | public render(): ReactNode { 56 | return this.state.isVisible 57 | ? this.props.children 58 | : null; 59 | } 60 | 61 | /** 62 | * Handle the window resize. 63 | */ 64 | private resize(): void { 65 | const isVisible = this.calculateVisible(); 66 | 67 | this.setState({ 68 | isVisible 69 | }); 70 | } 71 | 72 | /** 73 | * Calculate if the child components should be visible. 74 | * @returns True if the children should be visible. 75 | */ 76 | private calculateVisible(): boolean { 77 | const windowSize = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 78 | 79 | return this.props.aboveBelow === "above" 80 | ? windowSize >= Breakpoint.SIZE_BREAKPOINTS[this.props.size] 81 | : windowSize < Breakpoint.SIZE_BREAKPOINTS[this.props.size]; 82 | } 83 | } 84 | 85 | export default Breakpoint; 86 | -------------------------------------------------------------------------------- /src/app/components/layout/BlockButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import { ReactComponent as CopyIcon } from "../../../assets/copy.svg"; 4 | import "./BlockButton.scss"; 5 | import { BlockButtonProps } from "./BlockButtonProps"; 6 | import { BlockButtonState } from "./BlockButtonState"; 7 | 8 | /** 9 | * Component which will display a block button. 10 | */ 11 | class BlockButton extends Component { 12 | /** 13 | * Create a new instance of BlockButton. 14 | * @param props The props. 15 | */ 16 | constructor(props: BlockButtonProps) { 17 | super(props); 18 | 19 | this.state = { 20 | active: false, 21 | message: props.buttonType === "copy" ? "Copied" : "" 22 | }; 23 | } 24 | 25 | /** 26 | * Render the component. 27 | * @returns The node to render. 28 | */ 29 | public render(): ReactNode { 30 | return ( 31 |
32 | 44 | {this.state.active && this.state.message && ( 45 | 52 | {this.state.message} 53 | 54 | )} 55 |
56 | ); 57 | } 58 | 59 | /** 60 | * Activate the button. 61 | */ 62 | private activate(): void { 63 | this.props.onClick(); 64 | 65 | this.setState({ active: true }); 66 | setTimeout( 67 | () => { 68 | this.setState({ active: false }); 69 | }, 70 | 2000); 71 | } 72 | } 73 | 74 | export default BlockButton; 75 | -------------------------------------------------------------------------------- /src/app/components/layout/Dialog.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | .dialog-click-shield { 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | opacity: 0.2; 12 | background: var(--dialog-shield); 13 | } 14 | 15 | .dialog-container { 16 | display: flex; 17 | position: absolute; 18 | top: 0; 19 | right: 0; 20 | bottom: 0; 21 | left: 0; 22 | align-items: center; 23 | justify-content: center; 24 | 25 | .dialog { 26 | display: flex; 27 | flex-direction: column; 28 | width: 500px; 29 | min-height: 400px; 30 | max-height: 800px; 31 | padding: 20px; 32 | border: 1px solid var(--panel-border); 33 | border-radius: 10px; 34 | background: var(--panel-background); 35 | box-shadow: 5px 5px 5px 0 var(--drop-shadow); 36 | 37 | @include tablet-down { 38 | min-width: 90%; 39 | } 40 | 41 | .dialog-header { 42 | margin-bottom: 20px; 43 | border-bottom: 1px solid var(--panel-border); 44 | } 45 | 46 | .dialog-content { 47 | flex: 1; 48 | 49 | .dialog--label { 50 | @include font-size(10px); 51 | 52 | margin-top: $spacing-small; 53 | margin-bottom: calc($spacing-tiny / 2); 54 | color: var(--text-color-secondary); 55 | font-family: $font-sans; 56 | font-weight: 500; 57 | text-transform: uppercase; 58 | } 59 | } 60 | 61 | .dialog-footer { 62 | display: flex; 63 | justify-content: flex-end; 64 | 65 | button { 66 | @include font-size(14px); 67 | 68 | display: flex; 69 | flex-direction: row; 70 | align-items: center; 71 | margin-top: $spacing-medium; 72 | padding: 6px 12px; 73 | border: 1px solid var(--text-color-secondary); 74 | border-radius: $form-input-radius; 75 | outline: 0; 76 | background: none; 77 | color: var(--text-color-primary); 78 | font-family: $font-sans; 79 | cursor: pointer; 80 | 81 | +button { 82 | margin-left: $spacing-small; 83 | } 84 | 85 | &:hover { 86 | color: var(--accent-primary); 87 | } 88 | 89 | &:focus { 90 | box-shadow: 0 0 3px 0 var(--accent-primary); 91 | } 92 | 93 | &:disabled { 94 | opacity: 0.5; 95 | pointer-events: none; 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/services/tangleService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceFactory } from "../factories/serviceFactory"; 2 | import { SingleNodeClient } from "../models/clients/singleNodeClient"; 3 | import { IClient } from "../models/IClient"; 4 | import { INodeInfo } from "../models/info/INodeInfo"; 5 | import { IBlock } from "../models/tangle/IBlock"; 6 | import { AuthService } from "./authService"; 7 | /** 8 | * Service to handle api requests. 9 | */ 10 | export class TangleService { 11 | /** 12 | * The node info. 13 | */ 14 | private _nodeInfo?: INodeInfo; 15 | 16 | /** 17 | * The auth service. 18 | */ 19 | private readonly _authService: AuthService; 20 | 21 | /** 22 | * Create a new instance of TangleService. 23 | */ 24 | constructor() { 25 | this._authService = ServiceFactory.get("auth"); 26 | } 27 | 28 | /** 29 | * Get the node info. 30 | * @returns The node info. 31 | */ 32 | public async info(): Promise { 33 | const client = this.buildClient(); 34 | this._nodeInfo = await client.info(); 35 | return this._nodeInfo; 36 | } 37 | 38 | /** 39 | * Get the block payload. 40 | * @param blockId The block to get. 41 | * @returns The response data. 42 | */ 43 | public async block(blockId: string): Promise { 44 | try { 45 | const client = this.buildClient(); 46 | return await client.block(blockId); 47 | } catch {} 48 | } 49 | 50 | /** 51 | * Add a peer. 52 | * @param peerAddress The peer address. 53 | * @param peerAlias The peer alias. 54 | */ 55 | public async peerAdd(peerAddress: string, peerAlias: string): Promise { 56 | const client = this.buildClient(); 57 | 58 | await client.peerAdd(peerAddress, peerAlias); 59 | } 60 | 61 | /** 62 | * Delete a peer. 63 | * @param peerId The peer to delete. 64 | */ 65 | public async peerDelete(peerId: string): Promise { 66 | const client = this.buildClient(); 67 | 68 | await client.peerDelete(peerId); 69 | } 70 | 71 | 72 | /** 73 | * Build a client with auth header. 74 | * @returns The client. 75 | */ 76 | private buildClient(): IClient { 77 | const headers = this._authService.buildAuthHeaders(); 78 | 79 | return new SingleNodeClient( 80 | `${window.location.protocol}//${window.location.host}`, 81 | { 82 | basePath: "/dashboard/api/", 83 | headers 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/utils/downloadHelper.ts: -------------------------------------------------------------------------------- 1 | import { Converter } from "@iota/util.js"; 2 | 3 | /** 4 | * Class to help with downloading. 5 | */ 6 | export class DownloadHelper { 7 | /** 8 | * Get a filename base on the type. 9 | * @param id The id of the item. 10 | * @param type The type of the file. 11 | * @returns The filename. 12 | */ 13 | public static filename(id: string, type: string): string { 14 | return `${id}.${type}`; 15 | } 16 | 17 | /** 18 | * Create a data url for an object. 19 | * @param object The object to create the url for. 20 | * @returns The data url. 21 | */ 22 | public static createJsonDataUrl(object: unknown): string { 23 | const b64 = Converter.bytesToBase64(Converter.utf8ToBytes((JSON.stringify(object, undefined, "\t")))); 24 | return `data:application/json;base64,${b64}`; 25 | } 26 | 27 | /** 28 | * Create a data url for binary data. 29 | * @param data The data to create the url for. 30 | * @returns The data url. 31 | */ 32 | public static createBinaryDataUrl(data: Uint8Array): string { 33 | const b64 = Converter.bytesToBase64(data); 34 | return `data:application/octet;base64,${b64}`; 35 | } 36 | 37 | /** 38 | * Create a data url for hex data. 39 | * @param data The data to create the url for. 40 | * @returns The data url. 41 | */ 42 | public static createHexDataUrl(data: Uint8Array): string { 43 | const b64 = Converter.bytesToBase64(Converter.utf8ToBytes(Converter.bytesToHex(data))); 44 | return `data:plain/text;base64,${b64}`; 45 | } 46 | 47 | /** 48 | * Create a data url for base64 data. 49 | * @param data The data to create the url for. 50 | * @returns The data url. 51 | */ 52 | public static createBase64DataUrl(data: Uint8Array): string { 53 | const b64 = Converter.bytesToBase64(Converter.utf8ToBytes(Converter.bytesToBase64(data))); 54 | return `data:plain/text;base64,${b64}`; 55 | } 56 | 57 | /** 58 | * Trigger a file download. 59 | * @param dataUrl The data url to download. 60 | * @param filename The filename. 61 | * @returns true if downloaded. 62 | */ 63 | public static downloadFile(dataUrl: string, filename: string): boolean { 64 | try { 65 | const link = document.createElement("a"); 66 | link.href = dataUrl; 67 | link.download = filename; 68 | 69 | link.click(); 70 | link.remove(); 71 | 72 | return true; 73 | } catch { 74 | return false; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/services/localStorageService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class to use local storage. 3 | */ 4 | export class LocalStorageService { 5 | /** 6 | * Load an item from local storage. 7 | * @param key The key of the item to load. 8 | * @returns The item loaded. 9 | */ 10 | public load(key: string): T { 11 | let obj; 12 | if (window.localStorage) { 13 | try { 14 | const json = window.localStorage.getItem(key); 15 | 16 | if (json) { 17 | obj = JSON.parse(json); 18 | } 19 | } catch { 20 | // Nothing to do 21 | } 22 | } 23 | 24 | return obj as T; 25 | } 26 | 27 | /** 28 | * Save an item to local storage. 29 | * @param key The key of the item to store. 30 | * @param item The item to store. 31 | */ 32 | public save(key: string, item: T): void { 33 | if (window.localStorage) { 34 | try { 35 | const json = JSON.stringify(item); 36 | window.localStorage.setItem(key, json); 37 | } catch { 38 | // Nothing to do 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Delete an item in local storage. 45 | * @param key The key of the item to store. 46 | */ 47 | public remove(key: string): void { 48 | if (window.localStorage) { 49 | try { 50 | window.localStorage.removeItem(key); 51 | } catch { 52 | // Nothing to do 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Clear the local storage. 59 | * @param rootKey Clear all items that start with the root key, if undefined clear everything. 60 | */ 61 | public clear(rootKey: string): void { 62 | if (window.localStorage) { 63 | try { 64 | if (rootKey) { 65 | const keysToRemove = []; 66 | const len = window.localStorage.length; 67 | for (let i = 0; i < len; i++) { 68 | const key = window.localStorage.key(i); 69 | if (key?.startsWith(rootKey)) { 70 | keysToRemove.push(key); 71 | } 72 | } 73 | for (const key of keysToRemove) { 74 | window.localStorage.removeItem(key); 75 | } 76 | } else { 77 | window.localStorage.clear(); 78 | } 79 | } catch { 80 | // Nothing to do 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/db-icon.svg: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/services/sessionStorageService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class to use session storage. 3 | */ 4 | export class SessionStorageService { 5 | /** 6 | * Load an item from session storage. 7 | * @param key The key of the item to load. 8 | * @returns The item loaded. 9 | */ 10 | public load(key: string): T { 11 | let obj; 12 | if (window.sessionStorage) { 13 | try { 14 | const json = window.sessionStorage.getItem(key); 15 | 16 | if (json) { 17 | obj = JSON.parse(json); 18 | } 19 | } catch { 20 | // Nothing to do 21 | } 22 | } 23 | 24 | return obj as T; 25 | } 26 | 27 | /** 28 | * Save an item to session storage. 29 | * @param key The key of the item to store. 30 | * @param item The item to store. 31 | */ 32 | public save(key: string, item: T): void { 33 | if (window.sessionStorage) { 34 | try { 35 | const json = JSON.stringify(item); 36 | window.sessionStorage.setItem(key, json); 37 | } catch { 38 | // Nothing to do 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Delete an item in session storage. 45 | * @param key The key of the item to store. 46 | */ 47 | public remove(key: string): void { 48 | if (window.sessionStorage) { 49 | try { 50 | window.sessionStorage.removeItem(key); 51 | } catch { 52 | // Nothing to do 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Clear the session storage. 59 | * @param rootKey Clear all items that start with the root key, if undefined clear everything. 60 | */ 61 | public clear(rootKey: string): void { 62 | if (window.sessionStorage) { 63 | try { 64 | if (rootKey) { 65 | const keysToRemove = []; 66 | const len = window.sessionStorage.length; 67 | for (let i = 0; i < len; i++) { 68 | const key = window.sessionStorage.key(i); 69 | if (key?.startsWith(rootKey)) { 70 | keysToRemove.push(key); 71 | } 72 | } 73 | for (const key of keysToRemove) { 74 | window.sessionStorage.removeItem(key); 75 | } 76 | } else { 77 | window.sessionStorage.clear(); 78 | } 79 | } catch { 80 | // Nothing to do 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

IOTA Node Dashboard

2 | 3 |

4 | Discord 5 | StackExchange 6 | Apache-2.0 license 7 |

8 | 9 |

10 | About ◈ 11 | Prerequisites ◈ 12 | Getting started ◈ 13 | Supporting the project ◈ 14 | Joining the discussion 15 |

16 | 17 | # About 18 | 19 | Dashboard used by the IOTA-Core node software. 20 | 21 | ## Prerequisites 22 | 23 | To deploy your own version of the Node Dashboard, you need to have at least [version 14 of Node.js](https://nodejs.org/en/download/) installed on your device. 24 | 25 | To check if you have Node.js installed, run the following command: 26 | 27 | ```bash 28 | node -v 29 | ``` 30 | 31 | If Node.js is installed, you should see the version that's installed. 32 | 33 | # Getting Started 34 | 35 | You need to run a local version of the IOTA-Core node software from the develop branch [https://github.com/iotaledger/iota-core/](https://github.com/iotaledger/iota-core/) 36 | 37 | 1. Make sure to set `dashboard.dev` to true in the config, to enable the node to serve assets 38 | from the dev instance. 39 | 2. Install all needed npm modules via `npm install`. 40 | 3. Run a dev-server instance by running `npm run start` within the repo root directory. 41 | 4. Using default port config, you should now be able to access the dashboard under http://127.0.0.1:8081 42 | 43 | The dashboard is hot-reload enabled. 44 | 45 | ## Supporting the project 46 | 47 | If the Node Dashboard has been useful to you and you feel like contributing, consider submitting a [bug report](https://github.com/iotaledger/node-dashboard/issues/new), [feature request](https://github.com/iotaledger/node-dashboard/issues/new) or a [pull request](https://github.com/iotaledger/node-dashboard/pulls/). 48 | 49 | See our [contributing guidelines](.github/CONTRIBUTING.md) for more information. 50 | 51 | ## Joining the discussion 52 | 53 | If you want to get involved in the community, need help with getting set up, have any issues or just want to discuss IOTA, feel free to join our [Discord](https://discord.iota.org/). -------------------------------------------------------------------------------- /src/app/routes/HomeState.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface HomeState { 3 | /** 4 | * The name or alias of the node. 5 | */ 6 | nodeName?: string; 7 | 8 | /** 9 | * The node id. 10 | */ 11 | nodeId?: string; 12 | 13 | /** 14 | * The multiaddress of the node. 15 | */ 16 | multiAddress?: string; 17 | 18 | /** 19 | * The version. 20 | */ 21 | version?: string; 22 | 23 | /** 24 | * Latest version. 25 | */ 26 | latestVersion?: string; 27 | 28 | /** 29 | * The version. 30 | */ 31 | displayVersion?: string; 32 | 33 | /** 34 | * Latest version. 35 | */ 36 | displayLatestVersion?: string; 37 | 38 | /** 39 | * Current slot. 40 | */ 41 | currentSlot?: string; 42 | 43 | /** 44 | * Current epoch. 45 | */ 46 | currentEpoch?: string; 47 | 48 | /** 49 | * Latest accepted block slot. 50 | */ 51 | latestAcceptedBlockSlot?: string; 52 | 53 | /** 54 | * Latest finalized slot. 55 | */ 56 | latestFinalizedSlot?: string; 57 | 58 | /** 59 | * Latest committed slot. 60 | */ 61 | latestCommitmentSlot?: string; 62 | 63 | /** 64 | * Blocks per second. 65 | */ 66 | bps: string; 67 | 68 | /** 69 | * Referenced blocks per second. 70 | */ 71 | rbps: string; 72 | 73 | /** 74 | * Referenced rate. 75 | */ 76 | referencedRate: string; 77 | 78 | /** 79 | * The pruning epoch. 80 | */ 81 | pruningEpoch?: string; 82 | 83 | /** 84 | * Uptime. 85 | */ 86 | uptime?: string; 87 | 88 | /** 89 | * Memory usage. 90 | */ 91 | memory?: string; 92 | 93 | /** 94 | * Permanent database size. 95 | */ 96 | dbSizePermanentFormatted: string; 97 | 98 | /** 99 | * Prunable database size. 100 | */ 101 | dbSizePrunableFormatted: string; 102 | 103 | /** 104 | * TxRetainer database size. 105 | */ 106 | dbSizeTxRetainerFormatted: string; 107 | 108 | /** 109 | * Total database size. 110 | */ 111 | dbSizeTotalFormatted: string; 112 | 113 | /** 114 | * Last received bps time. 115 | */ 116 | lastReceivedBpsTime: number; 117 | 118 | /** 119 | * The blocks per second incoming. 120 | */ 121 | bpsIncoming: number[]; 122 | 123 | /** 124 | * The blocks per second outgoing. 125 | */ 126 | bpsOutgoing: number[]; 127 | 128 | /** 129 | * The banner logo source. 130 | */ 131 | bannerSrc: string; 132 | 133 | /** 134 | * Hide any details. 135 | */ 136 | blindMode: boolean; 137 | } 138 | -------------------------------------------------------------------------------- /src/app/components/layout/NavMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from "react"; 2 | import { RouteComponentProps, withRouter } from "react-router-dom"; 3 | import { ServiceFactory } from "../../../factories/serviceFactory"; 4 | import { EventAggregator } from "../../../services/eventAggregator"; 5 | import { ThemeService } from "../../../services/themeService"; 6 | import { BrandHelper } from "../../../utils/brandHelper"; 7 | import "./NavMenu.scss"; 8 | import { NavMenuProps } from "./NavMenuProps"; 9 | import { NavMenuState } from "./NavMenuState"; 10 | 11 | /** 12 | * Navigation menu. 13 | */ 14 | class NavMenu extends Component { 15 | /** 16 | * The theme service. 17 | */ 18 | private readonly _themeService: ThemeService; 19 | 20 | /** 21 | * Create a new instance of NavMenu; 22 | * @param props The props. 23 | */ 24 | constructor(props: RouteComponentProps & NavMenuProps) { 25 | super(props); 26 | 27 | this._themeService = ServiceFactory.get("theme"); 28 | 29 | this.state = { 30 | logoSrc: "", 31 | isOpen: false 32 | }; 33 | } 34 | 35 | /** 36 | * The component mounted. 37 | */ 38 | public async componentDidMount(): Promise { 39 | this.setState({ 40 | logoSrc: await BrandHelper.getLogoNavigation(this._themeService.get()) 41 | }); 42 | 43 | EventAggregator.subscribe("theme", "navmenu", async (theme: string) => { 44 | this.setState({ 45 | logoSrc: await BrandHelper.getLogoNavigation(theme) 46 | }); 47 | }); 48 | } 49 | 50 | /** 51 | * The component will unmount. 52 | */ 53 | public componentWillUnmount(): void { 54 | EventAggregator.unsubscribe("theme", "navmenu"); 55 | } 56 | 57 | /** 58 | * Render the component. 59 | * @returns The node to render. 60 | */ 61 | public render(): ReactNode { 62 | return ( 63 |
this.state.isOpen && this.setState({ isOpen: false })} 66 | > 67 | 73 | {this.state.isOpen && ( 74 |
75 | {this.props.children} 76 |
77 | )} 78 |
79 | ); 80 | } 81 | } 82 | 83 | export default withRouter(NavMenu); 84 | -------------------------------------------------------------------------------- /src/app/components/layout/TabPanel.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { Component, ReactNode } from "react"; 3 | import "./TabPanel.scss"; 4 | import { TabPanelProps } from "./TabPanelProps"; 5 | import { TabPanelState } from "./TabPanelState"; 6 | 7 | /** 8 | * Tab panel. 9 | */ 10 | class TabPanel extends Component { 11 | /** 12 | * Create a new instance of TabPanel. 13 | * @param props The props. 14 | */ 15 | constructor(props: TabPanelProps) { 16 | super(props); 17 | 18 | this.state = { 19 | activeTab: props.activeTab.toLowerCase() 20 | }; 21 | } 22 | 23 | /** 24 | * The component updated. 25 | * @param prevProps The previous props. 26 | */ 27 | public componentDidUpdate(prevProps: TabPanelProps): void { 28 | if (this.props.activeTab.toLowerCase() !== prevProps.activeTab.toLowerCase()) { 29 | this.setState({ activeTab: this.props.activeTab.toLowerCase() }); 30 | } 31 | } 32 | 33 | /** 34 | * Render the component. 35 | * @returns The node to render. 36 | */ 37 | public render(): ReactNode { 38 | return ( 39 |
40 |
41 | {this.props.tabs.map(l => ( 42 | 60 | ))} 61 |
62 | {this.props.children?.flat().map((c, idx) => ( 63 | 64 | {React.isValidElement(c) && c.props["data-label"] && 65 | c.props["data-label"].toLowerCase() === this.state.activeTab && c} 66 | 67 | ))} 68 |
69 | ); 70 | } 71 | } 72 | 73 | export default TabPanel; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-dashboard", 3 | "description": "Dashboard for Nodes", 4 | "version": "2.0.0-rc.1", 5 | "author": "Martyn Janes ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/iotaledger/node-dashboard" 9 | }, 10 | "homepage": "/dashboard", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@iota/crypto.js": "^1.8.6", 14 | "@iota/util.js": "^1.8.6", 15 | "classnames": "^2.3.1", 16 | "humanize-duration": "^3.27.3", 17 | "moment": "^2.29.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-router-dom": "^5.2.0", 21 | "vivagraphjs": "^0.12.0" 22 | }, 23 | "engines": { 24 | "node": ">=14 <=16.16" 25 | }, 26 | "scripts": { 27 | "start": "cross-env PORT=9090 REACT_APP_BRAND_ID=iota-core craco start", 28 | "lint": "eslint src --fix --ext .tsx,.ts", 29 | "sass-lint": "stylelint ./src/**/*.scss --custom-syntax postcss-scss", 30 | "build-internal": "craco build", 31 | "build": "cross-env REACT_APP_BRAND_ID=iota-core run-s lint sass-lint build-internal", 32 | "test": "craco test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ], 41 | "devDependencies": { 42 | "@craco/craco": "^7.0.0-alpha.7", 43 | "@types/classnames": "^2.3.1", 44 | "@types/humanize-duration": "^3.25.0", 45 | "@types/node": "^18.11.18", 46 | "@types/react": "^18.0.21", 47 | "@types/react-dom": "^18.0.6", 48 | "@types/react-helmet": "^6.1.1", 49 | "@types/react-router-dom": "^5.1.7", 50 | "@typescript-eslint/eslint-plugin": "^5.48.1", 51 | "@typescript-eslint/parser": "^5.48.1", 52 | "buffer": "^6.0.3", 53 | "cross-env": "^7.0.3", 54 | "eslint": "^8.31.0", 55 | "eslint-plugin-import": "^2.23.2", 56 | "eslint-plugin-jsdoc": "^39.3.3", 57 | "eslint-plugin-react": "^7.30.1", 58 | "eslint-plugin-unicorn": "^43.0.2", 59 | "npm-run-all": "^4.1.5", 60 | "postcss-scss": "^4.0.5", 61 | "react-scripts": "^5.0.1", 62 | "sass": "^1.55.0", 63 | "stylelint": "^14.12.1", 64 | "stylelint-config-property-sort-order-smacss": "^9.0.0", 65 | "stylelint-config-recommended": "^9.0.0", 66 | "stylelint-config-recommended-scss": "7.0.0", 67 | "stylelint-config-sass-guidelines": "^9.0.1", 68 | "stylelint-config-standard": "^28.0.0", 69 | "stylelint-scss": "^4.0.0", 70 | "typescript": "^4.9.4" 71 | }, 72 | "overrides": { 73 | "@svgr/webpack": "^6.3.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/routes/Peer.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .peer { 6 | display: flex; 7 | flex: 1; 8 | justify-content: center; 9 | padding: 60px; 10 | 11 | @include desktop-down { 12 | padding: $spacing-small; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | max-width: $content-width-desktop; 18 | 19 | .peer--icon-button { 20 | border: 0; 21 | outline: 0; 22 | background: none; 23 | color: var(--text-color-secondary); 24 | cursor: pointer; 25 | 26 | &:hover { 27 | color: var(--accent-primary); 28 | } 29 | } 30 | 31 | .banner { 32 | .node-info { 33 | display: flex; 34 | flex: 1; 35 | flex-direction: column; 36 | justify-content: space-between; 37 | padding: $spacing-small; 38 | 39 | .block-button .block-button-btn svg { 40 | width: 16px; 41 | height: 16px; 42 | } 43 | } 44 | 45 | .health-indicators { 46 | justify-content: space-around; 47 | border-left: 1px solid var(--panel-border); 48 | 49 | .child { 50 | padding: 0 $spacing-small; 51 | } 52 | 53 | @include tablet-down-only { 54 | justify-content: space-between; 55 | margin: 0 $spacing-small; 56 | 57 | .child { 58 | padding: $spacing-small 0; 59 | } 60 | } 61 | 62 | @include phone-down { 63 | .child { 64 | padding: $spacing-tiny $spacing-small; 65 | } 66 | } 67 | } 68 | } 69 | 70 | .info { 71 | @include tablet-down { 72 | flex-direction: column; 73 | } 74 | 75 | .info-panel + .info-panel { 76 | margin-left: $spacing-small; 77 | 78 | @include tablet-down { 79 | margin-top: $spacing-small; 80 | margin-left: 0; 81 | } 82 | } 83 | } 84 | 85 | .blocks-graph-panel { 86 | .graph { 87 | padding: 24px; 88 | } 89 | } 90 | 91 | .gossip { 92 | padding: $spacing-small 0 0 $spacing-small; 93 | 94 | .gossip-item { 95 | width: 155px; 96 | height: 75px; 97 | margin-right: $spacing-small; 98 | 99 | @include desktop-down { 100 | width: 160px; 101 | } 102 | 103 | h4 { 104 | min-height: 30px; 105 | } 106 | 107 | .gossip-value { 108 | @include font-size(24px); 109 | 110 | color: var(--text-color-primary); 111 | font-family: $font-sans; 112 | font-weight: bold; 113 | white-space: nowrap; 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Found a bug? 3 | description: Fill in this form to report it, and help us improve 4 | title: '[Bug]: ' 5 | labels: type:bug report 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: '## Reporting a bug' 11 | - type: markdown 12 | attributes: 13 | value: | 14 | Thank you for helping us make node-dashboard better, by reporting a bug you have found. This issue may already be reported! Please join our [discord](https://discord.iota.org/) for help. 15 | 16 | - type: textarea 17 | id: description 18 | attributes: 19 | label: Issue description 20 | description: Briefly describe the issue. 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: expected_behaviour 26 | attributes: 27 | label: Expected behaviour 28 | description: A concise description of what you expected to happen. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: actual_behaviour 34 | attributes: 35 | label: Actual behaviour 36 | description: A concise description of what actually happened. 37 | validations: 38 | required: true 39 | 40 | - type: dropdown 41 | id: can_repro 42 | attributes: 43 | label: Can the issue reliably be reproduced? 44 | options: 45 | - 'Yes' 46 | - 'No' 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: repro_steps 52 | attributes: 53 | label: Steps to reproduce the issue 54 | description: Explain how the maintainer can reproduce the issue. 55 | placeholder: | 56 | 1. 57 | 2. 58 | 3. 59 | ... 60 | 61 | - type: textarea 62 | id: errors 63 | attributes: 64 | label: Errors 65 | description: Paste any errors (or screenshots). 66 | render: shell 67 | 68 | - type: checkboxes 69 | id: duplicate_declaration 70 | attributes: 71 | label: Duplicate declaration 72 | description: Please confirm that you are not creating a duplicate issue. 73 | options: 74 | - label: I have searched the issues tracker for this issue and there is none 75 | required: true 76 | 77 | - type: checkboxes 78 | id: terms 79 | attributes: 80 | label: Code of Conduct 81 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/iotaledger/node-dashboard/blob/main/.github/CODE_OF_CONDUCT.md). 82 | options: 83 | - label: I agree to follow this project's Code of Conduct 84 | required: true 85 | -------------------------------------------------------------------------------- /src/assets/pruning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/routes/Peers.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .peers { 6 | display: flex; 7 | flex: 1; 8 | justify-content: center; 9 | padding: 60px; 10 | 11 | @include desktop-down { 12 | padding: $spacing-small; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | max-width: $content-width-desktop; 18 | 19 | .peers--icon-button { 20 | margin-right: $spacing-small; 21 | border: 0; 22 | outline: 0; 23 | background: none; 24 | color: var(--text-color-secondary); 25 | cursor: pointer; 26 | 27 | &:hover { 28 | color: var(--accent-primary); 29 | } 30 | } 31 | 32 | .add-button { 33 | @include font-size(14px); 34 | 35 | display: flex; 36 | flex-direction: row; 37 | align-items: center; 38 | padding: 6px 12px; 39 | border: 1px solid var(--text-color-secondary); 40 | border-radius: $form-input-radius; 41 | outline: 0; 42 | background: var(--bar-color-2); 43 | color: var(--text-color-primary); 44 | font-family: $font-sans; 45 | cursor: pointer; 46 | 47 | &:hover { 48 | color: var(--accent-primary); 49 | } 50 | 51 | &:focus { 52 | box-shadow: 0 0 3px 0 var(--accent-primary); 53 | } 54 | } 55 | 56 | .peers-panel { 57 | display: flex; 58 | flex-direction: row; 59 | flex-wrap: wrap; 60 | justify-content: space-between; 61 | 62 | @media (max-width: 1340px) { 63 | flex-direction: column; 64 | flex-wrap: nowrap; 65 | justify-content: flex-start; 66 | } 67 | 68 | .peers-panel--item { 69 | width: calc($content-width-desktop / 2 - $spacing-small / 2); 70 | margin-top: $spacing-small; 71 | overflow: hidden; 72 | 73 | @media (max-width: 1340px) { 74 | width: 100%; 75 | } 76 | 77 | .card { 78 | padding: $spacing-large; 79 | 80 | .peer-health { 81 | width: 16px; 82 | height: 16px; 83 | margin-right: $spacing-small; 84 | } 85 | 86 | .peer-id { 87 | @include font-size(14px); 88 | 89 | @media (min-width: 1340px) { 90 | @include font-size(12px); 91 | } 92 | 93 | display: flex; 94 | flex-direction: column; 95 | color: var(--text-color-primary); 96 | font-family: $font-sans; 97 | font-weight: 500; 98 | } 99 | 100 | .graph { 101 | margin-top: 24px; 102 | } 103 | 104 | .peer-actions { 105 | justify-content: space-between; 106 | margin-top: $spacing-small; 107 | 108 | @media (max-width: 720px) { 109 | flex-direction: column; 110 | 111 | .card--action { 112 | justify-content: center; 113 | } 114 | 115 | p { 116 | display: flex; 117 | justify-content: center; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/routes/Visualizer.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/card'; 2 | @import '../../scss/fonts'; 3 | @import '../../scss/media-queries'; 4 | 5 | .visualizer { 6 | display: flex; 7 | position: relative; 8 | flex: 1; 9 | height: 100%; 10 | overflow: hidden; 11 | 12 | .canvas { 13 | position: absolute; 14 | z-index: 0; 15 | top: 0; 16 | right: 0; 17 | bottom: 0; 18 | left: 0; 19 | } 20 | 21 | .action-panel-container { 22 | display: flex; 23 | position: absolute; 24 | z-index: 2; 25 | top: 30px; 26 | right: 30px; 27 | } 28 | 29 | .stats-panel-container { 30 | display: flex; 31 | position: absolute; 32 | z-index: 1; 33 | top: 0; 34 | right: 30px; 35 | bottom: 0; 36 | align-items: center; 37 | justify-content: center; 38 | pointer-events: none; 39 | 40 | .stats-panel { 41 | .card--value, 42 | .card--label { 43 | text-align: right; 44 | } 45 | } 46 | } 47 | 48 | .key-panel-container { 49 | display: flex; 50 | position: absolute; 51 | z-index: 1; 52 | right: 30px; 53 | bottom: 30px; 54 | left: 30px; 55 | justify-content: center; 56 | pointer-events: none; 57 | 58 | .key-panel { 59 | display: flex; 60 | flex-direction: row; 61 | flex-wrap: wrap; 62 | padding: $spacing-small; 63 | 64 | .key-panel-item { 65 | display: flex; 66 | flex-direction: row; 67 | align-items: center; 68 | margin: 0 $spacing-small; 69 | 70 | @include desktop-down { 71 | width: 110px; 72 | margin: 0; 73 | } 74 | 75 | .key-marker { 76 | width: 12px; 77 | height: 12px; 78 | margin-right: $spacing-tiny; 79 | border-radius: 3px; 80 | } 81 | 82 | .key-label { 83 | @include font-size(14px); 84 | 85 | color: var(--text-color-secondary); 86 | font-family: $font-sans; 87 | font-weight: 500; 88 | } 89 | } 90 | } 91 | } 92 | 93 | .info-panel-container { 94 | display: flex; 95 | position: absolute; 96 | z-index: 2; 97 | top: 30px; 98 | left: 30px; 99 | width: 320px; 100 | 101 | @include phone-down { 102 | top: 10px; 103 | left: 10px; 104 | width: 90%; 105 | } 106 | 107 | .info-panel--key { 108 | width: 16px; 109 | height: 16px; 110 | margin-right: $spacing-tiny; 111 | border-radius: 3px; 112 | } 113 | } 114 | 115 | .vertex-state--unknown { 116 | background-color: #9aadce; 117 | } 118 | 119 | .vertex-state--pending { 120 | background-color: #ec9a1e; 121 | } 122 | 123 | .vertex-state--accepted { 124 | background-color: #f5f24f; 125 | } 126 | 127 | .vertex-state--confirmed { 128 | background-color: #5cfaff; 129 | } 130 | 131 | .vertex-state--finalized { 132 | background-color: #61e884; 133 | } 134 | 135 | .vertex-state--transaction { 136 | background-color: #c061e8; 137 | } 138 | 139 | .vertex-state--validation { 140 | background-color: #2260e7; 141 | } 142 | 143 | .vertex-state--tip { 144 | background-color: #d92121; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/app/components/layout/InfoPanel.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/fonts'; 2 | @import '../../../scss/media-queries'; 3 | @import '../../../scss/variables'; 4 | 5 | .info-panel { 6 | display: flex; 7 | flex: 1; 8 | flex-direction: row; 9 | height: 100px; 10 | width: 345px; 11 | 12 | @include phone-down { 13 | height: 80px; 14 | } 15 | 16 | .info--labels { 17 | flex: 1; 18 | justify-content: center; 19 | } 20 | 21 | .value { 22 | @include font-size(24px); 23 | 24 | margin-top: $spacing-tiny; 25 | overflow: hidden; 26 | color: var(--text-color-primary); 27 | font-family: $font-sans; 28 | font-weight: bold; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | 32 | .value--small { 33 | @include font-size(14px); 34 | } 35 | 36 | .lmi { 37 | font-size: 14px; 38 | 39 | @include desktop-down { 40 | @include font-size(18px); 41 | } 42 | 43 | @include tablet-down { 44 | @include font-size(17px); 45 | } 46 | 47 | @include phone-down { 48 | @include font-size(16px); 49 | } 50 | } 51 | 52 | @include desktop-down { 53 | @include font-size(18px); 54 | } 55 | 56 | @include tablet-down { 57 | @include font-size(17px); 58 | } 59 | 60 | @include phone-down { 61 | @include font-size(16px); 62 | } 63 | } 64 | 65 | .icon-container { 66 | display: flex; 67 | position: relative; 68 | align-items: center; 69 | justify-content: center; 70 | width: 80px; 71 | height: 80px; 72 | margin-right: 20px; 73 | border-radius: $spacing-small; 74 | 75 | @include phone-down { 76 | width: 60px; 77 | height: 60px; 78 | } 79 | 80 | .icon-fill { 81 | svg { 82 | z-index: 1; 83 | flex: 1; 84 | } 85 | 86 | &.icon-fill--green { 87 | svg { 88 | path { 89 | fill: #16e1d5; 90 | } 91 | } 92 | } 93 | 94 | &.icon-fill--orange { 95 | svg { 96 | path { 97 | fill: #ff8b5c; 98 | } 99 | } 100 | } 101 | 102 | &.icon-fill--blue { 103 | svg { 104 | path { 105 | fill: #4baaff; 106 | } 107 | } 108 | } 109 | 110 | &.icon-fill--purple { 111 | svg { 112 | path { 113 | fill: #666af6; 114 | } 115 | } 116 | } 117 | 118 | &.icon-fill--grey { 119 | svg { 120 | path { 121 | fill: #d8d8d8; 122 | } 123 | } 124 | } 125 | } 126 | 127 | .icon-background { 128 | position: absolute; 129 | width: 80px; 130 | height: 80px; 131 | border-radius: $spacing-small; 132 | opacity: 0.1; 133 | 134 | @include phone-down { 135 | width: 60px; 136 | height: 60px; 137 | } 138 | 139 | &.icon-background--green { 140 | background-color: #16e1d5; 141 | } 142 | 143 | &.icon-background--orange { 144 | background-color: #ff8b5c; 145 | } 146 | 147 | &.icon-background--blue { 148 | background-color: #4baaff; 149 | } 150 | 151 | &.icon-background--purple { 152 | background-color: #666af6; 153 | } 154 | 155 | &.icon-background--grey { 156 | background-color: #d8d8d8; 157 | } 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-top-level-await */ 2 | import React from "react"; 3 | import { createRoot } from "react-dom/client"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import App from "./app/App"; 6 | import { ServiceFactory } from "./factories/serviceFactory"; 7 | import "./index.scss"; 8 | import { IBrandConfiguration } from "./models/IBrandConfiguration"; 9 | import { AuthService } from "./services/authService"; 10 | import { DashboardConfigService } from "./services/dashboardConfigService"; 11 | import { EventAggregator } from "./services/eventAggregator"; 12 | import { LocalStorageService } from "./services/localStorageService"; 13 | import { MetricsService } from "./services/metricsService"; 14 | import { NodeConfigService } from "./services/nodeConfigService"; 15 | import { SessionStorageService } from "./services/sessionStorageService"; 16 | import { SettingsService } from "./services/settingsService"; 17 | import { TangleService } from "./services/tangleService"; 18 | import { ThemeService } from "./services/themeService"; 19 | import { VisualizerService } from "./services/visualizerService"; 20 | import { WebSocketService } from "./services/webSocketService"; 21 | import { BrandHelper } from "./utils/brandHelper"; 22 | 23 | initServices() 24 | .then(brandConfiguration => { 25 | /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ 26 | const container = document.querySelector("#root")!; 27 | const root = createRoot(container); 28 | root.render( 29 | !brandConfiguration 30 | ? (
REACT_APP_BRAND_ID is not set
) 31 | : ( 32 | 33 | 34 | 35 | ) 36 | ); 37 | }) 38 | .catch(err => console.error(err)); 39 | 40 | /** 41 | * Initialise the services. 42 | * @returns The brand configuration. 43 | */ 44 | async function initServices(): Promise { 45 | ServiceFactory.register("local-storage", () => new LocalStorageService()); 46 | ServiceFactory.register("session-storage", () => new SessionStorageService()); 47 | const settingsService = new SettingsService(); 48 | ServiceFactory.register("settings", () => settingsService); 49 | 50 | const authService = new AuthService(); 51 | await authService.initialize(); 52 | ServiceFactory.register("auth", () => authService); 53 | 54 | const webSocketService = new WebSocketService(); 55 | ServiceFactory.register("web-socket", () => webSocketService); 56 | ServiceFactory.register("tangle", () => new TangleService()); 57 | 58 | const themeService = new ThemeService(); 59 | themeService.initialize(); 60 | ServiceFactory.register("theme", () => themeService); 61 | 62 | const dashboardConfigService = new DashboardConfigService(); 63 | await dashboardConfigService.initialize(); 64 | ServiceFactory.register("dashboard-config", () => dashboardConfigService); 65 | 66 | const nodeConfigService = new NodeConfigService(); 67 | await nodeConfigService.initialize(); 68 | ServiceFactory.register("node-config", () => nodeConfigService); 69 | 70 | const metricsService = new MetricsService(); 71 | ServiceFactory.register("metrics", () => metricsService); 72 | metricsService.initialize(); 73 | 74 | ServiceFactory.register("visualizer", () => new VisualizerService()); 75 | 76 | EventAggregator.subscribe("auth-state", "init", async () => { 77 | webSocketService.resubscribe(); 78 | }); 79 | 80 | EventAggregator.subscribe("online", "init", async o => { 81 | if (o) { 82 | await nodeConfigService.initialize(); 83 | webSocketService.resubscribe(); 84 | } 85 | }); 86 | 87 | settingsService.initialize(); 88 | 89 | return BrandHelper.initialize(); 90 | } 91 | -------------------------------------------------------------------------------- /src/scss/layout.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | @import "./media-queries"; 3 | 4 | .row { 5 | display: flex; 6 | flex-direction: row; 7 | 8 | &.start { 9 | justify-content: flex-start; 10 | } 11 | 12 | &.middle { 13 | align-items: center; 14 | } 15 | 16 | &.end { 17 | justify-content: flex-end; 18 | } 19 | 20 | &.bottom { 21 | align-items: flex-end; 22 | } 23 | 24 | &.inline { 25 | display: inline-flex; 26 | } 27 | 28 | &.spread { 29 | justify-content: space-between; 30 | } 31 | 32 | &.wrap { 33 | flex-wrap: wrap; 34 | } 35 | } 36 | 37 | .col { 38 | display: flex; 39 | flex-direction: column; 40 | } 41 | 42 | .col, 43 | .row { 44 | @include phone-down { 45 | &.phone-down-column { 46 | flex-direction: column; 47 | } 48 | 49 | &.start { 50 | align-items: flex-start; 51 | } 52 | } 53 | 54 | @include tablet-down { 55 | &.tablet-down-column { 56 | flex-direction: column; 57 | } 58 | 59 | &.start { 60 | align-items: flex-start; 61 | } 62 | } 63 | 64 | @include tablet-down-only { 65 | &.tablet-down-only-column { 66 | flex-direction: column; 67 | 68 | &.start { 69 | align-items: flex-start; 70 | } 71 | } 72 | } 73 | 74 | @include desktop-down { 75 | &.desktop-down-column { 76 | flex-direction: column; 77 | } 78 | } 79 | 80 | @include phone-down { 81 | &.phone-down-row { 82 | flex-direction: row; 83 | } 84 | } 85 | 86 | @include tablet-down { 87 | &.tablet-down-row { 88 | flex-direction: row; 89 | } 90 | } 91 | 92 | @include tablet-down-only { 93 | &.tablet-down-only-row { 94 | flex-direction: row; 95 | } 96 | } 97 | 98 | @include desktop-down { 99 | &.desktop-down-row { 100 | flex-direction: row; 101 | } 102 | } 103 | } 104 | 105 | .fill { 106 | flex: 1; 107 | } 108 | 109 | .margin-t-t { 110 | margin-top: $spacing-tiny; 111 | } 112 | 113 | .margin-t-s { 114 | margin-top: $spacing-small; 115 | } 116 | 117 | .margin-t-m { 118 | margin-top: $spacing-medium; 119 | } 120 | 121 | .margin-t-l { 122 | margin-top: $spacing-large; 123 | } 124 | 125 | .margin-b-t { 126 | margin-bottom: $spacing-tiny; 127 | } 128 | 129 | .margin-b-s { 130 | margin-bottom: $spacing-small; 131 | } 132 | 133 | .margin-b-m { 134 | margin-bottom: $spacing-medium; 135 | } 136 | 137 | .margin-b-l { 138 | margin-bottom: $spacing-large; 139 | } 140 | 141 | .margin-r-t { 142 | margin-right: $spacing-tiny; 143 | } 144 | 145 | .margin-r-s { 146 | margin-right: $spacing-small; 147 | } 148 | 149 | .margin-r-m { 150 | margin-right: $spacing-medium; 151 | } 152 | 153 | .margin-r-l { 154 | margin-right: $spacing-large; 155 | } 156 | 157 | .margin-l-t { 158 | margin-left: $spacing-tiny; 159 | } 160 | 161 | .margin-l-s { 162 | margin-left: $spacing-small; 163 | } 164 | 165 | .margin-l-m { 166 | margin-left: $spacing-medium; 167 | } 168 | 169 | .margin-l-l { 170 | margin-left: $spacing-large; 171 | } 172 | 173 | .padding-t { 174 | padding: $spacing-tiny; 175 | } 176 | 177 | .padding-s { 178 | padding: $spacing-small; 179 | } 180 | 181 | .padding-m { 182 | padding: $spacing-medium; 183 | } 184 | 185 | .padding-l { 186 | padding: $spacing-large; 187 | } 188 | 189 | .padding-t-m { 190 | padding-top: $spacing-medium; 191 | } 192 | 193 | .padding-b-m { 194 | padding-bottom: $spacing-medium; 195 | } 196 | 197 | .padding-l-m { 198 | padding-left: $spacing-medium; 199 | } 200 | 201 | .padding-r-m { 202 | padding-right: $spacing-medium; 203 | } 204 | 205 | .padding-t-s { 206 | padding-top: $spacing-small; 207 | } 208 | 209 | .padding-b-s { 210 | padding-bottom: $spacing-small; 211 | } 212 | 213 | .padding-l-s { 214 | padding-left: $spacing-small; 215 | } 216 | 217 | .padding-r-s { 218 | padding-right: $spacing-small; 219 | } 220 | 221 | .padding-t-0 { 222 | padding-top: 0; 223 | } 224 | 225 | .padding-b-0 { 226 | padding-bottom: 0; 227 | } 228 | 229 | .padding-l-0 { 230 | padding-left: 0; 231 | } 232 | 233 | .padding-r-0 { 234 | padding-right: 0; 235 | } 236 | 237 | .padding-0 { 238 | padding: 0 !important; 239 | } 240 | --------------------------------------------------------------------------------