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: + + + 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 ( -
    - - - - - - - - - - - - - - - - - - - - {chains && - <> - - - - - - - - - } - - + 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); - + const getHeaderClassName = (key) => { + return visibleColumns && visibleColumns[key] + ? "tableHeader" + : "tableHeader hidden"; + }; - - - - - - - - - - - - - - {chains && - <> - - - - - - - - - } - - - - ))} - - -
    clickSortHeader('node_address')}>NODEclickSortHeader('age')}>AGEclickSortHeader('action')}>ACTIONclickSortHeader('isp')}>ISPclickSortHeader('location')}>LOCATIONclickSortHeader('bond')}>BONDclickSortHeader('bond_providers')}>PROVIDERSclickSortHeader('current_award')}>REWARDSclickSortHeader('apy')}>APYclickSortHeader('slash_points')}>SLASHESclickSortHeader('score')}>SCOREclickSortHeader('version')}>VERSIONclickSortHeader('leave')}>{leaveIcon}#######RPCBFR
    {index+1} - { + return visibleColumns && visibleColumns[key] ? "" : "hidden"; + }; + + const updatePagingItem = (value) => { + setItemsPerPage(value); + }; + return ( + <> +
    + Nodes per page: + + + + + + + + + + + + + + + + + + + + + + + {chains && ( + <> + + + + + + + + + + + + + )} + + + + {currentItems.map((item, index) => ( + + + + + + + + + + + + + + + + + {/* */} + + + + + {chains && ( + <> + + + + + + + + + + )} + + ))} + + + + +
    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 + clickSortHeader("BNB")} + > +
    + # + +
    +
    clickSortHeader("BTC")} + > +
    + # + +
    +
    clickSortHeader("ETH")} + > +
    + # + +
    +
    clickSortHeader("LTC")} + > +
    + # + +
    +
    clickSortHeader("BCH")} + > +
    + # + +
    +
    clickSortHeader("DOGE")} + > +
    + # + +
    +
    clickSortHeader("GAIA")} + > +
    + # + +
    +
    clickSortHeader("AVAX")} + > +
    + # + +
    +
    + {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"} + +
    +
    +
    +
      +
    • - 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' : '-'}{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
    - - -
    - - +
    +
    + # + 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! +
    +
    + )} + + )} +
    + )}
    -
    Thornode Monitor - 2022
    - - - - - + + ); } } 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: + + + 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: + + + 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() {