Network Explorer (#881)

* fixed styled component

* dynamic colour for isSelected

* corrected type names

* wrapped nav for routing and positioning of main section and dynamic colour for selected section

* overview info panes added

* quick refactor break out components separately

* WorldMap implemented but not data

* map changed and updated to shades correctly

* live data in cards

* added any types for react simple map

* nested routing added but needs tidying and types refactored

* added tooltip to worldmap

* worldmap killed unused props

* updated MUI version to stable v5

* dark mode and ContentCard refactor complete

* refactor of DarkMode context and API into class and context setup

* context refactor for multiple APIs at top level

* mui typography used for error msging instead of jsx/html

* added typeDefs for node api types

* small changes to sx styling

* added types for api responses and main context

* promiseAll for better error handling of individual async calls

* switch out to live API for country nodes

* removal of unnecessary type any and shortening sx style block

* routing and basic mixnodes table and linking

* fixed TS error handling and ts exclude files

* refactor of class API fetch reqs

* renaming to more appropriate explorer-api

* broken - passing to Fouad

* fix for types in context main

* mixnode detail page

* rebasing back before fetch mixnode by ID was implemented

* added basic cache for huge dump of mixnode data

* broken mixnodes context

* fixed mixnode detail fetch

* added hardcoded BondBreakdown section

* added 2 col table for detail page and small refactor of ApiState type for consistent use throughout app

* basic chart with basic dark/theme implemented - no live data

* added scrollToTop useRef for Detail page

* tidied grid items

* media qry for smaller screens

* small changes

* added live data to bonds breakdown 1/2

* small changes/tweaks

* Bondbreakdown retrieves live data

* mixnode stats using live data

* added node status live data

* uptime story added with live data

* date formatting added

* mixnode map

* error handling for mixnode stats

* error handling for port stats

* improved error handling for table - unfinished

* error handling for mixnode table

* handle Loading state for 2colSmallTable

* Uptime story loading handling

* set up data grid component

* remove mixnode value check as handled inside MixnodesDataGrid component

* use loading prop in data grid component

* undo unintentional code formatting

* map blur and linkable data-grid added

* getting ready for gateways and removing con logs

* quickfix for map blur

* PR comment changes

* refactored data grid for reuseability

* Link to open Big Dipper for Blocks

* passing element to title instead of string for routing to Big Dipper

* quick fix for element passed as title for contentCard

* fix for colour coding nodes

* nuts and bolts of search and results per page are working

* media query for responsive search and no-per-page toolbar

* broke out search and pagesize to separate toolbar

* fix for going back to mixnodes datagrid and refetching

* corrected typings for WorldMap

* removed API for topojson

* Cleaner implementation of formatting inline for datagrid

* added Type to Datagrid Rows for mixnode

* removed optional from type for Datagrid

* added page listing the Gateway nodes

* adding clickable location to handleSearch

* tidying util functions and removing dead useEffect

* Add missing constant

* Validators link to Big Dipper

* added validators link to side nav as per Issues card

* SVG icons

* PR tweak to move logic to routes

* removed dead code post rebase

* fixed light dark mode for DataGrid

* light dark mode works on SVGs in Nav

* moving logic back to Nav to avoid window object issues

* neater ternary for SVG icons dark mode

* Better Linking/Styling for cells

* corrected prop/attr name in svg to Reactify it

* moved api url to constants

* SVGs dark now governed by context not props so reverting renderIcon method back to key value setup

* percentage for bond total added

* SVGs for Overview cards Mixnodes Gateways and Validators

* decimalised formatted punks and % of bond for BondBreakdown card

* number formatting via validator module

* adding cossmjs math pkg

* unfinished refactor BondBreakdown

* first few ui tweaks

* Adding google font Open Sans as per designs

* DataGrid unstylable in theme so nuking in css

* adding theming to Block Height card not hardcoded colours

* DataGrid styling

* Nav styling colours but without hover fix

* theme for bond breakdown

* killing con logs

* Datagrid styling

* Nav bar working

* added lines to nav

* removed cursive from fallback fonts

* trimming and refactoring

* removed dead isActive code from nav options

* Color correction for theme on 2col table

* Moved cell styles out to UniversalDatagrid for reuseability

* Nav colors moved to theme

* Removing comments and dead code

* DataGrid UI improvements

* theming for Overview content card

* Bonds updated from UPUNK to PUNK

* corrected SVG warning on stroke-width

* added Boolean class instead of ternary

* fixing up svg attr to jsx props

* merging UPUNK changes into ui-tweaks

* corrected SVG warning on stroke-width

* added Boolean class instead of ternary

* last instance of Boolean

* BondBreakdown handles 0 delegations

* formatting for webpack config and svgs

* Add `npm run lint` and `npm run lint:fix` targets to `package.json`

* Allow `.vscode` directories - exclude them individually like has been done already in the `.gitignore` directory

* Add `vscode` action to run `eslint` on save for the `/explorer/**` sub-directory

* eslint auto fix

* Fix some easy eslint issues

* removing grid pipes and pastel map colors

* Grid xl lg values to align with Search Toolbar

* GitHub Actions: do not trigger Rust actions when the paths are only `/explorer/**`

See https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for details.

* GitHub Actions: run eslint and annotate pull requests

* socials added to Appbar and Footer

* smaller darkmode icon for mobile

* cleaner code for nav dark light selector

* almost all lint fixes

* post linting Nav fix

* killed con log and removed unused dep

* ref type and removed 1x ts ignore from worldMap

* disabl nested tern w/ nav is refactored on diff br

* icons smaller put into mui List format

* Added hover effect to match DarkLight switch

* ts ignore for worldMap vs no ts decl

* parking changes

* Flipped to MUI SVGs

* re-added external links to Socials

* nav functionality working

* spacing on Mixnode detail page

* datagrid alignment & detail page spacing

* map but no datagrid yet

* killed old SVGs now using mui icons

* added palette instead of strings

* Mixnode Map page working still needs tidying

* better lg xl responsive on Overview and sanitized Page Titles

* removed typography from imports as unused now

* search, sort working & added LG XL responsiveness

* Routing root reqs direct to Overview

* basic 404 page and btn back to overview

* killed fragments and comments

* updated Bond total in column

* Change bond col to type number not text

* Added field to DataGrid and updated MixnodeToGridrow logic

* Added type number so sorting works properly.

* added %self to Detail page

* basic scroll working desktop

* delegations now popout and scroll according to designs

* added stickyHeader and killed dead code

* ExpandMore only renders if delegations exist

* killed old svg icons

* added theme to Overview SVGs

* bringing Title into other pages

* linting fix

* pagination and spacing of gateways cols

* linting fix

* style override for pagination

* added hamburger and changed appbar to fixed

* bringing in other lint fix to pass linting

* PR feedback changes

* Add README.md for theme customisation

* Add hook to get app state context

* Add Nym theme typings to MUI `Theme` types

* Use new theme provider

* Fixing up components to use theme typings
Updated Overview Footer and ContentCard
Footer and Nav socials
Title, Nav chevron and Nav SVGs
Overview SVGs
Light Dark switch
BondBreakdown and 2 col table
DataGrid and 2col Tables
WorldMap UptimeChart and theme changes
WorldMap colors
merge changes
added StatsCard for overview

* Bug fix: do no close drawer when clicking on mixnodes or gateways

* Theme primary colour set to orange highlight, so that default/primary actions are clear to the user. This fixes colours on the pagination page list.

* Fixing up map projection

* Map view uses stroke colour from theme

* added useTheme from correct pkg

* react types upgrade to kill SXProps issue

* SXProps fix removing dead mixins from fixed AppBar

* Scale of Map changed to see more countries

* return type for main Context required

* Fixed map so more countries show

* type for useMainContext hook added

* Remove unused file

* Tidy up imports

* Remove use of `any` by using strongly typed hook to get the app state

* Remove module declaration so that @types/react-simple-maps is used

* API map response changed to indexed object

* Map view uses correct typings from `@types/react-simple-maps` and `d3-scale`

* Make content responsive and fills the view when screen widest

* Link network explorer in title to overview page

* Increase size of card headers to differentiate

* Fix column widths

* Fixed icons showing incorrectly the stats card in mixnode detail view

* Set default sort on mixnodes and gateways to be `bond` descending.

There is an error in MUI data-grid that does not adjust the sort caret based on initial `sortModel` value. Needs investigation.

* GitHub Action for deployment: prefix with network explorer to stop collisions with other deployment projects

* Mixnode list: fix up header title for `host`

* Fix up notification URLs and tidy up readme

* Fix up license information

Co-authored-by: Adrian Thompson <adrian@nymtech.net>
Co-authored-by: Adrian Thompson <adrianthompson@Adrians-MacBook-Air.local>
Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
Co-authored-by: Aid19801 <adrianThompson19801@gmail.com>
This commit is contained in:
Mark Sinclair
2021-11-08 12:23:27 +00:00
committed by GitHub
parent 6ad5badef4
commit 5dadd73dfd
84 changed files with 74740 additions and 8 deletions
+7 -1
View File
@@ -1,6 +1,12 @@
name: Continuous integration
on: [push, pull_request]
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
build:
+4 -1
View File
@@ -1,6 +1,9 @@
name: Clippy check
on: push
on:
push:
paths-ignore:
- 'explorer/**'
jobs:
clippy_check:
+7 -1
View File
@@ -1,6 +1,12 @@
name: Mixnet Contract
on: [push, pull_request]
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
mixnet-contract:
@@ -0,0 +1,23 @@
name: Linting for Network Explorer (eslint/prettier)
on:
pull_request:
paths:
- 'explorer/**'
defaults:
run:
working-directory: explorer
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
- name: Run ESLint
# GitHub should automatically annotate the PR
run: npm run lint
+2 -1
View File
@@ -35,7 +35,7 @@ jobs:
SOURCE: "explorer/dist/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/${{ env.GITHUB_REF_SLUG }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Keybase - Node Install
run: npm install
@@ -44,6 +44,7 @@ jobs:
env:
NYM_PROJECT_NAME: "Network Explorer"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
@@ -1,5 +1,5 @@
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.GITHUB_REF_SLUG }}.{{ env.NYM_CI_WWW_BASE }}/
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
> ✅ **SUCCESS**
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+7 -1
View File
@@ -1,6 +1,12 @@
name: Generate TS types
on: push
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
tauri-wallet-types:
+7 -1
View File
@@ -1,6 +1,12 @@
name: Wasm Client
on: [push, pull_request]
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
wasm:
-1
View File
@@ -11,7 +11,6 @@ target
/.vscode/settings.json
validator/.vscode
sample-configs/validator-config.toml
.vscode
scripts/deploy_qa.sh
scripts/run_gate.sh
scripts/run_mix.sh
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
+18
View File
@@ -0,0 +1,18 @@
# See http://editorconfig.org/
# EditorConfig helps developers define and maintain consistent coding styles
# between different editors and IDEs. The EditorConfig project consists of a
# file format for defining coding styles and a collection of text editor plugins
# that enable editors to read the file format and adhere to defined styles.
# EditorConfig files are easily readable and they work nicely with version
# control systems.
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
+103
View File
@@ -0,0 +1,103 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2019,
"sourceType": "module"
},
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
"extends": ["plugin:react/recommended", "airbnb", "prettier", "plugin:jest/recommended", "plugin:jest/style"],
"rules": {
"jest/prefer-strict-equal": "error",
"jest/prefer-to-have-length": "warn",
"prettier/prettier": "error",
"import/prefer-default-export": "off",
"react/prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-props-no-spreading": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.[jt]s",
"**/*.spec.[jt]s",
"**/*.test.[jt]sx",
"**/*.spec.[jt]sx"
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
]
},
"overrides": [
{
"files": "**/*.+(ts|tsx)",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint/eslint-plugin"],
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-use-before-define": [0],
"@typescript-eslint/no-use-before-define": [1],
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx"
]
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
}
}
],
"settings": {
"import/resolver": {
"root-import": {
"rootPathPrefix": "@",
"rootPathSuffix": "src",
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
dist
.DS_Store
detailAPI.md
+1
View File
@@ -0,0 +1 @@
14
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
+50
View File
@@ -0,0 +1,50 @@
# Nym Network Explorer
The network explorer lets you explore the Nym network.
## Getting started
You will need:
- NodeJS (use `nvm install` to automatically install the correct version)
- `npm`
From the `explorer` directory of the `nym` monorepo, run:
```
npm install
npm run start
```
You can then open a browser to http://localhost:3000 and start development.
## Development
Documentation for developers [can be found here](./docs).
## Deployment
Build the UI with:
```
npm install
npm run build
```
The output will be in the `dist` directory. Serve this with `nginx` or `httpd`.
Make sure you have built the [explorer-api](./explorer-api) and are running it as a service proxied through
`nginx` or `httpd` so that both the UI and API are running on the same host.
## License
Please see the [project README](./README.md) and the [LICENSES directory](../LICENSES) for license details for all Nym software.
## Contributing
If you would like to contribute to the Network Explorer send us a PR or
[raise an issue on GitHub](https://github.com/nymtech/nym/issues) and tag them with `network-explorer`.
## Development
Please see [development docs](./docs) here for more information on the structure and design of this app.
+46
View File
@@ -0,0 +1,46 @@
# Nym Network Explorer - Development Docs
## Getting started
You will need:
- NodeJS
- `nvm`
We use the following:
- Typescript
- `eslint`
- `webpack`
- `jest`
- `react-material-ui`
- `react` 17
## Development mode
Run the following:
```
npm install
npm run start
```
A development server with hot reloading will be running on http://localhost:3000.
## Linting
`eslint` and `prettier` are configured.
You can lint the code by running:
```
npm run lint
```
> **Note:** this will only show linting errors and will not fix them
To fix all linting errors automatically run:
```
npm run lint:fix
```
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+30149
View File
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
{
"name": "@nym/network-explorer",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"dependencies": {
"@cosmjs/math": "^0.26.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.0",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@mui/system": "^5.0.1",
"@mui/x-data-grid": "^5.0.0-beta.2",
"@nymproject/nym-validator-client": "^0.18.0",
"d3-scale": "^4.0.0",
"date-fns": "^2.24.0",
"i18n-iso-countries": "^6.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-google-charts": "^3.0.15",
"react-router-dom": "^5.3.0",
"react-simple-maps": "^2.3.0",
"react-tooltip": "^4.2.21"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/d3-fetch": "^3.0.1",
"@types/d3-geo": "^3.0.2",
"@types/d3-scale": "^4.0.1",
"@types/geojson": "^7946.0.8",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"@types/react-simple-maps": "^1.0.6",
"@types/react-tooltip": "^4.2.4",
"@types/topojson-client": "^3.1.0",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"babel-plugin-root-import": "^6.6.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"eslint": "7.32.0",
"eslint-config-airbnb": "18.2.1",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-root-import": "1.0.4",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.25.1",
"eslint-plugin-react-hooks": "4.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"html-webpack-plugin": "^5.3.2",
"imports-loader": "^3.0.0",
"jest": "^27.1.0",
"loader-utils": "^2.0.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "2.3.2",
"react-refresh": "^0.10.0",
"react-refresh-typescript": "^2.0.2",
"style-loader": "^3.2.1",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.4.2",
"webpack": "^5.52.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0",
"webpack-favicons": "^1.0.7",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.6.0"
},
"scripts": {
"start": "webpack serve --progress --port 3000",
"build": "webpack build --progress --config webpack.prod.js",
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
+9
View File
@@ -0,0 +1,9 @@
import * as React from 'react';
import { Nav } from './components/Nav';
import { Routes } from './routes/index';
export const App: React.FC = () => (
<Nav>
<Routes />
</Nav>
);
+24
View File
@@ -0,0 +1,24 @@
// master APIs
export const MASTER_URL = 'https://testnet-milhon-explorer.nymtech.net';
export const MASTER_VALIDATOR_URL =
'https://testnet-milhon-validator1.nymtech.net';
export const BIG_DIPPER = 'https://testnet-milhon-blocks.nymtech.net';
// specific API routes
export const MIXNODE_PING = `${MASTER_URL}/api/ping`;
export const MIXNODES_API = `${MASTER_URL}/api/mix-node`;
export const GATEWAYS_API = `${MASTER_VALIDATOR_URL}/api/v1/gateways`;
export const VALIDATORS_API = `${MASTER_VALIDATOR_URL}/validators`;
export const BLOCK_API = `${MASTER_VALIDATOR_URL}/block`;
export const COUNTRY_DATA_API = `${MASTER_URL}/api/countries`;
export const UPTIME_STORY_API = `${MASTER_VALIDATOR_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
// errors
export const MIXNODE_API_ERROR =
"We're having trouble finding that record, please try again or Contact Us.";
// socials
export const TELEGRAM_LINK = 'https://t.me/nymchan';
export const TWITTER_LINK = 'https://twitter.com/nymproject';
export const GITHUB_LINK = 'https://github.com/nymtech';
export const NYM_WEBSITE = 'https://nymtech.net';
+102
View File
@@ -0,0 +1,102 @@
import {
GATEWAYS_API,
MIXNODES_API,
VALIDATORS_API,
BLOCK_API,
COUNTRY_DATA_API,
MIXNODE_PING,
UPTIME_STORY_API,
} from './constants';
import {
MixNodeResponse,
GatewayResponse,
ValidatorsResponse,
CountryDataResponse,
MixNodeResponseItem,
DelegationsResponse,
StatsResponse,
StatusResponse,
UptimeStoryResponse,
} from '../typeDefs/explorer-api';
function getFromCache(key: string) {
const ts = Number(localStorage.getItem('ts'));
const hasExpired = Date.now() - ts > 200000;
const curr = localStorage.getItem(key);
if (curr && !hasExpired) {
return JSON.parse(curr);
}
return undefined;
}
function storeInCache(key: string, data: any) {
localStorage.setItem(key, data);
}
export class Api {
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
const cachedMixnodes = getFromCache('mixnodes');
if (cachedMixnodes) {
return cachedMixnodes;
}
const res = await fetch(MIXNODES_API);
const json = await res.json();
storeInCache('mixnodes', JSON.stringify(json));
storeInCache('ts', Date.now());
return json;
};
static fetchMixnodeByID = async (
id: string,
): Promise<MixNodeResponseItem | undefined> => {
const allMixnodes: MixNodeResponse = await Api.fetchMixnodes();
const matchedByID = allMixnodes.filter(
(eachRecord) => eachRecord.mix_node.identity_key === id,
);
return (matchedByID.length && matchedByID[0]) || undefined;
};
static fetchGateways = async (): Promise<GatewayResponse> => {
const res = await fetch(GATEWAYS_API);
return res.json();
};
static fetchValidators = async (): Promise<ValidatorsResponse> => {
const res = await fetch(VALIDATORS_API);
const json = await res.json();
return json.result;
};
static fetchBlock = async (): Promise<number> => {
const res = await fetch(BLOCK_API);
const json = await res.json();
const { height } = json.result.block.header;
return height;
};
static fetchCountryData = async (): Promise<CountryDataResponse> => {
const result: CountryDataResponse = {};
const res = await fetch(COUNTRY_DATA_API);
const json = await res.json();
Object.keys(json).forEach((ISO3) => {
result[ISO3] = { ISO3, nodes: json[ISO3] };
});
return result;
};
static fetchDelegationsById = async (
id: string,
): Promise<DelegationsResponse> =>
(await fetch(`${MIXNODES_API}/${id}/delegations`)).json();
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
(await fetch(`${MIXNODES_API}/${id}/stats`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> =>
(await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (
id: string,
): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
}
File diff suppressed because it is too large Load Diff
+217
View File
@@ -0,0 +1,217 @@
import * as React from 'react';
import { printableCoin } from '@nymproject/nym-validator-client';
import { Alert, CircularProgress, useMediaQuery, Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { useMainContext } from 'src/context/main';
import { ExpandMore } from '@mui/icons-material';
export const BondBreakdownTable: React.FC = () => {
const { mixnodeDetailInfo, delegations } = useMainContext();
const [allContentLoaded, setAllContentLoaded] =
React.useState<boolean>(false);
const [showError, setShowError] = React.useState<boolean>(false);
const [showDelegations, toggleShowDelegations] =
React.useState<boolean>(false);
const [bonds, setBonds] = React.useState({
delegations: '0',
pledges: '0',
bondsTotal: '0',
hasLoaded: false,
});
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
React.useEffect(() => {
if (mixnodeDetailInfo && mixnodeDetailInfo.data?.length) {
const thisMixnode = mixnodeDetailInfo?.data[0];
// delegations
const decimalisedDelegations = printableCoin({
amount: thisMixnode.total_delegation.amount.toString(),
denom: thisMixnode.total_delegation.denom,
});
// pledges
const decimalisedPledges = printableCoin({
amount: thisMixnode.bond_amount.amount.toString(),
denom: thisMixnode.bond_amount.denom,
});
// bonds total (del + pledges)
const pledgesSum = Number(thisMixnode.bond_amount.amount);
const delegationsSum = Number(thisMixnode.total_delegation.amount);
const bondsTotal = printableCoin({
amount: (delegationsSum + pledgesSum).toString(),
denom: 'upunk',
});
setBonds({
delegations: decimalisedDelegations,
pledges: decimalisedPledges,
bondsTotal,
hasLoaded: true,
});
}
}, [mixnodeDetailInfo]);
React.useEffect(() => {
const hasError = Boolean(mixnodeDetailInfo?.error || delegations?.error);
const hasAllMixnodeInfo = Boolean(
mixnodeDetailInfo?.data !== undefined &&
mixnodeDetailInfo?.data[0].mix_node,
);
const hasAllDelegationsInfo = Boolean(
delegations?.data !== undefined && delegations?.data,
);
const hasAllData = Boolean(
!hasError && hasAllMixnodeInfo && hasAllDelegationsInfo,
);
setShowError(hasError);
setAllContentLoaded(hasAllData);
}, [mixnodeDetailInfo, delegations]);
const expandDelegations = () => {
if (delegations?.data && delegations.data.length > 0) {
toggleShowDelegations(!showDelegations);
}
};
const calcBondPercentage = (num: number) => {
if (mixnodeDetailInfo?.data !== undefined && mixnodeDetailInfo?.data[0]) {
const rawDeligationAmount = Number(
mixnodeDetailInfo.data[0].total_delegation.amount,
);
const rawPledgeAmount = Number(
mixnodeDetailInfo.data[0].bond_amount.amount,
);
const rawTotalBondsAmount = rawDeligationAmount + rawPledgeAmount;
return ((num * 100) / rawTotalBondsAmount).toFixed(1);
}
return 0;
};
if (mixnodeDetailInfo?.isLoading) {
return <CircularProgress />;
}
if (showError) {
return (
<Alert severity="warning">
We are unable to retrieve a Mixnode with that ID. Please try later or
Contact Us.
</Alert>
);
}
if (!showError && allContentLoaded) {
return (
<>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
<TableBody>
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
<TableCell
sx={{
fontWeight: 'bold',
width: '150px',
}}
align="left"
>
Bond total
</TableCell>
<TableCell align="left">{bonds.bondsTotal}</TableCell>
</TableRow>
<TableRow>
<TableCell align="left">Pledge total</TableCell>
<TableCell align="left">{bonds.pledges}</TableCell>
</TableRow>
<TableRow>
<TableCell onClick={expandDelegations} align="left">
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
Delegation total {'\u00A0'}
{delegations?.data && delegations?.data?.length > 0 && (
<ExpandMore />
)}
</Box>
</TableCell>
<TableCell align="left">{bonds.delegations}</TableCell>
</TableRow>
</TableBody>
</Table>
{showDelegations && (
<Box
sx={{
maxHeight: 400,
overflowY: 'scroll',
}}
>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell
sx={{ fontWeight: 'bold', background: '#242C3D' }}
align="left"
>
Delegators
</TableCell>
<TableCell
sx={{ fontWeight: 'bold', background: '#242C3D' }}
align="left"
>
Stake
</TableCell>
<TableCell
sx={{
fontWeight: 'bold',
background: '#242C3D',
width: '200px',
}}
align="left"
>
Share from bond
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{delegations?.data?.map(
({ owner, amount: { amount, denom } }) => (
<TableRow key={owner}>
<TableCell
sx={matches ? { width: 190 } : null}
align="left"
>
{owner}
</TableCell>
<TableCell align="left">
{printableCoin({ amount: amount.toString(), denom })}
</TableCell>
<TableCell align="left">
{calcBondPercentage(amount)}%
</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
</Box>
)}
</TableContainer>
</>
);
}
return null;
};
@@ -0,0 +1,11 @@
import { Typography } from '@mui/material';
import * as React from 'react';
export const ComponentError: React.FC<{ text: string }> = ({ text }) => (
<Typography
sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }}
variant="body1"
>
{text}
</Typography>
);
+47
View File
@@ -0,0 +1,47 @@
import { Card, CardHeader, CardContent, Typography } from '@mui/material';
import React, { ReactEventHandler } from 'react';
type ContentCardProps = {
title?: string | React.ReactNode;
subtitle?: string;
Icon?: React.ReactNode;
Action?: React.ReactNode;
errorMsg?: string;
onClick?: ReactEventHandler;
};
export const ContentCard: React.FC<ContentCardProps> = ({
title,
Icon,
Action,
subtitle,
errorMsg,
children,
onClick,
}) => (
<Card onClick={onClick} sx={{ height: '100%' }}>
{title && (
<CardHeader
title={title || ''}
avatar={Icon}
action={Action}
subheader={subtitle}
/>
)}
{children && <CardContent>{children}</CardContent>}
{errorMsg && (
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
{errorMsg}
</Typography>
)}
</Card>
);
ContentCard.defaultProps = {
title: undefined,
subtitle: undefined,
Icon: null,
Action: null,
errorMsg: undefined,
onClick: () => null,
};
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Box, Typography } from '@mui/material';
import { ExpandLess, ExpandMore } from '@mui/icons-material';
export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
headingTitle,
}) => {
const [filter, toggleFilter] = React.useState<boolean>(false);
const handleClick = () => {
toggleFilter(!filter);
};
return (
<Box alignItems="center" display="flex" onClick={handleClick}>
<Typography
sx={{
fontWeight: 'bold',
fontSize: 14,
padding: 0,
}}
>
{headingTitle}&nbsp;
</Typography>
{filter ? <ExpandMore /> : <ExpandLess />}
</Box>
);
};
+49
View File
@@ -0,0 +1,49 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Socials } from './Socials';
export const Footer: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
height: 'auto',
mt: 3,
pt: 3,
pb: 3,
}}
>
{isMobile && (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
width: 'auto',
justifyContent: 'center',
alignItems: 'center',
mb: 2,
}}
>
<Socials isFooter />
</Box>
)}
<Typography
sx={{
fontSize: 12,
textAlign: isMobile ? 'center' : 'end',
color: theme.palette.nym.text.footer,
}}
>
© 2021 Nym Technologies SA, all rights reserved
</Typography>
</Box>
);
};
+397
View File
@@ -0,0 +1,397 @@
/* eslint-disable no-nested-ternary */
import * as React from 'react';
import { Link } from 'react-router-dom';
import { ChevronLeft, ExpandLess, ExpandMore, Menu } from '@mui/icons-material';
import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles';
import MuiLink from '@mui/material/Link';
import Box from '@mui/material/Box';
import ListItem from '@mui/material/ListItem';
import MuiDrawer from '@mui/material/Drawer';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
import { BIG_DIPPER, NYM_WEBSITE } from 'src/api/constants';
import { useMediaQuery } from '@mui/material';
import { OverviewSVG } from '../icons/OverviewSVG';
import { NetworkComponentsSVG } from '../icons/NetworksSVG';
import { NodemapSVG } from '../icons/NodemapSVG';
import { Socials } from './Socials';
import { Footer } from './Footer';
import { DarkLightSwitchDesktop, DarkLightSwitchMobile } from './Switch';
const drawerWidth = 300;
const openedMixin = (theme: Theme): CSSObject => ({
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
});
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: `calc(${theme.spacing(7)} + 1px)`,
});
const DrawerHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
height: 64,
}));
const Drawer = styled(MuiDrawer, {
shouldForwardProp: (prop) => prop !== 'open',
})(({ theme, open }) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme),
}),
}));
type navOptionType = {
id: number;
isActive?: boolean;
url: string;
title: string;
Icon?: React.ReactNode;
nested?: navOptionType[];
isExpandedChild?: boolean;
};
const originalNavOptions: navOptionType[] = [
{
id: 0,
isActive: false,
url: '/overview',
title: 'Overview',
Icon: <OverviewSVG />,
},
{
id: 1,
isActive: false,
url: '/network-components',
title: 'Network Components',
Icon: <NetworkComponentsSVG />,
nested: [
{
id: 3,
url: '/network-components/mixnodes',
title: 'Mixnodes',
},
{
id: 4,
url: '/network-components/gateways',
title: 'Gateways',
},
{
id: 5,
url: `${BIG_DIPPER}/validators`,
title: 'Validators',
},
],
},
{
id: 2,
isActive: false,
url: '/nodemap',
title: 'Nodemap',
Icon: <NodemapSVG />,
},
];
type ExpandableButtonType = {
id: number;
title: string;
url: string;
isActive?: boolean;
Icon?: React.ReactNode;
nested?: navOptionType[];
isChild?: boolean;
openDrawer: () => void;
closeDrawer: () => void;
drawIsOpen: boolean;
setToActive: (num: number) => void;
};
const ExpandableButton: React.FC<ExpandableButtonType> = ({
id,
url,
setToActive,
isActive,
openDrawer,
closeDrawer,
drawIsOpen,
Icon,
title,
nested,
isChild,
}) => {
const [dynamicStyle, setDynamicStyle] = React.useState({});
const [nestedOptions, toggleNestedOptions] = React.useState(false);
const [isExternal, setIsExternal] = React.useState<boolean>(false);
const { palette } = useTheme();
const handleClick = () => {
openDrawer();
if (title === 'Network Components' && nested) {
toggleNestedOptions(!nestedOptions);
}
setToActive(id);
};
React.useEffect(() => {
if (url) {
setIsExternal(url.includes('http'));
}
if (nested) {
setDynamicStyle({
background: '#242C3D',
borderRight: `3px solid ${palette.nym.highlight}`,
});
}
if (isChild) {
setDynamicStyle({
background: '#3C4558',
fontWeight: 800,
});
}
if (!nested && !isChild) {
setDynamicStyle({
background: '#242C3D',
borderRight: `3px solid ${palette.nym.highlight}`,
});
}
}, [url]);
React.useEffect(() => {
if (!drawIsOpen && nestedOptions) {
toggleNestedOptions(false);
}
}, [drawIsOpen]);
return (
<>
<ListItem
disablePadding
disableGutters
component={!nested ? Link : 'div'}
to={isExternal ? { pathname: url } : url}
target={isExternal ? '_blank' : ''}
sx={
isActive
? dynamicStyle
: { background: '#111826', borderRight: 'none' }
}
>
<ListItemButton
onClick={handleClick}
sx={{
pt: 2,
pb: 2,
background: isChild ? '#3C4558' : 'none',
}}
>
<ListItemIcon>{Icon}</ListItemIcon>
<ListItemText
primary={title}
sx={{
color: palette.nym.networkExplorer.nav.text,
}}
primaryTypographyProps={{
style: {
fontWeight: isActive ? 800 : 300,
},
}}
/>
{nested && nestedOptions && <ExpandLess />}
{nested && !nestedOptions && <ExpandMore />}
</ListItemButton>
</ListItem>
{nestedOptions &&
nested?.map((each) => (
<ExpandableButton
id={each.id}
url={each.url}
key={each.title}
title={each.title}
openDrawer={openDrawer}
drawIsOpen={drawIsOpen}
closeDrawer={closeDrawer}
setToActive={setToActive}
isChild
/>
))}
</>
);
};
ExpandableButton.defaultProps = {
Icon: null,
nested: undefined,
isChild: false,
isActive: false,
};
export const Nav: React.FC = ({ children }) => {
const [navOptionsState, updateNavOptionsState] =
React.useState(originalNavOptions);
const [open, setOpen] = React.useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const setToActive = (id: number) => {
const updated = navOptionsState.map((option) => ({
...option,
isActive: option.id === id,
}));
updateNavOptionsState(updated);
};
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<>
<Box sx={{ display: 'flex' }}>
<AppBar
sx={{
background: theme.palette.nym.networkExplorer.nav.background,
}}
>
<Toolbar
disableGutters
sx={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: 205,
}}
>
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
<NymLogoSVG />
</IconButton>
<Typography
variant="h6"
noWrap
sx={{
color: theme.palette.nym.networkExplorer.nav.text,
fontSize: '18px',
}}
>
<MuiLink
component={Link}
to="/overview"
underline="none"
color="inherit"
>
Network Explorer
</MuiLink>
</Typography>
</Box>
<Box
sx={{
mr: 2,
alignItems: 'center',
display: 'flex',
}}
>
{!isMobile && (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
width: 'auto',
pr: 0,
pl: 2,
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<Socials />
<DarkLightSwitchDesktop defaultChecked />
</Box>
)}
{isMobile && <DarkLightSwitchMobile />}
</Box>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
open={open}
sx={{
background: theme.palette.nym.networkExplorer.nav.background,
}}
>
<DrawerHeader
sx={{
justifyContent: open ? 'flex-end' : 'center',
paddingLeft: 0,
}}
>
<IconButton
onClick={open ? handleDrawerClose : handleDrawerOpen}
sx={{
padding: 0,
ml: '7px',
color: theme.palette.nym.networkExplorer.nav.text,
}}
>
{open ? <ChevronLeft /> : <Menu />}
</IconButton>
</DrawerHeader>
<List sx={{ pt: 0, pb: 0 }}>
{navOptionsState.map((props) => (
<ExpandableButton
setToActive={setToActive}
key={props.url}
openDrawer={handleDrawerOpen}
drawIsOpen={open}
closeDrawer={handleDrawerClose}
{...props}
/>
))}
</List>
</Drawer>
<Box sx={{ width: '100%', p: 4, mt: 7 }}>
{children}
<Footer />
</Box>
</Box>
</>
);
};
+31
View File
@@ -0,0 +1,31 @@
import * as React from 'react';
import { Box, IconButton } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import TelegramIcon from '@mui/icons-material/Telegram';
import GitHubIcon from '@mui/icons-material/GitHub';
import TwitterIcon from '@mui/icons-material/Twitter';
import { GITHUB_LINK, TELEGRAM_LINK, TWITTER_LINK } from 'src/api/constants';
export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
const theme = useTheme();
const color = isFooter
? theme.palette.nym.networkExplorer.footer.socialIcons
: theme.palette.nym.networkExplorer.topNav.socialIcons;
return (
<Box>
<IconButton component="a" href={TELEGRAM_LINK} target="_blank">
<TelegramIcon sx={{ color }} />
</IconButton>
<IconButton component="a" href={GITHUB_LINK} target="_blank">
<GitHubIcon sx={{ color }} />
</IconButton>
<IconButton component="a" href={TWITTER_LINK} target="_blank">
<TwitterIcon sx={{ color }} />
</IconButton>
</Box>
);
};
Socials.defaultProps = {
isFooter: false,
};
+57
View File
@@ -0,0 +1,57 @@
import * as React from 'react';
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material';
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
interface StatsCardProps {
icon: React.ReactNode;
title: string;
count: string | number;
errorMsg?: Error | string;
onClick?: () => void;
}
export const StatsCard: React.FC<StatsCardProps> = ({
icon,
title,
count,
onClick,
errorMsg,
}) => (
<Card onClick={onClick} sx={{ height: '100%' }}>
<CardContent
sx={{
padding: 2,
'&:last-child': {
paddingBottom: 2,
},
cursor: 'pointer',
}}
>
<Box
display="flex"
alignItems="center"
sx={{ color: (theme) => theme.palette.text.primary, fontSize: 18 }}
>
{icon}
<Typography ml={3} mr={0.75} fontSize="inherit">
{count}
</Typography>
<Typography mr={1} fontSize="inherit">
{title}
</Typography>
<IconButton color="inherit">
<ArrowRightAltIcon />
</IconButton>
</Box>
{errorMsg && (
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
{errorMsg}
</Typography>
)}
</CardContent>
</Card>
);
StatsCard.defaultProps = {
onClick: undefined,
errorMsg: undefined,
};
+72
View File
@@ -0,0 +1,72 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Switch from '@mui/material/Switch';
import { Button } from '@mui/material';
import { useMainContext } from 'src/context/main';
import { LightSwitchSVG } from '../icons/LightSwitchSVG';
export const DarkLightSwitch = styled(Switch)(({ theme }) => ({
width: 55,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 2,
transform: 'translateX(4px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage:
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')',
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
},
},
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.nym.networkExplorer.nav.text,
width: 25,
height: 25,
marginTop: '2px',
'&:before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage:
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')',
},
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2,
},
}));
export const DarkLightSwitchMobile: React.FC = () => {
const { toggleMode } = useMainContext();
return (
<Button onClick={() => toggleMode()}>
<LightSwitchSVG />
</Button>
);
};
export const DarkLightSwitchDesktop: React.FC<{ defaultChecked: boolean }> = ({
defaultChecked,
}) => {
const { toggleMode } = useMainContext();
return (
<Button onClick={() => toggleMode()}>
<DarkLightSwitch defaultChecked={defaultChecked} />
</Button>
);
};
+56
View File
@@ -0,0 +1,56 @@
import * as React from 'react';
import { Box, useMediaQuery, TextField, MenuItem } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Select, { SelectChangeEvent } from '@mui/material/Select';
type TableToolBarProps = {
onChangeSearch: (arg: string) => void;
onChangePageSize: (event: SelectChangeEvent<string>) => void;
pageSize: string;
searchTerm: string;
};
export const TableToolbar: React.FC<TableToolBarProps> = ({
searchTerm,
onChangeSearch,
onChangePageSize,
pageSize,
}) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
return (
<>
<Box
sx={{
width: '100%',
marginBottom: 2,
display: 'flex',
flexDirection: matches ? 'column' : 'row',
justifyContent: 'space-between',
}}
>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={pageSize}
onChange={onChangePageSize}
sx={{
width: 200,
marginBottom: matches ? 2 : 0,
}}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={30}>30</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
<TextField
sx={{ width: 350 }}
value={searchTerm}
placeholder="search"
onChange={(event) => onChangeSearch(event.target.value)}
/>
</Box>
</>
);
};
+13
View File
@@ -0,0 +1,13 @@
import * as React from 'react';
import { Typography } from '@mui/material';
export const Title: React.FC<{ text: string }> = ({ text }) => (
<Typography
variant="h5"
sx={{
mb: 3,
}}
>
{text}
</Typography>
);
@@ -0,0 +1,79 @@
import * as React from 'react';
import { CircularProgress, Typography } from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp';
import ErrorIcon from '@mui/icons-material/Error';
interface TableProps {
title?: string;
icons?: boolean[];
keys: string[];
values: number[];
marginBottom?: boolean;
error?: string;
loading: boolean;
}
export const TwoColSmallTable: React.FC<TableProps> = ({
loading,
title,
icons,
keys,
values,
marginBottom,
error,
}) => (
<>
{title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>}
<TableContainer
component={Paper}
sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}
>
<Table aria-label="two col small table">
<TableBody>
{keys.map((each: string, i: number) => (
<TableRow key={each}>
{icons && (
<TableCell>
{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}
</TableCell>
)}
<TableCell sx={error ? { opacity: 0.4 } : null}>{each}</TableCell>
<TableCell sx={error ? { opacity: 0.4 } : null} align="right">
{values[i]}
</TableCell>
{error && (
<TableCell align="right" sx={{ opacity: 0.4 }}>
{values[i]}
</TableCell>
)}
{!error && loading && (
<TableCell align="right">
<CircularProgress />
</TableCell>
)}
{error && !icons && (
<TableCell sx={{ opacity: 0.2 }} align="right">
<ErrorIcon />
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
TwoColSmallTable.defaultProps = {
title: undefined,
icons: undefined,
marginBottom: false,
error: undefined,
};
@@ -0,0 +1,107 @@
import * as React from 'react';
import { makeStyles } from '@mui/styles';
import {
DataGrid,
GridColumns,
GridRowData,
GridSortModel,
useGridSlotComponentProps,
} from '@mui/x-data-grid';
import Pagination from '@mui/material/Pagination';
import { SxProps } from '@mui/system';
const useStyles = makeStyles({
root: {
display: 'flex',
},
});
type DataGridProps = {
loading?: boolean;
rows: GridRowData[];
columnsData: GridColumns;
pageSize?: string;
pagination?: boolean;
hideFooter?: boolean;
sortModel?: GridSortModel;
};
export const cellStyles: SxProps = {
width: '100%',
padding: 0,
maxHeight: 100,
color: 'inherit',
textDecoration: 'none',
fontWeight: 400,
fontSize: 12,
lineHeight: 2,
textAlign: 'start',
wordBreak: 'break-word',
whiteSpace: 'break-spaces',
};
function CustomPagination() {
const { state, apiRef } = useGridSlotComponentProps();
const classes = useStyles();
return (
<Pagination
className={classes.root}
color="primary"
count={state.pagination.pageCount}
page={state.pagination.page + 1}
onChange={(event, value) => apiRef.current.setPage(value - 1)}
/>
);
}
export const UniversalDataGrid: React.FC<DataGridProps> = ({
loading,
rows,
columnsData,
pageSize,
pagination,
hideFooter,
sortModel,
}) => {
const [sortModelState, setSortModelState] = React.useState<
GridSortModel | undefined
>(sortModel);
if (columnsData && rows) {
return (
<DataGrid
pagination
components={{
Pagination: CustomPagination,
}}
loading={loading}
columns={columnsData}
rows={rows}
pageSize={Number(pageSize)}
rowsPerPageOptions={[5]}
hideFooterPagination={!pagination}
disableColumnFilter
disableColumnMenu
disableSelectionOnClick
columnBuffer={0}
autoHeight
hideFooter={hideFooter}
sortModel={sortModelState}
onSortModelChange={setSortModelState}
style={{
width: '100%',
border: 'none',
}}
/>
);
}
return null;
};
UniversalDataGrid.defaultProps = {
loading: false,
pageSize: undefined,
pagination: false,
hideFooter: true,
sortModel: undefined,
};
+129
View File
@@ -0,0 +1,129 @@
import * as React from 'react';
import { CircularProgress, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Chart } from 'react-google-charts';
import { ApiState, UptimeStoryResponse } from 'src/typeDefs/explorer-api';
import { format } from 'date-fns';
interface ChartProps {
title?: string;
xLabel: string;
yLabel: string;
uptimeStory: ApiState<UptimeStoryResponse>;
loading: boolean;
}
type FormattedDateRecord = [string, number, number];
type FormattedChartHeadings = string[];
type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord];
export const UptimeChart: React.FC<ChartProps> = ({
title,
xLabel,
yLabel,
uptimeStory,
loading,
}) => {
const [formattedChartData, setFormattedChartData] =
React.useState<FormattedChartData>();
const theme = useTheme();
const color = theme.palette.text.primary;
React.useEffect(() => {
if (uptimeStory.data?.history) {
const allFormattedChartData: FormattedChartData = [
['Date', 'UptimeV4', 'UptimeV6'],
];
uptimeStory.data.history.forEach((eachDate) => {
const formattedDateUptimeRecord: FormattedDateRecord = [
format(new Date(eachDate.date), 'MMM dd'),
eachDate.ipv4_uptime,
eachDate.ipv6_uptime,
];
allFormattedChartData.push(formattedDateUptimeRecord);
});
setFormattedChartData(allFormattedChartData);
} else {
const emptyData: any = [
['Date', 'UptimeV4', 'UptimeV6'],
['Jul 27', 10, 10],
];
setFormattedChartData(emptyData);
}
}, [uptimeStory]);
return (
<>
{title && <Typography>{title}</Typography>}
{loading && <CircularProgress />}
{!loading && uptimeStory && (
<Chart
style={{ minHeight: 480 }}
chartType="LineChart"
loader={<p>...</p>}
data={
uptimeStory.data
? formattedChartData
: [
['Date', 'UptimeV4', 'UptimeV6'],
[format(new Date(Date.now()), 'MMM dd'), 0, 0],
]
}
options={{
backgroundColor:
theme.palette.mode === 'dark'
? theme.palette.nym.networkExplorer.background.tertiary
: undefined,
color: uptimeStory.error
? 'rgba(255, 255, 255, 0.4)'
: 'rgba(255, 255, 255, 1)',
colors: ['#FB7A21', '#CC808A'],
legend: {
textStyle: {
color,
opacity: uptimeStory.error ? 0.4 : 1,
},
},
intervals: { style: 'sticks' },
hAxis: {
// horizontal / date
title: xLabel,
titleTextStyle: {
color,
},
textStyle: {
color,
// fontSize: 11
},
gridlines: {
count: -1,
},
},
vAxis: {
// % uptime vertical
viewWindow: {
min: 0,
max: 100,
},
title: yLabel,
titleTextStyle: {
color,
opacity: uptimeStory.error ? 0.4 : 1,
},
textStyle: {
color,
fontSize: 11,
opacity: uptimeStory.error ? 0.4 : 1,
},
},
}}
/>
)}
</>
);
};
UptimeChart.defaultProps = {
title: undefined,
};
+132
View File
@@ -0,0 +1,132 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import * as React from 'react';
import { scaleLinear } from 'd3-scale';
import {
ComposableMap,
Geographies,
Geography,
Marker,
ZoomableGroup,
} from 'react-simple-maps';
import ReactTooltip from 'react-tooltip';
import { ApiState, CountryDataResponse } from 'src/typeDefs/explorer-api';
import { CircularProgress } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import MAP_TOPOJSON from '../assets/world-110m.json';
type MapProps = {
userLocation?: [number, number];
countryData?: ApiState<CountryDataResponse>;
loading: boolean;
};
export const WorldMap: React.FC<MapProps> = ({
countryData,
userLocation,
loading,
}) => {
const { palette } = useTheme();
const colorScale = React.useMemo(() => {
if (countryData?.data) {
const heighestNumberOfNodes = Math.max(
...Object.values(countryData.data).map((country) => country.nodes),
);
return scaleLinear<string, string>()
.domain([
0,
heighestNumberOfNodes / 4,
heighestNumberOfNodes / 2,
heighestNumberOfNodes,
])
.range(palette.nym.networkExplorer.map.fills)
.unknown(palette.nym.networkExplorer.map.fills[0]);
}
return () => palette.nym.networkExplorer.map.fills[0];
}, [countryData, palette]);
const [tooltipContent, setTooltipContent] = React.useState<string | null>(
null,
);
if (loading) {
return <CircularProgress />;
}
return (
<>
<ComposableMap
data-tip=""
style={{
backgroundColor: palette.nym.networkExplorer.background.tertiary,
width: '100%',
height: 'auto',
}}
viewBox="0, 50, 800, 350"
projection="geoMercator"
projectionConfig={{
scale: userLocation ? 200 : 100,
center: userLocation,
}}
>
<ZoomableGroup>
<Geographies geography={MAP_TOPOJSON}>
{({ geographies }) =>
geographies.map((geo) => {
const d = (countryData?.data || {})[geo.properties.ISO_A3];
return (
<Geography
key={geo.rsmKey}
geography={geo}
fill={colorScale(d?.nodes || 0)}
stroke={palette.nym.networkExplorer.map.stroke}
strokeWidth={0.2}
onMouseEnter={() => {
const { NAME_LONG } = geo.properties;
if (!userLocation) {
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
}
}}
onMouseLeave={() => {
setTooltipContent('');
}}
style={{
hover:
!userLocation && countryData
? {
fill: palette.nym.highlight,
outline: 'white',
}
: undefined,
}}
/>
);
})
}
</Geographies>
{userLocation && (
<Marker coordinates={userLocation}>
<g
fill="grey"
stroke="#FF5533"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
transform="translate(-12, -10)"
>
<circle cx="12" cy="10" r="5" />
</g>
</Marker>
)}
</ZoomableGroup>
</ComposableMap>
<ReactTooltip>{tooltipContent}</ReactTooltip>
</>
);
};
WorldMap.defaultProps = {
userLocation: undefined,
countryData: undefined,
};
+295
View File
@@ -0,0 +1,295 @@
import { PaletteMode } from '@mui/material';
import * as React from 'react';
import { MIXNODE_API_ERROR } from 'src/api/constants';
import {
CountryDataResponse,
GatewayResponse,
MixNodeResponse,
ValidatorsResponse,
BlockResponse,
ApiState,
MixNodeResponseItem,
DelegationsResponse,
StatsResponse,
StatusResponse,
UptimeStoryResponse,
} from 'src/typeDefs/explorer-api';
import { Api } from '../api';
interface State {
mode: PaletteMode;
toggleMode: () => void;
mixnodes?: ApiState<MixNodeResponse>;
fetchMixnodes: () => void;
filterMixnodes: (arg: MixNodeResponse) => void;
gateways?: ApiState<GatewayResponse>;
validators?: ApiState<ValidatorsResponse>;
block?: ApiState<BlockResponse>;
countryData?: ApiState<CountryDataResponse>;
globalError?: string | undefined;
mixnodeDetailInfo?: ApiState<MixNodeResponse>;
fetchMixnodeById: (arg: string) => void;
fetchDelegationsById: (arg: string) => void;
delegations?: ApiState<DelegationsResponse>;
stats?: ApiState<StatsResponse>;
fetchStatsById: (arg: string) => void;
status?: ApiState<StatusResponse>;
fetchStatusById: (arg: string) => void;
fetchUptimeStoryById: (arg: string) => void;
uptimeStory?: ApiState<UptimeStoryResponse>;
}
// TODO: remove the export and replace all uses with `useMainContext()` hook
export const MainContext = React.createContext<State>({
mode: 'dark',
fetchMixnodeById: () => null,
toggleMode: () => undefined,
fetchDelegationsById: () => null,
fetchStatsById: () => null,
fetchStatusById: () => null,
fetchUptimeStoryById: () => null,
filterMixnodes: () => null,
fetchMixnodes: () => null,
status: { data: undefined, isLoading: false, error: undefined },
stats: { data: undefined, isLoading: false, error: undefined },
mixnodeDetailInfo: { data: undefined, isLoading: false, error: undefined },
delegations: { data: undefined, isLoading: false, error: undefined },
});
export const useMainContext = (): React.ContextType<typeof MainContext> =>
React.useContext<State>(MainContext);
export const MainContextProvider: React.FC = ({ children }) => {
// light/dark mode
const [mode, setMode] = React.useState<PaletteMode>('dark');
// global / banner error messaging
const [globalError, setGlobalError] = React.useState<string>();
// various APIs for Overview page
const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>();
const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>();
const [validators, setValidators] =
React.useState<ApiState<ValidatorsResponse>>();
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
const [countryData, setCountryData] =
React.useState<ApiState<CountryDataResponse>>();
const [mixnodeDetailInfo, setMixnodeDetailInfo] =
React.useState<ApiState<MixNodeResponse>>();
// various APIs for Detail page
const [delegations, setDelegations] =
React.useState<ApiState<DelegationsResponse>>();
const [status, setStatus] = React.useState<ApiState<StatusResponse>>();
const [stats, setStats] = React.useState<ApiState<StatsResponse>>();
const [uptimeStory, setUptimeStory] =
React.useState<ApiState<UptimeStoryResponse>>();
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
// actions passed down to Overview and Detail pages
const fetchUptimeStoryById = async (id: string) => {
setUptimeStory({
data: uptimeStory?.data,
isLoading: true,
error: uptimeStory?.error,
});
try {
const data = await Api.fetchUptimeStoryById(id);
setUptimeStory({ data, isLoading: false });
} catch (error) {
setUptimeStory({
error:
error instanceof Error ? error : new Error('Uptime Story api fail'),
isLoading: false,
});
}
};
const fetchDelegationsById = async (id: string) => {
setDelegations({ data: delegations?.data, isLoading: true });
try {
const data = await Api.fetchDelegationsById(id);
setDelegations({ data, isLoading: false });
} catch (error) {
setDelegations({
error:
error instanceof Error ? error : new Error('Delegations api fail'),
isLoading: false,
});
}
};
const fetchStatusById = async (id: string) => {
setStatus({ data: status?.data, isLoading: true, error: status?.error });
try {
const data = await Api.fetchStatusById(id);
setStatus({ data, isLoading: false });
} catch (error) {
setStatus({
error: error instanceof Error ? error : new Error('Status api fail'),
isLoading: false,
});
}
};
const fetchStatsById = async (id: string) => {
setStats({ data: stats?.data, isLoading: true, error: stats?.error });
try {
const data = await Api.fetchStatsById(id);
setStats({ data, isLoading: false });
} catch (error) {
setStats({
error: error instanceof Error ? error : new Error('Stats api fail'),
isLoading: false,
});
}
};
const fetchMixnodeById = async (id: string) => {
setMixnodeDetailInfo({ data: mixnodeDetailInfo?.data, isLoading: true });
// 1. if mixnode data already exists filter down to this ID
if (mixnodes && mixnodes.data) {
const [matchedToID] = mixnodes.data.filter(
(eachMixnode: MixNodeResponseItem) =>
eachMixnode.mix_node.identity_key === id,
);
// b) SUCCESS | if there *IS* a matched ID in mixnodes
if (matchedToID) {
setMixnodeDetailInfo({ data: [matchedToID], isLoading: false });
}
// b) FAIL | if there is no matching ID in mixnodes
if (!matchedToID) {
setGlobalError(MIXNODE_API_ERROR);
setMixnodeDetailInfo({
isLoading: false,
error: new Error(MIXNODE_API_ERROR),
});
}
} else {
// 2. if mixnode data DOESNT already exist, fetch this specific ID's information.
try {
const data = await Api.fetchMixnodeByID(id);
// a) fetches from cache^, then API, then filters down then dumps in `mixnodes` context.
if (data) {
setMixnodeDetailInfo({ data: [data], isLoading: false });
} else {
throw Error('api failed to retrieve mixnode via id');
}
// NOTE: Only returning mixnodes api info at the moment. Other `ping` api required also.
} catch (error) {
setGlobalError(MIXNODE_API_ERROR);
setMixnodeDetailInfo({
isLoading: false,
error: new Error(MIXNODE_API_ERROR),
});
}
}
};
const fetchMixnodes = async () => {
setMixnodes((d) => ({ ...d, isLoading: true }));
try {
const data = await Api.fetchMixnodes();
setMixnodes({ data, isLoading: false });
} catch (error) {
setMixnodes({
error: error instanceof Error ? error : new Error('Mixnode api fail'),
isLoading: false,
});
}
};
const filterMixnodes = (arr: MixNodeResponse) => {
setMixnodes({ data: arr, isLoading: false });
};
const fetchGateways = async () => {
try {
const data = await Api.fetchGateways();
setGateways({ data, isLoading: false });
} catch (error) {
setGateways({
error: error instanceof Error ? error : new Error('Gateways api fail'),
isLoading: false,
});
}
};
const fetchValidators = async () => {
try {
const data = await Api.fetchValidators();
setValidators({ data, isLoading: false });
} catch (error) {
setValidators({
error:
error instanceof Error ? error : new Error('Validators api fail'),
isLoading: false,
});
}
};
const fetchBlock = async () => {
try {
const data = await Api.fetchBlock();
setBlock({ data, isLoading: false });
} catch (error) {
setBlock({
error: error instanceof Error ? error : new Error('Block api fail'),
isLoading: false,
});
}
};
const fetchCountryData = async () => {
setCountryData({ data: undefined, isLoading: true });
try {
const res = await Api.fetchCountryData();
setCountryData({ data: res, isLoading: false });
} catch (error) {
setCountryData({
error:
error instanceof Error ? error : new Error('Country Data api fail'),
isLoading: false,
});
}
};
React.useEffect(() => {
Promise.all([
fetchMixnodes(),
fetchGateways(),
fetchValidators(),
fetchBlock(),
fetchCountryData(),
]);
}, []);
return (
<MainContext.Provider
value={{
mode,
toggleMode,
mixnodes,
filterMixnodes,
fetchMixnodes,
gateways,
validators,
block,
countryData,
globalError,
mixnodeDetailInfo,
fetchMixnodeById,
fetchDelegationsById,
delegations,
stats,
fetchStatsById,
status,
fetchStatusById,
uptimeStory,
fetchUptimeStoryById,
}}
>
{children}
</MainContext.Provider>
);
};
+14
View File
@@ -0,0 +1,14 @@
import { useRef, useEffect, useCallback } from 'react';
export function useIsMounted(): () => boolean {
const ref = useRef(false);
useEffect(() => {
ref.current = true;
return () => {
ref.current = false;
};
}, []);
return useCallback(() => ref.current, [ref]);
}
+46
View File
@@ -0,0 +1,46 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const GatewaysSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.2 12H22.7"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M1.30005 12H12"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M20.1 9.40015L22.7 12.0001L20.1 14.6001"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
</svg>
);
};
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const LightSwitchSVG: React.FC = () => {
const { palette } = useTheme();
return (
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z"
fill={palette.background.default}
/>
<path
d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z"
fill={palette.text.primary}
/>
</svg>
);
};
+114
View File
@@ -0,0 +1,114 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const MixnodesSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" />
<path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" />
<path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" />
<path
d="M2.97681 23.0621L23.0437 2.99512"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M23.0437 23.0621L2.97681 2.99512"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M13.0103 23.0621L23.0437 2.99512"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M2.97681 2.99512L13.0103 23.0621"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
<path
d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z"
fill="#242C3D"
stroke={color}
strokeWidth="1.2"
strokeMiterlimit="10"
/>
</svg>
);
};
+32
View File
@@ -0,0 +1,32 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const NetworkComponentsSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.nym.networkExplorer.nav.text;
return (
<svg
width="25"
height="25"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z"
stroke={color}
strokeMiterlimit="10"
/>
</svg>
);
};
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const NodemapSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.nym.networkExplorer.nav.text;
return (
<svg
width="25"
height="25"
viewBox="0 0 19 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z"
stroke={color}
strokeMiterlimit="10"
/>
<path
d="M1.5 22.5999H17.1"
stroke={color}
strokeMiterlimit="10"
strokeLinecap="round"
/>
</svg>
);
};
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react';
export const NymLogoSVG: React.FC = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 5389.9 5389.9"
width="40px"
height="40px"
{...props}
>
<circle cx={2695} cy={2695} r={2585} fill="#121726" />
<linearGradient
id="nym_logo_svg__a"
gradientUnits="userSpaceOnUse"
x1={0}
y1={8058.165}
x2={5390}
y2={8058.165}
gradientTransform="matrix(1 0 0 -1 0 10753.165)"
>
<stop offset={0} stopColor="#f77846" />
<stop offset={1} stopColor="#ed3572" />
</linearGradient>
<path
d="M2695 5390c-182.8 0-365.5-18.4-543-54.8-173.1-35.4-343.3-88.3-506-157.1-159.7-67.6-313.7-151.2-457.8-248.5-142.7-96.4-276.8-207.1-398.8-329-121.9-121.9-232.6-256.1-329-398.8-97.4-144-181-298-248.6-457.8-68.8-162.6-121.6-332.9-157-506C18.4 3060.5 0 2877.8 0 2695s18.4-365.5 54.8-543c35.4-173.1 88.3-343.3 157.1-506 67.6-159.7 151.2-313.7 248.5-457.8 96.4-142.7 207.1-276.8 329-398.8s256.1-232.6 398.8-329c144.1-97.3 298.1-180.9 457.8-248.5 162.7-68.8 332.9-121.7 506-157.1C2329.5 18.4 2512.2 0 2695 0s365.5 18.4 543 54.8c173.1 35.4 343.3 88.3 506 157.1 159.7 67.6 313.7 151.2 457.8 248.5 142.7 96.4 276.8 207.1 398.8 329 121.9 121.9 232.6 256.1 329 398.8 97.3 144.1 180.9 298.1 248.5 457.8 68.8 162.7 121.7 332.9 157.1 506 36.3 177.5 54.8 360.2 54.8 543s-18.4 365.5-54.8 543c-35.4 173.1-88.3 343.3-157.1 506-67.6 159.7-151.2 313.7-248.5 457.8-96.4 142.7-207.1 276.8-329 398.8-121.9 121.9-256.1 232.6-398.8 329-144.1 97.3-298.1 180.9-457.8 248.5-162.7 68.8-332.9 121.7-506 157.1-177.5 36.4-360.2 54.8-543 54.8zm0-5170c-168 0-335.9 16.9-498.9 50.3-158.9 32.5-315.1 81-464.4 144.2-146.6 62-288.1 138.8-420.4 228.2C1180.2 731.3 1057 833 944.9 945c-112 112-213.7 235.3-302.3 366.4-89.4 132.3-166.2 273.7-228.2 420.4-63.2 149.3-111.7 305.6-144.2 464.4C236.9 2359.1 220 2527 220 2695s16.9 335.9 50.3 498.9c32.5 158.9 81 315.1 144.2 464.4 62 146.6 138.8 288.1 228.2 420.4C731.3 4209.8 833 4333 945 4445.1c112 112 235.3 213.7 366.4 302.3 132.3 89.4 273.7 166.2 420.4 228.2 149.3 63.2 305.6 111.7 464.4 144.2 163.1 33.4 330.9 50.3 498.9 50.3s335.9-16.9 498.9-50.3c158.9-32.5 315.1-81 464.4-144.2 146.6-62 288.1-138.8 420.4-228.2 131.1-88.6 254.3-190.3 366.4-302.3 112-112 213.7-235.3 302.3-366.4 89.4-132.3 166.2-273.7 228.2-420.4 63.2-149.3 111.7-305.6 144.2-464.4 33.4-163.1 50.3-330.9 50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4-62-146.6-138.8-288.1-228.2-420.4-88.6-131.1-190.3-254.3-302.3-366.4-112-112-235.3-213.7-366.4-302.3-132.3-89.4-273.7-166.2-420.4-228.2-149.3-63.2-305.6-111.7-464.4-144.2-163.1-33.3-331-50.2-499-50.2z"
fill="url(#nym_logo_svg__a)"
/>
<path
d="M1958.5 3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8 725.1v-725.1h211.9v930.9zm2420.4-930.9l-335.7 330.9-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4 340.6c26.7 26.3 69.6 26.3 96.3 0l345.4-340.6v725.3h218.4v-930.9h-276.3zm-1789.8 485.9v445h218.4v-445l502.7-485.9H3034l-335.9 330.9-335.7-330.9h-276.2l502.9 485.9z"
fill="#fff"
/>
</svg>
);
+42
View File
@@ -0,0 +1,42 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const OverviewSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.nym.networkExplorer.nav.text;
return (
<svg
width="25"
height="25"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.4 21.6H22.6"
stroke={color}
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z"
stroke={color}
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z"
stroke={color}
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M7.4 11.8H3.2V21.6H7.4V11.8Z"
stroke={color}
strokeMiterlimit="10"
strokeLinecap="round"
/>
</svg>
);
};
+53
View File
@@ -0,0 +1,53 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const ValidatorsSVG: React.FC = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0)">
<path
d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8"
stroke={color}
strokeWidth="1.3"
strokeMiterlimit="10"
strokeLinecap="round"
/>
<path
d="M5 12.3L7.9 15.3L14.5 8.69995"
stroke={color}
strokeWidth="2"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
};
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nym Network Explorer</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { App } from './App';
import { MainContextProvider } from './context/main';
import './styles.css';
import { NetworkExplorerThemeProvider } from './theme';
ReactDOM.render(
<MainContextProvider>
<NetworkExplorerThemeProvider>
<Router>
<App />
</Router>
</NetworkExplorerThemeProvider>
</MainContextProvider>,
document.getElementById('app'),
);
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#121726;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g>
<circle class="st0" cx="250" cy="250.1" r="239.8"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="4.980469e-02" y1="5613.9395" x2="500.0498" y2="5613.9395" gradientTransform="matrix(1 0 0 -1 0 5863.9897)">
<stop offset="0" style="stop-color:#E1864B"/>
<stop offset="1" style="stop-color:#DA465B"/>
</linearGradient>
<path class="st1" d="M250,500.1c-17,0-33.9-1.7-50.4-5.1c-16.1-3.3-31.8-8.2-46.9-14.6c-14.8-6.3-29.1-14-42.5-23.1
c-13.2-8.9-25.7-19.2-37-30.5c-11.3-11.3-21.6-23.8-30.5-37c-9-13.4-16.8-27.6-23.1-42.5c-6.4-15.1-11.3-30.9-14.6-46.9
C1.8,284,0,267,0,250.1s1.7-33.9,5.1-50.4c3.3-16.1,8.2-31.8,14.6-46.9c6.3-14.8,14-29.1,23.1-42.5C51.7,97,62,84.6,73.3,73.3
s23.8-21.6,37-30.5c13.4-9,27.7-16.8,42.5-23.1c15.1-6.4,30.9-11.3,46.9-14.6c16.5-3.4,33.4-5.1,50.4-5.1c17,0,33.9,1.7,50.4,5.1
c16.1,3.3,31.8,8.2,46.9,14.6c14.8,6.3,29.1,14,42.5,23.1c13.2,8.9,25.7,19.2,37,30.5c11.3,11.3,21.6,23.8,30.5,37
c9,13.4,16.8,27.7,23.1,42.5c6.4,15.1,11.3,30.9,14.6,46.9c3.4,16.5,5.1,33.4,5.1,50.4s-1.7,33.9-5.1,50.4
c-3.3,16.1-8.2,31.8-14.6,46.9c-6.3,14.8-14,29.1-23.1,42.5c-8.9,13.2-19.2,25.7-30.5,37c-11.3,11.3-23.8,21.6-37,30.5
c-13.4,9-27.7,16.8-42.5,23.1c-15.1,6.4-30.9,11.3-46.9,14.6C284,498.3,267,500.1,250,500.1z M250,20.5c-15.6,0-31.2,1.6-46.3,4.7
c-14.7,3-29.2,7.5-43.1,13.4c-13.6,5.8-26.7,12.9-39,21.2c-12.2,8.2-23.6,17.7-34,28c-10.4,10.4-19.8,21.8-28,34
c-8.3,12.3-15.4,25.4-21.2,39c-5.9,13.8-10.4,28.3-13.4,43.1c-3.1,15.1-4.7,30.7-4.7,46.3s1.6,31.2,4.7,46.3
c3,14.7,7.5,29.2,13.4,43.1c5.8,13.6,12.9,26.7,21.2,39c8.2,12.2,17.7,23.6,28,34c10.4,10.4,21.8,19.8,34,28
c12.3,8.3,25.4,15.4,39,21.2c13.8,5.9,28.3,10.4,43.1,13.4c15.1,3.1,30.7,4.7,46.3,4.7c15.6,0,31.2-1.6,46.3-4.7
c14.7-3,29.2-7.5,43.1-13.4c13.6-5.8,26.7-12.9,39-21.2c12.2-8.2,23.6-17.7,34-28c10.4-10.4,19.8-21.8,28-34
c8.3-12.3,15.4-25.4,21.2-39c5.9-13.8,10.4-28.3,13.4-43.1c3.1-15.1,4.7-30.7,4.7-46.3s-1.6-31.2-4.7-46.3
c-3-14.7-7.5-29.2-13.4-43.1c-5.8-13.6-12.9-26.7-21.2-39c-8.2-12.2-17.7-23.6-28-34c-10.4-10.4-21.8-19.8-34-28
c-12.3-8.3-25.4-15.4-39-21.2c-13.8-5.9-28.3-10.4-43.1-13.4C281.2,22,265.6,20.5,250,20.5z"/>
</g>
</g>
<path class="st2" d="M369.8,341.2h-52.8l-144-142v142h-42.8V158.9h54.1l144,141.9V158.9h41.5V341.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useHistory } from 'react-router-dom';
import { useMainContext } from 'src/context/main';
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
export const Page404: React.FC = () => {
const history = useHistory();
const { mode } = useMainContext();
const theme = useTheme();
return (
<Box component="main" sx={{ flexGrow: 1 }}>
<Grid container spacing={0} alignItems="center" justifyContent="center">
<Grid item xs={12} sm={12} md={6}>
<Paper
sx={{
p: 3,
height: 450,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-evenly',
flexDirection: 'column',
background:
mode === 'dark'
? theme.palette.secondary.dark
: theme.palette.primary.light,
borderRadius: 10,
}}
>
<NymLogoSVG />
<Typography variant="h2">Oh No!</Typography>
<Typography variant="body1">
It looks like you might be lost.
</Typography>
<Typography variant="body1" textAlign="center">
Please try the link again or navigate back to{' '}
</Typography>
<Button
sx={{
fontWeight: 'bold',
bgcolor: theme.palette.primary.main,
color: theme.palette.secondary.main,
}}
onClick={() => history.push('/overview')}
>
Overview
</Button>
</Paper>
</Grid>
</Grid>
</Box>
);
};
+150
View File
@@ -0,0 +1,150 @@
import * as React from 'react';
import { Button, Grid, Typography } from '@mui/material';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { printableCoin } from '@nymproject/nym-validator-client';
import { SelectChangeEvent } from '@mui/material/Select';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { useMainContext } from 'src/context/main';
import { gatewayToGridRow } from 'src/utils';
import { GatewayResponse } from 'src/typeDefs/explorer-api';
import { TableToolbar } from 'src/components/TableToolbar';
import { ContentCard } from 'src/components/ContentCard';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { Title } from 'src/components/Title';
export const PageGateways: React.FC = () => {
const { gateways } = useMainContext();
const [filteredGateways, setFilteredGateways] =
React.useState<GatewayResponse>([]);
const [pageSize, setPageSize] = React.useState<string>('50');
const [searchTerm, setSearchTerm] = React.useState<string>('');
const handleSearch = (str: string) => {
setSearchTerm(str.toLowerCase());
};
React.useEffect(() => {
if (searchTerm === '' && gateways?.data) {
setFilteredGateways(gateways?.data);
} else {
const filtered = gateways?.data?.filter((g) => {
if (
g.gateway.location.toLowerCase().includes(searchTerm) ||
g.gateway.identity_key.toLocaleLowerCase().includes(searchTerm) ||
g.owner.toLowerCase().includes(searchTerm)
) {
return g;
}
return null;
});
if (filtered) {
setFilteredGateways(filtered);
}
}
}, [searchTerm, gateways?.data]);
const columns: GridColDef[] = [
{
field: 'owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
{
field: 'identity_key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
{
field: 'bond',
width: 120,
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
headerClassName: 'MuiDataGrid-header-override',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => {
const bondAsPunk = printableCoin({
amount: params.value as string,
denom: 'upunk',
});
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
},
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 150,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
>
{params.value}
</Button>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
setPageSize(event.target.value);
};
if (gateways?.data) {
return (
<>
<Title text="Gateways" />
<Grid>
<Grid item>
<ContentCard>
<TableToolbar
onChangeSearch={handleSearch}
onChangePageSize={handlePageSize}
pageSize={pageSize}
searchTerm={searchTerm}
/>
<UniversalDataGrid
loading={gateways?.isLoading}
columnsData={columns}
rows={gatewayToGridRow(filteredGateways)}
pageSize={pageSize}
pagination={gateways?.data?.length >= 12}
hideFooter={gateways?.data?.length < 12}
sortModel={[
{
field: 'bond',
sort: 'desc',
},
]}
/>
</ContentCard>
</Grid>
</Grid>
</>
);
}
return null;
};
+283
View File
@@ -0,0 +1,283 @@
import * as React from 'react';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { Box, Grid, Typography } from '@mui/material';
import { useMainContext } from 'src/context/main';
import { useParams } from 'react-router-dom';
import { ContentCard } from 'src/components/ContentCard';
import { WorldMap } from 'src/components/WorldMap';
import { BondBreakdownTable } from 'src/components/BondBreakdown';
import { TwoColSmallTable } from 'src/components/TwoColSmallTable';
import { UptimeChart } from 'src/components/UptimeChart';
import { mixnodeToGridRow, scrollToRef } from 'src/utils';
import { ComponentError } from 'src/components/ComponentError';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { MixNodeResponseItem } from 'src/typeDefs/explorer-api';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { printableCoin } from '@nymproject/nym-validator-client';
import { Title } from 'src/components/Title';
const columns: GridColDef[] = [
{
field: 'owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<div>
<Typography sx={cellStyles}>{params.value}</Typography>
</div>
),
},
{
field: 'identity_key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<div>
<Typography sx={cellStyles}>{params.value}</Typography>
</div>
),
},
{
field: 'bond',
headerName: 'Bond',
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => {
const bondAsPunk = printableCoin({
amount: params.value as string,
denom: 'upunk',
});
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
},
},
{
field: 'self_percentage',
headerName: 'Self %',
headerAlign: 'left',
type: 'number',
width: 99,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
renderCell: (params: GridRenderCellParams) => (
<div>
<Typography sx={cellStyles}>{params.value}%</Typography>
</div>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<div>
<Typography sx={cellStyles}>{params.value}</Typography>
</div>
),
},
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<div>
<Typography sx={cellStyles}>{params.value}</Typography>
</div>
),
},
{
field: 'layer',
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
type: 'number',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
];
export const PageMixnodeDetail: React.FC = () => {
const ref = React.useRef();
const [row, setRow] = React.useState<MixNodeResponseItem[]>([]);
const {
fetchMixnodeById,
mixnodeDetailInfo,
fetchStatsById,
fetchDelegationsById,
fetchUptimeStoryById,
fetchStatusById,
stats,
status,
uptimeStory,
} = useMainContext();
const { id }: any = useParams();
React.useEffect(() => {
const hasNoDetail = id && !mixnodeDetailInfo;
const hasIncorrectDetail =
id &&
mixnodeDetailInfo?.data &&
mixnodeDetailInfo?.data[0].mix_node.identity_key !== id;
if (hasNoDetail || hasIncorrectDetail) {
fetchMixnodeById(id);
fetchDelegationsById(id);
fetchStatsById(id);
fetchStatusById(id);
fetchUptimeStoryById(id);
} else if (mixnodeDetailInfo?.data !== undefined) {
setRow(mixnodeDetailInfo?.data);
}
}, [id, mixnodeDetailInfo]);
React.useEffect(() => {
scrollToRef(ref);
}, [ref]);
return (
<>
<Box component="main" ref={ref}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Title text="Mixnode Detail" />
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
{mixnodeDetailInfo && (
<ContentCard>
<UniversalDataGrid
columnsData={columns}
rows={mixnodeToGridRow(row)}
loading={mixnodeDetailInfo.isLoading}
pageSize="1"
pagination={false}
hideFooter
/>
</ContentCard>
)}
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<ContentCard title="Bond Breakdown">
<BondBreakdownTable />
</ContentCard>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12} md={4}>
<ContentCard title="Mixnode Stats">
{stats && (
<>
{stats.error && (
<ComponentError text="There was a problem retrieving this nodes stats." />
)}
<TwoColSmallTable
loading={stats.isLoading}
error={stats?.error?.message}
title="Since startup"
keys={['Received', 'Sent', 'Explicitly dropped']}
values={[
stats?.data?.packets_received_since_startup || 0,
stats?.data?.packets_sent_since_startup || 0,
stats?.data?.packets_explicitly_dropped_since_startup ||
0,
]}
/>
<TwoColSmallTable
loading={stats.isLoading}
error={stats?.error?.message}
title="Since last update"
keys={['Received', 'Sent', 'Explicitly dropped']}
values={[
stats?.data?.packets_received_since_last_update || 0,
stats?.data?.packets_sent_since_last_update || 0,
stats?.data
?.packets_explicitly_dropped_since_last_update || 0,
]}
marginBottom
/>
</>
)}
{!stats && <Typography>No stats information</Typography>}
</ContentCard>
</Grid>
<Grid item xs={12} md={8}>
{uptimeStory && (
<ContentCard title="Uptime story">
{uptimeStory.error && (
<ComponentError text="There was a problem retrieving uptime history." />
)}
<UptimeChart
loading={uptimeStory.isLoading}
xLabel="date"
yLabel="uptime"
uptimeStory={uptimeStory}
/>
</ContentCard>
)}
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12} md={4}>
{status && (
<ContentCard title="Mixnode Status">
{status.error && (
<ComponentError text="There was a problem retrieving port information" />
)}
<TwoColSmallTable
loading={status.isLoading}
error={status?.error?.message}
keys={['Mix port', 'Verloc port', 'HTTP port']}
values={[1789, 1790, 8000].map((each) => each)}
icons={
(status?.data?.ports &&
Object.values(status.data.ports)) || [false, false, false]
}
/>
</ContentCard>
)}
</Grid>
<Grid item xs={12} md={8}>
{mixnodeDetailInfo && (
<ContentCard title="Location">
{mixnodeDetailInfo?.error && (
<ComponentError text="There was a problem retrieving this mixnode location" />
)}
{mixnodeDetailInfo.data &&
mixnodeDetailInfo?.data[0]?.location && (
<WorldMap
loading={mixnodeDetailInfo.isLoading}
userLocation={[
mixnodeDetailInfo?.data[0]?.location?.lng,
mixnodeDetailInfo?.data[0]?.location?.lat,
]}
/>
)}
</ContentCard>
)}
</Grid>
</Grid>
</Box>
</>
);
};
+211
View File
@@ -0,0 +1,211 @@
import * as React from 'react';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { printableCoin } from '@nymproject/nym-validator-client';
import { Link as RRDLink } from 'react-router-dom';
import { Button, Grid, Link as MuiLink } from '@mui/material';
import { SelectChangeEvent } from '@mui/material/Select';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { useMainContext } from 'src/context/main';
import { mixnodeToGridRow } from 'src/utils';
import { TableToolbar } from 'src/components/TableToolbar';
import { MixNodeResponse } from 'src/typeDefs/explorer-api';
import { BIG_DIPPER } from 'src/api/constants';
import { ContentCard } from 'src/components/ContentCard';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { Title } from 'src/components/Title';
export const PageMixnodes: React.FC = () => {
const { mixnodes } = useMainContext();
const [filteredMixnodes, setFilteredMixnodes] =
React.useState<MixNodeResponse>([]);
const [pageSize, setPageSize] = React.useState<string>('10');
const [searchTerm, setSearchTerm] = React.useState<string>('');
const handleSearch = (str: string) => {
setSearchTerm(str.toLowerCase());
};
React.useEffect(() => {
if (searchTerm === '' && mixnodes?.data) {
setFilteredMixnodes(mixnodes?.data);
} else {
const filtered = mixnodes?.data?.filter((m) => {
if (
m.location?.country_name.toLowerCase().includes(searchTerm) ||
m.mix_node.identity_key.toLocaleLowerCase().includes(searchTerm) ||
m.owner.toLowerCase().includes(searchTerm)
) {
return m;
}
return null;
});
if (filtered) {
setFilteredMixnodes(filtered);
}
}
}, [searchTerm, mixnodes?.data]);
const columns: GridColDef[] = [
{
field: 'owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
flex: 3,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
href={`${BIG_DIPPER}/account/${params.value}`}
target="_blank"
sx={cellStyles}
>
{params.value}
</MuiLink>
),
},
{
field: 'identity_key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
flex: 3,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
component={RRDLink}
to={`/network-components/mixnodes/${params.value}`}
>
{params.value}
</MuiLink>
),
},
{
field: 'bond',
headerName: 'Bond',
type: 'number',
headerAlign: 'left',
flex: 1,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
renderCell: (params: GridRenderCellParams) => {
const bondAsPunk = printableCoin({
amount: params.value as string,
denom: 'upunk',
});
return (
<MuiLink
sx={cellStyles}
component={RRDLink}
to={`/network-components/mixnodes/${params.row.identity_key}`}
>
{bondAsPunk}
</MuiLink>
);
},
},
{
field: 'self_percentage',
headerName: 'Self %',
headerAlign: 'left',
type: 'number',
width: 99,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
component={RRDLink}
to={`/network-components/mixnodes/${params.row.identity_key}`}
>
{params.value}%
</MuiLink>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
component={RRDLink}
to={`/network-components/mixnodes/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
>
{params.value}
</Button>
),
},
{
field: 'layer',
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
flex: 1,
type: 'number',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles, textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnodes/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
setPageSize(event.target.value);
};
return (
<>
<Title text="Mixnodes" />
<Grid>
<Grid item>
<ContentCard>
<TableToolbar
onChangeSearch={handleSearch}
onChangePageSize={handlePageSize}
pageSize={pageSize}
searchTerm={searchTerm}
/>
<UniversalDataGrid
loading={mixnodes?.isLoading}
columnsData={columns}
rows={mixnodeToGridRow(filteredMixnodes)}
pageSize={pageSize}
pagination
hideFooter={false}
sortModel={[
{
field: 'bond',
sort: 'desc',
},
]}
/>
</ContentCard>
</Grid>
</Grid>
</>
);
};
+135
View File
@@ -0,0 +1,135 @@
import * as React from 'react';
import {
Alert,
Box,
CircularProgress,
Grid,
SelectChangeEvent,
Typography,
} from '@mui/material';
import { WorldMap } from 'src/components/WorldMap';
import { useMainContext } from 'src/context/main';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { TableToolbar } from 'src/components/TableToolbar';
import { CountryDataRowType, countryDataToGridRow } from 'src/utils';
import { Title } from 'src/components/Title';
import { ContentCard } from '../../components/ContentCard';
export const PageMixnodesMap: React.FC = () => {
const { countryData } = useMainContext();
const [pageSize, setPageSize] = React.useState<string>('10');
const [formattedCountries, setFormattedCountries] = React.useState<
CountryDataRowType[]
>([]);
const [searchTerm, setSearchTerm] = React.useState<string>('');
const handleSearch = (str: string) => {
setSearchTerm(str.toLowerCase());
};
const handlePageSize = (event: SelectChangeEvent<string>) => {
setPageSize(event.target.value);
};
const columns: GridColDef[] = [
{
field: 'countryName',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
{
field: 'nodes',
renderHeader: () => (
<CustomColumnHeading headingTitle="Number of Nodes" />
),
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
{
field: 'percentage',
renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles}>{params.value}</Typography>
),
},
];
React.useEffect(() => {
if (countryData?.data && searchTerm === '') {
setFormattedCountries(
countryDataToGridRow(Object.values(countryData.data)),
);
} else if (countryData?.data !== undefined && searchTerm !== '') {
const formatted = countryDataToGridRow(Object.values(countryData?.data));
const filtered = formatted.filter((m) => {
if (
m.countryName.toLowerCase().includes(searchTerm) ||
m.ISO3.toLowerCase().includes(searchTerm)
) {
return m;
}
return null;
});
if (filtered) {
setFormattedCountries(filtered);
}
}
}, [searchTerm, countryData?.data]);
if (countryData?.isLoading) {
return <CircularProgress />;
}
if (countryData?.data && !countryData.isLoading) {
return (
<Box component="main" sx={{ flexGrow: 1 }}>
<Grid container spacing={1} sx={{ mb: 4 }}>
<Grid item xs={12}>
<Title text="Mixnodes Around the Globe" />
</Grid>
<Grid item xs={12} lg={9}>
<Grid container spacing={2}>
<Grid item xs={12}>
<ContentCard title="Distribution of nodes">
<WorldMap loading={false} countryData={countryData} />
<Box sx={{ marginTop: 2 }} />
<TableToolbar
onChangeSearch={handleSearch}
onChangePageSize={handlePageSize}
pageSize={pageSize}
searchTerm={searchTerm}
/>
<UniversalDataGrid
loading={countryData?.isLoading}
columnsData={columns}
rows={formattedCountries}
pageSize={pageSize}
pagination
/>
</ContentCard>
</Grid>
</Grid>
</Grid>
</Grid>
</Box>
);
}
return <Alert severity="error">{countryData?.error}</Alert>;
};
+90
View File
@@ -0,0 +1,90 @@
import * as React from 'react';
import { Box, Grid, Link } from '@mui/material';
import { WorldMap } from 'src/components/WorldMap';
import { useHistory } from 'react-router-dom';
import { useMainContext } from 'src/context/main';
import { formatNumber } from 'src/utils';
import { BIG_DIPPER } from 'src/api/constants';
import { ValidatorsSVG } from 'src/icons/ValidatorsSVG';
import { GatewaysSVG } from 'src/icons/GatewaysSVG';
import { MixnodesSVG } from 'src/icons/MixnodesSVG';
import { Title } from 'src/components/Title';
import { ContentCard } from '../../components/ContentCard';
import { StatsCard } from '../../components/StatsCard';
export const PageOverview: React.FC = () => {
const history = useHistory();
const { mixnodes, gateways, validators, block, countryData } =
useMainContext();
return (
<>
<Box component="main" sx={{ flexGrow: 1 }}>
<Grid>
<Grid item>
<Title text="Overview" />
</Grid>
<Grid item>
<Grid container spacing={2}>
{mixnodes && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => history.push('/network-components/mixnodes')}
title="Mixnodes"
icon={<MixnodesSVG />}
count={mixnodes?.data?.length || ''}
errorMsg={mixnodes?.error}
/>
</Grid>
)}
{gateways && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => history.push('/network-components/gateways')}
title="Gateways"
count={gateways?.data?.length || ''}
errorMsg={gateways?.error}
icon={<GatewaysSVG />}
/>
</Grid>
)}
{validators && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
title="Validators"
count={validators?.data?.count || ''}
errorMsg={validators?.error}
icon={<ValidatorsSVG />}
/>
</Grid>
)}
{block?.data && (
<Grid item xs={12}>
<ContentCard
title={
<Link
href={`${BIG_DIPPER}/blocks`}
target="_blank"
rel="noreferrer"
underline="none"
color="inherit"
>
Current block height is {formatNumber(block.data)}
</Link>
}
/>
</Grid>
)}
<Grid item xs={12}>
<ContentCard title="Distribution of nodes around the world">
<WorldMap loading={false} countryData={countryData} />
</ContentCard>
</Grid>
</Grid>
</Grid>
</Grid>
</Box>
</>
);
};
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { PageOverview } from 'src/pages/Overview';
import { PageMixnodesMap } from 'src/pages/MixnodesMap';
import { Page404 } from '../pages/404';
import { NetworkComponentsRoutes } from './network-components';
export const Routes: React.FC = () => (
<Switch>
<Route exact path="/">
<Redirect to="/overview" />
</Route>
<Route exact path="/overview">
<PageOverview />
</Route>
<Route path="/network-components">
<NetworkComponentsRoutes />
</Route>
<Route path="/nodemap">
<PageMixnodesMap />
</Route>
<Route component={Page404} />
</Switch>
);
@@ -0,0 +1,33 @@
import * as React from 'react';
import { Switch, Route, RouteComponentProps } from 'react-router-dom';
import { BIG_DIPPER } from 'src/api/constants';
import { PageGateways } from 'src/pages/Gateways';
import { PageMixnodeDetail } from 'src/pages/MixnodeDetail';
import { PageMixnodes } from '../pages/Mixnodes';
export const NetworkComponentsRoutes: React.FC = () => (
<>
<Switch>
<Route exact path="/network-components/mixnodes">
<PageMixnodes />
</Route>
<Route path="/network-components/mixnodes/:id">
<PageMixnodeDetail />
</Route>
<Route path="/network-components/gateways">
<PageGateways />
</Route>
<Route
path="/network-components/validators"
component={(props: RouteComponentProps) => {
window.open(`${BIG_DIPPER}/validators`);
props.history.goBack();
return null;
}}
/>
<Route path="/network-components/gateways/:id">
<h1> Specific Gateways ID</h1>
</Route>
</Switch>
</>
);
+33
View File
@@ -0,0 +1,33 @@
/* last resort for styles that cannot be handled by Material UI and Emotion JS */
/* TODO - this override should take place in either
the theme declaration in index.tsx or the style prop
in <DataGrid /> */
.MuiDataGrid-sortIcon {
display: none !important;
}
.MuiDataGrid-columnSeparator {
visibility: hidden;
}
/* TODO - this should be managed somehow in MUI DataGrid
but documentation doesnt offer a way to do it. Possibly only
included in Data Grid Pro package */
.MuiDataGrid-header-override {
height: 55px;
min-height: 55px;
}
/* Again, no offered way to add sx to this specific div to kill the padding
which puts it out of sync with other (sx styled) cells */
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
padding-left: 0;
}
/* Unable to style nested MUI paper within the Drawer
so styling manually here */
div.MuiDrawer-root > .MuiDrawer-paperAnchorLeft {
background-color: #111826;
}
+13
View File
@@ -0,0 +1,13 @@
import React, { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Nav } from '../components/Nav';
describe('Nav', () => {
beforeEach(() => {
render(<Nav />);
});
it('should render without exploding', () => {
const { container } = render(<Nav />);
expect(container.firstChild).toBeInTheDocument();
});
});
+28
View File
@@ -0,0 +1,28 @@
import React, { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { WorldMap } from '../components/WorldMap';
describe('WorldMap', () => {
beforeEach(() => {
render(<WorldMap loading={false} />);
});
it('should render without exploding', () => {
const { container } = render(<WorldMap loading={false} />);
expect(container.firstChild).toBeInTheDocument();
});
it('should render the expected container/child element', () => {
expect(screen.getByTestId('worldMap__container')).toBeInTheDocument();
});
it('should render the title', () => {
expect(screen.getByText('mix-nodes around the globe')).toBeTruthy();
});
it('should render the map/SVG', () => {
expect(screen.getByTestId('svg')).toBeInTheDocument();
});
it('should render map at correct size/dims', () => {
const expectedWidth = '1000';
const expectedHeight = '800';
expect(screen.getByTestId('svg')).toHaveAttribute('width', expectedWidth);
expect(screen.getByTestId('svg')).toHaveAttribute('height', expectedHeight);
});
});
+7
View File
@@ -0,0 +1,7 @@
describe('testing jest config', () => {
test('jest works with typescript', () => {
const foo = () => 42;
expect(foo()).toBe(42);
});
});
+24
View File
@@ -0,0 +1,24 @@
# Nym Material UI Theme
This directory contains the theme overrides that customise the Material UI default theme to Nym branding.
## Getting started
If you are unfamiliar with Material UI theming, please read the following first:
- https://mui.com/customization/theming/
- https://mui.com/customization/palette/
- https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
[mui-theme.d.ts](./mui-theme.d.ts) adds typings to the theme using Typescript's module augmentation.
Read the following if you are unfamiliar with module augmentation and declaration merging. Then
look at the recommendations from Material UI docs for implementation:
- https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
- https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
- https://mui.com/customization/palette/#adding-new-colors
## Typings
[mui-theme.d.ts](./mui-theme.d.ts) contains the typings for the Nym theme additions and specific Network Explorer theme elements.
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { getDesignTokens } from './theme';
import { useMainContext } from '../context/main';
/**
* Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
*/
export const NetworkExplorerThemeProvider: React.FC = ({ children }) => {
const { mode } = useMainContext();
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
};
+114
View File
@@ -0,0 +1,114 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */
import {
Theme,
ThemeOptions,
Palette,
PaletteOptions,
} from '@mui/material/styles';
import { PaletteMode } from '@mui/material';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This interface defines a palette used across Nym for branding
*/
interface NymPalette {
highlight: string;
text: {
nav: string;
footer: string;
};
}
interface NymPaletteVariant {
mode: PaletteMode;
background: {
main: string;
paper: string;
};
text: {
main: string;
};
topNav: {
background: string;
};
nav: {
background: string;
hover: string;
};
}
/**
* A palette definition only for the Network Explorer that extends the Nym palette
*/
interface NetworkExplorerPalette {
networkExplorer: {
map: {
stroke: string;
fills: string[];
};
background: {
tertiary: string;
};
topNav: {
background: string;
socialIcons: string;
};
nav: {
selected: {
main: string;
nested: string;
};
background: string;
hover: string;
text: string;
};
footer: {
socialIcons: string;
};
};
}
interface NymPaletteAndNetworkExplorerPalette {
nym: NymPalette & NetworkExplorerPalette;
}
type NymPaletteAndNetworkExplorerPaletteOptions =
Partial<NymPaletteAndNetworkExplorerPalette>;
/**
* Add anything not palette related to the theme here
*/
interface NymTheme {}
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteAndNetworkExplorerPalette {}
interface PaletteOptions extends NymPaletteAndNetworkExplorerPaletteOptions {}
}
+248
View File
@@ -0,0 +1,248 @@
import { PaletteMode } from '@mui/material';
import {
PaletteOptions,
NymPalette,
NetworkExplorerPalette,
ThemeOptions,
createTheme,
NymPaletteVariant,
} from '@mui/material/styles';
//-----------------------------------------------------------------------------------------------
// Nym palette type definitions
//
/**
* The Nym palette.
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
const nymPalette: NymPalette = {
/** emphasises important elements */
highlight: '#FB6E4E',
text: {
nav: '#F2F2F2',
/** footer text colour */
footer: '#666B77',
},
};
const darkMode: NymPaletteVariant = {
mode: 'dark',
background: {
main: '#111826',
paper: '#242C3D',
},
text: {
main: '#F2F2F2',
},
topNav: {
background: '#111826',
},
nav: {
background: '#242C3D',
hover: '#111826',
},
};
const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#F2F2F2',
paper: '#FFFFFF',
},
text: {
main: '#666666',
},
topNav: {
background: '#111826',
},
nav: {
background: '#242C3D',
hover: '#111826',
},
};
/**
* Nym palette specific to the Network Explorer
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
const networkExplorerPalette = (
variant: NymPaletteVariant,
): NetworkExplorerPalette => ({
networkExplorer: {
/** world map styles */
map: {
stroke: '#333333',
fills: ['#EFEFEF', '#FBE7E1', '#F7D1C6', '#F09379'],
},
background: {
tertiary: variant.mode === 'light' ? '#F4F8FA' : '#323C51',
},
/** left nav styles */
nav: {
selected: {
main: '#111826',
nested: '#3C4558',
},
background: variant.nav.background,
hover: variant.nav.hover,
text: nymPalette.text.nav,
},
topNav: {
...variant.topNav,
socialIcons: '#F2F2F2',
},
footer: {
socialIcons:
variant.mode === 'light' ? nymPalette.text.footer : darkMode.text.main,
},
},
});
//-----------------------------------------------------------------------------------------------
// Nym palettes for light and dark mode
//
/**
* Map a Nym palette variant onto the MUI palette
*/
const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
text: {
primary: variant.text.main,
},
primary: {
main: nymPalette.highlight,
contrastText: '#fff',
},
background: {
default: variant.background.main,
paper: variant.background.paper,
},
});
/**
* Returns the Network Explorer palette for light mode.
*/
const createLightModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
...networkExplorerPalette(lightMode),
},
...variantToMUIPalette(lightMode),
});
/**
* Returns the Network Explorer palette for dark mode.
*/
const createDarkModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
...networkExplorerPalette(darkMode),
},
...variantToMUIPalette(darkMode),
});
/**
* IMPORANT: if you need to get the default MUI theme, use the following
*
* import { createTheme as systemCreateTheme } from '@mui/system';
*
* // get the MUI system defaults for light mode
* const systemTheme = systemCreateTheme({ palette: { mode: 'light' } });
*
*
* return {
* // change `primary` to default MUI `success`
* primary: {
* main: systemTheme.palette.success.main,
* },
* nym: {
* ...nymPalette,
* ...networkExplorerPalette,
* },
* };
*/
//-----------------------------------------------------------------------------------------------
// Nym theme overrides
//
/**
* Gets the theme options to be passed to `createTheme`.
*
* Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette.
*
* @param mode The theme mode: 'light' or 'dark'
*/
export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
// first, create the palette from user's choice of light or dark mode
const { palette } = createTheme({
palette: {
mode,
...(mode === 'light'
? createLightModePalette()
: createDarkModePalette()),
},
});
// then customise theme and components
return {
typography: {
fontFamily: [
'Open Sans',
'sans-serif',
'BlinkMacSystemFont',
'Roboto',
'Oxygen',
'Ubuntu',
'Helvetica Neue',
].join(','),
fontSize: 14,
fontWeightRegular: 600,
},
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
},
},
components: {
MuiCardHeader: {
styleOverrides: {
title: {
fontSize: 18,
fontWeight: 800,
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
background: palette.secondary.dark,
marginTop: 64,
},
},
},
MuiListItem: {
styleOverrides: {
root: {
background: palette.secondary.dark,
},
},
},
},
palette,
};
};
+167
View File
@@ -0,0 +1,167 @@
/* eslint-disable camelcase */
export interface ClientConfig {
url: string;
version: string;
}
export interface MixNode {
host: string;
location: string;
sphinx_key: string;
identity_key: string;
version: string;
mix_port: number;
verloc_port: number;
http_api_port: number;
}
export interface Gateway {
host: string;
mix_port: number;
clients_port: number;
location: string;
sphinx_key: string;
identity_key: string;
version: string;
}
export interface Amount {
denom: string;
amount: number;
}
export interface MixNodeResponseItem {
bond_amount: Amount;
total_delegation: Amount;
owner: string;
layer: string;
location: {
country_name: string;
lat: number;
lng: number;
three_letter_iso_country_code: string;
two_letter_iso_country_code: string;
};
mix_node: MixNode;
}
export type MixNodeResponse = MixNodeResponseItem[];
export interface MixNodeReportResponse {
identity: string;
owner: string;
most_recent_ipv4: boolean;
most_recent_ipv6: boolean;
last_hour_ipv4: number;
last_hour_ipv6: number;
last_day_ipv4: number;
last_day_ipv6: number;
}
export interface StatsResponse {
update_time: Date;
previous_update_time: Date;
packets_received_since_startup: number;
packets_sent_since_startup: number;
packets_explicitly_dropped_since_startup: number;
packets_received_since_last_update: number;
packets_sent_since_last_update: number;
packets_explicitly_dropped_since_last_update: number;
}
export type MixNodeHistoryResponse = StatsResponse;
export interface GatewayResponseItem {
block_height: number;
bond_amount: Amount;
total_delegation: Amount;
owner: string;
gateway: Gateway;
}
export type GatewayResponse = GatewayResponseItem[];
export interface GatewayReportResponse {
identity: string;
owner: string;
most_recent_ipv4: boolean;
most_recent_ipv6: boolean;
last_hour_ipv4: number;
last_hour_ipv6: number;
last_day_ipv4: number;
last_day_ipv6: number;
}
export type GatewayHistoryResponse = StatsResponse;
export interface MixNodeDescriptionResponse {
name: string;
description: string;
link: string;
location: string;
}
export type MixNodeStatsResponse = StatsResponse;
export interface Validator {
address: string;
proposer_priority: string;
pub_key: {
type: string;
value: string;
};
}
export interface ValidatorsResponse {
block_height: number;
count: string;
total: string;
validators: Validator[];
}
export type CountryData = {
ISO3: string;
nodes: number;
};
export type Delegation = {
owner: string;
amount: Amount;
block_height: number;
};
export type DelegationsResponse = Delegation[];
export interface CountryDataResponse {
[threeLetterCountryCode: string]: CountryData;
}
export type BlockType = number;
export type BlockResponse = BlockType;
export interface ApiState<RESPONSE> {
isLoading: boolean;
data?: RESPONSE;
error?: Error;
}
export type StatusResponse = {
pending: boolean;
ports: {
1789: boolean;
1790: boolean;
8000: boolean;
};
};
export type UptimeTime = {
date: string;
ipv4_uptime: number;
ipv6_uptime: number;
};
export type UptimeStoryResponse = {
history: UptimeTime[];
identity: string;
owner: string;
};
+8
View File
@@ -0,0 +1,8 @@
export type TableHeading = {
id: string;
numeric: boolean;
disablePadding: boolean;
label: string;
};
export type TableHeadingsType = TableHeading[];
+9
View File
@@ -0,0 +1,9 @@
declare module '*.jpeg' {
const value: any;
export default value;
}
declare module '*.jpg' {
const value: any;
export default value;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.json' {
const content: any;
export default content;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.png' {
const content: any;
export default content;
}
+1
View File
@@ -0,0 +1 @@
declare module 'react-tooltip';
+4
View File
@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}
+104
View File
@@ -0,0 +1,104 @@
/* eslint-disable camelcase */
import { MutableRefObject } from 'react';
import {
CountryData,
GatewayResponse,
MixNodeResponse,
} from 'src/typeDefs/explorer-api';
import { registerLocale, getName } from 'i18n-iso-countries';
registerLocale(require('i18n-iso-countries/langs/en.json'));
export function formatNumber(num: number): string {
return new Intl.NumberFormat().format(num);
}
export function scrollToRef(
ref: MutableRefObject<HTMLDivElement | undefined>,
): void {
if (ref?.current) ref.current.scrollIntoView();
}
export type MixnodeRowType = {
id: string;
owner: string;
location: string;
identity_key: string;
bond: number;
host: string;
layer: string;
};
export type GatewayRowType = {
id: string;
owner: string;
identity_key: string;
bond: number;
host: string;
location: string;
};
export type CountryDataRowType = {
id: number;
ISO3: string;
nodes: number;
countryName: string;
percentage: string;
};
export function countryDataToGridRow(
countriesData: CountryData[],
): CountryDataRowType[] {
const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0);
const formatted = countriesData.map((each: CountryData, index: number) => {
const updatedCountryRecord: CountryDataRowType = {
...each,
id: index,
countryName: getName(each.ISO3, 'en', { select: 'official' }),
percentage: ((each.nodes * 100) / totalNodes).toFixed(1),
};
return updatedCountryRecord;
});
const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1));
return sorted;
}
export function mixnodeToGridRow(
arrayOfMixnodes: MixNodeResponse,
): MixnodeRowType[] {
return !arrayOfMixnodes
? []
: arrayOfMixnodes.map((mn) => {
const pledge = Number(mn.bond_amount.amount) || 0;
const delegations = Number(mn.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
return {
id: mn.owner,
owner: mn.owner,
location: mn?.location?.country_name || '',
identity_key: mn.mix_node.identity_key || '',
bond: totalBond || 0,
self_percentage: selfPercentage,
host: mn.mix_node.host || '',
layer: mn.layer || '',
};
});
}
export function gatewayToGridRow(
arrayOfGateways: GatewayResponse,
): GatewayRowType[] {
return !arrayOfGateways
? []
: arrayOfGateways.map((gw) => ({
id: gw.owner,
owner: gw.owner,
identity_key: gw.gateway.identity_key || '',
location: gw?.gateway?.location || '',
bond: gw.bond_amount.amount || 0,
host: gw.gateway.host || '',
}));
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Material-UI SAS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
# @mui/x-data-grid
This package is the community edition of the data grid component.
It's part of Material-UI X, an open core extension of Material-UI, with advanced components.
## Installation
Install the package in your project directory with:
```sh
// with npm
npm install @mui/x-data-grid
// with yarn
yarn add @mui/x-data-grid
```
This component has two peer dependencies that you will need to install as well.
```json
"peerDependencies": {
"@material-ui/core": "^4.12.0 || ^5.0.0-beta.0",
"react": "^17.0.0"
},
```
## Documentation
[The documentation](https://material-ui.com/components/data-grid/)
+89
View File
@@ -0,0 +1,89 @@
{
"_from": "@mui/x-data-grid",
"_id": "@mui/x-data-grid@4.0.0",
"_inBundle": false,
"_integrity": "sha512-BYn7uLx5tJbMarcWltjjVArWNBdC22/2xOpq3Azhltbb3TRx3h2RLUeKwZI685xmGHmzvtu6QqaoYqQgFe/h+g==",
"_location": "/@mui/x-data-grid",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "@mui/x-data-grid",
"name": "@mui/x-data-grid",
"escapedName": "@mui%2fx-data-grid",
"scope": "@mui",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-4.0.0.tgz",
"_shasum": "e9e9c33a8b86e85872c48f30f4a8de72ee819153",
"_spec": "@mui/x-data-grid",
"_where": "/Users/adrianthompson/Documents/nym/explorer",
"author": {
"name": "MUI Team"
},
"bugs": {
"url": "https://github.com/mui-org/material-ui-x/issues"
},
"bundleDependencies": false,
"dependencies": {
"@material-ui/utils": "^5.0.0-beta.4",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"reselect": "^4.0.0"
},
"deprecated": false,
"description": "The community edition of the data grid component (Material-UI X).",
"engines": {
"node": ">=12.0.0"
},
"files": [
"dist/*"
],
"gitHead": "940d6238fe499f027ee27e818d714f79ca40e9a5",
"homepage": "https://material-ui.com/components/data-grid/",
"keywords": [
"react",
"react-component",
"material-ui",
"mui",
"react-table",
"table",
"datatable",
"data-table",
"datagrid",
"data-grid"
],
"license": "MIT",
"main": "dist/index-cjs.js",
"module": "dist/index-esm.js",
"name": "@mui/x-data-grid",
"peerDependencies": {
"@material-ui/core": "^4.12.0 || ^5.0.0-beta.0",
"@mui/styles": "^4.11.4 || ^5.0.0-beta.0",
"react": "^17.0.0"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mui-org/material-ui-x.git",
"directory": "packages/grid/data-grid"
},
"scripts": {
"build": "cd ../ && rollup --config rollup.data-grid.config.js",
"typescript": "tsc -p tsconfig.json"
},
"setupFiles": [
"<rootDir>/src/setupTests.js"
],
"sideEffects": false,
"types": "dist/data-grid.d.ts",
"version": "4.0.0"
}
+32
View File
@@ -0,0 +1,32 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": true,
"baseUrl": "."
},
"exclude": [
"node_modules",
"dist",
"jest.config.js",
"webpack.config.js",
"webpack.prod.js",
"webpack.common.js"
]
}
+86
View File
@@ -0,0 +1,86 @@
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const WebpackFavicons = require('webpack-favicons');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.mdx?$/,
use: ['babel-loader', './webpack/plugins/mdx-loader'],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2|md)$/i,
// More information here https://webpack.js.org/guides/asset-modules/
type: 'asset',
},
{
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
plugins: [new TsconfigPathsPlugin()],
alias: {
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
},
fallback: {
fs: false,
tls: false,
path: false,
http: false,
https: false,
stream: false,
crypto: false,
net: false,
zlib: false,
},
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, 'src/index.html'),
}),
new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
new WebpackFavicons({
src: 'src/logo.svg',
}),
],
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
};
+82
View File
@@ -0,0 +1,82 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'development',
entry: './src/index.tsx',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
options: {
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
transpileOnly: true,
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
// More information here https://webpack.js.org/guides/asset-modules/
type: 'asset',
},
{
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader',
},
],
},
plugins: [
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
new webpack.HotModuleReplacementPlugin(),
],
target: 'web',
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
historyApiFallback: true,
},
// recommended for faster rebuild
optimization: {
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
cache: {
type: 'filesystem',
buildDependencies: {
// restart on config change
config: ['./webpack.config.js'],
},
},
});
+42
View File
@@ -0,0 +1,42 @@
const { mergeWithRules } = require('webpack-merge');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'production',
// TODO: no source maps, add back
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
optimization: {
minimizer: [`...`, new CssMinimizerPlugin()],
splitChunks: {
chunks: 'all',
},
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
output: {
pathinfo: false,
filename: '[name].[contenthash].js',
},
});