import { action, computed, observable, runInAction } from "mobx"; import axios from "axios"; import debounce from "lodash/debounce"; import ReactTooltip from "react-tooltip"; import { isString } from "../utils/strings"; import { Store } from "./Store"; export interface IClient { id: string; // "d5c4ca86b077c3f9557f08aac697a554b9ac1327" name: string; // "Infura" beaconClientVersion: string; // "Lighthouse/v0.1.0-unstable/x86_64-linux" genesisTime: string; //" 2020-02-14T09:00:00Z" //TODO should be date ??? latestHead: { headSlot: number; // 174442 headBlockRoot: string; //"0xf1810d471643752db6294bf7eba2c097d365f42c0892d68a1111472223ddef9b" finalizedSlot: number; // 174368 finalizedBlockRoot: string; //"0x4f510b5febf058c1d50c203cc98dc5abec540849042b685981e0812170dafc98" justifiedSlot: number; // 174400 justifiedBlockRoot: string; //"0xff968f96313de8946c2fcb032eeca29641b5b01fb2aac9725f650ab38db9a4ed" }; online: boolean; // true peers: number; // 16 attestations: null | string; // null syncing: null | string; // null syncingRate: null | number; // null location: { city: string; //"Ashburn" lat: number; // 39.0481 long: number; // -77.4728 }; memoryUsage: null | string; // null clientVersion: string; //"eth2stats-client/v0.0.6+b3215a9" clientVersionStatus: string; //"ok" } export class ClientsStore { @observable sortBy: string; @observable sortOrder: number; // 1 desc -1 asc @observable pinned: string[]; @observable clients: IClient[]; @observable clientsLoading = true; @observable counts = { total: 0, online: 0 }; main: Store; fetchDebounced = debounce(() => { return this.fetch(); }, 1000, { leading: false, trailing: true, maxWait: 1000 }); constructor(main: Store) { this.main = main; this.sortBy = JSON.parse(localStorage.getItem("sortBy")!) || "headSlot"; this.sortOrder = JSON.parse(localStorage.getItem("sortOrder")!) || 1; this.pinned = JSON.parse(localStorage.getItem("pinned")!) || []; } @computed get sortedClients() { return this.clients ? this.clients.slice().sort(this.sortFunction.bind(this)) : []; } clientByID(id: string) { return this.clients.find((client) => client.id === id); } @computed get locations() { // const currentSlot = this.main.stats.currentSlot; let locations = observable.map({}); this.sortedClients.forEach(client => { if (client.location !== null && client.online) { const id = `${client.location.city}:${client.location.long}:${client.location.lat}`; const newClient = observable.object( { id: client.id, name: client.name, online: client.online, latestHead: client.latestHead, peers: client.peers } ); if (locations.has(id)) { // append client at this location let location = locations.get(id); location.clients.push(newClient); } else { // new location locations.set(id, { id, city: client.location.city, long: client.location.long, lat: client.location.lat, clients: observable.array([newClient]) }); } } }); return locations; } @action.bound fetch() { return new Promise((resolve, reject) => { axios.get(this.main.getNetworkConfig()!.HTTP_API + "/clients").then((response) => { runInAction(() => { this.updateList(response.data.data); this.clientsLoading = false; resolve(); }); }).catch((err) => { reject(err); }); }); } updateList(data: IClient[]) { this.clients = data; // this.clients.replace(data); //TODO why replace ??? this.counts.total = data.length; this.counts.online = data.filter(x => x.online).length; ReactTooltip.rebuild(); } sortFunction(a: IClient, b: IClient) { const statusA = this.pinned.includes(a.id) ? 3 : a.online ? 2 : 1; const statusB = this.pinned.includes(b.id) ? 3 : b.online ? 2 : 1; if (statusA === statusB) { return this.compare(a, b); } else if (statusA > statusB) { return -1; } return 1; } compare(a: IClient, b: IClient) { if ((a.online && b.online) || (!a.online && !b.online)) { let aVal; let bVal; switch (this.sortBy) { case "name": aVal = a.name; bVal = b.name; break; case "peers": aVal = a.peers; bVal = b.peers; break; case "attestations": aVal = a.attestations; bVal = b.attestations; break; case "headSlot": // also the default default: // revert to default aVal = a.latestHead.headSlot; bVal = b.latestHead.headSlot; } let cmp = 1; if (isString(aVal) && isString(bVal)) { cmp = -1 * (aVal as string).toLowerCase().localeCompare((bVal as string).toLowerCase()); // inverted } else { if ((aVal as number) > (bVal as number)) { cmp = -1; } } return this.sortOrder * cmp; } else if (a.online) { return -1; } return 1; } reset() { this.clientsLoading = true; this.clients = []; this.counts.total = 0; this.counts.online = 0; } loading(isLoading: boolean) { this.clientsLoading = isLoading; } @action setSort(field: string) { if (field === this.sortBy) { this.sortOrder *= -1; this.saveSort(); return; } this.sortBy = field; this.saveSort(); } @action setSortWithOrder(field: string, order: number) { this.sortBy = field; this.sortOrder = order; this.saveSort(); } saveSort() { localStorage.setItem("sortBy", JSON.stringify(this.sortBy)); localStorage.setItem("sortOrder", JSON.stringify(this.sortOrder)); } @action pin(id: string) { const index = this.pinned.indexOf(id); if (index === -1) { // add this.pinned.push(id); } else { // remove this.pinned.splice(index, 1); } // save localStorage.setItem("pinned", JSON.stringify(this.pinned)); } }