;
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 | You need to enable JavaScript to run this app.
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 | this.setState({ value: !this.state.value },
49 | () => {
50 | this.props.onChanged(this.state.value);
51 | })}
52 | >
53 |
54 |
55 |
56 |
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 | this.activate()}
39 | >
40 | {this.props.buttonType === "copy" && (
41 |
42 | )}
43 |
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 |
5 |
6 |
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 |
this.setState({ isOpen: !this.state.isOpen })}
70 | >
71 |
72 |
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 |
this.setState(
50 | { activeTab: l.toLowerCase() },
51 | () => {
52 | if (this.props.onTabChanged) {
53 | this.props.onTabChanged(this.state.activeTab);
54 | }
55 | })}
56 | >
57 | {l}
58 |
59 |
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 |
--------------------------------------------------------------------------------