diff --git a/.gitignore b/.gitignore index 0b309ec..9905f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules # local env files +env .env .env.local .env.*.local diff --git a/backend/Controller.cs b/backend/Controller.cs index 21514bb..efa0040 100644 --- a/backend/Controller.cs +++ b/backend/Controller.cs @@ -52,17 +52,31 @@ public ActionResult GetTimeOfRequest() return timeOfRequest; } + [HttpGet("/iptoserverid")] + [ProducesResponseType(Status200OK)] + public ActionResult> GetIpToServerId() + { + return _dataStorage.ipToServerId; + } + + [HttpGet("/leaflinks")] + [ProducesResponseType(Status200OK)] + public ActionResult> GetLeafLinks() + { + return _dataStorage.leafLinks; + } + [HttpGet("/gatewayLinks")] [ProducesResponseType(Status200OK)] - public ActionResult> GetGatewayLinks() + public ActionResult> GetGatewayLinks() { - var gatewayLinks = new List(); + var gatewayLinks = new List(); foreach (var cluster in _dataStorage.clusterConnectionErrors) { var split = cluster.Key.Split(" NAMESPLIT "); var source = split[0]; var target = split[1]; - var link = new Link (source, target, cluster.Value.Count > 0); + var link = new GatewayLink (source, target, cluster.Value.Count > 0); link.errors = cluster.Value; foreach (var err in cluster.Value) { diff --git a/backend/DataStorage.cs b/backend/DataStorage.cs index d762237..0798ccc 100644 --- a/backend/DataStorage.cs +++ b/backend/DataStorage.cs @@ -13,7 +13,7 @@ public class DataStorage public ConcurrentBag connections; public ConcurrentBag routes; public ConcurrentBag gateways; - public ConcurrentBag leafs; + public List leafs; public List links; public ConcurrentBag processedServers; public ConcurrentBag processedClusters; @@ -24,7 +24,9 @@ public class DataStorage public Dictionary serverToCluster = new Dictionary(); public Dictionary> clusterConnectionErrors = new Dictionary>(); public List errorClusters = new List(); - + public Dictionary ipToServerId; + public List leafLinks; + public HashSet leafConnections; public DataStorage() { @@ -33,18 +35,22 @@ public DataStorage() { connections = new ConcurrentBag(); routes = new ConcurrentBag(); gateways = new ConcurrentBag(); - leafs = new ConcurrentBag(); + leafs = new List(); links = new List(); processedServers = new ConcurrentBag(); processedClusters = new ConcurrentBag(); serverToMissingServer = new Dictionary>(); + ipToServerId = new Dictionary(); + leafLinks = new List(); + missingServerIds = new HashSet(); foundServers = new HashSet(); serverToCluster = new Dictionary(); clusterConnectionErrors = new Dictionary>(); errorClusters = new List(); + leafConnections = new HashSet(); } } } diff --git a/backend/DrawablesProcessor.cs b/backend/DrawablesProcessor.cs index 341e55b..c842699 100644 --- a/backend/DrawablesProcessor.cs +++ b/backend/DrawablesProcessor.cs @@ -29,6 +29,7 @@ public void ProcessData() ProcessServers(); ProcessClusters(); ProcessLinks(); + ProcessLeafs(); // Patch for a missing node from varz // TODO dynamically handle these types of errors @@ -58,6 +59,77 @@ public void ProcessData() } } + public void ProcessLeafs() + { + + var leafIps = new HashSet(); + foreach (var entry in _dataStorage.leafs) + { + if (entry.leafs is null) continue; + foreach (var leaf in entry.leafs) + { + leafIps.Add(leaf.ip); + } + } + + foreach (var entry in _dataStorage.routes) + { + foreach (var route in entry.routes) + { + if (_dataStorage.ipToServerId.ContainsKey(route.ip)) continue; + if (leafIps.Contains(route.ip)) + { + _dataStorage.ipToServerId.Add(route.ip, route.remote_id); + } + } + } + ConstructSingleLeafConnections(); + } + + public void ConstructSingleLeafConnections() + { + // TODO functionality about bidirectionality is currently not being used. Should maybe be removed. + foreach (var server in _dataStorage.leafs) + { + if (server.leafs is null) continue; + foreach (var leaf in server.leafs) + { + // TODO leafs to unknown servers are not handled + if (!_dataStorage.ipToServerId.ContainsKey(leaf.ip)) continue; + var targetId = _dataStorage.ipToServerId[leaf.ip]; + + var oppositeLink = _dataStorage.leafLinks.Where(l => + l.source == targetId && + l.target == server.server_id + ).Select(l => l).FirstOrDefault(); + + var identicalLink = _dataStorage.leafLinks.Where(l => + l.target == targetId && + l.source == server.server_id + ).Select(l => l).FirstOrDefault(); + + if (identicalLink is not null) + { + identicalLink.connections.Add(leaf); // Add leafnode to be fetched when link is clicked + } + else if (oppositeLink is null) + { + var link = new LeafLink ( + server.server_id, + targetId + ); + link.connections.Add(leaf); + _dataStorage.leafLinks.Add(link); + } + else + { + oppositeLink.connections.Add(leaf); + } + + } + } + } + public void ProcessClusters() { // TODO crashed node is not in cluster, decide whether it should be. @@ -170,9 +242,9 @@ public void ProcessServers() }); } - public string clusterTupleString(string cluster1, string cluster2) + public string tupleString(string id1, string id2) { - return cluster1 + " NAMESPLIT " + cluster2; + return id1 + " NAMESPLIT " + id2; } public (string, string) stringToTuple (string input) @@ -187,26 +259,26 @@ public void detectGatewaysToCrashedServers() { if (gateway.name is null) continue; foreach (var outbound in gateway.outbound_gateways) { - if (!_dataStorage.clusterConnectionErrors.ContainsKey(clusterTupleString(gateway.name, outbound.Key)) && !_dataStorage.clusterConnectionErrors.ContainsKey(clusterTupleString(outbound.Key, gateway.name))) + if (!_dataStorage.clusterConnectionErrors.ContainsKey(tupleString(gateway.name, outbound.Key)) && !_dataStorage.clusterConnectionErrors.ContainsKey(tupleString(outbound.Key, gateway.name))) { - _dataStorage.clusterConnectionErrors.Add(clusterTupleString(gateway.name, outbound.Key), new List()); + _dataStorage.clusterConnectionErrors.Add(tupleString(gateway.name, outbound.Key), new List()); } if (!_dataStorage.idToServer.ContainsKey(outbound.Value.connection.name)) { - _dataStorage.clusterConnectionErrors[clusterTupleString(gateway.name, outbound.Key)].Add("Outbound gateway to crashed server. From " + gateway.server_id + " to " + outbound.Value.connection.name); + _dataStorage.clusterConnectionErrors[tupleString(gateway.name, outbound.Key)].Add("Outbound gateway to crashed server. From " + gateway.server_id + " to " + outbound.Value.connection.name); } } foreach (var inbound in gateway.inbound_gateways) { - if (!_dataStorage.clusterConnectionErrors.ContainsKey(clusterTupleString(gateway.name, inbound.Key)) && !_dataStorage.clusterConnectionErrors.ContainsKey(clusterTupleString(inbound.Key, gateway.name))) + if (!_dataStorage.clusterConnectionErrors.ContainsKey(tupleString(gateway.name, inbound.Key)) && !_dataStorage.clusterConnectionErrors.ContainsKey(tupleString(inbound.Key, gateway.name))) { - _dataStorage.clusterConnectionErrors.Add(clusterTupleString(gateway.name, inbound.Key), new List()); + _dataStorage.clusterConnectionErrors.Add(tupleString(gateway.name, inbound.Key), new List()); } foreach (var inboundEntry in inbound.Value) { if (!_dataStorage.idToServer.ContainsKey(inboundEntry.connection.name)) { - _dataStorage.clusterConnectionErrors[clusterTupleString(gateway.name, inbound.Key)].Add("Inbound gateway to crashed server. To " + gateway.server_id + " from " + inboundEntry.connection.name); + _dataStorage.clusterConnectionErrors[tupleString(gateway.name, inbound.Key)].Add("Inbound gateway to crashed server. To " + gateway.server_id + " from " + inboundEntry.connection.name); } } @@ -214,6 +286,30 @@ public void detectGatewaysToCrashedServers() { } } + public void detectLeafConnections () + { + foreach (var leafLink in _dataStorage.leafLinks) + { + if (! _dataStorage.leafConnections.Contains(tupleString(leafLink.source, leafLink.target))) + { + if (! _dataStorage.leafConnections.Contains(tupleString(leafLink.target, leafLink.source))) + { + _dataStorage.leafConnections.Add(tupleString(leafLink.source, leafLink.target)); //<-- only one leaf connection is shown + continue; + } + } + + if (! _dataStorage.leafConnections.Contains(tupleString(leafLink.target, leafLink.source))) + { + if (! _dataStorage.leafConnections.Contains(tupleString(leafLink.source, leafLink.target))) + { + _dataStorage.leafConnections.Add(tupleString(leafLink.target, leafLink.source)); //<-- only one leaf connection is shown + + continue; + } + } + } + } public void constructClustersOfBrokenGateways() { foreach (var gateway in _dataStorage.gateways) diff --git a/backend/drawables/GatewayLink.cs b/backend/drawables/GatewayLink.cs new file mode 100644 index 0000000..31d6ba6 --- /dev/null +++ b/backend/drawables/GatewayLink.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using backend.models; + +namespace backend.drawables +{ + public class GatewayLink : Link + { + public List errors { get; set; } + public string errorsAsString { get; set; } + + public GatewayLink(string source, string target, bool ntv_error = false) : base(source, target, ntv_error) + { + errors = new List(); + errorsAsString = source + " to/from " + target; + } + } +} \ No newline at end of file diff --git a/backend/drawables/LeafLink.cs b/backend/drawables/LeafLink.cs new file mode 100644 index 0000000..8af81fd --- /dev/null +++ b/backend/drawables/LeafLink.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using backend.models; + +namespace backend.drawables +{ + public class LeafLink : Link + { + public List connections { get; set; } + public LeafLink(string source, string target) : base(source, target) + { + connections = new List(); + } + } +} \ No newline at end of file diff --git a/backend/drawables/Link.cs b/backend/drawables/Link.cs index 845e62d..be4f783 100644 --- a/backend/drawables/Link.cs +++ b/backend/drawables/Link.cs @@ -1,15 +1,14 @@ using System.Collections.Generic; +using System; namespace backend.drawables { - public class Link + public class Link { public string source { get; } public string target { get; } - public bool ntv_error { get; set; } - public List errors { get; set; } - public string errorsAsString { get; set; } + public Link(string source, string target, bool ntv_error = false) @@ -17,8 +16,6 @@ public Link(string source, string target, bool ntv_error = false) this.source = source; this.target = target; this.ntv_error = ntv_error; - errors = new List(); - errorsAsString = source + " to/from " + target; } } } \ No newline at end of file diff --git a/backend/env b/backend/env deleted file mode 100644 index 7660600..0000000 --- a/backend/env +++ /dev/null @@ -1 +0,0 @@ -NATS_URL=nats://sysadmin:zZn6MvjhbSP8RG9f@nats1.westeurope.cloudapp.azure.com:4222/ \ No newline at end of file diff --git a/backend/models/Leaf.cs b/backend/models/Leaf.cs index d639159..d5c5f78 100644 --- a/backend/models/Leaf.cs +++ b/backend/models/Leaf.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Concurrent; namespace backend { @@ -9,7 +8,7 @@ public class Leaf public string server_id { get; set; } public string now { get; set; } public int leafnodes { get; set; } - public ConcurrentBag leafs { get; set; } + public List leafs { get; set; } } diff --git a/package-lock.json b/package-lock.json index b364028..496fb5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19891,7 +19891,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "dev": true, + "optional": true }, "@types/q": { "version": "1.5.4", @@ -20271,162 +20272,6 @@ "tslint": "^5.20.1", "webpack": "^4.0.0", "yorkie": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", - "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } } }, "@vue/cli-plugin-unit-jest": { @@ -20527,6 +20372,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "optional": true, "requires": { "color-convert": "^2.0.1" } @@ -20536,6 +20382,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, + "optional": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -20546,6 +20393,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "optional": true, "requires": { "color-name": "~1.1.4" } @@ -20554,19 +20402,22 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "optional": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "dev": true, + "optional": true }, "loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", "dev": true, + "optional": true, "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -20588,6 +20439,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "optional": true, "requires": { "has-flag": "^4.0.0" } @@ -20597,6 +20449,7 @@ "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz", "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==", "dev": true, + "optional": true, "requires": { "chalk": "^4.1.0", "hash-sum": "^2.0.0", @@ -21252,7 +21105,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true + "dev": true, + "optional": true }, "atob": { "version": "2.1.2", @@ -24844,6 +24698,178 @@ } } }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", + "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", + "dev": true, + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "optional": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "optional": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "optional": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "optional": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "optional": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "optional": true + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "optional": true + } + } + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -24910,7 +24936,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true + "dev": true, + "optional": true }, "fs-write-stream-atomic": { "version": "1.0.10", @@ -27405,6 +27432,7 @@ "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz", "integrity": "sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==", "dev": true, + "optional": true, "requires": { "fs-monkey": "1.0.3" } @@ -28443,6 +28471,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "optional": true, "requires": { "callsites": "^3.0.0" }, @@ -28451,7 +28480,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "dev": true, + "optional": true } } }, @@ -31000,7 +31030,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true + "dev": true, + "optional": true }, "terser": { "version": "4.8.0", @@ -31978,6 +32009,11 @@ "vue-template-es2015-compiler": "^1.6.0" } }, + "vue-json-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/vue-json-component/-/vue-json-component-0.4.1.tgz", + "integrity": "sha512-8Tw4C5LJOT09BRKjQx8F5itheP7UIeZtUUJvWnSltb0UFQZSrXw/4B32z3pelWo38MtoglaSmFTYpyxFIWWNRg==" + }, "vue-loader": { "version": "15.9.6", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.6.tgz", @@ -33171,7 +33207,8 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "dev": true, + "optional": true }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index 4eb4bd7..3112035 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "node-notifier": ">=8.0.1", "vue": "^2.6.12", "vue-class-component": "^7.2.3", + "vue-json-component": "^0.4.1", "vue-panzoom": "^1.1.6", "vue-property-decorator": "^9.1.2", "vue-router": "^3.2.0", diff --git a/src/App.vue b/src/App.vue index 98cdfb9..d02f3cc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,7 @@ :routes='this.routes' :clusters='this.clusters' :gateways='this.gateways' + :leafs='this.leafs' :dataLoaded='this.dataLoaded' > @@ -30,6 +31,7 @@ import Statusbar from '@/components/Statusbar.vue' import InfoPanel from '@/components/InfoPanel.vue' import Searchbar from "@/components/Searchbar.vue"; import Zoombar from '@/components/Zoombar.vue' +import LinkDatum from './components/Graph/LinkDatum' export default { name: 'App', @@ -39,11 +41,12 @@ export default { Searchbar, InfoPanel }, - data (): { - servers: ServerDatum[]; + data (): { + servers: ServerDatum[]; routes: RouteDatum[]; clusters: ClusterDatum[]; gateways: GatewayDatum[]; + leafs: RouteDatum[]; // TODO add LeafDatum? dataLoaded: boolean; isPanelOpen: boolean; } { @@ -52,6 +55,7 @@ export default { routes: [], clusters: [], gateways: [], + leafs: [], dataLoaded: false, isPanelOpen: false } @@ -68,11 +72,12 @@ export default { this.routes = (await axios.get(`${host}/links`)).data this.clusters =( await axios.get(`${host}/clusters`)).data this.gateways = (await axios.get(`${host}/gatewayLinks`)).data + this.leafs = (await axios.get(`${host}/leaflinks`)).data return true }, - onNodeClick () { - this.$refs.panel.onNodeClick() + onNodeClick ({nodeData, id}) { + this.$refs.panel.onNodeClick({nodeData, id}) }, onSearchInput (text: string) { this.$refs.graph.searchFilter(text) diff --git a/src/components/Graph/GatewayDatum.ts b/src/components/Graph/GatewayDatum.ts index 0bc80b4..99fee7e 100644 --- a/src/components/Graph/GatewayDatum.ts +++ b/src/components/Graph/GatewayDatum.ts @@ -2,5 +2,6 @@ import LinkDatum from './LinkDatum' import ClusterDatum from './ClusterDatum' export default class GatewayDatum extends LinkDatum { - + errors!: string[]; + errorsAsString!: string; } \ No newline at end of file diff --git a/src/components/Graph/Graph.vue b/src/components/Graph/Graph.vue index 4b69e02..236f11f 100644 --- a/src/components/Graph/Graph.vue +++ b/src/components/Graph/Graph.vue @@ -18,6 +18,7 @@ import ClusterDatum from './ClusterDatum' import { D3DragEvent, Selection, SubjectPosition } from 'd3' import LinkDatum from './LinkDatum' import RouteDatum from './RouteDatum' +import LeafDatum from './LeafDatum' import axios from 'axios' import { PropType } from 'vue' @@ -29,7 +30,8 @@ export default { servers: Array as PropType, routes: Array as PropType, clusters: Array as PropType, - gateways: Array as PropType + gateways: Array as PropType, + leafs: Array as PropType }, data (): { svg: Selection | null; @@ -55,6 +57,7 @@ export default { this.svg.append('g').attr('id', 'hulls') this.svg.append('g').attr('id', 'clusters') this.svg.append('g').attr('id', 'routes') + this.svg.append('g').attr('id', 'leafs') this.svg.append('g').attr('id', 'servers') this.drawGraph() @@ -103,6 +106,7 @@ export default { const routes = this.routes const clusters = this.clusters const gateways = this.gateways + const leafs = this.leafs // Cluster Map for fast lookup const clusterNameToCluster = new Map() @@ -117,19 +121,29 @@ export default { const simulation: d3.Simulation> = d3.forceSimulation(allNodes) .force('link', d3.forceLink(routes).id(d => d.server_id)) .force('link', d3.forceLink(gateways).id(d => d.name).strength(0.4).distance(50)) + .force('link', d3.forceLink(leafs).id(d => d.server_id).strength(0.01).distance(200)) .force('charge', d3.forceManyBody()) - .force('x', d3.forceX()) - .force('y', d3.forceY()) + .force('x', d3.forceX().strength(0.05)) + .force('y', d3.forceY().strength(0.05)) // // Gateways const gatewayLink = this.createGatewayLinkSelection(svg, gateways) const hull = this.createHullSelection(svg, clusters, simulation) const cluster = this.createClusterNodeSelection(svg, clusters, simulation) const routeLink = this.createRouteLinkSelection(svg, routes) + const leafLink = this.createLeafLinkSelection(svg, leafs) const serverNode = this.createServerNodeSelection(svg, servers, simulation) - // Update data on simulation tick + // Update data on simulation tick - also the order which the varz are drawn simulation.on('tick', () => { + cluster?.attr('cx', d => d.x) + .attr('cy', d => d.y) + + gatewayLink?.attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y) + serverNode?.attr('cx', d => d.x) .attr('cy', d => d.y) @@ -138,22 +152,19 @@ export default { .attr('x2', d => d.target.x) .attr('y2', d => d.target.y) + leafLink?.attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y) + + hull?.attr('d', d => this.getHullPath(d, servers)) + const k = simulation.alpha() * 0.3; servers.forEach(serverNode => { const cluster = clusterNameToCluster.get(serverNode.ntv_cluster) serverNode.y += (cluster!.y - serverNode.y) * k; serverNode.x += (cluster!.x - serverNode.x) * k; }) - - cluster?.attr('cx', d => d.x) - .attr('cy', d => d.y) - - hull?.attr('d', d => this.getHullPath(d, servers)) - - gatewayLink?.attr('x1', d => d.source.x) - .attr('y1', d => d.source.y) - .attr('x2', d => d.target.x) - .attr('y2', d => d.target.y) }) }, @@ -236,6 +247,31 @@ export default { return routeLink }, + createLeafLinkSelection( + svg: d3.Selection | null, + leafs: LeafDatum[]) + { + const leafLink = svg?.select('g#leafs') + .selectAll("line") + .data(leafs) + .join( + enter => enter.append('line'), + update => update, + exit => exit.remove() + ) + .style( "stroke", "#444" ) + .style( "stroke-width", 1 ) + .style ("stroke-dasharray", ("3,3")) + .on('click', (d, i) => { // Log the value of the chosen node on click + console.log(i.connections) // fetch data on click + }) + + leafLink?.append('title') // Set title (hover text) for erronious link + .text(d => d.ntv_error ? 'Something\'s Wrong' : '') + // .attr('stroke', d => d.ntv_error ? '#f00' : '#00f') // Set line to red, if it has an error + return leafLink + }, + createServerNodeSelection ( svg: d3.Selection | null, servers: ServerDatum[], @@ -256,14 +292,11 @@ export default { .attr('r', 5) .attr('fill', d => d.ntv_error ? '#f00' : '#000') // Make node red if it has error .style('opacity', d => d.isSearchMatch ? 1.0 : 0.2) + .style('cursor', 'pointer') .call(this.drag(simulation)) // Handle dragging of the nodes .on('click', (d, i) => { // Log the value of the chosen node on click - this.$emit('node-click') - axios.get('https://localhost:5001/varz/' + i.server_id).then(a => { - console.log(d) - console.log(i) - console.log(a.data) + this.$emit('node-click', {nodeData: a.data, id: i.server_id}) }) }) diff --git a/src/components/Graph/LeafDatum.ts b/src/components/Graph/LeafDatum.ts new file mode 100644 index 0000000..5b7055b --- /dev/null +++ b/src/components/Graph/LeafDatum.ts @@ -0,0 +1,15 @@ +import LinkDatum from './LinkDatum' +import ServerDatum from './ServerDatum' + +export default class LeafDatum extends LinkDatum { + connections!: [{ + account: string, + ip: string, + port: number, + in_msgs: number, + out_msgs: number, + in_bytes: number, + out_bytes: number, + subscriptions: number + }] +} \ No newline at end of file diff --git a/src/components/Graph/LinkDatum.ts b/src/components/Graph/LinkDatum.ts index 7b18bc6..db296cf 100644 --- a/src/components/Graph/LinkDatum.ts +++ b/src/components/Graph/LinkDatum.ts @@ -4,7 +4,5 @@ export default class LinkDatum implements d3.Simu source!: T; target!: T; ntv_error!: boolean; - errors!: string[]; - errorsAsString!: string; isSearchMatch = true; } \ No newline at end of file diff --git a/src/components/InfoPanel.vue b/src/components/InfoPanel.vue index b8b3049..88b2b66 100644 --- a/src/components/InfoPanel.vue +++ b/src/components/InfoPanel.vue @@ -2,17 +2,19 @@
-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

+
+

The selected node does not exist.

server_id:

+

"{{ errorId }}"

+
+
+ +
@@ -26,14 +28,30 @@ export default { components: { SidebarPanel }, data () { return { - isPanelOpen: false + isPanelOpen: false, + nodeData: null, + rootName: 'node', + errorId: '' } }, methods: { - onNodeClick () { + onNodeClick ({nodeData, id}) { if (!this.$data.isPanelOpen) { this.$data.isPanelOpen = true } + + const info = document.getElementById("info") + const error = document.getElementById("error") + + if (nodeData === '') { + info.style.display = "none" + error.style.display = "block" + this.errorId = id + } else { + info.style.display = "block" + error.style.display = "none" + this.nodeData = nodeData + } } } } @@ -41,4 +59,15 @@ export default { diff --git a/src/main.ts b/src/main.ts index 4ca8b33..52c704c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,8 @@ import BootstrapVue from 'bootstrap-vue' import VueZoomer from 'vue-zoomer' // @ts-ignore import Panzoom from 'vue-panzoom' -//import Panzoom from '@panzoom/panzoom' + +import JSONView from 'vue-json-component' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' @@ -18,7 +19,7 @@ Vue.config.productionTip = false Vue.use(BootstrapVue) Vue.use(VueZoomer) -//Vue.use(Panzoom) +Vue.use(JSONView) new Vue({ router, diff --git a/tests/backend.Tests/DrawablesProcessorTests.cs b/tests/backend.Tests/DrawablesProcessorTests.cs index f7df15b..da697ce 100644 --- a/tests/backend.Tests/DrawablesProcessorTests.cs +++ b/tests/backend.Tests/DrawablesProcessorTests.cs @@ -77,10 +77,12 @@ public void ProcessLinksTest() server_id = "id0", routes = new ConcurrentBag(new RouteNode[] { new RouteNode { - remote_id = "id1" + remote_id = "id1", + ip = "1234" }, new RouteNode { - remote_id = "bruh" + remote_id = "bruh", + ip = "2345" } }) }, @@ -88,7 +90,8 @@ public void ProcessLinksTest() server_id = "id1", routes = new ConcurrentBag(new RouteNode[] { new RouteNode { - remote_id = "id0" + remote_id = "id0", + ip = "3456" } }) } @@ -119,6 +122,49 @@ public void ProcessLinksTest() Assert.Equal("id1", data.links[2].target); Assert.False(data.links[2].ntv_error); } + + [Fact] + public void ConstructSingleLeafConnectionsTest() + { + var data = new DataStorage(); + data.servers = new List(new Server[] + { + new Server {server_id = "id0", server_name = "name0"}, + new Server {server_id = "id1", server_name = "name1"}, + new Server {server_id = "id2", server_name = "name2"} + }); + + data.ipToServerId.Add("123", "id0"); + data.ipToServerId.Add("234", "id1"); + data.ipToServerId.Add("345", "id2"); + + var leafs = new List(new Leaf[]{ + new Leaf { + server_id = "id0", + leafs = new List(new LeafNode[]{ + new LeafNode { + ip = "234", + account = "bruh" + }, + new LeafNode { + ip = "234", + account = "oline" + }, + new LeafNode { + ip = "345", + account = "lmao" + } + }) + } + }); + + data.leafs = leafs; + + var dp = new DrawablesProcessor(data, "thisTextDoesNotMatter"); + dp.ConstructSingleLeafConnections(); + + Assert.Equal(2, data.leafLinks.Count()); + } [Fact] public void ProcessClustersTest()