diff --git a/README.md b/README.md
index 39ee474..c9e0342 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,62 @@
# Thornode Monitor
+The Monitor dashboard is used to collate information on active, standby and old nodes on the rune network it collates information
+from multiple sources and displays them on a single dashboard. This repo consists of both the front end (js react app)
+and the backend module (a python api server).
+
+1) Front end:
+ - This is all the code in this repository, minus the lambda functions folder and python backend folder
+ - Built using react
+ - It pulls data from the backend api
+
+2) Back end:
+ - Several parts to the backend
+ 1) mySQL database to store all the data
+ 2) python app responsible to collect data and provide an api access point to the front end
+
+
+ - The database is fairly simple and consists of 2 tables with the following structure
+
+
+ - thornode_monitor_global - holds network information
+ - primary_key - int
+ - maxHeight - int (current blockheight used to pull other data)
+ - retiring - boolean (indicates if a vault is retiring)
+ - coingecko - string (json string containing price/trading info pulled from coingecko api)
+ - lastChurn - int (block of last churn)
+ - secondsPerBlock - float
+ - churnInterval - int
+ - BadValidatorRedline - int
+
+
+ - thornode_monitor - holds information on each node in the network
+ - node_address - string (primary key)
+ - active_block_height - int (block at which the node became active)
+ - bond_provider - string (json string containing information on bonds and providers)
+ - bond -int (amount bonded)
+ - current_award - int (amount awarded this epoch)
+ - slash_points - int (slash points this epoch)
+ - forced_to_leave - boolean
+ - requested_to_leave - boolean
+ - jail - string (json string containing information on any past jailing)
+ - observe_chains - string (json string containing block heights for downstream chains)
+ - preflight_status - string
+ - status - string
+ - status_since - int
+ - version - string
+ - ip_address - string
+ - location - string
+ - isp - string
+ - rpc - string (json string of response from health rpc calls)
+ - bifrost - string (bifrost p2pid)
+
+
+ - The python app runs off a main loop which pulls data every 20 seconds and updates the DB, it listens to api calls to
+ /api/grabData and returns the data in json.
+
+
+# Old deployment via AWS lambda (no longer maintained in this fork)
+
There are several moving parts to this dashboard
1) Front end:
diff --git a/backend python/backend.py b/backend python/backend.py
new file mode 100644
index 0000000..e206ecd
--- /dev/null
+++ b/backend python/backend.py
@@ -0,0 +1,55 @@
+import time
+
+from common import grabQuery
+from flask import Flask
+from flask_cors import CORS, cross_origin
+from thormonitor_collect_data import gradDataAndSaveToDB
+from thormonitor_update_ips import updateIPs
+from thornode_collect_data_global import collectDataGlobal
+from thormonitor_collect_data_rpc_bifrost import biFrostGrabDataAndSaveToDB
+
+from threading import Thread
+
+app = Flask(__name__)
+cors = CORS(app)
+app.config['CORS_HEADERS'] = 'Content-Type'
+
+
+def flaskThread():
+ app.run(host='0.0.0.0', port=6000)
+
+
+@app.route('/thor/api/grabData', methods=['GET'])
+@cross_origin()
+def grabData():
+ """
+ grabData is used to output the DB in json format, fires on api accesses
+
+ return: json containing the current data from thornode_monitor and thornode_monitor_global tables
+ """
+ currentDBData = (grabQuery('SELECT * FROM noderunner.thornode_monitor'))
+ globalData = (grabQuery('SELECT * FROM noderunner.thornode_monitor_global'))
+
+ return {'data': currentDBData, 'globalData': globalData[0]}
+
+
+def main():
+ """
+ main contains the main loop which simply spins every minuite and update the various DBs
+ """
+ worker = Thread(target=flaskThread)
+ worker.start()
+
+ while (1):
+ try:
+ gradDataAndSaveToDB()
+ updateIPs()
+ collectDataGlobal()
+ biFrostGrabDataAndSaveToDB()
+ except Exception as e:
+ print(e)
+ time.sleep(60)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend python/common.py b/backend python/common.py
new file mode 100644
index 0000000..2b43d2b
--- /dev/null
+++ b/backend python/common.py
@@ -0,0 +1,51 @@
+from dotenv import load_dotenv
+import mysql.connector
+import os
+
+HOST = os.getenv('host')
+USER = os.getenv('user')
+PASSWORD = os.getenv('password')
+DATABASE = os.getenv('db')
+
+load_dotenv()
+
+
+def getDB():
+ """
+ getDB is used to get a sql connector for the db defined in the env file
+
+ :return: db connection
+ """
+ dbConnection = mysql.connector.connect(host=HOST, user=USER, password=PASSWORD, database=DATABASE)
+ return dbConnection
+
+
+
+def commitQuery(query):
+ """
+ commitQuery is used to update items in the database
+
+ :param query: SQL query to be run against the DB
+ """
+ db = getDB()
+ cursor = db.cursor(prepared=True, dictionary=True)
+ cursor.execute(query)
+ db.commit()
+ cursor.close()
+ db.close()
+
+
+def grabQuery(query):
+ """
+ grabQuery is used to grab data from the DB
+
+ :param query: SQL query to be run against the DB
+ :return: returns the output of the query in dictionary format
+ """
+ db = getDB()
+ cursor = db.cursor(prepared=True, dictionary=True)
+ cursor.execute(query)
+ data = cursor.fetchall()
+ cursor.close()
+ db.close()
+ return data
diff --git a/backend python/thormonitor_collect_data.py b/backend python/thormonitor_collect_data.py
new file mode 100644
index 0000000..bc5c671
--- /dev/null
+++ b/backend python/thormonitor_collect_data.py
@@ -0,0 +1,148 @@
+import time
+
+import requests
+import json
+import random
+from common import commitQuery, grabQuery
+
+
+def grabLatestBlockHeight(nodes):
+ """
+ grabLatestBlockHeight looks at 3 random nodes in the active pool and returns the max height from those 3 nodes
+
+ :param nodes: all thor nodes currently on the network pulled from ninerelms api
+
+ :return: the latest block height
+ """
+ activeNodes = [x for x in nodes if "Active" == x['status']]
+
+ status_code = 0
+ while status_code != 200:
+ randomOffsets = [random.randint(0, len(activeNodes) - 1), random.randint(0, len(activeNodes) - 1),
+ random.randint(0, len(activeNodes) - 1)]
+
+ status = []
+ for i in range(0, len(randomOffsets)):
+ try:
+ state = requests.get('http://' + activeNodes[randomOffsets[i]]['ip_address'] + ":27147/status?",
+ timeout=5)
+ if state.status_code == 200:
+ status.append(json.loads(state.text))
+ except Exception as e:
+ print("timed out")
+
+ #check if we have any blocks, if yes break from loop if not try another 3 random nodes
+ if len(status) != 0:
+ status_code = 200
+
+ blocks = [x['result']['sync_info']['latest_block_height'] for x in status]
+
+ return max(blocks)
+
+
+def splitNodes(nodes):
+ """
+ splitNodes compares the list of nodes currently in the DB and what is returned by ninerelms API
+
+ :param nodes: all thor nodes currently on the network pulled from ninerelms api
+
+ :return dataForExistingNodes: list of nodes already in our DB
+ :return dataForNewNodes: list of nodes not already in our DB
+ """
+ currentDBData = (grabQuery('SELECT * FROM noderunner.thornode_monitor'))
+ fullAddrList = [x['node_address'] for x in nodes]
+ currentAddrList = [x['node_address'] for x in currentDBData]
+ newList = list(set(fullAddrList).symmetric_difference(set(currentAddrList)))
+
+ dataForExistingNodes = [x for x in nodes if x['node_address'] in currentAddrList]
+ dataForNewNodes = [x for x in nodes if x['node_address'] in newList]
+
+ return dataForExistingNodes, dataForNewNodes
+
+
+def gradDataAndSaveToDB():
+ """
+ gradDataAndSaveToDB used to update thornode_monitor_global database
+ """
+ response_API = requests.get('https://thornode.ninerealms.com/thorchain/nodes')
+ data = json.loads(response_API.text)
+ # sanitise data remove any empty elements
+ nodes = [x for x in data if '' != x['node_address']]
+
+ maxHeight = grabLatestBlockHeight(nodes)
+
+ query = 'UPDATE noderunner.thornode_monitor_global SET maxHeight = {field} WHERE primary_key = 1;'.format(
+ field=maxHeight)
+ commitQuery(query)
+
+ # check if we are in a churn
+
+ dataForExistingNodes, dataForNewNodes = splitNodes(nodes)
+ for node in dataForExistingNodes:
+ query = "UPDATE noderunner.thornode_monitor SET " \
+ "active_block_height = '{active_block_height}'," \
+ "bond_providers = '{bond_providers}'," \
+ "bond = '{bond}'," \
+ "current_award = '{current_award}'," \
+ "slash_points = '{slash_points}'," \
+ "forced_to_leave = '{forced_to_leave}'," \
+ "requested_to_leave = '{requested_to_leave}'," \
+ "jail = '{jail}' ," \
+ "bond_address = '{bond_address}'," \
+ "observe_chains = '{observe_chains}'," \
+ "preflight_status = '{preflight_status}'," \
+ "status = '{status}'," \
+ "status_since = '{status_since}'," \
+ "version = '{version}' WHERE (node_address = '{node_address}');".format(
+ active_block_height=node['active_block_height'],
+ bond_providers=json.dumps(node['bond_providers']),
+ bond=int(node['total_bond']),
+ current_award=int(node['current_award']),
+ slash_points=node['slash_points'],
+ forced_to_leave=int(node['forced_to_leave']),
+ requested_to_leave=int(node['requested_to_leave']),
+ jail=json.dumps(node['jail']), bond_address='',
+ observe_chains=json.dumps(node['observe_chains']),
+ preflight_status=json.dumps(node['preflight_status']),
+ status=node['status'],
+ status_since=node['status_since'], version=node['version'], node_address=node['node_address'])
+ commitQuery(query)
+
+ # Loop over new nodes and grab IP addr
+ for node in dataForNewNodes:
+ if node['ip_address'] != "":
+ response_code = 0
+ while response_code != 200:
+ response = requests.get("http://ip-api.com/json/" + node['ip_address'])
+ response_code = response.status_code
+ if response_code == 429:
+ print("rate limited wait 60seconds")
+ time.sleep(60)
+ elif response_code == 200:
+ ip_data = json.loads(response.text)
+ node['ip_data'] = ip_data
+
+ else:
+ node['ip_data'] = {}
+ node['ip_data']['city'] = ""
+ node['ip_data']['isp'] = ""
+
+ query = "INSERT INTO noderunner.thornode_monitor (node_address, ip_address, location, isp, " \
+ "active_block_height, bond_providers, bond, current_award, slash_points,forced_to_leave, " \
+ "requested_to_leave, jail, bond_address, observe_chains, preflight_status, status, " \
+ "status_since, version) VALUES ('{node_address}', '{ip_address}','{city}','{isp}'," \
+ "'{active_block_height}','{bond_providers}','{bond}','{current_award}','{slash_points}'," \
+ "'{forced_to_leave}','{requested_to_leave}', '{jail}','{bond_address}'," \
+ "'{observe_chains}','{preflight_status}','{status}','{status_since}'," \
+ "'{version}')".format(node_address=node['node_address'], ip_address=node['ip_address'],
+ city=node['ip_data']['city'], isp=node['ip_data']['isp'],
+ active_block_height=node['active_block_height'],
+ bond_providers=json.dumps(node['bond_providers']), bond=int(node['total_bond']),
+ current_award=int(node['current_award']), slash_points=node['slash_points'],
+ forced_to_leave=int(node['forced_to_leave']),
+ requested_to_leave=int(node['requested_to_leave']),
+ jail=json.dumps(node['jail']), bond_address='',
+ observe_chains=json.dumps(node['observe_chains']),
+ preflight_status=json.dumps(node['preflight_status']), status=node['status'],
+ status_since=node['status_since'], version=node['version'])
+ commitQuery(query)
diff --git a/backend python/thormonitor_collect_data_rpc_bifrost.py b/backend python/thormonitor_collect_data_rpc_bifrost.py
new file mode 100644
index 0000000..7964afe
--- /dev/null
+++ b/backend python/thormonitor_collect_data_rpc_bifrost.py
@@ -0,0 +1,87 @@
+import requests
+import json
+from common import commitQuery, grabQuery
+from multiprocessing import Queue
+
+import time
+from threading import Thread
+
+def requestThread(data, Queue):
+ """
+ requestThread thread to grab p2p id and health of a given node
+ :param data: node to grab info for
+ :param Queue: queue to push output too
+ """
+ if data['ip_address'] != '':
+ bifrostURL = "http://" + data['ip_address'] + ":6040/p2pid"
+ healthUrl = "http://" + data['ip_address'] + ":27147/health?"
+ bifrost = ""
+ health = ""
+
+ try:
+ state = requests.get(bifrostURL, timeout=2)
+ if state.status_code == 200:
+ bifrost = (state.text)
+ state = requests.get(healthUrl, timeout=2)
+ if state.status_code == 200:
+ health = (json.loads(state.text))
+
+ dataReturn = {'node_address': data['node_address'], 'bifrost': bifrost, 'rpc': health,
+ 'bifrostURL': bifrostURL, 'healthURL': healthUrl}
+ Queue.put(dataReturn)
+ except Exception as e:
+ return
+
+
+def biFrostGrabDataAndSaveToDB():
+ """
+ biFrostGrabDataAndSaveToDB used to update rpc and bifrost info in thornode_monitor
+ """
+ responseQueue = Queue()
+ currentDBData = (grabQuery('SELECT * FROM noderunner.thornode_monitor'))
+ fullAddrList = [x['node_address'] for x in currentDBData]
+ currentAddrList = []
+ threads = list()
+ for node in currentDBData:
+ # print("create and start thread ", str(index))
+ x = Thread(target=requestThread,
+ args=(node, responseQueue))
+ threads.append(x)
+
+ for index, thread in enumerate(threads):
+ thread.start()
+ if index % 20 == 0:
+ time.sleep(3)
+
+ for index, thread in enumerate(threads):
+ thread.join()
+
+ while not responseQueue.empty():
+ resp = responseQueue.get()
+ currentAddrList.append(resp['node_address'])
+ if len(resp['rpc']['result'] == 0):
+ query = "UPDATE noderunner.thornode_monitor SET " \
+ "rpc = '{rpc}', bifrost = '{bifrost}' " \
+ "WHERE (node_address = '{address}');".format(rpc=json.dumps(resp['rpc']),bifrost=resp['bifrost'],address=resp['node_address'])
+
+ commitQuery(query)
+ else:
+ #rpc has an error so report as bad
+ query = "UPDATE noderunner.thornode_monitor SET " \
+ "rpc = '{rpc}', bifrost = '{bifrost}' " \
+ "WHERE (node_address = '{address}');".format(rpc="null", bifrost=resp['bifrost'],
+ address=resp['node_address'])
+
+ commitQuery(query)
+
+ # Collect a list of all node which have been missed in the queue, this indicates that there were errors pulling the data
+ # inside the thread so report any missing as null
+ newList = list(set(fullAddrList).symmetric_difference(set(currentAddrList)))
+
+ for node in newList:
+ query = "UPDATE noderunner.thornode_monitor SET " \
+ "rpc = '{rpc}', bifrost = '{bifrost}' " \
+ "WHERE (node_address = '{address}');".format(rpc="null", bifrost="null",
+ address=node)
+
+ commitQuery(query)
\ No newline at end of file
diff --git a/backend python/thormonitor_update_ips.py b/backend python/thormonitor_update_ips.py
new file mode 100644
index 0000000..ffdc7a1
--- /dev/null
+++ b/backend python/thormonitor_update_ips.py
@@ -0,0 +1,46 @@
+import time
+
+import requests
+import json
+from common import grabQuery, commitQuery
+
+def updateIPs():
+ """
+ updateIPs looks to see if IPs on chain don't match what is in the DB, if they don't match pull location and isp
+ info and update the db
+ """
+ currentDBData = (grabQuery('SELECT * FROM noderunner.thornode_monitor'))
+ # build IP table
+ ipTableOld = {}
+ for node in currentDBData:
+ ipTableOld[node['node_address']] = node['ip_address']
+
+
+ response_API = requests.get('https://thornode.ninerealms.com/thorchain/nodes')
+ newData = json.loads(response_API.text)
+ ipTableNew = {}
+ for node in newData:
+ ipTableNew[node['node_address']] = node['ip_address']
+
+ #check for any missmatches
+ mismatch = {}
+ for key in ipTableNew:
+ if key in ipTableOld:
+ if ipTableNew[key] != ipTableOld[key]:
+ mismatch[key] = ipTableNew[key]
+
+ for key in mismatch:
+ if mismatch[key] != "":
+ response_code = 0
+ while response_code != 200:
+ response = requests.get("http://ip-api.com/json/" + mismatch[key])
+ response_code = response.status_code
+ if response_code == 429:
+ print("rate limited wait 60seconds")
+ time.sleep(60)
+ elif response_code == 200:
+ ip_data = json.loads(response.text)
+ query = "UPDATE noderunner.thornode_monitor SET ip_address = '{ip}', location = '{city}', " \
+ "isp = '{isp}' WHERE node_address = '{node_address}' ".format(ip=mismatch[key],city=ip_data['city'],isp=ip_data['isp'],node_address=key)
+
+ commitQuery(query)
diff --git a/backend python/thornode_collect_data_global.py b/backend python/thornode_collect_data_global.py
new file mode 100644
index 0000000..7a16689
--- /dev/null
+++ b/backend python/thornode_collect_data_global.py
@@ -0,0 +1,164 @@
+import requests
+import json
+import math
+import datetime
+from common import getDB, commitQuery, grabQuery
+
+def getAndSaveBlockTime(height):
+ """
+ getAndSaveBlockTime looks over the last 100 blocks and passes back the average block time
+
+ :param height: current block height
+
+ :return avgBlock: the average block time over the last 100 blocks
+ """
+ url1 = "https://rpc.ninerealms.com/block?height="+str(height)
+ url2 = "https://rpc.ninerealms.com/block?height="+str(height-100)
+
+ bt1_resp = requests.get(url1, timeout=5)
+ bt1 = json.loads(bt1_resp.text)["result"]["block"]["header"]["time"]
+ date_format = datetime.datetime.strptime(bt1.split('.', 1)[0] + 'Z', "%Y-%m-%dT%H:%M:%SZ")
+ bt1_unix = datetime.datetime.timestamp(date_format)
+
+ bt2_resp = requests.get(url2, timeout=5)
+ bt2 = json.loads(bt2_resp.text)["result"]["block"]["header"]["time"]
+ date_format = datetime.datetime.strptime(bt2.split('.', 1)[0] + 'Z', "%Y-%m-%dT%H:%M:%SZ")
+ bt2_unix = datetime.datetime.timestamp(date_format)
+
+ diff = bt1_unix - bt2_unix
+ avgBlock = str(diff/100)
+ query = "UPDATE noderunner.thornode_monitor_global SET secondsPerBlock = '{field}' WHERE primary_key = 1;".format(field=avgBlock)
+ commitQuery(query)
+ return avgBlock
+
+def getCoinGeckoInfoAndSave():
+ """
+ getCoinGeckoInfoAndSave pulls thor data from coingecko api and save to thornode_monitor_global DB
+
+ :return avgBlock: the average block time over the last 100 blocks
+ """
+ url = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=thorchain&order=market_cap_desc' \
+ '&per_page=100&page=1&sparkline=false '
+ resp = requests.get(url)
+ data = json.loads(resp.text)
+ query = "UPDATE noderunner.thornode_monitor_global SET coingecko = '{field}' WHERE primary_key = 1;".format(field=json.dumps(data))
+ commitQuery(query)
+ return data
+
+def getAndSaveLastChurn():
+ """
+ getAndSaveLastChurn update the last churn block in the db
+
+ :return status_since: returns the last churn block
+ """
+ url = 'https://thornode.ninerealms.com/thorchain/vaults/asgard'
+ resp = requests.get(url)
+ status_since = json.loads(resp.text)[0]["status_since"]
+ query = "UPDATE noderunner.thornode_monitor_global SET lastChurn = '{field}' WHERE primary_key = 1;".format(field=int(status_since))
+ commitQuery(query)
+ return status_since
+
+def checkRetiringVaults():
+ """
+ checkRetiringVaults checks if any vaults are returing and updates the DB accordingly
+
+ :return areWeRetiring: returns the retiring flag
+ """
+ url = 'https://thornode.ninerealms.com/thorchain/vaults/asgard'
+ resp = requests.get(url)
+ data = json.loads(resp.text)
+ areWeRetiring = len([x for x in data if "RetiringVault" == x['status']]) > 0
+
+ query = "UPDATE noderunner.thornode_monitor_global SET retiring = '{field}' WHERE primary_key = 1;".format(field=int(areWeRetiring))
+ commitQuery(query)
+ return areWeRetiring
+
+def grabMaxEffectiveBond():
+ """
+ grabMaxEffectiveStake used to calculate and update the max effective bond
+
+ :return maxBond: returns the current maxEffectiveBond
+ """
+ response_API = requests.get('https://thornode.ninerealms.com/thorchain/nodes')
+ data = json.loads(response_API.text)
+ nodes = sorted([int(x['total_bond']) for x in data if "Active" == x['status']])
+ position = math.ceil((len(nodes)+1)*2/3)
+ maxBond = nodes[position]
+ query = "UPDATE noderunner.thornode_monitor_global SET maxEffectiveStake = '{field}' WHERE primary_key = 1;".format(field=int(maxBond))
+ commitQuery(query)
+ return maxBond
+
+def cleanUpDB():
+ """
+ cleanUpDB purges our DB of nodes they are no longer present
+
+ :return: returns true if purged false if nothing to purge
+ """
+ currentDBData = (grabQuery('SELECT * FROM noderunner.thornode_monitor'))
+ currentAddrList = [x['node_address'] for x in currentDBData]
+
+ response_API = requests.get('https://thornode.ninerealms.com/thorchain/nodes')
+ data = json.loads(response_API.text)
+ # sanitise data remove any empty elements
+ nodes = [x for x in data if '' != x['node_address']]
+ nineRelms = [x['node_address'] for x in nodes]
+
+ removeList = list(set(currentAddrList).symmetric_difference(set(nineRelms)))
+ if len(removeList) == 0:
+ return False
+
+ toRemoveString = "'"+"', '".join(removeList)+"'"
+ query = "DELETE FROM noderunner.thornode_monitor where node_address IN {field}".format(
+ field=toRemoveString)
+
+ commitQuery(query)
+ return True
+
+def getConstants():
+ """
+ getConstants grabs the current vaules of CHURNINTERVAL and BADVALIDATORREDLINE and updates the DB
+
+ :returns CHURNINTERVAL BADVALIDATORREDLINE
+ """
+ url = "https://thornode.ninerealms.com/thorchain/mimir"
+ response_API = requests.get(url)
+ data = json.loads(response_API.text)
+
+ query = "UPDATE noderunner.thornode_monitor_global SET churnInterval = '{field}' WHERE primary_key = 1;".format(
+ field=data['CHURNINTERVAL'])
+ commitQuery(query)
+
+ query = "UPDATE noderunner.thornode_monitor_global SET BadValidatorRedline = '{field}' WHERE primary_key = 1;".format(
+ field=data['BADVALIDATORREDLINE'])
+ commitQuery(query)
+
+ return data['CHURNINTERVAL'],data['BADVALIDATORREDLINE']
+
+def collectDataGlobal():
+ """
+ collectDataGlobal update the thornode_monitor_global DB
+ """
+ #Grab the current block height
+ height = grabQuery('SELECT maxHeight FROM noderunner.thornode_monitor_global')[0]['maxHeight']
+
+ constants = getConstants()
+
+ # Calculate average block time over the past 100 blocks
+ secondsPerBlock = getAndSaveBlockTime(height)
+
+ # Grab the latest coingecko info
+ getCoinGeckoInfo = getCoinGeckoInfoAndSave()
+
+ # Grab the last churn block
+ lastChurn = getAndSaveLastChurn()
+
+ # Check for any retired vaults
+ retiringVault = checkRetiringVaults()
+
+ # Clean up any old nodes from the database
+ cleaned = cleanUpDB()
+
+ maxBond = grabMaxEffectiveBond()
+
+ print(str(secondsPerBlock),str(getCoinGeckoInfo),str(lastChurn),str(retiringVault),str(cleaned), str(maxBond))
+
diff --git a/build/static/media/heart-blank.8c7b45fc.png b/build/static/media/heart-blank.8c7b45fc.png
index ffe73a1..bf669f1 100644
Binary files a/build/static/media/heart-blank.8c7b45fc.png and b/build/static/media/heart-blank.8c7b45fc.png differ
diff --git a/build/static/media/heart-full.95f2338a.png b/build/static/media/heart-full.95f2338a.png
index e5ecd04..5e082d2 100644
Binary files a/build/static/media/heart-full.95f2338a.png and b/build/static/media/heart-full.95f2338a.png differ
diff --git a/public/index.html b/public/index.html
index 9127c14..c7b77c4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -31,11 +31,10 @@
-->
-
+
+
+
+
+
+
+
diff --git a/src/assets/images/atandt.png b/src/assets/images/atandt.png
new file mode 100644
index 0000000..8b7576c
Binary files /dev/null and b/src/assets/images/atandt.png differ
diff --git a/src/assets/images/atom.png b/src/assets/images/atom.png
index bd53d9d..62ef35f 100644
Binary files a/src/assets/images/atom.png and b/src/assets/images/atom.png differ
diff --git a/src/assets/images/avax.png b/src/assets/images/avax.png
new file mode 100644
index 0000000..73ff507
Binary files /dev/null and b/src/assets/images/avax.png differ
diff --git a/src/assets/images/binance.png b/src/assets/images/binance.png
index a0862bf..b21240e 100644
Binary files a/src/assets/images/binance.png and b/src/assets/images/binance.png differ
diff --git a/src/assets/images/chartercoms.png b/src/assets/images/chartercoms.png
new file mode 100644
index 0000000..c4641f0
Binary files /dev/null and b/src/assets/images/chartercoms.png differ
diff --git a/src/assets/images/choopa.png b/src/assets/images/choopa.png
new file mode 100644
index 0000000..051ab88
Binary files /dev/null and b/src/assets/images/choopa.png differ
diff --git a/src/assets/images/comcast.png b/src/assets/images/comcast.png
new file mode 100644
index 0000000..5cea70c
Binary files /dev/null and b/src/assets/images/comcast.png differ
diff --git a/src/assets/images/datacamp.png b/src/assets/images/datacamp.png
new file mode 100644
index 0000000..33e62e9
Binary files /dev/null and b/src/assets/images/datacamp.png differ
diff --git a/src/assets/images/eth.png b/src/assets/images/eth.png
index 45a7c8b..81bc9e9 100644
Binary files a/src/assets/images/eth.png and b/src/assets/images/eth.png differ
diff --git a/src/assets/images/leaseweb.png b/src/assets/images/leaseweb.png
new file mode 100644
index 0000000..49d84eb
Binary files /dev/null and b/src/assets/images/leaseweb.png differ
diff --git a/src/assets/images/overview/24high_trading.svg b/src/assets/images/overview/24high_trading.svg
new file mode 100644
index 0000000..b93c46d
--- /dev/null
+++ b/src/assets/images/overview/24high_trading.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/24low_trading.svg b/src/assets/images/overview/24low_trading.svg
new file mode 100644
index 0000000..edcd752
--- /dev/null
+++ b/src/assets/images/overview/24low_trading.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/Bond_icon.svg b/src/assets/images/overview/Bond_icon.svg
new file mode 100644
index 0000000..799494b
--- /dev/null
+++ b/src/assets/images/overview/Bond_icon.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/overview/active_icon.svg b/src/assets/images/overview/active_icon.svg
new file mode 100644
index 0000000..de4d0ce
--- /dev/null
+++ b/src/assets/images/overview/active_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/arrow-down.svg b/src/assets/images/overview/arrow-down.svg
new file mode 100644
index 0000000..54cad1a
--- /dev/null
+++ b/src/assets/images/overview/arrow-down.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/overview/block_icon.svg b/src/assets/images/overview/block_icon.svg
new file mode 100644
index 0000000..8a93fbf
--- /dev/null
+++ b/src/assets/images/overview/block_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/overview/churns_icon.svg b/src/assets/images/overview/churns_icon.svg
new file mode 100644
index 0000000..1dc1593
--- /dev/null
+++ b/src/assets/images/overview/churns_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/dots_three_circle.svg b/src/assets/images/overview/dots_three_circle.svg
new file mode 100644
index 0000000..4a095bd
--- /dev/null
+++ b/src/assets/images/overview/dots_three_circle.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/overview/filter.svg b/src/assets/images/overview/filter.svg
new file mode 100644
index 0000000..367fdf5
--- /dev/null
+++ b/src/assets/images/overview/filter.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/github_link_icon.svg b/src/assets/images/overview/github_link_icon.svg
new file mode 100644
index 0000000..95da7c3
--- /dev/null
+++ b/src/assets/images/overview/github_link_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/hex_map_bg.png b/src/assets/images/overview/hex_map_bg.png
new file mode 100644
index 0000000..3114300
Binary files /dev/null and b/src/assets/images/overview/hex_map_bg.png differ
diff --git a/src/assets/images/overview/liquify_bg.png b/src/assets/images/overview/liquify_bg.png
new file mode 100644
index 0000000..2f1a924
Binary files /dev/null and b/src/assets/images/overview/liquify_bg.png differ
diff --git a/src/assets/images/overview/liquify_logo.svg b/src/assets/images/overview/liquify_logo.svg
new file mode 100644
index 0000000..31df643
--- /dev/null
+++ b/src/assets/images/overview/liquify_logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/overview/loading.png b/src/assets/images/overview/loading.png
new file mode 100644
index 0000000..9f797af
Binary files /dev/null and b/src/assets/images/overview/loading.png differ
diff --git a/src/assets/images/overview/marcket_Cap.svg b/src/assets/images/overview/marcket_Cap.svg
new file mode 100644
index 0000000..3160952
--- /dev/null
+++ b/src/assets/images/overview/marcket_Cap.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/overview/mcap_rank.svg b/src/assets/images/overview/mcap_rank.svg
new file mode 100644
index 0000000..231d307
--- /dev/null
+++ b/src/assets/images/overview/mcap_rank.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/overview/power.svg b/src/assets/images/overview/power.svg
new file mode 100644
index 0000000..d517844
--- /dev/null
+++ b/src/assets/images/overview/power.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/rune_usdt.svg b/src/assets/images/overview/rune_usdt.svg
new file mode 100644
index 0000000..8eba921
--- /dev/null
+++ b/src/assets/images/overview/rune_usdt.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/overview/sort.svg b/src/assets/images/overview/sort.svg
new file mode 100644
index 0000000..3326ca5
--- /dev/null
+++ b/src/assets/images/overview/sort.svg
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/overview/time_icon.svg b/src/assets/images/overview/time_icon.svg
new file mode 100644
index 0000000..0de9ea7
--- /dev/null
+++ b/src/assets/images/overview/time_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/overview/total_supply.svg b/src/assets/images/overview/total_supply.svg
new file mode 100644
index 0000000..f7aa413
--- /dev/null
+++ b/src/assets/images/overview/total_supply.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/overview/twitter_link_icon.svg b/src/assets/images/overview/twitter_link_icon.svg
new file mode 100644
index 0000000..341cc4b
--- /dev/null
+++ b/src/assets/images/overview/twitter_link_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/thornode.svg b/src/assets/images/thornode.svg
new file mode 100644
index 0000000..45ec232
--- /dev/null
+++ b/src/assets/images/thornode.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/zenlayer.png b/src/assets/images/zenlayer.png
new file mode 100644
index 0000000..e54e33b
Binary files /dev/null and b/src/assets/images/zenlayer.png differ
diff --git a/src/assets/styles/globalStyle.js b/src/assets/styles/globalStyle.js
index a3ba055..471ca3a 100644
--- a/src/assets/styles/globalStyle.js
+++ b/src/assets/styles/globalStyle.js
@@ -4,7 +4,20 @@ import { palette, font } from 'styled-theme';
const GlobalStyles = createGlobalStyle`
.ant-btn{
- border-radius: 4px;
+ border-radius: 7px;
+ }
+
+ .ant-btn-primary{
+ background: #1C39BB;
+ border-color: #1C39BB;
+ }
+
+ .ant-switch-checked {
+ background: #1C39BB;
+ }
+
+ .uppercase{
+ text-transform: uppercase;
}
.header {
@@ -47,7 +60,7 @@ const GlobalStyles = createGlobalStyle`
.ant-select-dropdown-menu-item {
padding: 8px 12px;
color: #000000;
- font-family: 'Roboto';
+ font-family: 'Montserrat';
font-weight: 400;
}
}
@@ -199,6 +212,8 @@ svg {
body {
-webkit-overflow-scrolling: touch;
+ color: #182233;
+ letter-spacing: -0.015em;
}
html h1,
@@ -218,7 +233,7 @@ html,
body,
html a {
margin-bottom: 0;
- font-family: 'Roboto', sans-serif;
+ font-family: 'Montserrat', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
diff --git a/src/aws-exports.js b/src/aws-exports.js
new file mode 100644
index 0000000..2c23a5c
--- /dev/null
+++ b/src/aws-exports.js
@@ -0,0 +1,3 @@
+// eslint-disable-next-line no-lone-blocks
+{
+}
\ No newline at end of file
diff --git a/src/commonFunctions.js b/src/commonFunctions.js
index 3b65717..137a5c3 100644
--- a/src/commonFunctions.js
+++ b/src/commonFunctions.js
@@ -6,6 +6,8 @@ import { awsConfig } from './aws-exports';
import moment from 'moment';
import semver from 'semver';
+import axios from 'axios';
+
import { store } from './redux/store';
import authActions from '@iso/redux/auth/actions';
@@ -17,7 +19,15 @@ export const getData = async () => {
//const [firstLoad, setFirstLoad] = useState(false);
const blockTime = 6 //6 seconds Need to calc this properly
- const val = await API.get('MyAWSApi', '');
+ // const val = await API.get('MyAWSApi', '');
+ // const val = prodData;
+ var val = {};
+ try {
+ const res = await fetch("https://api.liquify.com/thor/api/grabData");
+ val = await res.json();
+ } catch (error) {
+ val = {};
+ }
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
console.log('DEV ONLY: Raw getData API Call Results: ', val)
@@ -108,16 +118,17 @@ export const getData = async () => {
const maxBCHHeight = reduceDown(data2, 'BCH')
const maxBNBHeight = reduceDown(data2, 'BNB')
const maxGAIAHeight = reduceDown(data2, 'GAIA')
+ const maxAVAXHeight = reduceDown(data2, 'AVAX')
const totalBondedValue = (val.data.map(item => item.bond).reduce((prev, next) => prev + next))/100000000;
globalData.totalBondedValue = totalBondedValue;
- return {data: val.data, globalData: globalData, maxChainHeights: {BTC: maxBTCHeight, DOGE: maxDogeHeight, ETH: maxEthHeight, LTC: maxLTCHeight, GAIA: maxGAIAHeight, BCH: maxBCHHeight, BNB: maxBNBHeight}}
+ return {data: val.data, globalData: globalData, maxChainHeights: {BTC: maxBTCHeight, DOGE: maxDogeHeight, ETH: maxEthHeight, LTC: maxLTCHeight, GAIA: maxGAIAHeight, BCH: maxBCHHeight, BNB: maxBNBHeight, AVAX: maxAVAXHeight}}
}
export const refreshData = async () => {
+ return false
- apiCall('getUserData')
.then(results => {
store.dispatch(authActions.saveData(results))
diff --git a/src/components/VisibleColumn/VisibleColumn.js b/src/components/VisibleColumn/VisibleColumn.js
new file mode 100644
index 0000000..7a9598f
--- /dev/null
+++ b/src/components/VisibleColumn/VisibleColumn.js
@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import { Modal, Button, Switch } from 'antd';
+
+import filterIcon from '@iso/assets/images/overview/filter.svg';
+
+function VisibleColumn({ initialConfig, onConfigUpdate }) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [config, setConfig] = useState({
+ ...initialConfig
+ });
+
+ const handleConfigChange = (key, value) => {
+ setConfig(prevConfig => ({ ...prevConfig, [key]: value }));
+ };
+
+ const handleOk = () => {
+ setIsModalVisible(false);
+ console.log('Updated config:', config);
+ if (onConfigUpdate) {
+ onConfigUpdate(config);
+ }
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ const handleOpenModal = () => {
+ setIsModalVisible(true);
+ };
+
+ return (
+
+
+
+
+
+ Nodes
+ handleConfigChange('nodes', checked)} />
+
+
+ Age
+ handleConfigChange('age', checked)} />
+
+
+ Action
+ handleConfigChange('action', checked)} />
+
+
+ ISP
+ handleConfigChange('isp', checked)} />
+
+
+ Bond
+ handleConfigChange('bond', checked)} />
+
+
+ Providers
+ handleConfigChange('providers', checked)} />
+
+
+ Rewards
+ handleConfigChange('rewards', checked)} />
+
+
+ APY
+ handleConfigChange('apy', checked)} />
+
+
+ Slashes
+ handleConfigChange('slashes', checked)} />
+
+
+ Score
+ handleConfigChange('score', checked)} />
+
+
+ Version
+ handleConfigChange('version', checked)} />
+
+
+ RPC
+ handleConfigChange('rpc', checked)} />
+
+
+ BFR
+ handleConfigChange('bfr', checked)} />
+
+
+
+
+
+ );
+}
+
+export default VisibleColumn;
\ No newline at end of file
diff --git a/src/containers/A_monitor/CustomLineChart.js b/src/containers/A_monitor/CustomLineChart.js
new file mode 100644
index 0000000..6372421
--- /dev/null
+++ b/src/containers/A_monitor/CustomLineChart.js
@@ -0,0 +1,24 @@
+import React, { Component } from "react";
+import { Chart } from "chart.js";
+
+class CustomLineChart extends Component {
+ chartRef = React.createRef();
+
+ componentDidMount() {
+ this.chartInstance = new Chart(this.chartRef.current, {
+ type: "line",
+ data: this.props.data,
+ options: this.props.options,
+ });
+ }
+
+ componentWillUnmount() {
+ this.chartInstance.destroy();
+ }
+
+ render() {
+ return ;
+ }
+}
+
+export default CustomLineChart;
diff --git a/src/containers/A_monitor/activepage.js b/src/containers/A_monitor/activepage.js
new file mode 100644
index 0000000..336a9b2
--- /dev/null
+++ b/src/containers/A_monitor/activepage.js
@@ -0,0 +1,2417 @@
+import React, { Component, useState } from "react";
+import isEmpty from "lodash/isEmpty";
+import { Line } from "react-chartjs-2";
+import CustomLineChart from "./CustomLineChart";
+import Modals from "@iso/components/Feedback/Modal";
+import Popover from "@iso/components/uielements/popover";
+import { getData, setCookie, getCookie } from "CommonFunctions";
+import { ModalContent } from "../Feedback/Modal/Modal.styles";
+import { Layout, Button, Input, Modal, Switch, Breadcrumb, Select } from "antd";
+import "./styles.css";
+import { Link } from "react-router-dom";
+import { PUBLIC_ROUTE } from "../../route.constants";
+import { SearchOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
+
+import heartBlank from "@iso/assets/images/heart-blank.png";
+import heartFull from "@iso/assets/images/heart-full.png";
+
+import imageDO from "@iso/assets/images/do.png";
+import imageAWS from "@iso/assets/images/aws.png";
+import imageGCP from "@iso/assets/images/gcp.png";
+import imageAZURE from "@iso/assets/images/azure.png";
+import imageHETZNER from "@iso/assets/images/hetzner.png";
+import imageVULTR from "@iso/assets/images/vultr.png";
+import imageLeaseweb from "@iso/assets/images/leaseweb.png";
+import imageDatacamp from "@iso/assets/images/datacamp.png";
+import imageComcast from "@iso/assets/images/comcast.png";
+import imageChoopa from "@iso/assets/images/choopa.png";
+import imageChartercoms from "@iso/assets/images/chartercoms.png";
+import imageATandT from "@iso/assets/images/atandt.png";
+import imageZenlayer from "@iso/assets/images/zenlayer.png";
+
+import binance from "@iso/assets/images/binance.png";
+import eth from "@iso/assets/images/eth.png";
+import bitcoin from "@iso/assets/images/bitcoin.png";
+import litecoin from "@iso/assets/images/litecoin.png";
+import bitcoincash from "@iso/assets/images/bitcoincash.png";
+import dogecoin from "@iso/assets/images/dogecoin.png";
+import gaia from "@iso/assets/images/atom.png";
+import avax from "@iso/assets/images/avax.png";
+
+import thornode from "@iso/assets/images/thornode.svg";
+
+import blockIcon from "@iso/assets/images/overview/block_icon.svg";
+import highTradingIcon from "@iso/assets/images/overview/24high_trading.svg";
+import lowTradingIcon from "@iso/assets/images/overview/24low_trading.svg";
+import bondIcon from "@iso/assets/images/overview/Bond_icon.svg";
+import churnsIcon from "@iso/assets/images/overview/churns_icon.svg";
+import marcketCapIcon from "@iso/assets/images/overview/marcket_Cap.svg";
+import mcapRankIcon from "@iso/assets/images/overview/mcap_rank.svg";
+import runeUsdtIcon from "@iso/assets/images/overview/rune_usdt.svg";
+import timeIcon from "@iso/assets/images/overview/time_icon.svg";
+import totalSupplyIcon from "@iso/assets/images/overview/total_supply.svg";
+import filterIcon from "@iso/assets/images/overview/filter.svg";
+import loadingIcon from "@iso/assets/images/overview/loading.png";
+import githubIcon from "@iso/assets/images/overview/github_link_icon.svg";
+import twitterIcon from "@iso/assets/images/overview/twitter_link_icon.svg";
+import liquifyLogo from "@iso/assets/images/overview/liquify_logo.svg";
+
+import threeDotsIcon from "@iso/assets/images/overview/dots_three_circle.svg";
+import powerIcon from "@iso/assets/images/overview/power.svg";
+import activeIcon from "@iso/assets/images/overview/active_icon.svg";
+import arrowDownIcon from "@iso/assets/images/overview/arrow-down.svg";
+
+import VisibleColumn from "@iso/components/VisibleColumn/VisibleColumn";
+const leaveIcon = (
+
+);
+const { Header, Footer, Content } = Layout;
+
+const headerStyle = {
+ cursor: "pointer",
+ padding: "12px 15px",
+ fontSize: 15,
+ color: "#ffffff",
+ backgroundColor: "rgba(24, 34, 51, 0.4)",
+ height: 55,
+ fontWeight: 600,
+};
+const tdStyle = {
+ minWidth: 60,
+ textAlign: "right",
+ fontSize: 14,
+ padding: "10px 15px",
+};
+const trStyle = { height: 45 };
+const iconStyle = {
+ minWidth: 25,
+ padding: 5,
+ paddingLeft: 10,
+ paddingRight: 10,
+};
+
+async function copyToClipWithPopup(msg, ip) {
+ copyToClipboard(ip);
+ popUpModal(msg, ip);
+}
+
+const copyToClipboard = (str) => {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText)
+ return navigator.clipboard.writeText(str);
+ return Promise.reject("The Clipboard API is not available.");
+};
+
+function popUpModal(msg, ip) {
+ Modals.info({
+ title: Success
,
+ content: (
+
+
+ {`${msg} `} {`${ip}`}
+
+
+ ),
+ onOk() {},
+ okText: "OK",
+ cancelText: "Cancel",
+ className: "feedback-modal",
+ });
+}
+
+const SortIcon = ({ column, sortBy, sortDirection }) => {
+ if (sortBy === column) {
+ return (
+
+ );
+ }
+ return null;
+};
+
+const Icons = ({ address, ip_address, addToFav, whichHeart }) => {
+ const firstURL = `https://thornode.ninerealms.com/thorchain/node/${address}`;
+ const secondURL = `https://viewblock.io/thorchain/address/${address}`;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ copyToClipWithPopup("IP Copied to clipboard:", ip_address)
+ }
+ >
+
+
+
+
addToFav(address)}
+ src={whichHeart(address)}
+ style={{
+ cursor: "pointer",
+ marginLeft: 5,
+ marginTop: 2,
+ width: 15,
+ height: 15,
+ }}
+ />
+
+ );
+};
+
+const GlobalData = ({ globalData, animateBlockCount, state }) => {
+ let timeToDisplay = "";
+ let msgTitle = "";
+ if (globalData?.churnTry && globalData?.retiring === "false") {
+ msgTitle = "(CHURN) RETRY IN";
+ timeToDisplay = `${globalData?.timeUntilRetry?.days}d ${globalData?.timeUntilRetry?.hours}h ${globalData?.timeUntilRetry?.minutes}m`;
+ } else if (globalData?.retiring === "true") {
+ msgTitle = "(CHURN) CURRENTLY CHURNING";
+ timeToDisplay = "Churning";
+ } else {
+ msgTitle = "(CHURN) TIME UNTIL";
+ timeToDisplay = `${globalData?.timeUntilChurn?.days}d ${globalData?.timeUntilChurn?.hours}h ${globalData?.timeUntilChurn?.minutes}m`;
+ }
+
+ return (
+ <>
+
+

+
+
CURRENT BLOCK
+
+ {parseInt(globalData.maxHeight).toLocaleString()}
+
+
+
+
+

+
+
{msgTitle}
+
{timeToDisplay}
+
+
+
+

+
+
TOTAL BONDED VALUE
+
+ ᚱ
+ {state.activeNodes.length > 0
+ ? parseInt(
+ state.activeNodes
+ .map((item) => item.bond)
+ .reduce((prev, next) => prev + next) / 100000000
+ ).toLocaleString()
+ : "0"}
+
+
+
+
+

+
+
MARKET CAP
+
+ ${globalData?.coingecko?.market_cap?.toLocaleString()}
+
+
+
+
+

+
+
24 HR VOLUME
+
+ ${globalData?.coingecko?.total_volume?.toLocaleString()}
+
+
+
+
+

+
+
MAX EFFECTIVE BOND
+
+ ᚱ
+ {parseInt(
+ globalData.maxEffectiveStake / 100000000
+ ).toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const CoinGeckoData = ({ globalData }) => {
+ return (
+ <>
+
+

+
+
PRICE
+
+ ${globalData?.coingecko?.current_price?.toLocaleString()}
+
+
+
+
+

+
+
24 HR HIGH
+
+ ${globalData?.coingecko?.high_24h}
+
+
+
+
+

+
+
24 HR LOW
+
+ ${globalData?.coingecko?.low_24h}
+
+
+
+
+

+
+
MARKET CAP RANK
+
+ {globalData?.coingecko?.market_cap_rank}
+
+
+
+
+

+
+
TOTAL SUPPLY
+
+ ᚱ{globalData?.coingecko?.total_supply?.toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const ReturnIspImage = ({ isp }) => {
+ const style = { width: 25, height: 25 };
+
+ if (
+ isp === "Amazon.com, Inc." ||
+ isp === "Amazon Technologies Inc." ||
+ isp === "Amazon.com"
+ ) {
+ return
;
+ }
+ if (isp === "DigitalOcean, LLC" || isp === "DigitalOcean") {
+ return
;
+ }
+ if (isp === "Google LLC") {
+ return
;
+ }
+
+ if (isp === "Microsoft Corporation") {
+ return
;
+ }
+
+ if (isp === "Hetzner Online GmbH") {
+ return
;
+ }
+
+ if (isp === "The Constant Company" || isp === "The Constant Company, LLC") {
+ return
;
+ }
+
+ if (isp === "Leaseweb UK Limited") {
+ return
;
+ }
+
+ if (isp === "Datacamp Limited") {
+ return
;
+ }
+
+ if (isp === "Comcast Cable Communications, LLC") {
+ return
;
+ }
+
+ if (isp === "Choopa") {
+ return
;
+ }
+
+ if (isp === "Charter Communications Inc") {
+ return
;
+ }
+
+ if (isp === "AT&T Services, Inc.") {
+ return
;
+ }
+
+ if (isp === "Zenlayer Inc") {
+ return
;
+ }
+
+ return "-";
+};
+
+const ChainTD = ({ chain, obchains, maxChainHeights }) => {
+ const delta = obchains[chain] - maxChainHeights[chain];
+ return (
+
+ {delta === 0 ? "OK" : delta.toString()}
+ |
+ );
+};
+
+const BondProviderPopOver = ({ data }) => {
+ const totalBond = data
+ .map((item) => parseInt(item.bond))
+ .reduce((a, b) => a + b, 0);
+
+ const d = data.map((item, index) => {
+ return (
+
+
+ {item.bond_address.substring(
+ item.bond_address.length - 4,
+ item.bond_address.length
+ )}
+
+ {((item.bond / totalBond) * 100).toFixed(2)}%
+
+ ᚱ{parseInt((item.bond / 100000000).toFixed()).toLocaleString()}
+
+
+ );
+ });
+ return d;
+};
+
+const NodeTable = ({
+ nodeData,
+ clickSortHeader,
+ handleClickBond,
+ handleClickRewards,
+ handleClickSlashes,
+ sortColour,
+ maxChainHeights,
+ chains,
+ addToFav,
+ whichHeart,
+ chartDataConfig,
+ bondOptions,
+ rewardsOptions,
+ slashesOptions,
+ visibleColumns = { ...defaulColumns },
+ sortBy = "",
+ sortDirection = "",
+}) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(100);
+
+ const totalPages = Math.ceil(nodeData.length / itemsPerPage);
+ const pageNumbers = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ pageNumbers.push(i);
+ }
+
+ const handleClick = (event) => {
+ setCurrentPage(Number(event.target.id));
+ };
+
+ const handleNext = () => {
+ setCurrentPage((prevPage) =>
+ prevPage === totalPages ? prevPage : prevPage + 1
+ );
+ };
+
+ const handlePrev = () => {
+ setCurrentPage((prevPage) => (prevPage === 1 ? prevPage : prevPage - 1));
+ };
+
+ const renderPageNumbers = pageNumbers.map((number) => {
+ return (
+
+ {number}
+
+ );
+ });
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = nodeData.slice(indexOfFirstItem, indexOfLastItem);
+
+ const getHeaderClassName = (key) => {
+ return visibleColumns && visibleColumns[key]
+ ? "tableHeader"
+ : "tableHeader hidden";
+ };
+
+ const getCellClassName = (key) => {
+ return visibleColumns && visibleColumns[key] ? "" : "hidden";
+ };
+
+ const updatePagingItem = (value) => {
+ setItemsPerPage(value);
+ };
+ return (
+ <>
+
+ Nodes per page:
+
+
+
+
+
+
+
+
+ |
+ clickSortHeader("node_address")}
+ >
+
+ Validator Nodes
+
+
+ |
+ clickSortHeader("age")}
+ >
+
+ Age
+
+
+ |
+ clickSortHeader("action")}
+ >
+
+ Action
+
+
+ |
+ clickSortHeader("isp")}
+ >
+
+ ISP
+
+
+ |
+ clickSortHeader("location")}
+ >
+
+ Location
+
+
+ |
+ clickSortHeader("bond")}
+ >
+
+ Bond
+
+
+ |
+ clickSortHeader("bond_providers")}
+ >
+
+ Providers
+
+
+ |
+ clickSortHeader("current_award")}
+ >
+
+ Rewards
+
+
+ |
+ clickSortHeader("apy")}
+ >
+
+ APY
+
+
+ |
+ clickSortHeader("slash_points")}
+ >
+
+ Slashes
+
+
+ |
+ clickSortHeader("score")}
+ >
+
+ Score
+
+
+ |
+ clickSortHeader("version")}
+ >
+
+ Version
+
+
+ |
+ clickSortHeader("leave")}
+ >
+
+ {leaveIcon}
+
+
+ |
+
+ RPC
+ |
+
+ BFR
+ |
+
+ {chains && (
+ <>
+ clickSortHeader("BNB")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("BTC")}
+ >
+
+ 
+
+
+ |
+
+ clickSortHeader("ETH")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("LTC")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("BCH")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("DOGE")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("GAIA")}
+ >
+
+ 
+
+
+ |
+
+ clickSortHeader("AVAX")}
+ >
+
+ 
+
+
+ |
+ >
+ )}
+
+
+
+ {currentItems.map((item, index) => (
+
+ |
+ {index + 1}
+ |
+
+
+
+ copyToClipWithPopup(
+ "Node address copied to clipboard:",
+ item.node_address
+ )
+ }
+ >
+ {`...${item.node_address.substring(
+ item.node_address.length - 4
+ )}`}
+
+
+
+ |
+
+ {item.age.toFixed(2)}
+ |
+
+ {item.action}
+ |
+
+
+
+
+
+
+ |
+
+ {item.location}
+ |
+ handleClickBond(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Bond Value Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ ᚱ
+ {parseInt(
+ (item.bond / 100000000).toFixed()
+ ).toLocaleString()}
+
+ |
+
+ {" "}
+
+ }
+ title={"Bond Providers"}
+ trigger="hover"
+ >
+
+ {item.bond_providers.providers.length}
+
+
+ |
+ handleClickRewards(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Rewards Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ ᚱ
+ {parseInt(
+ (item.current_award / 100000000).toFixed()
+ ).toLocaleString()}
+
+ |
+
+ {item.apy}
+ |
+ handleClickSlashes(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Slashes Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ {parseInt(item.slash_points).toLocaleString()}
+
+ |
+
+ {item.score}
+ |
+
+ {item.version}
+ |
+ {/* {item.rpc === 'true' ? '*' : 'BAD'} | */}
+
+ {item.forced_to_leave === 1 || item.requested_to_leave === 1
+ ? "yes"
+ : "-"}
+ |
+
+
+ {item.rpc !== "null" ? "*" : "Bad"}
+
+ |
+
+
+ {item.bifrost !== "null" ? "*" : "Bad"}
+
+ |
+
+ {chains && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ ))}
+
+ |
+
+
+
+
+
+
+ -
+
+
+ {renderPageNumbers}
+ -
+
+
+
+
+
+ >
+ );
+};
+
+let timer = null;
+
+const defaulColumns = {
+ nodes: true,
+ age: true,
+ action: true,
+ isp: true,
+ bond: true,
+ providers: true,
+ location: true,
+ leave: true,
+ rewards: true,
+ apy: true,
+ slashes: true,
+ score: true,
+ version: true,
+ rpc: true,
+ bfr: true,
+ BTC: true,
+ BNB: true,
+ ETH: true,
+ LTC: true,
+ BCH: true,
+ DOGE: true,
+ GAIA: true,
+ AVAX: true,
+};
+export default class extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ chartData: [],
+ data: [],
+ globalData: [],
+ sortBy: "bond",
+ sortDirection: "desc",
+ activeNodes: [],
+ standByNodes: [],
+ whitelistedNodes: [],
+ animateBlockCount: false,
+ myFavNodes: [],
+ searchTerm: "",
+ visibleColumns: defaulColumns,
+ nodesFilter: {},
+ loading: true,
+ sortByChain: null,
+ };
+ this.clickSortHeader = this.clickSortHeader.bind(this);
+ this.handleClickRewards = this.handleClickRewards.bind(this);
+ this.handleClickSlashes = this.handleClickSlashes.bind(this);
+ this.handleClickBond = this.handleClickBond.bind(this);
+ }
+
+ async componentWillMount() {
+ const myFavNodes = getCookie("myFavNodes");
+
+ const tmp = myFavNodes.length > 0 ? JSON.parse(myFavNodes) : [];
+
+ this.setState({ myFavNodes: tmp });
+
+ this.refreshData();
+ }
+
+ async refreshData() {
+ const data = await getData();
+
+ if (this.state.loading) {
+ this.setState({ loading: false });
+ }
+
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
+ console.log("DEV ONLY: Refresh Data Results: ", data);
+ }
+
+ this.setState(
+ {
+ data: data.data,
+ globalData: data.globalData,
+ maxChainHeights: data.maxChainHeights,
+ animateBlockCount: false,
+ },
+ () => this.setData()
+ ); //Change animateBlockCount to true here for animation
+ }
+
+ componentDidMount() {
+ timer = setInterval(() => {
+ this.setState({ animateBlockCount: false }, () => this.refreshData());
+ //this.refreshData()
+ }, 6000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(timer);
+ }
+
+ addToFav(address) {
+ //setCookie('myFavNodes', '')
+ //return
+
+ //Below works to add, but need to check if already exists, and if so remove
+
+ const myFavNodes = getCookie("myFavNodes"); //JSON.parse(
+
+ if (myFavNodes.length === 0) {
+ //in here no current fav nodes
+ const singleAddress = JSON.stringify([address]);
+ setCookie("myFavNodes", singleAddress);
+ this.setState({ myFavNodes: singleAddress }, () => this.setData());
+ } else {
+ //If we already have some fav nodes
+ const newFaveNodes = JSON.parse(myFavNodes);
+
+ //Need to check if already exists
+ if (newFaveNodes.indexOf(address) > -1) {
+ //In the array!
+
+ const newArrayWithRemove = newFaveNodes.filter(
+ (item) => item !== address
+ );
+
+ const newFaveNodesNew = JSON.stringify(newArrayWithRemove);
+ this.setState({ myFavNodes: newArrayWithRemove }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ } else {
+ //Not in the array
+ newFaveNodes[newFaveNodes.length] = address;
+
+ const newFaveNodesNew = JSON.stringify(newFaveNodes);
+
+ this.setState({ myFavNodes: newFaveNodes }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ }
+ }
+ }
+
+ returnSearchedData(data) {
+ if (this.state.searchTerm === "") {
+ return data;
+ } else {
+ const filteredNodes = data.filter((item) =>
+ item.node_address.includes(this.state.searchTerm)
+ );
+ return filteredNodes;
+ }
+ }
+
+ setData() {
+ // Grab our state so we can mutate it
+ let myData = JSON.parse(JSON.stringify(this.state.data));
+
+ // Add faves to the data, then we can sort by then below
+ const newItems = myData.map((item) => {
+ if (this.state.myFavNodes.includes(item.node_address)) {
+ item.fave = 1;
+ } else {
+ item.fave = 0;
+ }
+ return item;
+ });
+
+ // Filter for our three tables
+ let activeNodes = newItems.filter((item) => item.status === "Active");
+ let standbyNodes = newItems.filter(
+ (item) =>
+ (item.status === "Standby" || item.status === "Ready") &&
+ item.version === this.state.globalData.maxVersion
+ );
+
+ // Create an array of all nodes that are active and standby
+ const active_standy_nodes = [
+ ...activeNodes.map((item) => item.node_address),
+ ...standbyNodes.map((item) => item.node_address),
+ ];
+
+ // White listed are all other nodes which are not active or on standby
+ let whitelisted = newItems.filter(
+ (item) => !active_standy_nodes.includes(item.node_address)
+ );
+
+ activeNodes = this.findChurnOuts(activeNodes); // Add in the actions for churning
+ standbyNodes = this.findChurnIns(standbyNodes); // Add in the actions for nodes churning in
+
+ // Filter here if any searchTerm from the search bar
+ activeNodes = this.returnSearchedData(activeNodes);
+ standbyNodes = this.returnSearchedData(standbyNodes);
+ whitelisted = this.returnSearchedData(whitelisted);
+
+ // Sort and add our favs to the top
+ let activeNodesSorted = this.sortData(
+ activeNodes,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+ const favActiveNodesSorted = activeNodesSorted.filter(
+ (item) => item.fave === 1
+ ); // Get our favourites
+ activeNodesSorted = activeNodesSorted.filter((item) => item.fave === 0); // Get our non favourites
+ activeNodesSorted = [...favActiveNodesSorted, ...activeNodesSorted]; // Join faves at top with non favourites
+
+ const standBySorted = this.sortData(
+ standbyNodes,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+ const whitelistedSorted = this.sortData(
+ whitelisted,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+
+ this.setState({
+ activeNodes: activeNodesSorted,
+ standByNodes: standBySorted,
+ whitelistedNodes: whitelistedSorted,
+ });
+ }
+
+ onColumnUpdate(config) {
+ this.setState({ visibleColumns: config });
+ }
+
+ onNodesFilter(key) {
+ this.setState((prevState) => ({
+ nodesFilter: {
+ ...prevState.nodesFilter,
+ [key]: !prevState.nodesFilter[key],
+ },
+ }));
+ }
+
+ /*
+Split the data into over 300ks and under 300ks
+With the over 300ks, take the top 3 if they exist and apply churn in action
+If 4 nodes churn in instead of 3 each time, add another row
+*/
+ findChurnIns(standbyNodes) {
+ if (standbyNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ const over300 = standbyNodes.filter((item) => item.bond >= 30000000000000);
+ const over300Sorted = this.sortData(over300, "bond", "desc");
+
+ if (over300Sorted.length > 0) {
+ over300Sorted[Math.min(0, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(1, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(2, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(3, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(4, over300Sorted.length - 1)].action = "Churn In";
+ }
+ const under300 = standbyNodes.filter((item) => item.bond < 30000000000000);
+
+ return [...over300Sorted, ...under300];
+ }
+
+ /*
+Lowest Bond
+Oldest Node
+Worst Performer (Can't churn out if just churned in, one cycle grace period)
+*/
+ findChurnOuts(activeNodes) {
+ if (activeNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ let activeNodesSorted = this.sortData(activeNodes, "age", "desc");
+ activeNodesSorted[0].action = "Oldest";
+
+ activeNodesSorted = this.sortData(activeNodes, "bond", "asc");
+ activeNodesSorted[0].action = "Smallest Bond";
+
+ activeNodesSorted = this.sortData(activeNodes, "score", "asc", true);
+ //we set the 'Worst Performing' tag in the sortData function
+
+ this.calcBadValidatorRedline(activeNodes);
+
+ return activeNodesSorted;
+ }
+
+ calcBadValidatorRedline(activeNodes) {
+ //Only get nodes with slashes greater than 100
+ const greater100Slashes = activeNodes.filter(
+ (item) => item.slash_points > 100
+ );
+ //Add all the scores together for thes nodes
+ const sum = greater100Slashes.reduce((accumulator, object) => {
+ return accumulator + parseFloat(object.score);
+ }, 0);
+ //Find the average
+ const averageScore = sum / (greater100Slashes.length + 1);
+
+ const validatorLine =
+ averageScore / this.state.globalData.BadValidatorRedline;
+
+ activeNodes.map((item) => {
+ if (item.score < validatorLine) {
+ item.action = "Bad Redline";
+ }
+ return 0;
+ });
+ }
+
+ /*
+Sort by either string or number
+We use string sort function if value is one of the arrays else do second sort number
+*/
+ sortData(data, value = null, direction = null, worst_perform = false) {
+ const toSortBy = value === null ? this.state.sortBy : value;
+ let newData = [];
+ if (this.state.sortChain) {
+ // New sorting logic for chains
+ const chain = toSortBy;
+ newData = [...data].sort((a, b) => {
+ const deltaA = a.obchains[chain] - this.state.maxChainHeights[chain];
+ const deltaB = b.obchains[chain] - this.state.maxChainHeights[chain];
+ if (deltaA === deltaB) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return deltaA - deltaB;
+ });
+ } else {
+ if (
+ [
+ "node",
+ "isp",
+ "location",
+ "version",
+ "action",
+ "node_address",
+ ].includes(toSortBy)
+ ) {
+ //This sort function for strings
+ newData = data.sort((a, b) => a[toSortBy].localeCompare(b[toSortBy]));
+ } else if (toSortBy === "bond_providers") {
+ //This is for bond provider sort as we need to go another layer deep in the object
+ newData = data.sort(
+ (a, b) => a[toSortBy].providers.length - b[toSortBy].providers.length
+ );
+ } else if (worst_perform === true) {
+ //This is for when we are sorting for action of worst performance as we want to exclude any with age under 3 days
+ const ageCutOffDays = 3;
+ const a = data.filter((item) => parseFloat(item.age) > ageCutOffDays);
+ const b = data.filter((item) => parseFloat(item.age) <= ageCutOffDays);
+ const aSorted = a.sort((a, b) => b[toSortBy] - a[toSortBy]);
+
+ aSorted[aSorted.length - 1].action = "Worst Performing";
+ newData = [...aSorted, ...b];
+ } else {
+ //This sort function for numbers
+ //When sorting, if values are the same, sort by node_address
+ newData = data.sort(function (a, b) {
+ if (a[toSortBy] === b[toSortBy]) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return a[toSortBy] > b[toSortBy] ? 1 : -1;
+ });
+ }
+ }
+ //If we pass it a direction, we set it here, if not we take it from the state
+ const toDirection =
+ direction === null ? this.state.sortDirection : direction;
+ if (toDirection === "desc") {
+ newData.reverse();
+ }
+
+ return newData;
+ }
+
+ clickSortHeader(item) {
+ const isChain = [
+ "BNB",
+ "BTC",
+ "ETH",
+ "LTC",
+ "BCH",
+ "DOGE",
+ "GAIA",
+ "AVAX",
+ ].includes(item);
+ const direction =
+ this.state.sortBy !== item
+ ? "desc"
+ : this.state.sortDirection === "desc"
+ ? "asc"
+ : "desc";
+ this.setState(
+ { sortBy: item, sortDirection: direction, sortChain: isChain },
+ () => this.setData()
+ );
+ window.setTimeout(() => {}, 200);
+ }
+
+ sortColour(item) {
+ return "#ffffff";
+ }
+
+ whichHeart(address) {
+ return this.state.myFavNodes.includes(address) ? heartFull : heartBlank;
+ }
+ handleClickSlashes = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabSlashes=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickRewards = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabRewards=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickBond = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabBond=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Math.round(Number(y) / 100000000),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ searchBar() {
+ return (
+
+
+ this.setState(
+ { searchTerm: event.target.value.trim().toLowerCase() },
+ () => this.setData()
+ )
+ }
+ prefix={}
+ />
+
+ );
+ }
+
+ render() {
+ const {
+ loading,
+ data,
+ nodesFilter,
+ visibleColumns,
+ activeNodes,
+ standByNodes,
+ whitelistedNodes,
+ } = this.state;
+
+ const noNodesFilter =
+ !nodesFilter.active && !nodesFilter.standby && !nodesFilter.orthers;
+ const showActive =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.active;
+ const showStandby =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.standby;
+ const showOthers =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.orthers;
+
+ const chartDataConfig = this.state.chartData
+ ? {
+ datasets: [
+ {
+ label: "Value",
+ data: this.state.chartData,
+ fill: false,
+ backgroundColor: "rgb(28, 57, 182)",
+ borderColor: "rgba(28, 57, 187, 0.2)",
+ },
+ ],
+ }
+ : {};
+
+ const slashesOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Slashes Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const rewardsOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Reward Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const bondOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ min: Math.min(...this.state.chartData.map((data) => data.x)),
+ stepSize: 20000,
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Bond Value",
+ },
+ ticks: {
+ min: Math.min(...this.state.chartData.map((data) => data.y)),
+ stepSize: 20000,
+ callback: function (value) {
+ return value.toString();
+ },
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ return (
+
+
+
+

+
Thornode Monitor
+
+
+
this.onNodesFilter("active")}
+ >
+

+
+
this.onNodesFilter("standby")}
+ >
+

+
+
this.onNodesFilter("orthers")}
+ >
+

+
+
+
+
+ {loading && (
+
+

+
+ )}
+
+ {!loading && (
+
+
}>
+
Dashboard
+
+ Active Nodes
+
+
+
+
+
+ {showActive && (
+ <>
+
+
+
+
+
+
+ {this.searchBar()}
+
+
+
+
+ {activeNodes.length > 0 && (
+
+ )}
+ {activeNodes.length === 0 && (
+
+
+ No Active Data Available!
+
+
+ )}
+
+ >
+ )}
+
+ {showStandby && (
+ <>
+
+
+
+
+
+
+
+ {!showActive && (
+ <>
+ {this.searchBar()}
+
+ >
+ )}
+
+
+ {standByNodes.length > 0 && (
+
+ )}
+ {standByNodes.length === 0 && (
+
+
+ No Standby Data Available!
+
+
+ )}
+
+ >
+ )}
+ {showOthers && (
+ <>
+
+
+
+
+
+
+
+ {!showActive && !showStandby && (
+ <>
+ {this.searchBar()}
+
+ >
+ )}
+
+
+ {whitelistedNodes.length > 0 && (
+
+ )}
+ {whitelistedNodes.length === 0 && (
+
+
+ No Other Data Available!
+
+
+ )}
+ >
+ )}
+
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/containers/A_monitor/monitorpage.js b/src/containers/A_monitor/monitorpage.js
index fa124fb..021bd55 100644
--- a/src/containers/A_monitor/monitorpage.js
+++ b/src/containers/A_monitor/monitorpage.js
@@ -1,412 +1,1665 @@
-import React, { Component } from 'react';
+import React, { Component, useState } from "react";
+import isEmpty from "lodash/isEmpty";
+import { UpOutlined, DownOutlined } from "@ant-design/icons";
+import { Line } from "react-chartjs-2";
+import CustomLineChart from "./CustomLineChart";
//import LayoutContentWrapper from '@iso/components/utility/layoutWrapper';
//import LayoutContent from '@iso/components/utility/layoutContent';
-import Modals from '@iso/components/Feedback/Modal';
-import Popover from '@iso/components/uielements/popover';
-import { getData, setCookie, getCookie } from 'CommonFunctions'
+import Modals from "@iso/components/Feedback/Modal";
+import Popover from "@iso/components/uielements/popover";
+import { getData, setCookie, getCookie } from "CommonFunctions";
//import { someFunc } from './monitor_functions'
//import Spin from '@iso/ui/Antd/Spin/Spin';
-import Input from '@iso/components/uielements/input';
-import { ModalContent } from '../Feedback/Modal/Modal.styles';
-import { Layout } from 'antd';
+// import Input from '@iso/components/uielements/input';
+import { ModalContent } from "../Feedback/Modal/Modal.styles";
+import { Layout, Button, Input, Breadcrumb, Select } from "antd";
import "./styles.css";
+import { Link } from "react-router-dom";
+import { PUBLIC_ROUTE } from "../../route.constants";
+import { SearchOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
//import { retiringVault } from './data.js' //https://thornode.ninerealms.com/thorchain/vaults/asgard
-import heartBlank from '@iso/assets/images/heart-blank.png';
-import heartFull from '@iso/assets/images/heart-full.png';
-
-import imageDO from '@iso/assets/images/do.png';
-import imageAWS from '@iso/assets/images/aws.png';
-import imageGCP from '@iso/assets/images/gcp.png';
-import imageAZURE from '@iso/assets/images/azure.png';
-import imageHETZNER from '@iso/assets/images/hetzner.png';
-import imageVULTR from '@iso/assets/images/vultr.png';
-
-import binance from '@iso/assets/images/binance.png';
-import eth from '@iso/assets/images/eth.png';
-import bitcoin from '@iso/assets/images/bitcoin.png';
-import litecoin from '@iso/assets/images/litecoin.png';
-import bitcoincash from '@iso/assets/images/bitcoincash.png';
-import dogecoin from '@iso/assets/images/dogecoin.png';
+import heartBlank from "@iso/assets/images/heart-blank.png";
+import heartFull from "@iso/assets/images/heart-full.png";
+
+import imageDO from "@iso/assets/images/do.png";
+import imageAWS from "@iso/assets/images/aws.png";
+import imageGCP from "@iso/assets/images/gcp.png";
+import imageAZURE from "@iso/assets/images/azure.png";
+import imageHETZNER from "@iso/assets/images/hetzner.png";
+import imageVULTR from "@iso/assets/images/vultr.png";
+import imageLeaseweb from "@iso/assets/images/leaseweb.png";
+import imageDatacamp from "@iso/assets/images/datacamp.png";
+import imageComcast from "@iso/assets/images/comcast.png";
+import imageChoopa from "@iso/assets/images/choopa.png";
+import imageChartercoms from "@iso/assets/images/chartercoms.png";
+import imageATandT from "@iso/assets/images/atandt.png";
+import imageZenlayer from "@iso/assets/images/zenlayer.png";
+
+import binance from "@iso/assets/images/binance.png";
+import eth from "@iso/assets/images/eth.png";
+import bitcoin from "@iso/assets/images/bitcoin.png";
+import litecoin from "@iso/assets/images/litecoin.png";
+import bitcoincash from "@iso/assets/images/bitcoincash.png";
+import dogecoin from "@iso/assets/images/dogecoin.png";
//import luna from '@iso/assets/images/luna.png';
-import gaia from '@iso/assets/images/atom.png';
-
-const leaveIcon =
+import gaia from "@iso/assets/images/atom.png";
+import thornode from "@iso/assets/images/thornode.svg";
+import avax from "@iso/assets/images/avax.png";
+
+import blockIcon from "@iso/assets/images/overview/block_icon.svg";
+import highTradingIcon from "@iso/assets/images/overview/24high_trading.svg";
+import lowTradingIcon from "@iso/assets/images/overview/24low_trading.svg";
+import bondIcon from "@iso/assets/images/overview/Bond_icon.svg";
+import churnsIcon from "@iso/assets/images/overview/churns_icon.svg";
+import marcketCapIcon from "@iso/assets/images/overview/marcket_Cap.svg";
+import mcapRankIcon from "@iso/assets/images/overview/mcap_rank.svg";
+import runeUsdtIcon from "@iso/assets/images/overview/rune_usdt.svg";
+import timeIcon from "@iso/assets/images/overview/time_icon.svg";
+import totalSupplyIcon from "@iso/assets/images/overview/total_supply.svg";
+import filterIcon from "@iso/assets/images/overview/filter.svg";
+import loadingIcon from "@iso/assets/images/overview/loading.png";
+import githubIcon from "@iso/assets/images/overview/github_link_icon.svg";
+import twitterIcon from "@iso/assets/images/overview/twitter_link_icon.svg";
+import liquifyLogo from "@iso/assets/images/overview/liquify_logo.svg";
+
+import threeDotsIcon from "@iso/assets/images/overview/dots_three_circle.svg";
+import powerIcon from "@iso/assets/images/overview/power.svg";
+import activeIcon from "@iso/assets/images/overview/active_icon.svg";
+import arrowDownIcon from "@iso/assets/images/overview/arrow-down.svg";
+
+import VisibleColumn from "@iso/components/VisibleColumn/VisibleColumn";
+
+const leaveIcon = (
+
+);
const { Header, Footer, Content } = Layout;
-const headerStyle = {cursor: 'pointer', paddingLeft: 10, paddingRight: 10}
-const tdStyle = {minWidth: 60, textAlign: 'right', fontFamily: 'monospace', fontSize: 14}
-const iconStyle = {minWidth: 25, padding: 5, paddingLeft: 10, paddingRight: 10}
-
+const headerStyle = {
+ cursor: "pointer",
+ padding: "12px 15px",
+ fontSize: 15,
+ color: "#ffffff",
+ backgroundColor: "rgba(24, 34, 51, 0.4)",
+ height: 55,
+ fontWeight: 600,
+};
+const tdStyle = {
+ minWidth: 60,
+ textAlign: "right",
+ fontSize: 14,
+ padding: "10px 15px",
+};
+const trStyle = { height: 45 };
+const iconStyle = {
+ minWidth: 25,
+ padding: 5,
+ paddingLeft: 10,
+ paddingRight: 10,
+};
async function copyToClipWithPopup(msg, ip) {
- copyToClipboard(ip)
- popUpModal(msg, ip)
+ copyToClipboard(ip);
+ popUpModal(msg, ip);
}
-const copyToClipboard = str => {
+const copyToClipboard = (str) => {
if (navigator && navigator.clipboard && navigator.clipboard.writeText)
return navigator.clipboard.writeText(str);
- return Promise.reject('The Clipboard API is not available.');
+ return Promise.reject("The Clipboard API is not available.");
};
-
-
-
function popUpModal(msg, ip) {
Modals.info({
title: Success
,
content: (
- {`${msg} ${ip}`}
+ {`${msg} `} {`${ip}`}
),
onOk() {},
- okText: 'OK',
- cancelText: 'Cancel',
+ okText: "OK",
+ cancelText: "Cancel",
+ className: "feedback-modal",
});
}
-const Icons = ({address, ip_address, addToFav, whichHeart}) => {
- const firstURL = `https://thornode.ninerealms.com/thorchain/node/${address}`
- const secondURL = `https://viewblock.io/thorchain/address/${address}`
- return (
-
+const SortIcon = ({ column, sortBy, sortDirection }) => {
+ if (sortBy === column) {
+ return (
+
+ );
+ }
+ return null;
+};
-
-
-
-
+const Icons = ({ address, ip_address, addToFav, whichHeart }) => {
+ const firstURL = `https://thornode.ninerealms.com/thorchain/node/${address}`;
+ const secondURL = `https://viewblock.io/thorchain/address/${address}`;
+ return (
+
+
+
+ {/* */}
+
+
-
-
-
-
+
+
+ {/* */}
+
+
-
-
-
- copyToClipWithPopup('IP Copied to clipboard:', ip_address)}>
-
+
+
+ copyToClipWithPopup("IP Copied to clipboard:", ip_address)
+ }
+ >
+ {/* */}
+
-
-
addToFav(address)} src={whichHeart(address)} style={{ cursor:'pointer', marginLeft: 5, marginTop: 2, width: 15, height: 15, opacity: 0.5}}/>
-
+
addToFav(address)}
+ src={whichHeart(address)}
+ style={{
+ cursor: "pointer",
+ marginLeft: 5,
+ marginTop: 2,
+ width: 15,
+ height: 15,
+ }}
+ />
-)
-}
-
-const GlobalData = ({ globalData, animateBlockCount, state}) => {
- if (globalData.length===0) return null
-
- const title = {fontWeight: 800, textAlign: 'right', borderStyle: 'none', minWidth: 170}
- const values = {textAlign: 'right', borderStyle: 'none', paddingLeft: 20}
- const tr = {}
+ );
+};
- /*
- Logic to display whether we are normal, trying to churn or churning
- If churnTry is true then seconds to churn is negative, if retiring is also false (no RetiringVaults found) this means we have finished the 3 day period and now in retrying churn phase
- Soon as retiring = true means we have found RetiringVault in the api response and this means we are in the churn phase
- If neither of these conditions are met, ie churnTry = false and retiring = false then we are in normal countdown until next churn
- */
- let timeToDisplay = '';
- let msgTitle = ''
- if (globalData.churnTry && globalData.retiring === 'false') {
- msgTitle = '(Churn) Retry In:'
- timeToDisplay = `${globalData.timeUntilRetry.days}d ${globalData.timeUntilRetry.hours}h ${globalData.timeUntilRetry.minutes}m`
- } else if (globalData.retiring === 'true') {
- msgTitle = '(Churn) Currently Churning:'
- timeToDisplay = 'Churning'
+const GlobalData = ({ globalData, animateBlockCount, state }) => {
+ let timeToDisplay = "";
+ let msgTitle = "";
+ if (globalData?.churnTry && globalData?.retiring === "false") {
+ msgTitle = "(CHURN) RETRY IN";
+ timeToDisplay = `${globalData?.timeUntilRetry?.days}d ${globalData?.timeUntilRetry?.hours}h ${globalData?.timeUntilRetry?.minutes}m`;
+ } else if (globalData?.retiring === "true") {
+ msgTitle = "(CHURN) CURRENTLY CHURNING";
+ timeToDisplay = "Churning";
} else {
- msgTitle = '(Churn) Time Until:'
- timeToDisplay = `${globalData.timeUntilChurn.days}d ${globalData.timeUntilChurn.hours}h ${globalData.timeUntilChurn.minutes}m`
+ msgTitle = "(CHURN) TIME UNTIL";
+ timeToDisplay = `${globalData?.timeUntilChurn?.days}d ${globalData?.timeUntilChurn?.hours}h ${globalData?.timeUntilChurn?.minutes}m`;
}
- //timeToDisplay = `${globalData.timeUntilChurn.days}d ${globalData.timeUntilChurn.hours}h ${globalData.timeUntilChurn.minutes}m`
-
return (
-
-
-
- | Current Block: |
- {parseInt(globalData.maxHeight).toLocaleString()} |
-
-
- | {msgTitle} |
- {timeToDisplay} |
-
-
- | Total Bonded Value: |
- ᚱ{state.activeNodes.length>0 ? parseInt((state.activeNodes.map(item => item.bond).reduce((prev, next) => prev + next))/100000000).toLocaleString() : '0'} |
-
-
- | Market Cap: |
- ${globalData.coingecko.market_cap.toLocaleString()} |
-
-
- | 24hr Vol: |
- ${globalData.coingecko.total_volume.toLocaleString()} |
-
-
-
- )
-}
+ <>
+
+

+
+
CURRENT BLOCK
+
+ {parseInt(globalData.maxHeight).toLocaleString()}
+
+
+
+
+

+
+
{msgTitle}
+
{timeToDisplay}
+
+
+
+

+
+
TOTAL BONDED VALUE
+
+ ᚱ
+ {state.activeNodes.length > 0
+ ? parseInt(
+ state.activeNodes
+ .map((item) => item.bond)
+ .reduce((prev, next) => prev + next) / 100000000
+ ).toLocaleString()
+ : "0"}
+
+
+
+
+

+
+
MARKET CAP
+
+ ${globalData?.coingecko?.market_cap?.toLocaleString()}
+
+
+
+
+

+
+
24 HR VOLUME
+
+ ${globalData?.coingecko?.total_volume?.toLocaleString()}
+
+
+
+
+

+
+
MAX EFFECTIVE BOND
+
+ ᚱ
+ {parseInt(
+ globalData.maxEffectiveStake / 100000000
+ ).toLocaleString()}
+
+
+
+ >
+ );
+};
const CoinGeckoData = ({ globalData }) => {
- if (globalData.length===0) return null
-
- const title = {fontWeight: 800, textAlign: 'right', borderStyle: 'none', minWidth: 170}
- const values = {textAlign: 'right', borderStyle: 'none', paddingLeft: 20}
- const tr = {}
return (
-
-
-
- | RUNE/USDT: |
- ${globalData.coingecko.current_price.toLocaleString()} |
-
-
- | 24hr High: |
- ${globalData.coingecko.high_24h} |
-
-
- | 24hr Low: |
- ${globalData.coingecko.low_24h} |
-
-
- | Market Cap Rank: |
- {globalData.coingecko.market_cap_rank} |
-
-
- | Total Supply: |
- ᚱ{globalData.coingecko.total_supply.toLocaleString()} |
-
-
-
- )
+ <>
+
+

+
+
PRICE
+
+ ${globalData?.coingecko?.current_price?.toLocaleString()}
+
+
+
+
+

+
+
24 HR HIGH
+
+ ${globalData?.coingecko?.high_24h}
+
+
+
+
+

+
+
24 HR LOW
+
+ ${globalData?.coingecko?.low_24h}
+
+
+
+
+

+
+
MARKET CAP RANK
+
+ {globalData?.coingecko?.market_cap_rank}
+
+
+
+
+

+
+
TOTAL SUPPLY
+
+ ᚱ{globalData?.coingecko?.total_supply?.toLocaleString()}
+
+
+
+ >
+ );
+};
-}
+const ReturnIspImage = ({ isp }) => {
+ const style = { width: 25, height: 25 };
-const ReturnIspImage = ({isp}) => {
+ if (
+ isp === "Amazon.com, Inc." ||
+ isp === "Amazon Technologies Inc." ||
+ isp === "Amazon.com"
+ ) {
+ return
;
+ }
+ if (isp === "DigitalOcean, LLC" || isp === "DigitalOcean") {
+ return
;
+ }
+ if (isp === "Google LLC") {
+ return
;
+ }
- const style = {width: 25, height: 25}
+ if (isp === "Microsoft Corporation") {
+ return
;
+ }
- if (isp ==='Amazon.com, Inc.' || isp === 'Amazon Technologies Inc.' || isp === 'Amazon.com'){
- return
+ if (isp === "Hetzner Online GmbH") {
+ return
;
}
- if (isp ==='DigitalOcean, LLC' || isp==='DigitalOcean'){
- return
+
+ if (isp === "The Constant Company" || isp === "The Constant Company, LLC") {
+ return
;
}
- if (isp ==='Google LLC'){
- return
+
+ if (isp === "Leaseweb UK Limited" || isp === "Leaseweb USA, Inc.") {
+ return
;
}
- if (isp ==='Microsoft Corporation'){
- return
+ if (isp === "Datacamp Limited") {
+ return
;
}
- if (isp ==='Hetzner Online GmbH'){
- return
+ if (isp === "Comcast Cable Communications, LLC") {
+ return
;
}
- if (isp ==='The Constant Company' || isp === 'The Constant Company, LLC'){
- return
+ if (isp === "Choopa") {
+ return
;
}
+ if (isp === "Charter Communications Inc") {
+ return
;
+ }
- return '-'
-}
+ if (isp === "AT&T Services, Inc.") {
+ return
;
+ }
+
+ if (isp === "Zenlayer Inc") {
+ return
;
+ }
+
+ return "-";
+};
-const ChainTD = ({chain, obchains, maxChainHeights}) => {
- const delta = obchains[chain]-maxChainHeights[chain]
+const ChainTD = ({ chain, obchains, maxChainHeights }) => {
+ const delta = obchains[chain] - maxChainHeights[chain];
return (
- {delta===0 ? 'OK' : delta.toString()} |
- )
-}
+
+ {delta === 0 ? "OK" : delta.toString()}
+ |
+ );
+};
-const BondProviderPopOver = ({data}) => {
- const totalBond = data.map(item=>parseInt(item.bond)).reduce((a, b) => a + b, 0)
+const BondProviderPopOver = ({ data }) => {
+ const totalBond = data
+ .map((item) => parseInt(item.bond))
+ .reduce((a, b) => a + b, 0);
- const d = data.map((item,index) => {
+ const d = data.map((item, index) => {
return (
-
- {item.bond_address.substring(item.bond_address.length-4, item.bond_address.length)}
- {((item.bond/totalBond)*100).toFixed(2)}%
- ᚱ{parseInt((item.bond/100000000).toFixed()).toLocaleString()}
-
- )
- })
- return d
-}
+
+
+ {item.bond_address.substring(
+ item.bond_address.length - 4,
+ item.bond_address.length
+ )}
+
+ {((item.bond / totalBond) * 100).toFixed(2)}%
+
+ ᚱ{parseInt((item.bond / 100000000).toFixed()).toLocaleString()}
+
+
+ );
+ });
+ return d;
+};
-const NodeTable = ({nodeData, clickSortHeader, sortColour, maxChainHeights, chains, addToFav, whichHeart}) => {
+const NodeTable = ({
+ nodeData,
+ clickSortHeader,
+ handleClickBond,
+ handleClickRewards,
+ handleClickSlashes,
+ sortColour,
+ maxChainHeights,
+ chains,
+ addToFav,
+ whichHeart,
+ chartDataConfig,
+ bondOptions,
+ rewardsOptions,
+ slashesOptions,
+ visibleColumns = { ...defaulColumns },
+ sortBy = "",
+ sortDirection = "",
+}) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(100);
+
+ const totalPages = Math.ceil(nodeData.length / itemsPerPage);
+ const pageNumbers = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ pageNumbers.push(i);
+ }
- return (
-
-
-
-
- |
- clickSortHeader('node_address')}>NODE |
- clickSortHeader('age')}>AGE |
- clickSortHeader('action')}>ACTION |
- clickSortHeader('isp')}>ISP |
- clickSortHeader('location')}>LOCATION |
- clickSortHeader('bond')}>BOND |
- clickSortHeader('bond_providers')}>PROVIDERS |
- clickSortHeader('current_award')}>REWARDS |
- clickSortHeader('apy')}>APY |
- clickSortHeader('slash_points')}>SLASHES |
- clickSortHeader('score')}>SCORE |
- clickSortHeader('version')}>VERSION |
- clickSortHeader('leave')}>{leaveIcon} |
-
-
- {chains &&
- <>
-  |
-  |
-  |
-  |
-  |
-  |
-  |
- >
- }
- RPC |
- BFR |
+ const handleClick = (event) => {
+ setCurrentPage(Number(event.target.id));
+ };
+ const handleNext = () => {
+ setCurrentPage((prevPage) =>
+ prevPage === totalPages ? prevPage : prevPage + 1
+ );
+ };
-
-
-
- {nodeData.map((item, index) => (
+ const handlePrev = () => {
+ setCurrentPage((prevPage) => (prevPage === 1 ? prevPage : prevPage - 1));
+ };
-
+ const renderPageNumbers = pageNumbers.map((number) => {
+ return (
+
+ {number}
+
+ );
+ });
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = nodeData.slice(indexOfFirstItem, indexOfLastItem);
- | {index+1} |
+ const getHeaderClassName = (key) => {
+ return visibleColumns && visibleColumns[key]
+ ? "tableHeader"
+ : "tableHeader hidden";
+ };
-
- {
+ return visibleColumns && visibleColumns[key] ? "" : "hidden";
+ };
+
+ const updatePagingItem = (value) => {
+ setItemsPerPage(value);
+ };
+ return (
+ <>
+
+ Nodes per page:
+
+
+
+
+
+
+
+
+ |
+ clickSortHeader("node_address")}
+ >
+
+ Validator Nodes
+
+
+ |
+ clickSortHeader("age")}
+ >
+
+ Age
+
+
+ |
+ clickSortHeader("action")}
+ >
+
+ Action
+
+
+ |
+ clickSortHeader("isp")}
+ >
+
+ ISP
+
+
+ |
+ clickSortHeader("location")}
+ >
+
+ Location
+
+
+ |
+ clickSortHeader("bond")}
+ >
+
+ Bond
+
+
+ |
+ clickSortHeader("bond_providers")}
+ >
+
+ Providers
+
+
+ |
+ clickSortHeader("current_award")}
+ >
+
+ Rewards
+
+
+ |
+ clickSortHeader("apy")}
+ >
+
+ APY
+
+
+ |
+ clickSortHeader("slash_points")}
+ >
+
+ Slashes
+
+
+ |
+ clickSortHeader("score")}
+ >
+
+ Score
+
+
+ |
+ clickSortHeader("version")}
+ >
+
+ Version
+
+
+ |
+ clickSortHeader("leave")}
+ >
+
+ {leaveIcon}
+
+
+ |
+
+
+ RPC
+ |
+
+ BFR
+ |
+
+ {chains && (
+ <>
+ clickSortHeader("BNB")}
+ >
+
+ 
+
+
+ |
+
+ clickSortHeader("BTC")}
+ >
+
+ 
+
+
+ |
+
+ clickSortHeader("ETH")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("LTC")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("BCH")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("DOGE")}
+ >
+
+ 
+
+
+ |
+ clickSortHeader("GAIA")}
+ >
+
+ 
+
+
+ |
+
+ clickSortHeader("AVAX")}
+ >
+
+ 
+
+
+ |
+ >
+ )}
+
+
+
+ {currentItems.map((item, index) => (
+
+ |
+ {index + 1}
+ |
+
+
+
+ copyToClipWithPopup(
+ "Node address copied to clipboard:",
+ item.node_address
+ )
+ }
+ >
+ {`...${item.node_address.substring(
+ item.node_address.length - 4
+ )}`}
+
+
+
+ |
+
+ {item.age.toFixed(2)}
+ |
+
+ {item.action}
+ |
+
+
+
+
+
+
+ |
+
+ {item.location}
+ |
+ handleClickBond(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Bond Value Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ ᚱ
+ {parseInt(
+ (item.bond / 100000000).toFixed()
+ ).toLocaleString()}
+
+ |
+
+ {" "}
+
+ }
+ title={"Bond Providers"}
+ trigger="hover"
+ >
+
+ {item.bond_providers.providers.length}
+
+
+ |
+ handleClickRewards(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Rewards Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ ᚱ
+ {parseInt(
+ (item.current_award / 100000000).toFixed()
+ ).toLocaleString()}
+
+ |
+
+
+ {item.apy}
+ |
+ handleClickSlashes(item.node_address)}
+ >
+ 0 ? (
+
+ ) : (
+ No data available
+ )
+ }
+ title={`Slashes Over Time for ${item.node_address.slice(
+ -4
+ )}`}
+ trigger="click"
+ overlayClassName="my-custom-popover"
+ >
+ {parseInt(item.slash_points).toLocaleString()}
+
+ |
+
+
+ {item.score}
+ |
+
+ {item.version}
+ |
+ {/* {item.rpc === 'true' ? '*' : 'BAD'} | */}
+
+ {item.forced_to_leave === 1 || item.requested_to_leave === 1
+ ? "yes"
+ : "-"}
+ |
+
+
+ {item.rpc !== "null" ? "*" : "Bad"}
+
+ |
+
+
+ {item.bifrost !== "null" ? "*" : "Bad"}
+
+ |
+
+ {chains && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ ))}
+
+ |
+
+
+
+
+
+
+ -
- copyToClipWithPopup('Node address copied to clipboard:', item.node_address)}>
- {item.node_address.substring(item.node_address.length-4,item.node_address.length)}
-
-
-
-
|
- {item.age.toFixed(2)} |
- {item.action} |
- |
- {item.location} |
- ᚱ{parseInt((item.bond/100000000).toFixed()).toLocaleString()} |
- }
- title={'Bond Providers'}
- trigger="hover"
- >{item.bond_providers.providers.length}
-
- |
- ᚱ{parseInt((item.current_award/100000000).toFixed()).toLocaleString()} |
- {item.apy} |
- {parseInt(item.slash_points).toLocaleString()} |
- {item.score} |
- {item.version} |
- {item.forced_to_leave === "true" || item.requested_to_leave === "true" ? 'yes' : '-'} |
- {chains &&
- <>
-
-
-
-
-
-
-
- >
- }
- {item.rpc === 'true' ? '*' : 'BAD'} |
- {item.bifrost === 'true' ? '*' : 'BAD' } |
-
- ))}
-
-
-
-
- )
-}
+
+
+ {renderPageNumbers}
+
+
+
+
+
+
+ >
+ );
+};
let timer = null;
-export default class extends Component {
-
+const defaulColumns = {
+ nodes: true,
+ age: true,
+ action: true,
+ isp: true,
+ bond: true,
+ providers: true,
+ location: true,
+ leave: true,
+ rewards: true,
+ apy: true,
+ slashes: true,
+ score: true,
+ version: true,
+ rpc: true,
+ bfr: true,
+ BTC: true,
+ BNB: true,
+ ETH: true,
+ LTC: true,
+ BCH: true,
+ DOGE: true,
+ GAIA: true,
+ AVAX: true,
+};
+export default class extends Component {
constructor(props) {
super(props);
- this.state = {
- data: [],
- globalData: [],
- sortBy: 'bond',
- sortDirection: 'desc',
- activeNodes: [],
- standByNodes: [],
- whitelistedNodes: [],
- animateBlockCount: false,
- myFavNodes: [],
- searchTerm: '',
- };
+ this.state = {
+ chartData: [],
+ data: [],
+ globalData: [],
+ sortBy: "bond",
+ sortDirection: "desc",
+ activeNodes: [],
+ standByNodes: [],
+ whitelistedNodes: [],
+ animateBlockCount: false,
+ myFavNodes: [],
+ searchTerm: "",
+ visibleColumns: defaulColumns,
+ nodesFilter: {},
+ loading: true,
+ sortByChain: null,
+ };
+ this.clickSortHeader = this.clickSortHeader.bind(this);
+ this.handleClickRewards = this.handleClickRewards.bind(this);
+ this.handleClickSlashes = this.handleClickSlashes.bind(this);
+ this.handleClickBond = this.handleClickBond.bind(this);
}
-
-
async componentWillMount() {
+ const myFavNodes = getCookie("myFavNodes");
- const myFavNodes = getCookie('myFavNodes')
-
- const tmp = myFavNodes.length>0 ? JSON.parse(myFavNodes) : []
+ const tmp = myFavNodes.length > 0 ? JSON.parse(myFavNodes) : [];
- this.setState({ myFavNodes: tmp })
+ this.setState({ myFavNodes: tmp });
- this.refreshData()
+ this.refreshData();
}
async refreshData() {
- const data = await getData()
+ const data = await getData();
- if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
- console.log('DEV ONLY: Refresh Data Results: ', data)
+ if (this.state.loading) {
+ this.setState({ loading: false });
}
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
+ // console.log('DEV ONLY: Refresh Data Results: ', data)
+ }
- this.setState({data: data.data, globalData: data.globalData, maxChainHeights: data.maxChainHeights, animateBlockCount: false}, ()=>this.setData()) //Change animateBlockCount to true here for animation
-
+ this.setState(
+ {
+ data: data.data,
+ globalData: data.globalData,
+ maxChainHeights: data.maxChainHeights,
+ animateBlockCount: false,
+ },
+ () => this.setData()
+ ); //Change animateBlockCount to true here for animation
}
-
componentDidMount() {
-
timer = setInterval(() => {
- this.setState({animateBlockCount: false}, ()=>this.refreshData())
+ this.setState({ animateBlockCount: false }, () => this.refreshData());
//this.refreshData()
- }, 6000)
+ }, 6000);
}
componentWillUnmount() {
@@ -414,293 +1667,789 @@ export default class extends Component {
}
addToFav(address) {
-
//setCookie('myFavNodes', '')
//return
//Below works to add, but need to check if already exists, and if so remove
- const myFavNodes = getCookie('myFavNodes')//JSON.parse(
+ const myFavNodes = getCookie("myFavNodes"); //JSON.parse(
- if(myFavNodes.length===0) {
+ if (myFavNodes.length === 0) {
//in here no current fav nodes
- const singleAddress = JSON.stringify([address])
- setCookie('myFavNodes', singleAddress)
- this.setState({myFavNodes: singleAddress}, ()=>this.setData())
-
+ const singleAddress = JSON.stringify([address]);
+ setCookie("myFavNodes", singleAddress);
+ this.setState({ myFavNodes: singleAddress }, () => this.setData());
} else {
//If we already have some fav nodes
- const newFaveNodes = JSON.parse(myFavNodes)
+ const newFaveNodes = JSON.parse(myFavNodes);
//Need to check if already exists
if (newFaveNodes.indexOf(address) > -1) {
- //In the array!
-
- const newArrayWithRemove = newFaveNodes.filter(item => item!==address)
+ //In the array!
- const newFaveNodesNew = JSON.stringify(newArrayWithRemove)
- this.setState({myFavNodes: newArrayWithRemove}, ()=>this.setData())
- setCookie('myFavNodes', newFaveNodesNew)
+ const newArrayWithRemove = newFaveNodes.filter(
+ (item) => item !== address
+ );
+ const newFaveNodesNew = JSON.stringify(newArrayWithRemove);
+ this.setState({ myFavNodes: newArrayWithRemove }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
} else {
- //Not in the array
- newFaveNodes[newFaveNodes.length] = address
-
- const newFaveNodesNew = JSON.stringify(newFaveNodes)
+ //Not in the array
+ newFaveNodes[newFaveNodes.length] = address;
- this.setState({myFavNodes: newFaveNodes}, ()=>this.setData())
- setCookie('myFavNodes', newFaveNodesNew)
+ const newFaveNodesNew = JSON.stringify(newFaveNodes);
+ this.setState({ myFavNodes: newFaveNodes }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
}
-
}
-
}
returnSearchedData(data) {
- if (this.state.searchTerm === '') {
- return data
- } else {
- const filteredNodes = data.filter(item => item.node_address.includes(this.state.searchTerm))
- return filteredNodes
- }
+ if (this.state.searchTerm === "") {
+ return data;
+ } else {
+ const filteredNodes = data.filter((item) =>
+ item.node_address.includes(this.state.searchTerm)
+ );
+ return filteredNodes;
+ }
}
setData() {
- //Grab our state so we can mutate it
- let myData = JSON.parse(JSON.stringify(this.state.data))
-
+ // Grab our state so we can mutate it
+ let myData = JSON.parse(JSON.stringify(this.state.data));
+ // Add faves to the data, then we can sort by then below
+ const newItems = myData.map((item) => {
+ if (this.state.myFavNodes.includes(item.node_address)) {
+ item.fave = 1;
+ } else {
+ item.fave = 0;
+ }
+ return item;
+ });
+
+ // Filter for our three tables
+ let activeNodes = newItems.filter((item) => item.status === "Active");
+ let standbyNodes = newItems.filter(
+ (item) =>
+ (item.status === "Standby" || item.status === "Ready") &&
+ item.version === this.state.globalData.maxVersion
+ );
+ // Create an array of all nodes that are active and standby
+ const active_standy_nodes = [
+ ...activeNodes.map((item) => item.node_address),
+ ...standbyNodes.map((item) => item.node_address),
+ ];
- //Add faves to the data, then we can sort by then below
- const newItems = myData.map(item => {
- if (this.state.myFavNodes.includes(item.node_address)) {
- item.fave = 1
- } else {
- item.fave = 0
- }
- return item
- });
-
-/*
-Whitelisted
-Active
-Standby
-Ready
-Disabled
-*/
-
- //Filter for our three tables
- let activeNodes = newItems.filter(item => item.status ==='Active')
- let standbyNodes = newItems.filter(item => (item.status ==='Standby' || item.status ==='Ready') && item.version === this.state.globalData.maxVersion)
-
- //Create an array of all nodes that are active and standby
- const active_standy_nodes = [ ...activeNodes.map(item => {return item.node_address}), ...standbyNodes.map(item => {return item.node_address})]
- //White listed are all other nodes which are not active or on standby
- //White listed is really just "Other"
- let whitelisted = newItems.filter(item => !active_standy_nodes.includes(item.node_address))
-
- //let whitelisted = newItems.filter(item => !(item.status ==='Active' || item.status ==='Standby' || item.status ==='Ready') /*&& item.version !== this.state.globalData.maxVersion*/)
-
- activeNodes = this.findChurnOuts(activeNodes) //Add in the actions for churning
- standbyNodes = this.findChurnIns(standbyNodes) //Add in the actions for nodes churning in
+ // White listed are all other nodes which are not active or on standby
+ let whitelisted = newItems.filter(
+ (item) => !active_standy_nodes.includes(item.node_address)
+ );
- //Filter here if any searchTerm from the search bar
- //Need to do after adding the actions
- activeNodes = this.returnSearchedData(activeNodes)
- standbyNodes = this.returnSearchedData(standbyNodes)
- whitelisted = this.returnSearchedData(whitelisted)
+ activeNodes = this.findChurnOuts(activeNodes); // Add in the actions for churning
+ standbyNodes = this.findChurnIns(standbyNodes); // Add in the actions for nodes churning in
- //Sort and add our favs to the top
- let activeNodesSorted = this.sortData(activeNodes)
- const favActiveNodesSorted = activeNodesSorted.filter(item => item.fave === 1) //Get our favourites
- activeNodesSorted = activeNodesSorted.filter(item => item.fave === 0)//Get our non favourites
+ // Filter here if any searchTerm from the search bar
+ activeNodes = this.returnSearchedData(activeNodes);
+ standbyNodes = this.returnSearchedData(standbyNodes);
+ whitelisted = this.returnSearchedData(whitelisted);
- activeNodesSorted = [...favActiveNodesSorted, ...activeNodesSorted] //Join faves at top with non favourites
+ // Sort and add our favs to the top
+ let activeNodesSorted = this.sortData(
+ activeNodes,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+ const favActiveNodesSorted = activeNodesSorted.filter(
+ (item) => item.fave === 1
+ ); // Get our favourites
+ activeNodesSorted = activeNodesSorted.filter((item) => item.fave === 0); // Get our non favourites
+ activeNodesSorted = [...favActiveNodesSorted, ...activeNodesSorted]; // Join faves at top with non favourites
+
+ const standBySorted = this.sortData(
+ standbyNodes,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+ const whitelistedSorted = this.sortData(
+ whitelisted,
+ this.state.sortBy,
+ this.state.sortDirection,
+ false
+ );
+ this.setState({
+ activeNodes: activeNodesSorted,
+ standByNodes: standBySorted,
+ whitelistedNodes: whitelistedSorted,
+ });
+ }
- const standBySorted = this.sortData(standbyNodes)
- const whitelistedSorted = this.sortData(whitelisted)
+ onColumnUpdate(config) {
+ this.setState({ visibleColumns: config });
+ }
- this.setState({
- activeNodes: activeNodesSorted,
- standByNodes: standBySorted,
- whitelistedNodes: whitelistedSorted //This is really just other
- })
+ onNodesFilter(key) {
+ this.setState((prevState) => ({
+ nodesFilter: {
+ ...prevState.nodesFilter,
+ [key]: !prevState.nodesFilter[key],
+ },
+ }));
}
-/*
+ /*
Split the data into over 300ks and under 300ks
With the over 300ks, take the top 3 if they exist and apply churn in action
If 4 nodes churn in instead of 3 each time, add another row
*/
findChurnIns(standbyNodes) {
- if (standbyNodes.length === 0) return [] //Stops filter from breaking when search returns 0
+ if (standbyNodes.length === 0) return []; //Stops filter from breaking when search returns 0
- const over300 = standbyNodes.filter(item => item.bond >= 30000000000000)
- const over300Sorted = this.sortData(over300, 'bond', 'desc')
+ const over300 = standbyNodes.filter((item) => item.bond >= 30000000000000);
+ const over300Sorted = this.sortData(over300, "bond", "desc");
- if (over300Sorted.length > 0){
- over300Sorted[Math.min(0, over300Sorted.length-1)].action = 'Churn In'
- over300Sorted[Math.min(1, over300Sorted.length-1)].action = 'Churn In'
- over300Sorted[Math.min(2, over300Sorted.length-1)].action = 'Churn In'
- over300Sorted[Math.min(3, over300Sorted.length-1)].action = 'Churn In'
- over300Sorted[Math.min(4, over300Sorted.length-1)].action = 'Churn In'
+ if (over300Sorted.length > 0) {
+ over300Sorted[Math.min(0, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(1, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(2, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(3, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(4, over300Sorted.length - 1)].action = "Churn In";
}
- const under300 = standbyNodes.filter(item => item.bond < 30000000000000)
+ const under300 = standbyNodes.filter((item) => item.bond < 30000000000000);
- return [...over300Sorted, ...under300]
+ return [...over300Sorted, ...under300];
}
-/*
+ /*
Lowest Bond
Oldest Node
Worst Performer (Can't churn out if just churned in, one cycle grace period)
*/
findChurnOuts(activeNodes) {
- if (activeNodes.length === 0) return [] //Stops filter from breaking when search returns 0
+ if (activeNodes.length === 0) return []; //Stops filter from breaking when search returns 0
- let activeNodesSorted = this.sortData(activeNodes, 'age', 'desc')
- activeNodesSorted[0].action = 'Oldest'
+ let activeNodesSorted = this.sortData(activeNodes, "age", "desc");
+ activeNodesSorted[0].action = "Oldest";
- activeNodesSorted = this.sortData(activeNodes, 'bond', 'asc')
- activeNodesSorted[0].action = 'Smallest Bond'
+ activeNodesSorted = this.sortData(activeNodes, "bond", "asc");
+ activeNodesSorted[0].action = "Smallest Bond";
- activeNodesSorted = this.sortData(activeNodes, 'score', 'asc', true)
+ activeNodesSorted = this.sortData(activeNodes, "score", "asc", true);
//we set the 'Worst Performing' tag in the sortData function
- this.calcBadValidatorRedline(activeNodes)
+ this.calcBadValidatorRedline(activeNodes);
- return activeNodesSorted
+ return activeNodesSorted;
}
- calcBadValidatorRedline(activeNodes){
-
+ calcBadValidatorRedline(activeNodes) {
//Only get nodes with slashes greater than 100
- const greater100Slashes = activeNodes.filter(item => item.slash_points > 100)
+ const greater100Slashes = activeNodes.filter(
+ (item) => item.slash_points > 100
+ );
//Add all the scores together for thes nodes
const sum = greater100Slashes.reduce((accumulator, object) => {
return accumulator + parseFloat(object.score);
}, 0);
//Find the average
- const averageScore = sum/(greater100Slashes.length+1)
+ const averageScore = sum / (greater100Slashes.length + 1);
- const validatorLine = averageScore/this.state.globalData.BadValidatorRedline
+ const validatorLine =
+ averageScore / this.state.globalData.BadValidatorRedline;
- activeNodes.map(item => {
- if(item.score < validatorLine) {
- item.action = 'Bad Redline'
+ activeNodes.map((item) => {
+ if (item.score < validatorLine) {
+ item.action = "Bad Redline";
}
- return 0
- })
-
+ return 0;
+ });
}
-/*
+ /*
Sort by either string or number
We use string sort function if value is one of the arrays else do second sort number
*/
sortData(data, value = null, direction = null, worst_perform = false) {
- const toSortBy = value === null ? this.state.sortBy : value
- let newData = []
-
- if (['node', 'isp', 'location', 'version', 'action', 'node_address'].includes(toSortBy)){ //This sort function for strings
- newData = data.sort((a, b) => a[toSortBy].localeCompare(b[toSortBy]));
-
- } else if (toSortBy === 'bond_providers') {//This is for bond provider sort as we need to go another layer deep in the object
- newData = data.sort((a, b) => a[toSortBy].providers.length - b[toSortBy].providers.length);
-
- } else if (worst_perform === true) { //This is for when we are sorting for action of worst performance as we want to exclude any with age under 3 days
- const ageCutOffDays = 3
- const a = data.filter(item => parseFloat(item.age) > ageCutOffDays)
- const b = data.filter(item => parseFloat(item.age) <= ageCutOffDays)
- const aSorted = a.sort((a, b) => (b[toSortBy] - a[toSortBy]) );
-
- aSorted[aSorted.length-1].action = 'Worst Performing'
- newData = [...aSorted, ...b]
-
- } else { //This sort function for numbers
- //When sorting, if values are the same, sort by node_address
- newData = data.sort(
- function(a, b) {
- if (a[toSortBy] === b[toSortBy]) {
- return a['node_address'].localeCompare(b['node_address']);
- }
- return a[toSortBy] > b[toSortBy] ? 1 : -1;
- });
+ const toSortBy = value === null ? this.state.sortBy : value;
+ let newData = [];
+ if (this.state.sortChain) {
+ // New sorting logic for chains
+ const chain = toSortBy;
+ newData = [...data].sort((a, b) => {
+ const deltaA = a.obchains[chain] - this.state.maxChainHeights[chain];
+ const deltaB = b.obchains[chain] - this.state.maxChainHeights[chain];
+ if (deltaA === deltaB) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return deltaA - deltaB;
+ });
+ } else {
+ if (
+ [
+ "node",
+ "isp",
+ "location",
+ "version",
+ "action",
+ "node_address",
+ ].includes(toSortBy)
+ ) {
+ newData = data.sort((a, b) => a[toSortBy].localeCompare(b[toSortBy]));
+ } else if (toSortBy === "bond_providers") {
+ newData = data.sort(
+ (a, b) => a[toSortBy].providers.length - b[toSortBy].providers.length
+ );
+ } else if (worst_perform === true) {
+ const ageCutOffDays = 3;
+ const a = data.filter((item) => parseFloat(item.age) > ageCutOffDays);
+ const b = data.filter((item) => parseFloat(item.age) <= ageCutOffDays);
+ const aSorted = a.sort((a, b) => b[toSortBy] - a[toSortBy]);
+ aSorted[aSorted.length - 1].action = "Worst Performing";
+ newData = [...aSorted, ...b];
+ } else {
+ newData = data.sort(function (a, b) {
+ if (a[toSortBy] === b[toSortBy]) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return a[toSortBy] > b[toSortBy] ? 1 : -1;
+ });
+ }
}
- //If we pass it a direction, we set it here, if not we take it from the state
- const toDirection = direction === null ? this.state.sortDirection : direction
- if (toDirection === 'desc') {
- newData.reverse()
+
+ const toDirection =
+ direction === null ? this.state.sortDirection : direction;
+ if (toDirection === "desc") {
+ newData.reverse();
}
- return newData
+ return newData;
}
- clickSortHeader(item){
- const direction = this.state.sortBy !== item ? 'desc' : this.state.sortDirection === 'desc' ? 'asc' : 'desc';
- this.setState({sortBy: item, sortDirection: direction}, ()=> this.setData())
+ clickSortHeader(item) {
+ const isChain = [
+ "BNB",
+ "BTC",
+ "ETH",
+ "LTC",
+ "BCH",
+ "DOGE",
+ "GAIA",
+ "AVAX",
+ ].includes(item);
+ const direction =
+ this.state.sortBy !== item
+ ? "desc"
+ : this.state.sortDirection === "desc"
+ ? "asc"
+ : "desc";
+ this.setState(
+ { sortBy: item, sortDirection: direction, sortChain: isChain },
+ () => this.setData()
+ );
+ window.setTimeout(() => {}, 200);
}
sortColour(item) {
- if (item === this.state.sortBy) {
- return this.state.sortDirection ==='desc' ? '#065900' : '#590000'
- } else {
- return null
- }
+ return "#ffffff";
}
- whichHeart(address){
- return this.state.myFavNodes.includes(address) ? heartFull : heartBlank
+ whichHeart(address) {
+ return this.state.myFavNodes.includes(address) ? heartFull : heartBlank;
}
+ handleClickSlashes = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabSlashes=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickRewards = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabRewards=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickBond = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabBond=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Math.round(Number(y) / 100000000),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
searchBar() {
return (
- this.setState({ searchTerm: event.target.value.trim()},()=>this.setData())}
- />
- )
+
+
+ this.setState(
+ { searchTerm: event.target.value.trim().toLowerCase() },
+ () => this.setData()
+ )
+ }
+ prefix={}
+ />
+
+ );
}
render() {
-
-/*
- if (this.state.data.length === 0) {
- return
- }
-*/
+ const {
+ loading,
+ data,
+ nodesFilter,
+ visibleColumns,
+ activeNodes,
+ standByNodes,
+ whitelistedNodes,
+ } = this.state;
+
+ const noNodesFilter =
+ !nodesFilter.active && !nodesFilter.standby && !nodesFilter.orthers;
+ const showActive =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.active;
+ const showStandby =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.standby;
+ const showOthers =
+ isEmpty(nodesFilter) || noNodesFilter || nodesFilter.orthers;
+
+ const chartDataConfig = this.state.chartData
+ ? {
+ datasets: [
+ {
+ label: "Value",
+ data: this.state.chartData,
+ fill: false,
+ backgroundColor: "rgb(28, 57, 182)",
+ borderColor: "rgba(28, 57, 187, 0.2)",
+ },
+ ],
+ }
+ : {};
+
+ const slashesOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Slashes Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const rewardsOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Reward Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const bondOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ min: Math.min(...this.state.chartData.map((data) => data.x)),
+ stepSize: 20000,
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Bond Value",
+ },
+ ticks: {
+ min: Math.min(...this.state.chartData.map((data) => data.y)),
+ stepSize: 20000,
+ callback: function (value) {
+ return value.toString();
+ },
+ },
+ },
+ ],
+ },
+ }
+ : {};
return (
-
-
-
-
-
-
+
+
+

+
Thornode Monitor
-
- {this.searchBar()}
- Active
-
-
- {'Standby'}
-
-
- {'Other'}
-
+
+ {/*
this.onNodesFilter('active')}>

+
this.onNodesFilter('standby')}>

+
this.onNodesFilter('orthers')}>

*/}
+
this.onNodesFilter("active")}
+ >
+
+
+
+
+
this.onNodesFilter("standby")}
+ >
+
+
+
+
+
this.onNodesFilter("orthers")}
+ >
+
+
+
+
+
+
+
+ {loading && (
+
+

+
+ )}
+
+ {!loading && (
+
+
+ Dashboard
+
+
+
+
+ {showActive && (
+ <>
+
+
+
+
+
+
+ {this.searchBar()}
+
+
+
+
+ {activeNodes.length > 0 && (
+
+ )}
+ {activeNodes.length === 0 && (
+
+
+ No Active Data Available!
+
+
+ )}
+
+ >
+ )}
+
+ {showStandby && (
+ <>
+
+
+
+
+
+
+
+ {!showActive && (
+ <>
+ {this.searchBar()}
+
+ >
+ )}
+
+
+ {standByNodes.length > 0 && (
+
+ )}
+ {standByNodes.length === 0 && (
+
+
+ No Standby Data Available!
+
+
+ )}
+
+ >
+ )}
+ {showOthers && (
+ <>
+
+
+
+
+
+
+
+ {!showActive && !showStandby && (
+ <>
+ {this.searchBar()}
+
+ >
+ )}
+
+
+ {whitelistedNodes.length > 0 && (
+
+ )}
+ {whitelistedNodes.length === 0 && (
+
+
+ No Other Data Available!
+
+
+ )}
+ >
+ )}
+
+ )}
-
-
-
-
-
-
+
+
);
}
}
diff --git a/src/containers/A_monitor/otherpage.js b/src/containers/A_monitor/otherpage.js
new file mode 100644
index 0000000..cbb6e6b
--- /dev/null
+++ b/src/containers/A_monitor/otherpage.js
@@ -0,0 +1,2011 @@
+import React, { Component, useState } from "react";
+import isEmpty from "lodash/isEmpty";
+import Modals from "@iso/components/Feedback/Modal";
+import Popover from "@iso/components/uielements/popover";
+import { getData, setCookie, getCookie } from "CommonFunctions";
+import { ModalContent } from "../Feedback/Modal/Modal.styles";
+import { Layout, Button, Input, Breadcrumb, Select } from "antd";
+import { Line } from "react-chartjs-2";
+import CustomLineChart from "./CustomLineChart";
+import "./styles.css";
+import { Link } from "react-router-dom";
+import { PUBLIC_ROUTE } from "../../route.constants";
+import { SearchOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
+
+import heartBlank from "@iso/assets/images/heart-blank.png";
+import heartFull from "@iso/assets/images/heart-full.png";
+
+import imageDO from "@iso/assets/images/do.png";
+import imageAWS from "@iso/assets/images/aws.png";
+import imageGCP from "@iso/assets/images/gcp.png";
+import imageAZURE from "@iso/assets/images/azure.png";
+import imageHETZNER from "@iso/assets/images/hetzner.png";
+import imageVULTR from "@iso/assets/images/vultr.png";
+import imageLeaseweb from "@iso/assets/images/leaseweb.png";
+import imageDatacamp from "@iso/assets/images/datacamp.png";
+import imageComcast from "@iso/assets/images/comcast.png";
+import imageChoopa from "@iso/assets/images/choopa.png";
+import imageChartercoms from "@iso/assets/images/chartercoms.png";
+import imageATandT from "@iso/assets/images/atandt.png";
+import imageZenlayer from "@iso/assets/images/zenlayer.png";
+
+import binance from "@iso/assets/images/binance.png";
+import eth from "@iso/assets/images/eth.png";
+import bitcoin from "@iso/assets/images/bitcoin.png";
+import litecoin from "@iso/assets/images/litecoin.png";
+import bitcoincash from "@iso/assets/images/bitcoincash.png";
+import dogecoin from "@iso/assets/images/dogecoin.png";
+import gaia from "@iso/assets/images/atom.png";
+import thornode from "@iso/assets/images/thornode.svg";
+import avax from "@iso/assets/images/avax.png";
+
+import blockIcon from "@iso/assets/images/overview/block_icon.svg";
+import highTradingIcon from "@iso/assets/images/overview/24high_trading.svg";
+import lowTradingIcon from "@iso/assets/images/overview/24low_trading.svg";
+import bondIcon from "@iso/assets/images/overview/Bond_icon.svg";
+import churnsIcon from "@iso/assets/images/overview/churns_icon.svg";
+import marcketCapIcon from "@iso/assets/images/overview/marcket_Cap.svg";
+import mcapRankIcon from "@iso/assets/images/overview/mcap_rank.svg";
+import runeUsdtIcon from "@iso/assets/images/overview/rune_usdt.svg";
+import timeIcon from "@iso/assets/images/overview/time_icon.svg";
+import totalSupplyIcon from "@iso/assets/images/overview/total_supply.svg";
+import filterIcon from "@iso/assets/images/overview/filter.svg";
+import loadingIcon from "@iso/assets/images/overview/loading.png";
+import githubIcon from "@iso/assets/images/overview/github_link_icon.svg";
+import twitterIcon from "@iso/assets/images/overview/twitter_link_icon.svg";
+import liquifyLogo from "@iso/assets/images/overview/liquify_logo.svg";
+
+import threeDotsIcon from "@iso/assets/images/overview/dots_three_circle.svg";
+import powerIcon from "@iso/assets/images/overview/power.svg";
+import activeIcon from "@iso/assets/images/overview/active_icon.svg";
+import arrowDownIcon from "@iso/assets/images/overview/arrow-down.svg";
+
+import VisibleColumn from "@iso/components/VisibleColumn/VisibleColumn";
+const leaveIcon = (
+
+);
+
+const { Header, Footer, Content } = Layout;
+
+const headerStyle = {
+ cursor: "pointer",
+ padding: "12px 15px",
+ fontSize: 15,
+ color: "#ffffff",
+ backgroundColor: "rgba(24, 34, 51, 0.4)",
+ height: 55,
+ fontWeight: 600,
+};
+const tdStyle = {
+ minWidth: 60,
+ textAlign: "right",
+ fontSize: 14,
+ padding: "10px 15px",
+};
+const trStyle = { height: 45 };
+const iconStyle = {
+ minWidth: 25,
+ padding: 5,
+ paddingLeft: 10,
+ paddingRight: 10,
+};
+
+async function copyToClipWithPopup(msg, ip) {
+ copyToClipboard(ip);
+ popUpModal(msg, ip);
+}
+
+const copyToClipboard = (str) => {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText)
+ return navigator.clipboard.writeText(str);
+ return Promise.reject("The Clipboard API is not available.");
+};
+
+function popUpModal(msg, ip) {
+ Modals.info({
+ title: Success
,
+ content: (
+
+
+ {`${msg} `} {`${ip}`}
+
+
+ ),
+ onOk() {},
+ okText: "OK",
+ cancelText: "Cancel",
+ className: "feedback-modal",
+ });
+}
+
+const SortIcon = ({ sortBy, column, sortDirection }) => {
+ if (sortBy == column) {
+ return (
+
+ );
+ }
+ return null;
+};
+
+const Icons = ({ address, ip_address, addToFav, whichHeart }) => {
+ const firstURL = `https://thornode.ninerealms.com/thorchain/node/${address}`;
+ const secondURL = `https://viewblock.io/thorchain/address/${address}`;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ copyToClipWithPopup("IP Copied to clipboard:", ip_address)
+ }
+ >
+
+
+
+
addToFav(address)}
+ src={whichHeart(address)}
+ style={{
+ cursor: "pointer",
+ marginLeft: 5,
+ marginTop: 2,
+ width: 15,
+ height: 15,
+ }}
+ />
+
+ );
+};
+
+const GlobalData = ({ globalData, animateBlockCount, state }) => {
+ let timeToDisplay = "";
+ let msgTitle = "";
+ if (globalData?.churnTry && globalData?.retiring === "false") {
+ msgTitle = "(CHURN) RETRY IN";
+ timeToDisplay = `${globalData?.timeUntilRetry?.days}d ${globalData?.timeUntilRetry?.hours}h ${globalData?.timeUntilRetry?.minutes}m`;
+ } else if (globalData?.retiring === "true") {
+ msgTitle = "(CHURN) CURRENTLY CHURNING";
+ timeToDisplay = "Churning";
+ } else {
+ msgTitle = "(CHURN) TIME UNTIL";
+ timeToDisplay = `${globalData?.timeUntilChurn?.days}d ${globalData?.timeUntilChurn?.hours}h ${globalData?.timeUntilChurn?.minutes}m`;
+ }
+
+ return (
+ <>
+
+

+
+
CURRENT BLOCK
+
+ {parseInt(globalData.maxHeight).toLocaleString()}
+
+
+
+
+

+
+
{msgTitle}
+
{timeToDisplay}
+
+
+
+

+
+
TOTAL BONDED VALUE
+
+ ᚱ
+ {state.activeNodes.length > 0
+ ? parseInt(
+ state.activeNodes
+ .map((item) => item.bond)
+ .reduce((prev, next) => prev + next) / 100000000
+ ).toLocaleString()
+ : "0"}
+
+
+
+
+

+
+
MARKET CAP
+
+ ${globalData?.coingecko?.market_cap?.toLocaleString()}
+
+
+
+
+

+
+
24 HR VOLUME
+
+ ${globalData?.coingecko?.total_volume?.toLocaleString()}
+
+
+
+
+

+
+
MAX EFFECTIVE BOND
+
+ ᚱ
+ {parseInt(
+ globalData.maxEffectiveStake / 100000000
+ ).toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const CoinGeckoData = ({ globalData }) => {
+ return (
+ <>
+
+

+
+
PRICE
+
+ ${globalData?.coingecko?.current_price?.toLocaleString()}
+
+
+
+
+

+
+
24 HR HIGH
+
+ ${globalData?.coingecko?.high_24h}
+
+
+
+
+

+
+
24 HR LOW
+
+ ${globalData?.coingecko?.low_24h}
+
+
+
+
+

+
+
MARKET CAP RANK
+
+ {globalData?.coingecko?.market_cap_rank}
+
+
+
+
+

+
+
TOTAL SUPPLY
+
+ ᚱ{globalData?.coingecko?.total_supply?.toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const ReturnIspImage = ({ isp }) => {
+ const style = { width: 25, height: 25 };
+
+ if (
+ isp === "Amazon.com, Inc." ||
+ isp === "Amazon Technologies Inc." ||
+ isp === "Amazon.com"
+ ) {
+ return
;
+ }
+ if (isp === "DigitalOcean, LLC" || isp === "DigitalOcean") {
+ return
;
+ }
+ if (isp === "Google LLC") {
+ return
;
+ }
+
+ if (isp === "Microsoft Corporation") {
+ return
;
+ }
+
+ if (isp === "Hetzner Online GmbH") {
+ return
;
+ }
+
+ if (isp === "The Constant Company" || isp === "The Constant Company, LLC") {
+ return
;
+ }
+
+ if (isp === "Leaseweb UK Limited" || isp === "Leaseweb USA, Inc.") {
+ return
;
+ }
+
+ if (isp === "Datacamp Limited") {
+ return
;
+ }
+
+ if (isp === "Comcast Cable Communications, LLC") {
+ return
;
+ }
+
+ if (isp === "Choopa") {
+ return
;
+ }
+
+ if (isp === "Charter Communications Inc") {
+ return
;
+ }
+
+ if (isp === "AT&T Services, Inc.") {
+ return
;
+ }
+
+ if (isp === "Zenlayer Inc") {
+ return
;
+ }
+
+ return "-";
+};
+
+const ChainTD = ({ chain, obchains, maxChainHeights }) => {
+ const delta = obchains[chain] - maxChainHeights[chain];
+ return (
+
+ {delta === 0 ? "OK" : delta.toString()}
+ |
+ );
+};
+
+const BondProviderPopOver = ({ data }) => {
+ const totalBond = data
+ .map((item) => parseInt(item.bond))
+ .reduce((a, b) => a + b, 0);
+
+ const d = data.map((item, index) => {
+ return (
+
+
+ {item.bond_address.substring(
+ item.bond_address.length - 4,
+ item.bond_address.length
+ )}
+
+ {((item.bond / totalBond) * 100).toFixed(2)}%
+
+ ᚱ{parseInt((item.bond / 100000000).toFixed()).toLocaleString()}
+
+
+ );
+ });
+ return d;
+};
+
+const NodeTable = ({
+ nodeData,
+ clickSortHeader,
+ handleClickBond,
+ handleClickRewards,
+ handleClickSlashes,
+ sortColour,
+ maxChainHeights,
+ chains,
+ addToFav,
+ whichHeart,
+ chartDataConfig,
+ bondOptions,
+ rewardsOptions,
+ slashesOptions,
+ visibleColumns = { ...defaulColumns },
+ sortBy = "",
+ sortDirection = "",
+}) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(100);
+
+ const totalPages = Math.ceil(nodeData.length / itemsPerPage);
+ const pageNumbers = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ pageNumbers.push(i);
+ }
+
+ const handleClick = (event) => {
+ setCurrentPage(Number(event.target.id));
+ };
+
+ const handleNext = () => {
+ setCurrentPage((prevPage) =>
+ prevPage === totalPages ? prevPage : prevPage + 1
+ );
+ };
+
+ const handlePrev = () => {
+ setCurrentPage((prevPage) => (prevPage === 1 ? prevPage : prevPage - 1));
+ };
+
+ const renderPageNumbers = pageNumbers.map((number) => {
+ return (
+
+ {number}
+
+ );
+ });
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = nodeData.slice(indexOfFirstItem, indexOfLastItem);
+
+ const getHeaderClassName = (key) => {
+ return visibleColumns && visibleColumns[key]
+ ? "tableHeader"
+ : "tableHeader hidden";
+ };
+
+ const getCellClassName = (key) => {
+ return visibleColumns && visibleColumns[key] ? "" : "hidden";
+ };
+
+ const updatePagingItem = (value) => {
+ setItemsPerPage(value);
+ };
+ return (
+ <>
+
+ Nodes per page:
+
+
+
+
+
+
+
+ -
+
+
+ {renderPageNumbers}
+ -
+
+
+
+
+
+ >
+ );
+};
+
+let timer = null;
+
+const defaulColumns = {
+ nodes: true,
+ age: true,
+ action: true,
+ isp: true,
+ bond: true,
+ providers: true,
+ rewards: true,
+ apy: true,
+ slashes: true,
+ score: true,
+ version: true,
+ rpc: true,
+ bfr: true,
+};
+export default class extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ chartData: [],
+ data: [],
+ globalData: [],
+ sortBy: "bond",
+ sortDirection: "desc",
+ activeNodes: [],
+ standByNodes: [],
+ whitelistedNodes: [],
+ animateBlockCount: false,
+ myFavNodes: [],
+ searchTerm: "",
+ visibleColumns: defaulColumns,
+ nodesFilter: {},
+ loading: true,
+ };
+ this.clickSortHeader = this.clickSortHeader.bind(this);
+ this.handleClickRewards = this.handleClickRewards.bind(this);
+ this.handleClickSlashes = this.handleClickSlashes.bind(this);
+ this.handleClickBond = this.handleClickBond.bind(this);
+ }
+
+ async componentWillMount() {
+ const myFavNodes = getCookie("myFavNodes");
+
+ const tmp = myFavNodes.length > 0 ? JSON.parse(myFavNodes) : [];
+
+ this.setState({ myFavNodes: tmp });
+
+ this.refreshData();
+ }
+
+ async refreshData() {
+ const data = await getData();
+
+ if (this.state.loading) {
+ this.setState({ loading: false });
+ }
+
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
+ console.log("DEV ONLY: Refresh Data Results: ", data);
+ }
+
+ this.setState(
+ {
+ data: data.data,
+ globalData: data.globalData,
+ maxChainHeights: data.maxChainHeights,
+ animateBlockCount: false,
+ },
+ () => this.setData()
+ ); //Change animateBlockCount to true here for animation
+ }
+
+ componentDidMount() {
+ timer = setInterval(() => {
+ this.setState({ animateBlockCount: false }, () => this.refreshData());
+ //this.refreshData()
+ }, 6000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(timer);
+ }
+
+ addToFav(address) {
+ //Below works to add, but need to check if already exists, and if so remove
+
+ const myFavNodes = getCookie("myFavNodes"); //JSON.parse(
+
+ if (myFavNodes.length === 0) {
+ //in here no current fav nodes
+ const singleAddress = JSON.stringify([address]);
+ setCookie("myFavNodes", singleAddress);
+ this.setState({ myFavNodes: singleAddress }, () => this.setData());
+ } else {
+ //If we already have some fav nodes
+ const newFaveNodes = JSON.parse(myFavNodes);
+
+ //Need to check if already exists
+ if (newFaveNodes.indexOf(address) > -1) {
+ //In the array!
+
+ const newArrayWithRemove = newFaveNodes.filter(
+ (item) => item !== address
+ );
+
+ const newFaveNodesNew = JSON.stringify(newArrayWithRemove);
+ this.setState({ myFavNodes: newArrayWithRemove }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ } else {
+ //Not in the array
+ newFaveNodes[newFaveNodes.length] = address;
+
+ const newFaveNodesNew = JSON.stringify(newFaveNodes);
+
+ this.setState({ myFavNodes: newFaveNodes }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ }
+ }
+ }
+
+ returnSearchedData(data) {
+ if (this.state.searchTerm === "") {
+ return data;
+ } else {
+ const filteredNodes = data.filter((item) =>
+ item.node_address.includes(this.state.searchTerm)
+ );
+ return filteredNodes;
+ }
+ }
+
+ setData() {
+ //Grab our state so we can mutate it
+ let myData = JSON.parse(JSON.stringify(this.state.data));
+
+ //Add faves to the data, then we can sort by then below
+ const newItems = myData.map((item) => {
+ if (this.state.myFavNodes.includes(item.node_address)) {
+ item.fave = 1;
+ } else {
+ item.fave = 0;
+ }
+ return item;
+ });
+
+ /*
+Whitelisted
+Active
+Standby
+Ready
+Disabled
+*/
+
+ //Filter for our three tables
+ let activeNodes = newItems.filter((item) => item.status === "Active");
+ let standbyNodes = newItems.filter(
+ (item) =>
+ (item.status === "Standby" || item.status === "Ready") &&
+ item.version === this.state.globalData.maxVersion
+ );
+
+ //Create an array of all nodes that are active and standby
+ const active_standy_nodes = [
+ ...activeNodes.map((item) => {
+ return item.node_address;
+ }),
+ ...standbyNodes.map((item) => {
+ return item.node_address;
+ }),
+ ];
+ //White listed are all other nodes which are not active or on standby
+ //White listed is really just "Other"
+ let whitelisted = newItems.filter(
+ (item) => !active_standy_nodes.includes(item.node_address)
+ );
+
+ //let whitelisted = newItems.filter(item => !(item.status ==='Active' || item.status ==='Standby' || item.status ==='Ready') /*&& item.version !== this.state.globalData.maxVersion*/)
+
+ activeNodes = this.findChurnOuts(activeNodes); //Add in the actions for churning
+ standbyNodes = this.findChurnIns(standbyNodes); //Add in the actions for nodes churning in
+
+ //Filter here if any searchTerm from the search bar
+ //Need to do after adding the actions
+ activeNodes = this.returnSearchedData(activeNodes);
+ standbyNodes = this.returnSearchedData(standbyNodes);
+ whitelisted = this.returnSearchedData(whitelisted);
+
+ //Sort and add our favs to the top
+ let activeNodesSorted = this.sortData(activeNodes);
+ const favActiveNodesSorted = activeNodesSorted.filter(
+ (item) => item.fave === 1
+ ); //Get our favourites
+ activeNodesSorted = activeNodesSorted.filter((item) => item.fave === 0); //Get our non favourites
+
+ activeNodesSorted = [...favActiveNodesSorted, ...activeNodesSorted]; //Join faves at top with non favourites
+
+ const standBySorted = this.sortData(standbyNodes);
+ const whitelistedSorted = this.sortData(whitelisted);
+
+ this.setState({
+ activeNodes: activeNodesSorted,
+ standByNodes: standBySorted,
+ whitelistedNodes: whitelistedSorted, //This is really just other
+ });
+ }
+
+ onColumnUpdate(config) {
+ this.setState({ visibleColumns: config });
+ }
+
+ onNodesFilter(key) {
+ this.setState((prevState) => ({
+ nodesFilter: {
+ ...prevState.nodesFilter,
+ [key]: !prevState.nodesFilter[key],
+ },
+ }));
+ }
+
+ /*
+Split the data into over 300ks and under 300ks
+With the over 300ks, take the top 3 if they exist and apply churn in action
+If 4 nodes churn in instead of 3 each time, add another row
+*/
+ findChurnIns(standbyNodes) {
+ if (standbyNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ const over300 = standbyNodes.filter((item) => item.bond >= 30000000000000);
+ const over300Sorted = this.sortData(over300, "bond", "desc");
+
+ if (over300Sorted.length > 0) {
+ over300Sorted[Math.min(0, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(1, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(2, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(3, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(4, over300Sorted.length - 1)].action = "Churn In";
+ }
+ const under300 = standbyNodes.filter((item) => item.bond < 30000000000000);
+
+ return [...over300Sorted, ...under300];
+ }
+
+ /*
+Lowest Bond
+Oldest Node
+Worst Performer (Can't churn out if just churned in, one cycle grace period)
+*/
+ findChurnOuts(activeNodes) {
+ if (activeNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ let activeNodesSorted = this.sortData(activeNodes, "age", "desc");
+ activeNodesSorted[0].action = "Oldest";
+
+ activeNodesSorted = this.sortData(activeNodes, "bond", "asc");
+ activeNodesSorted[0].action = "Smallest Bond";
+
+ activeNodesSorted = this.sortData(activeNodes, "score", "asc", true);
+ //we set the 'Worst Performing' tag in the sortData function
+
+ this.calcBadValidatorRedline(activeNodes);
+
+ return activeNodesSorted;
+ }
+
+ calcBadValidatorRedline(activeNodes) {
+ //Only get nodes with slashes greater than 100
+ const greater100Slashes = activeNodes.filter(
+ (item) => item.slash_points > 100
+ );
+ //Add all the scores together for thes nodes
+ const sum = greater100Slashes.reduce((accumulator, object) => {
+ return accumulator + parseFloat(object.score);
+ }, 0);
+ //Find the average
+ const averageScore = sum / (greater100Slashes.length + 1);
+
+ const validatorLine =
+ averageScore / this.state.globalData.BadValidatorRedline;
+
+ activeNodes.map((item) => {
+ if (item.score < validatorLine) {
+ item.action = "Bad Redline";
+ }
+ return 0;
+ });
+ }
+
+ /*
+Sort by either string or number
+We use string sort function if value is one of the arrays else do second sort number
+*/
+ sortData(data, value = null, direction = null, worst_perform = false) {
+ const toSortBy = value === null ? this.state.sortBy : value;
+ let newData = [];
+
+ if (
+ ["node", "isp", "location", "version", "action", "node_address"].includes(
+ toSortBy
+ )
+ ) {
+ //This sort function for strings
+ newData = data.sort((a, b) => a[toSortBy].localeCompare(b[toSortBy]));
+ } else if (toSortBy === "bond_providers") {
+ //This is for bond provider sort as we need to go another layer deep in the object
+ newData = data.sort(
+ (a, b) => a[toSortBy].providers.length - b[toSortBy].providers.length
+ );
+ } else if (worst_perform === true) {
+ //This is for when we are sorting for action of worst performance as we want to exclude any with age under 3 days
+ const ageCutOffDays = 3;
+ const a = data.filter((item) => parseFloat(item.age) > ageCutOffDays);
+ const b = data.filter((item) => parseFloat(item.age) <= ageCutOffDays);
+ const aSorted = a.sort((a, b) => b[toSortBy] - a[toSortBy]);
+
+ aSorted[aSorted.length - 1].action = "Worst Performing";
+ newData = [...aSorted, ...b];
+ } else {
+ //This sort function for numbers
+ //When sorting, if values are the same, sort by node_address
+ newData = data.sort(function (a, b) {
+ if (a[toSortBy] === b[toSortBy]) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return a[toSortBy] > b[toSortBy] ? 1 : -1;
+ });
+ }
+ //If we pass it a direction, we set it here, if not we take it from the state
+ const toDirection =
+ direction === null ? this.state.sortDirection : direction;
+ if (toDirection === "desc") {
+ newData.reverse();
+ }
+
+ return newData;
+ }
+
+ clickSortHeader(item) {
+ const direction =
+ this.state.sortBy !== item
+ ? "desc"
+ : this.state.sortDirection === "desc"
+ ? "asc"
+ : "desc";
+ this.setState({ sortBy: item, sortDirection: direction }, () =>
+ this.setData()
+ );
+ }
+
+ sortColour(item) {
+ return "#ffffff";
+ }
+
+ whichHeart(address) {
+ return this.state.myFavNodes.includes(address) ? heartFull : heartBlank;
+ }
+
+ handleClickSlashes = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabSlashes=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickRewards = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabRewards=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickBond = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabBond=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Math.round(Number(y) / 100000000),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ searchBar() {
+ return (
+
+
+ this.setState(
+ { searchTerm: event.target.value.trim().toLowerCase() },
+ () => this.setData()
+ )
+ }
+ prefix={}
+ />
+
+ );
+ }
+
+ render() {
+ const { loading, nodesFilter, visibleColumns, whitelistedNodes } =
+ this.state;
+
+ const chartDataConfig = this.state.chartData
+ ? {
+ datasets: [
+ {
+ label: "Value",
+ data: this.state.chartData,
+ fill: false,
+ backgroundColor: "rgb(28, 57, 182)",
+ borderColor: "rgba(28, 57, 187, 0.2)",
+ },
+ ],
+ }
+ : {};
+
+ const slashesOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Slashes Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const rewardsOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Reward Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const bondOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ min: Math.min(...this.state.chartData.map((data) => data.x)),
+ stepSize: 20000,
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Bond Value",
+ },
+ ticks: {
+ min: Math.min(...this.state.chartData.map((data) => data.y)),
+ stepSize: 20000,
+ callback: function (value) {
+ return value.toString();
+ },
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ return (
+
+
+
+

+
Thornode Monitor
+
+
+
this.onNodesFilter("active")}
+ >
+

+
+
this.onNodesFilter("standby")}
+ >
+

+
+
this.onNodesFilter("orthers")}
+ >
+

+
+
+
+
+ {loading && (
+
+

+
+ )}
+
+ {!loading && (
+
+
}>
+
Dashboard
+
+ Other Nodes
+
+
+
+
+
+ <>
+
+
+
+
+ {this.searchBar()}
+
+
+
+
+ {whitelistedNodes.length > 0 && (
+
+ )}
+ {whitelistedNodes.length === 0 && (
+
+
+ No Other Data Available!
+
+
+ )}
+ >
+
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/containers/A_monitor/standbypage.js b/src/containers/A_monitor/standbypage.js
new file mode 100644
index 0000000..2f491b8
--- /dev/null
+++ b/src/containers/A_monitor/standbypage.js
@@ -0,0 +1,1995 @@
+import React, { Component, useState } from "react";
+import Modals from "@iso/components/Feedback/Modal";
+import Popover from "@iso/components/uielements/popover";
+import { getData, setCookie, getCookie } from "CommonFunctions";
+import { ModalContent } from "../Feedback/Modal/Modal.styles";
+import { Layout, Button, Input, Breadcrumb, Select } from "antd";
+import { Line } from "react-chartjs-2";
+import CustomLineChart from "./CustomLineChart";
+import "./styles.css";
+import { SearchOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
+import heartBlank from "@iso/assets/images/heart-blank.png";
+import heartFull from "@iso/assets/images/heart-full.png";
+import imageDO from "@iso/assets/images/do.png";
+import imageAWS from "@iso/assets/images/aws.png";
+import imageGCP from "@iso/assets/images/gcp.png";
+import imageAZURE from "@iso/assets/images/azure.png";
+import imageHETZNER from "@iso/assets/images/hetzner.png";
+import imageVULTR from "@iso/assets/images/vultr.png";
+import imageLeaseweb from "@iso/assets/images/leaseweb.png";
+import imageDatacamp from "@iso/assets/images/datacamp.png";
+import imageComcast from "@iso/assets/images/comcast.png";
+import imageChoopa from "@iso/assets/images/choopa.png";
+import imageChartercoms from "@iso/assets/images/chartercoms.png";
+import imageATandT from "@iso/assets/images/atandt.png";
+import imageZenlayer from "@iso/assets/images/zenlayer.png";
+
+import binance from "@iso/assets/images/binance.png";
+import eth from "@iso/assets/images/eth.png";
+import bitcoin from "@iso/assets/images/bitcoin.png";
+import litecoin from "@iso/assets/images/litecoin.png";
+import bitcoincash from "@iso/assets/images/bitcoincash.png";
+import dogecoin from "@iso/assets/images/dogecoin.png";
+import gaia from "@iso/assets/images/atom.png";
+import thornode from "@iso/assets/images/thornode.svg";
+import avax from "@iso/assets/images/avax.png";
+import blockIcon from "@iso/assets/images/overview/block_icon.svg";
+import highTradingIcon from "@iso/assets/images/overview/24high_trading.svg";
+import lowTradingIcon from "@iso/assets/images/overview/24low_trading.svg";
+import bondIcon from "@iso/assets/images/overview/Bond_icon.svg";
+import churnsIcon from "@iso/assets/images/overview/churns_icon.svg";
+import marcketCapIcon from "@iso/assets/images/overview/marcket_Cap.svg";
+import mcapRankIcon from "@iso/assets/images/overview/mcap_rank.svg";
+import runeUsdtIcon from "@iso/assets/images/overview/rune_usdt.svg";
+import timeIcon from "@iso/assets/images/overview/time_icon.svg";
+import totalSupplyIcon from "@iso/assets/images/overview/total_supply.svg";
+import loadingIcon from "@iso/assets/images/overview/loading.png";
+import githubIcon from "@iso/assets/images/overview/github_link_icon.svg";
+import twitterIcon from "@iso/assets/images/overview/twitter_link_icon.svg";
+import liquifyLogo from "@iso/assets/images/overview/liquify_logo.svg";
+import threeDotsIcon from "@iso/assets/images/overview/dots_three_circle.svg";
+import powerIcon from "@iso/assets/images/overview/power.svg";
+import activeIcon from "@iso/assets/images/overview/active_icon.svg";
+import VisibleColumn from "@iso/components/VisibleColumn/VisibleColumn";
+import arrowDownIcon from "@iso/assets/images/overview/arrow-down.svg";
+
+const leaveIcon = (
+
+);
+
+const { Header, Footer, Content } = Layout;
+
+const headerStyle = {
+ cursor: "pointer",
+ padding: "12px 15px",
+ fontSize: 15,
+ color: "#ffffff",
+ backgroundColor: "rgba(24, 34, 51, 0.4)",
+ height: 55,
+ fontWeight: 600,
+};
+const tdStyle = {
+ minWidth: 60,
+ textAlign: "right",
+ fontSize: 14,
+ padding: "10px 15px",
+};
+const trStyle = { height: 45 };
+const iconStyle = {
+ minWidth: 25,
+ padding: 5,
+ paddingLeft: 10,
+ paddingRight: 10,
+ padding: "10px 15px",
+};
+
+async function copyToClipWithPopup(msg, ip) {
+ copyToClipboard(ip);
+ popUpModal(msg, ip);
+}
+
+const copyToClipboard = (str) => {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText)
+ return navigator.clipboard.writeText(str);
+ return Promise.reject("The Clipboard API is not available.");
+};
+
+function popUpModal(msg, ip) {
+ Modals.info({
+ title: Success
,
+ content: (
+
+
+ {`${msg} `} {`${ip}`}
+
+
+ ),
+ onOk() {},
+ okText: "OK",
+ cancelText: "Cancel",
+ className: "feedback-modal",
+ });
+}
+
+const SortIcon = ({ sortBy, column, sortDirection }) => {
+ if (sortBy == column) {
+ return (
+
+ );
+ }
+ return null;
+};
+
+const Icons = ({ address, ip_address, addToFav, whichHeart }) => {
+ const firstURL = `https://thornode.ninerealms.com/thorchain/node/${address}`;
+ const secondURL = `https://viewblock.io/thorchain/address/${address}`;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ copyToClipWithPopup("IP Copied to clipboard:", ip_address)
+ }
+ >
+
+
+
+
addToFav(address)}
+ src={whichHeart(address)}
+ style={{
+ cursor: "pointer",
+ marginLeft: 5,
+ marginTop: 2,
+ width: 15,
+ height: 15,
+ }}
+ />
+
+ );
+};
+
+const GlobalData = ({ globalData, animateBlockCount, state }) => {
+ let timeToDisplay = "";
+ let msgTitle = "";
+ if (globalData?.churnTry && globalData?.retiring === "false") {
+ msgTitle = "(CHURN) RETRY IN";
+ timeToDisplay = `${globalData?.timeUntilRetry?.days}d ${globalData?.timeUntilRetry?.hours}h ${globalData?.timeUntilRetry?.minutes}m`;
+ } else if (globalData?.retiring === "true") {
+ msgTitle = "(CHURN) CURRENTLY CHURNING";
+ timeToDisplay = "Churning";
+ } else {
+ msgTitle = "(CHURN) TIME UNTIL";
+ timeToDisplay = `${globalData?.timeUntilChurn?.days}d ${globalData?.timeUntilChurn?.hours}h ${globalData?.timeUntilChurn?.minutes}m`;
+ }
+
+ return (
+ <>
+
+

+
+
CURRENT BLOCK
+
+ {parseInt(globalData.maxHeight).toLocaleString()}
+
+
+
+
+

+
+
{msgTitle}
+
{timeToDisplay}
+
+
+
+

+
+
TOTAL BONDED VALUE
+
+ ᚱ
+ {state.activeNodes.length > 0
+ ? parseInt(
+ state.activeNodes
+ .map((item) => item.bond)
+ .reduce((prev, next) => prev + next) / 100000000
+ ).toLocaleString()
+ : "0"}
+
+
+
+
+

+
+
MARKET CAP
+
+ ${globalData?.coingecko?.market_cap?.toLocaleString()}
+
+
+
+
+

+
+
24 HR VOLUME
+
+ ${globalData?.coingecko?.total_volume?.toLocaleString()}
+
+
+
+
+

+
+
MAX EFFECTIVE BOND
+
+ ᚱ
+ {parseInt(
+ globalData.maxEffectiveStake / 100000000
+ ).toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const CoinGeckoData = ({ globalData }) => {
+ return (
+ <>
+
+

+
+
PRICE
+
+ ${globalData?.coingecko?.current_price?.toLocaleString()}
+
+
+
+
+

+
+
24 HR HIGH
+
+ ${globalData?.coingecko?.high_24h}
+
+
+
+
+

+
+
24 HR LOW
+
+ ${globalData?.coingecko?.low_24h}
+
+
+
+
+

+
+
MARKET CAP RANK
+
+ {globalData?.coingecko?.market_cap_rank}
+
+
+
+
+

+
+
TOTAL SUPPLY
+
+ ᚱ{globalData?.coingecko?.total_supply?.toLocaleString()}
+
+
+
+ >
+ );
+};
+
+const ReturnIspImage = ({ isp }) => {
+ const style = { width: 25, height: 25 };
+
+ if (
+ isp === "Amazon.com, Inc." ||
+ isp === "Amazon Technologies Inc." ||
+ isp === "Amazon.com"
+ ) {
+ return
;
+ }
+ if (isp === "DigitalOcean, LLC" || isp === "DigitalOcean") {
+ return
;
+ }
+ if (isp === "Google LLC") {
+ return
;
+ }
+
+ if (isp === "Microsoft Corporation") {
+ return
;
+ }
+
+ if (isp === "Hetzner Online GmbH") {
+ return
;
+ }
+
+ if (isp === "The Constant Company" || isp === "The Constant Company, LLC") {
+ return
;
+ }
+
+ if (isp === "Leaseweb UK Limited" || isp === "Leaseweb USA, Inc.") {
+ return
;
+ }
+
+ if (isp === "Datacamp Limited") {
+ return
;
+ }
+
+ if (isp === "Comcast Cable Communications, LLC") {
+ return
;
+ }
+
+ if (isp === "Choopa") {
+ return
;
+ }
+
+ if (isp === "Charter Communications Inc") {
+ return
;
+ }
+
+ if (isp === "AT&T Services, Inc.") {
+ return
;
+ }
+
+ if (isp === "Zenlayer Inc") {
+ return
;
+ }
+
+ return "-";
+};
+
+const ChainTD = ({ chain, obchains, maxChainHeights }) => {
+ const delta = obchains[chain] - maxChainHeights[chain];
+ return (
+
+ {delta === 0 ? "OK" : delta.toString()}
+ |
+ );
+};
+
+const BondProviderPopOver = ({ data }) => {
+ const totalBond = data
+ .map((item) => parseInt(item.bond))
+ .reduce((a, b) => a + b, 0);
+
+ const d = data.map((item, index) => {
+ return (
+
+
+ {item.bond_address.substring(
+ item.bond_address.length - 4,
+ item.bond_address.length
+ )}
+
+ {((item.bond / totalBond) * 100).toFixed(2)}%
+
+ ᚱ{parseInt((item.bond / 100000000).toFixed()).toLocaleString()}
+
+
+ );
+ });
+ return d;
+};
+
+const NodeTable = ({
+ nodeData,
+ clickSortHeader,
+ handleClickBond,
+ handleClickRewards,
+ handleClickSlashes,
+ sortColour,
+ maxChainHeights,
+ chains,
+ addToFav,
+ whichHeart,
+ chartDataConfig,
+ bondOptions,
+ rewardsOptions,
+ slashesOptions,
+ visibleColumns = { ...defaulColumns },
+ sortBy = "",
+ sortDirection = "",
+}) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(100);
+
+ const totalPages = Math.ceil(nodeData.length / itemsPerPage);
+ const pageNumbers = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ pageNumbers.push(i);
+ }
+
+ const handleClick = (event) => {
+ setCurrentPage(Number(event.target.id));
+ };
+
+ const handleNext = () => {
+ setCurrentPage((prevPage) =>
+ prevPage === totalPages ? prevPage : prevPage + 1
+ );
+ };
+
+ const handlePrev = () => {
+ setCurrentPage((prevPage) => (prevPage === 1 ? prevPage : prevPage - 1));
+ };
+
+ const renderPageNumbers = pageNumbers.map((number) => {
+ return (
+
+ {number}
+
+ );
+ });
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = nodeData.slice(indexOfFirstItem, indexOfLastItem);
+
+ const getHeaderClassName = (key) => {
+ return visibleColumns && visibleColumns[key]
+ ? "tableHeader"
+ : "tableHeader hidden";
+ };
+
+ const getCellClassName = (key) => {
+ return visibleColumns && visibleColumns[key] ? "" : "hidden";
+ };
+
+ const updatePagingItem = (value) => {
+ setItemsPerPage(value);
+ };
+ return (
+ <>
+
+ Nodes per page:
+
+
+
+
+
+
+
+ -
+
+
+ {renderPageNumbers}
+ -
+
+
+
+
+
+ >
+ );
+};
+
+let timer = null;
+
+const defaulColumns = {
+ nodes: true,
+ age: true,
+ action: true,
+ isp: true,
+ bond: true,
+ providers: true,
+ rewards: true,
+ apy: true,
+ slashes: true,
+ score: true,
+ version: true,
+ rpc: true,
+ bfr: true,
+};
+export default class extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ chartData: [],
+ data: [],
+ globalData: [],
+ sortBy: "bond",
+ sortDirection: "desc",
+ activeNodes: [],
+ standByNodes: [],
+ whitelistedNodes: [],
+ animateBlockCount: false,
+ myFavNodes: [],
+ searchTerm: "",
+ visibleColumns: defaulColumns,
+ nodesFilter: {},
+ loading: true,
+ };
+ this.clickSortHeader = this.clickSortHeader.bind(this);
+ this.handleClickRewards = this.handleClickRewards.bind(this);
+ this.handleClickSlashes = this.handleClickSlashes.bind(this);
+ this.handleClickBond = this.handleClickBond.bind(this);
+ }
+
+ async componentWillMount() {
+ const myFavNodes = getCookie("myFavNodes");
+
+ const tmp = myFavNodes.length > 0 ? JSON.parse(myFavNodes) : [];
+
+ this.setState({ myFavNodes: tmp });
+
+ this.refreshData();
+ }
+
+ async refreshData() {
+ const data = await getData();
+
+ if (this.state.loading) {
+ this.setState({ loading: false });
+ }
+
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
+ console.log("DEV ONLY: Refresh Data Results: ", data);
+ }
+
+ this.setState(
+ {
+ data: data.data,
+ globalData: data.globalData,
+ maxChainHeights: data.maxChainHeights,
+ animateBlockCount: false,
+ },
+ () => this.setData()
+ ); //Change animateBlockCount to true here for animation
+ }
+
+ componentDidMount() {
+ timer = setInterval(() => {
+ this.setState({ animateBlockCount: false }, () => this.refreshData());
+ //this.refreshData()
+ }, 6000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(timer);
+ }
+
+ addToFav(address) {
+ const myFavNodes = getCookie("myFavNodes"); //JSON.parse(
+
+ if (myFavNodes.length === 0) {
+ //in here no current fav nodes
+ const singleAddress = JSON.stringify([address]);
+ setCookie("myFavNodes", singleAddress);
+ this.setState({ myFavNodes: singleAddress }, () => this.setData());
+ } else {
+ //If we already have some fav nodes
+ const newFaveNodes = JSON.parse(myFavNodes);
+
+ //Need to check if already exists
+ if (newFaveNodes.indexOf(address) > -1) {
+ //In the array!
+
+ const newArrayWithRemove = newFaveNodes.filter(
+ (item) => item !== address
+ );
+
+ const newFaveNodesNew = JSON.stringify(newArrayWithRemove);
+ this.setState({ myFavNodes: newArrayWithRemove }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ } else {
+ //Not in the array
+ newFaveNodes[newFaveNodes.length] = address;
+
+ const newFaveNodesNew = JSON.stringify(newFaveNodes);
+
+ this.setState({ myFavNodes: newFaveNodes }, () => this.setData());
+ setCookie("myFavNodes", newFaveNodesNew);
+ }
+ }
+ }
+
+ returnSearchedData(data) {
+ if (this.state.searchTerm === "") {
+ return data;
+ } else {
+ const filteredNodes = data.filter((item) =>
+ item.node_address.includes(this.state.searchTerm)
+ );
+ return filteredNodes;
+ }
+ }
+
+ setData() {
+ //Grab our state so we can mutate it
+ let myData = JSON.parse(JSON.stringify(this.state.data));
+
+ //Add faves to the data, then we can sort by then below
+ const newItems = myData.map((item) => {
+ if (this.state.myFavNodes.includes(item.node_address)) {
+ item.fave = 1;
+ } else {
+ item.fave = 0;
+ }
+ return item;
+ });
+
+ /*
+Whitelisted
+Active
+Standby
+Ready
+Disabled
+*/
+
+ //Filter for our three tables
+ let activeNodes = newItems.filter((item) => item.status === "Active");
+ let standbyNodes = newItems.filter(
+ (item) =>
+ (item.status === "Standby" || item.status === "Ready") &&
+ item.version === this.state.globalData.maxVersion
+ );
+
+ //Create an array of all nodes that are active and standby
+ const active_standy_nodes = [
+ ...activeNodes.map((item) => {
+ return item.node_address;
+ }),
+ ...standbyNodes.map((item) => {
+ return item.node_address;
+ }),
+ ];
+ //White listed are all other nodes which are not active or on standby
+ //White listed is really just "Other"
+ let whitelisted = newItems.filter(
+ (item) => !active_standy_nodes.includes(item.node_address)
+ );
+
+ //let whitelisted = newItems.filter(item => !(item.status ==='Active' || item.status ==='Standby' || item.status ==='Ready') /*&& item.version !== this.state.globalData.maxVersion*/)
+
+ activeNodes = this.findChurnOuts(activeNodes); //Add in the actions for churning
+ standbyNodes = this.findChurnIns(standbyNodes); //Add in the actions for nodes churning in
+
+ //Filter here if any searchTerm from the search bar
+ //Need to do after adding the actions
+ activeNodes = this.returnSearchedData(activeNodes);
+ standbyNodes = this.returnSearchedData(standbyNodes);
+ whitelisted = this.returnSearchedData(whitelisted);
+
+ //Sort and add our favs to the top
+ let activeNodesSorted = this.sortData(activeNodes);
+ const favActiveNodesSorted = activeNodesSorted.filter(
+ (item) => item.fave === 1
+ ); //Get our favourites
+ activeNodesSorted = activeNodesSorted.filter((item) => item.fave === 0); //Get our non favourites
+
+ activeNodesSorted = [...favActiveNodesSorted, ...activeNodesSorted]; //Join faves at top with non favourites
+
+ const standBySorted = this.sortData(standbyNodes);
+ const whitelistedSorted = this.sortData(whitelisted);
+
+ this.setState({
+ activeNodes: activeNodesSorted,
+ standByNodes: standBySorted,
+ whitelistedNodes: whitelistedSorted, //This is really just other
+ });
+ }
+
+ onColumnUpdate(config) {
+ this.setState({ visibleColumns: config });
+ }
+
+ onNodesFilter(key) {
+ this.setState((prevState) => ({
+ nodesFilter: {
+ ...prevState.nodesFilter,
+ [key]: !prevState.nodesFilter[key],
+ },
+ }));
+ }
+
+ /*
+Split the data into over 300ks and under 300ks
+With the over 300ks, take the top 3 if they exist and apply churn in action
+If 4 nodes churn in instead of 3 each time, add another row
+*/
+ findChurnIns(standbyNodes) {
+ if (standbyNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ const over300 = standbyNodes.filter((item) => item.bond >= 30000000000000);
+ const over300Sorted = this.sortData(over300, "bond", "desc");
+
+ if (over300Sorted.length > 0) {
+ over300Sorted[Math.min(0, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(1, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(2, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(3, over300Sorted.length - 1)].action = "Churn In";
+ over300Sorted[Math.min(4, over300Sorted.length - 1)].action = "Churn In";
+ }
+ const under300 = standbyNodes.filter((item) => item.bond < 30000000000000);
+
+ return [...over300Sorted, ...under300];
+ }
+
+ /*
+Lowest Bond
+Oldest Node
+Worst Performer (Can't churn out if just churned in, one cycle grace period)
+*/
+ findChurnOuts(activeNodes) {
+ if (activeNodes.length === 0) return []; //Stops filter from breaking when search returns 0
+
+ let activeNodesSorted = this.sortData(activeNodes, "age", "desc");
+ activeNodesSorted[0].action = "Oldest";
+
+ activeNodesSorted = this.sortData(activeNodes, "bond", "asc");
+ activeNodesSorted[0].action = "Smallest Bond";
+
+ activeNodesSorted = this.sortData(activeNodes, "score", "asc", true);
+ //we set the 'Worst Performing' tag in the sortData function
+
+ this.calcBadValidatorRedline(activeNodes);
+
+ return activeNodesSorted;
+ }
+
+ calcBadValidatorRedline(activeNodes) {
+ //Only get nodes with slashes greater than 100
+ const greater100Slashes = activeNodes.filter(
+ (item) => item.slash_points > 100
+ );
+ //Add all the scores together for thes nodes
+ const sum = greater100Slashes.reduce((accumulator, object) => {
+ return accumulator + parseFloat(object.score);
+ }, 0);
+ //Find the average
+ const averageScore = sum / (greater100Slashes.length + 1);
+
+ const validatorLine =
+ averageScore / this.state.globalData.BadValidatorRedline;
+
+ activeNodes.map((item) => {
+ if (item.score < validatorLine) {
+ item.action = "Bad Redline";
+ }
+ return 0;
+ });
+ }
+
+ /*
+Sort by either string or number
+We use string sort function if value is one of the arrays else do second sort number
+*/
+ sortData(data, value = null, direction = null, worst_perform = false) {
+ const toSortBy = value === null ? this.state.sortBy : value;
+ let newData = [];
+
+ if (
+ ["node", "isp", "location", "version", "action", "node_address"].includes(
+ toSortBy
+ )
+ ) {
+ //This sort function for strings
+ newData = data.sort((a, b) => a[toSortBy].localeCompare(b[toSortBy]));
+ } else if (toSortBy === "bond_providers") {
+ //This is for bond provider sort as we need to go another layer deep in the object
+ newData = data.sort(
+ (a, b) => a[toSortBy].providers.length - b[toSortBy].providers.length
+ );
+ } else if (worst_perform === true) {
+ //This is for when we are sorting for action of worst performance as we want to exclude any with age under 3 days
+ const ageCutOffDays = 3;
+ const a = data.filter((item) => parseFloat(item.age) > ageCutOffDays);
+ const b = data.filter((item) => parseFloat(item.age) <= ageCutOffDays);
+ const aSorted = a.sort((a, b) => b[toSortBy] - a[toSortBy]);
+
+ aSorted[aSorted.length - 1].action = "Worst Performing";
+ newData = [...aSorted, ...b];
+ } else {
+ //This sort function for numbers
+ //When sorting, if values are the same, sort by node_address
+ newData = data.sort(function (a, b) {
+ if (a[toSortBy] === b[toSortBy]) {
+ return a["node_address"].localeCompare(b["node_address"]);
+ }
+ return a[toSortBy] > b[toSortBy] ? 1 : -1;
+ });
+ }
+ //If we pass it a direction, we set it here, if not we take it from the state
+ const toDirection =
+ direction === null ? this.state.sortDirection : direction;
+ if (toDirection === "desc") {
+ newData.reverse();
+ }
+
+ return newData;
+ }
+
+ clickSortHeader(item) {
+ const direction =
+ this.state.sortBy !== item
+ ? "desc"
+ : this.state.sortDirection === "desc"
+ ? "asc"
+ : "desc";
+ this.setState({ sortBy: item, sortDirection: direction }, () =>
+ this.setData()
+ );
+ }
+
+ sortColour(item) {
+ return "#ffffff";
+ }
+
+ whichHeart(address) {
+ return this.state.myFavNodes.includes(address) ? heartFull : heartBlank;
+ }
+
+ handleClickSlashes = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabSlashes=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickRewards = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabRewards=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Number(y),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ handleClickBond = async (node_address) => {
+ const url = `https://api.liquify.com/thor/api/grabBond=${node_address}`;
+ try {
+ const response = await fetch(url);
+ const rawData = await response.json();
+ if (!rawData || Object.keys(rawData).length === 0) {
+ this.setState({ chartData: null });
+ } else {
+ const chartData = Object.entries(rawData).map(([x, y]) => ({
+ x: Number(x),
+ y: Math.round(Number(y) / 100000000),
+ }));
+ this.setState({ chartData });
+ }
+ } catch (error) {
+ console.error(`Error fetching data from ${url}:`, error);
+ }
+ };
+
+ searchBar() {
+ return (
+
+
+ this.setState(
+ { searchTerm: event.target.value.trim().toLowerCase() },
+ () => this.setData()
+ )
+ }
+ prefix={}
+ />
+
+ );
+ }
+
+ render() {
+ const { loading, nodesFilter, visibleColumns, standByNodes } = this.state;
+
+ const chartDataConfig = this.state.chartData
+ ? {
+ datasets: [
+ {
+ label: "Value",
+ data: this.state.chartData,
+ fill: false,
+ backgroundColor: "rgb(28, 57, 182)",
+ borderColor: "rgba(28, 57, 187, 0.2)",
+ },
+ ],
+ }
+ : {};
+
+ const slashesOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Slashes Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const rewardsOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Reward Value",
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ const bondOptions = this.state.chartData
+ ? {
+ scales: {
+ xAxes: [
+ {
+ type: "linear",
+ position: "bottom",
+ scaleLabel: {
+ display: true,
+ labelString: "Height",
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ min: Math.min(...this.state.chartData.map((data) => data.x)),
+ stepSize: 20000,
+ callback: function (value, index, values) {
+ return value;
+ },
+ },
+ },
+ ],
+ yAxes: [
+ {
+ scaleLabel: {
+ display: true,
+ labelString: "Bond Value",
+ },
+ ticks: {
+ min: Math.min(...this.state.chartData.map((data) => data.y)),
+ stepSize: 20000,
+ callback: function (value) {
+ return value.toString();
+ },
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
+ return (
+
+
+
+

+
Thornode Monitor
+
+
+
this.onNodesFilter("active")}
+ >
+

+
+
this.onNodesFilter("standby")}
+ >
+

+
+
this.onNodesFilter("orthers")}
+ >
+

+
+
+
+
+ {loading && (
+
+

+
+ )}
+
+ {!loading && (
+
+
}>
+
Dashboard
+
+ Standby Nodes
+
+
+
+
+
+ <>
+
+
+
+
+ {this.searchBar()}
+
+
+
+
+ {standByNodes.length > 0 && (
+
+ )}
+ {standByNodes.length === 0 && (
+
+
+ No Standby Data Available!
+
+
+ )}
+ >
+
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/containers/A_monitor/styles.css b/src/containers/A_monitor/styles.css
index c185533..88003f5 100644
--- a/src/containers/A_monitor/styles.css
+++ b/src/containers/A_monitor/styles.css
@@ -1,16 +1,26 @@
+#root {
+ height: 100%;
+}
+.ant-layout {
+ min-height: 100%;
+ min-width: 1920px;
+}
body {
- font-family: monospace!important;
+ font-family: monospace !important;
font-weight: 200;
}
-svg:hover {
- fill: #34c9eb;
+.icons-wrapper svg {
+ color: #7e7e7e;
+}
+.icons-wrapper svg:hover {
+ color: #1c39bb;
}
td {
border-style: solid;
border-width: 1px;
- border-color: rgba(0,0,0,0.07)
+ border-color: rgba(0, 0, 0, 0.07);
}
.animateGrow {
@@ -20,16 +30,550 @@ td {
animation-duration: 1s;
}
@keyframes animateBlockHeight {
- 0% {background-color:white;}
- 35% {background-color:rgba(230,255,117,0.5);}
- 65% {background-color:rgba(230,255,117,0.5);}
- 100% {background-color:white;}
+ 0% {
+ background-color: white;
+ }
+ 35% {
+ background-color: rgba(230, 255, 117, 0.5);
+ }
+ 65% {
+ background-color: rgba(230, 255, 117, 0.5);
+ }
+ 100% {
+ background-color: white;
+ }
}
.nodeaddress:hover {
- color: #34c9eb
+ color: #1c39bb;
+}
+
+.tableHeader span {
+ position: relative;
}
.tableHeader:hover {
- color: #004d59
+ color: #004d59;
+}
+
+.tableHeader span .sort-icon {
+ position: absolute;
+ top: 50%;
+ right: -5px;
+}
+.tableHeader span .sort-icon.desc {
+ transform: translate(100%, -50%) rotate(0deg);
+}
+.tableHeader span .sort-icon.asc {
+ transform: translate(100%, -50%) rotate(180deg);
+}
+
+.overview-list-wrapper {
+ border-bottom: 1px solid #1c39bb;
+ padding-bottom: 28px;
+}
+.overview-list {
+ column-count: 6;
+ background: linear-gradient(269.82deg, #ffffff -20.26%, #ffffff 99.82%);
+ opacity: 0.8;
+ border-radius: 10px;
+ padding: 36px 40px 10px;
+ filter: drop-shadow(0px 4px 10px rgba(0, 0, 0, 0.1));
+ max-width: 1880px;
+ margin: auto;
+}
+
+.overview-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 36px;
+}
+
+.overview-item__value {
+ padding-left: 22px;
+}
+
+.overview-item__value-title {
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+ letter-spacing: -0.015em;
+ color: #182233;
+}
+
+.overview-item__value-value {
+ font-style: normal;
+ font-weight: 600;
+ font-size: 13px;
+ line-height: 16px;
+ letter-spacing: -0.015em;
+ color: #1b34a0;
+}
+
+iframe {
+ display: none;
+}
+
+.cta-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 37px 0 16px;
+}
+
+.cta-wrapper .cta-link {
+ flex: 1;
+}
+
+.hidden {
+ display: none;
+}
+
+.paging-wrapper {
+ padding: 25px 0;
+ color: #9ca3af;
+}
+
+.page-numbers {
+ display: flex;
+ justify-content: center;
+}
+.page-numbers li {
+ border: 1.5px solid #9ca3af;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 3px 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+}
+.page-numbers li.paging-item {
+ margin-right: 15px;
+ line-height: 1;
+}
+
+.page-numbers li.paging-item:hover {
+ color: #182233;
+}
+
+.page-numbers li.active {
+ border: 1.5px solid #1c39bb;
+ color: #1c39bb;
+}
+
+.page-numbers .nav-button {
+ border: none;
+ cursor: pointer;
+}
+
+.nav-button--prev {
+ margin-right: 20px;
+}
+.nav-button--next {
+ margin-left: 5px;
+}
+
+.nav-button.disabled {
+ pointer-events: none;
+}
+
+.ant-layout-content {
+ background-image: url("./../../assets/images/overview/liquify_bg.png"),
+ url("./../../assets/images/overview/hex_map_bg.png");
+ background-repeat: no-repeat;
+ background-position: top 160px center, top calc(960px) center;
+ display: flex;
+ flex-direction: column;
+ padding: 17px 0 0 0 !important;
+}
+
+.loading {
+ position: relative;
+ margin: auto auto;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.loading_icon {
+ animation: spin 1s linear infinite;
+ width: 203px;
+ height: 203px;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.ant-layout-footer {
+ background-color: #fff;
+ height: 110px;
+ display: flex;
+ align-items: center;
+ position: relative;
+}
+
+.ant-layout-footer .link {
+ margin-right: 12px;
+ cursor: pointer;
+}
+
+.logo-wrapper {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 20px;
+ color: #182233;
+}
+.logo-wrapper span {
+ margin-right: 12px;
+}
+
+.data-table-wrapper {
+ box-shadow: 0px 4px 10px rgb(0 0 0 / 10%);
+ position: relative;
+ z-index: 1;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.data-table-wrapper::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90.11deg, #ffffff 1.08%, #eef0fa 100%);
+ opacity: 0.9;
+ z-index: -1;
+}
+.data-table-wrapper tbody {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 17px;
+ letter-spacing: -0.01em;
+ color: #182233;
+}
+
+table {
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+td,
+th {
+ border: none;
+ margin: 0;
+}
+
+tr {
+ box-shadow: 0px 0px 2px rgb(0 0 0 / 10%);
+}
+
+tr:first-child {
+ background: none;
+ margin-top: 2px;
+}
+
+.ant-layout-header {
+ min-width: unset !important;
+}
+
+.header-left {
+ flex: 1;
+}
+.header-left span {
+ font-family: "Montserrat";
+ font-style: normal;
+ font-weight: 700;
+ font-size: 25px;
+ line-height: 30px;
+ letter-spacing: -0.015em;
+}
+.header-right {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 60px;
+}
+
+.header-right .active-node {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 46px;
+ height: 46px;
+ border-radius: 50%;
+ cursor: pointer;
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+}
+.header-right .active-node::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(27, 52, 160, 0.8);
+ filter: blur(7.5px);
+ z-index: -1;
+ opacity: 0;
+ transition: opacity 0.3s ease-in-out;
+}
+.header-right .active-node:hover::before {
+ opacity: 1;
+}
+
+.header-right .active-node.active-node--active::before {
+ content: "";
+ background: #1c39bb;
+ filter: none;
+ z-index: -1;
+ opacity: 1;
+}
+
+.no-data {
+ height: 400px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.no-data__content {
+ width: 500px;
+ height: 180px;
+ background: #ffffff;
+ box-shadow: 0px 5px 20px rgb(0 0 0 / 25%);
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ font-size: 20px;
+ line-height: 32px;
+ letter-spacing: -0.015em;
+}
+
+.button-filter {
+ height: auto;
+ padding: 5px;
+ background: linear-gradient(90deg, #ffffff 0%, #edeffa 114.85%);
+ box-shadow: 0px 4px 10px rgb(0 0 0 / 25%);
+ border-radius: 10px;
+ margin-left: 40px;
+}
+
+.search-input .anticon-search svg {
+ width: 24px;
+ height: 24px;
+ fill: #c8cace;
+}
+
+.filter-modal .ant-modal,
+.filter-modal .ant-modal-title {
+ color: #182233;
+}
+
+.filter-modal .ant-switch {
+ background: #d9dde7;
+}
+.filter-modal .ant-switch-checked {
+ background: #1c39bb;
+}
+.filter-modal .ant-modal-content {
+ width: 600px;
+ background: #ffffff;
+ box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.25);
+ border-radius: 20px;
+ overflow: hidden;
+ padding: 38px;
+}
+.filter-modal .ant-modal-header {
+ border: none;
+}
+.filter-modal .ant-modal-title {
+ font-weight: 700;
+ font-size: 25px;
+ line-height: 30px;
+ letter-spacing: -0.01em;
+ color: #182233;
+ text-align: center;
+}
+.filter-modal .ant-modal-body {
+ text-align: center;
+ padding: 32px 0 42px;
+}
+.filter-modal .ant-modal-footer {
+ border: none;
+ text-align: center;
+ padding: 0;
+}
+.filter-modal .ant-modal-footer .ant-btn-default {
+ background: #6b7280;
+ color: #fff;
+}
+.filter-modal .ant-modal-footer .ant-btn {
+ min-width: 110px;
+}
+.filter-modal .filter-list {
+ column-count: 2;
+ display: inline-block;
+ margin: 0 auto;
+ column-gap: 100px;
+}
+
+.filter-modal .filter-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 160px;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 17px;
+ letter-spacing: -0.01em;
+ color: #182233;
+ margin-bottom: 24px;
+}
+.filter-modal .ant-switch {
+ height: 17px;
+ line-height: 17px;
+ border-radius: 39px;
+}
+.filter-modal .ant-switch-handle {
+ height: 17px;
+ line-height: 17px;
+ top: 0;
+ left: 0;
+}
+.filter-modal .ant-switch-checked .ant-switch-handle {
+ left: calc(100% - 17px - 0px);
+}
+.filter-modal .ant-switch-checked .ant-switch-handle::before {
+ background: #d9dde7;
+}
+.filter-modal .ant-switch-handle::before {
+ background: #1f1f43;
+}
+
+.ant-breadcrumb a,
+.ant-breadcrumb .current,
+.ant-breadcrumb-link,
+.ant-breadcrumb-separator {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 17px;
+ letter-spacing: -0.015em;
+ color: #182233;
+}
+
+.ant-breadcrumb .current {
+ color: #1c39bb;
+}
+
+.layout-content-wrapper {
+ margin: 0 70px;
+}
+
+.feedback-modal .ant-modal-content {
+ background: #ffffff;
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
+ border-radius: 6px;
+}
+.feedback-modal .anticon {
+ color: #76ca66 !important;
+}
+.feedback-modal .ant-modal-confirm-body .ant-modal-confirm-content {
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 15px;
+ letter-spacing: -0.01em;
+ color: #7e7e7e;
+}
+.feedback-modal .ant-modal-body {
+ padding: 36px 30px;
+}
+.feedback-modal .ant-modal-confirm-body .ant-modal-confirm-content strong {
+ font-weight: 600;
+}
+.feedback-modal.ant-modal-confirm .ant-modal-confirm-btns {
+ margin: 18px auto 0;
+ text-align: center;
+ float: none;
+}
+.feedback-modal .ant-btn-primary:focus {
+ background: #1c39bb;
+ border-color: #1c39bb;
+}
+
+.ant-modal-mask {
+ background: #333333 !important;
+ opacity: 0.6 !important;
+}
+
+.ant-popover-inner {
+ border-radius: 6px;
+ text-align: center;
+}
+.ant-popover-inner .ant-popover-title {
+ font-style: normal;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+ letter-spacing: -0.01em;
+ color: #7e7e7e;
+ padding: 8px 16px;
+ border-bottom: 0.2px solid #9ca3af;
+}
+.ant-popover-inner .ant-popover-inner-content {
+ font-style: normal;
+ font-weight: 400;
+ font-size: 10px;
+ line-height: 12px;
+ letter-spacing: -0.01em;
+ color: #7e7e7e;
+}
+
+.nodeaddress {
+ display: inline-block;
+ width: 85px;
+}
+
+.item-to-show {
+ padding: 20px 0;
+}
+.item-to-show .ant-select-selector {
+ border-radius: 6px !important;
+}
+
+.item-to-show > span {
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+ letter-spacing: -0.015em;
+ color: #182233;
+ margin-right: 20px;
+}
+
+.sort-icon.up {
+ transform: rotate(180deg);
+}
+
+.my-custom-popover .ant-popover-inner {
+ width: 1000px;
}
diff --git a/src/route.constants.js b/src/route.constants.js
index d42f1f3..7117f85 100644
--- a/src/route.constants.js
+++ b/src/route.constants.js
@@ -7,6 +7,9 @@ export const PUBLIC_ROUTE = {
PAGE_404: '/404',
PAGE_500: '/500',
AUTH0_CALLBACK: '/auth0loginCallback',
+ ACTIVE_DASHBOARD: '/active-dashboard',
+ STANDBY_DASHBOARD: '/standby-dashboard',
+ OTHER_DASHBOARD: '/other-dashboard',
};
export const PRIVATE_ROUTE = {
diff --git a/src/router.js b/src/router.js
index 5b37c98..8fac3a4 100644
--- a/src/router.js
+++ b/src/router.js
@@ -17,7 +17,22 @@ const publicRoutes = [
exact: true,
component: lazy(() => import('@iso/containers/A_monitor/monitorpage.js')),
//component: lazy(() => import('@iso/containers/Pages/SignIn/SignIn')),
- }
+ },
+ {
+ path: PUBLIC_ROUTE.ACTIVE_DASHBOARD,
+ exact: true,
+ component: lazy(() => import('@iso/containers/A_monitor/activepage.js')),
+ },
+ {
+ path: PUBLIC_ROUTE.STANDBY_DASHBOARD,
+ exact: true,
+ component: lazy(() => import('@iso/containers/A_monitor/standbypage.js')),
+ },
+ {
+ path: PUBLIC_ROUTE.OTHER_DASHBOARD,
+ exact: true,
+ component: lazy(() => import('@iso/containers/A_monitor/otherpage.js')),
+ },
];
export default function Routes() {